Understanding
and
Implementing
PSR

 


Thu 04 Apr 2019
Talk Agenda
  • What is PSR?
  • PSR-0: Autoloading Standard
  • PSR-1: Basic Coding Standard
  • PSR-2: Coding Style Guide
  • PSR-4: Autoloading Standard (again?)
  • PSR-7: HTTP Message Interface
What is PSR?

Credits: https://xkcd.com/927
Common terminology in PSRs
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
PSR-0: Autoloading Standard

In the beginning
<?php // login.php
$conn = new mysqli('localhost', 'username', 'password'); // Database connection
if (!$conn) { die('Connection failed: ' . mysqli_connect_error()); }

if (isset($_POST['submit'])) { // Check form submit
    $user = $_POST['user'];
    $pass = $_POST['pass'];
    $result = $conn->query(
        "SELECT * FROM user WHERE username='{$user}' AND password='{$pass}'"
    );
    header('Location:' . (mysqli_num_rows($result) > 0 ? 'ok.php' : 'fail.php'));
    exit;
}
?>
<form method="POST">
  Username: <input type="text" name="user"><br />
  Password: <input type="password" name="pass"><br />
  <input type="submit" />
</form>
Later, include files with common functions
<?php // login.php
include 'connection.php';
include 'functions.php';

if (isset($_POST['submit'])) { // Check form submit
    $user = $_POST['user'];
    $pass = $_POST['pass'];
    header('Location: ' . (isLoginValid($user, $pass) ? 'ok.php' : 'fail.php'));
    exit;
}
?>
<form>
  Username: <input type="text" name="user"><br />
  Password: <input type="password" name="pass"><br />
  <input type="submit" />
</form>
Classes and Namespaces
<?php // Foo.php
class Foo {}
<?php // libraries/Foo/Bar.php
namespace Foo;

class Bar {}
<?php // libraries/Baz/Qux.php
namespace Baz;

use Foo\Bar;

class Qux {
    function __construct() { $bar = new Bar; }
}
spl_autoload_register('autoload');
Mandatory
  • A fully-qualified namespace and class must have the following structure
    \<Vendor Name>\(<Namespace>\)*<Class Name>
  • Each namespace must have a top-level namespace ("Vendor Name").
  • Each namespace can have as many sub-namespaces as it wishes.
$object = new Foo\Bar\Baz\Qux;
Mandatory
  • Each namespace separator is converted to a
    DIRECTORY_SEPARATOR
    when loading from the file system.
  • Each
    _
    character in the CLASS NAME is converted to a
    DIRECTORY_SEPARATOR
    . The
    _
    character has no special meaning in the namespace.
  • The fully-qualified namespace and class is suffixed with
    .php
    when loading from the file system.
  • Alphabetic characters in vendor names, namespaces, and class names may be of any combination of lower case and upper case.
Examples
  • \Doctrine\Common\IsolatedClassLoader
    =>
    /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request
    =>
    /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl
    =>
    /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message
    =>
    /path/to/project/lib/vendor/Zend/Mail/Message.php
Underscores in Namespaces and Class Names
  • \namespace\package\Class_Name
    =>
    /path/to/project/lib/vendor/namespace/package/Class/Name.php
  • \namespace\package_name\Class_Name
    =>
    /path/to/project/lib/vendor/namespace/package_name/Class/Name.php
<?php // Sample PSR-0 autoloader - vendor/autoload.php
function autoload($className)
{
    // Resolve filename for $className based on PSR-0 and load class file
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    $ds = DIRECTORY_SEPARATOR;
    if ($lastNsPos = strrpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', $ds, $namespace) . $ds;
    }
    $fileName .= str_replace('_', $ds, $className) . '.php';

    require $fileName;
}

// Register autoloader
spl_autoload_register('autoload');

PSR-1: Basic Coding Standard
Method Names

  • function foobar()
  • function foo_bar()
  • function fooBar()
  • function Foobar()
  • function FooBar()
  • function FOOBAR()

Files
  • Files MUST use only
    <?php
    and
    <?=
    tags.
  • Files MUST use only UTF-8 without BOM for PHP code.
  • Files SHOULD either declare symbols or cause side-effects but SHOULD NOT do both.
<?php // example to avoid
ini_set('error_reporting', E_ALL); // side effect: change ini settings
include "file.php"; // side effect: loads a file
echo "<html>\n"; // side effect: generates output

// declaration
function foo()
{
    // function body
}

Namespaces and Class Names
  • Namespaces and classes MUST follow an "autoloading" PSR: [PSR-0, PSR-4].
  • Class names MUST be declared in
    StudlyCaps
    .
<?php // PHP 5.3 and later
namespace Vendor\Model;

class Foo
{
}
<?php // PHP 5.2.x and earlier
class Vendor_Model_Foo
{
}

Class Constants, Properties, and Methods
  • Class constants MUST be declared in all upper case with underscore separators.
  • Method names MUST be declared in
    camelCase
    .
<?php
namespace Vendor\Model;

class Foo
{
    const VERSION = '1.0';
    const DATE_APPROVED = '2012-06-01';

    public function myMethodName()
    {
        // method body
    }
}
PSR-2: Coding Style Guide

<?php
namespace Vendor\Package;

use FooInterface;
use OtherVendor\OtherPackage\BazClass as Bar;

class Foo extends Bar implements FooInterface
{
    public function sampleFunction($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }

    final public static function bar()
    {
    }
}

http://www.php-fig.org/psr/psr-2/#a-2-survey-legend
indent_type:
    tab: 7
    2: 1
    4: 14
line_length_limit_soft:
    ?: 2
    no: 3
    75: 4
    80: 6
    85: 1
    100: 1
    120: 4
    150: 1
line_length_limit_hard:
    ?: 2
    no: 11
    85: 4
    100: 3
    120: 2
class_names:
    ?: 1
    lower: 1
    lower_under: 1
    studly: 19
class_brace_line:
    next: 16
    same: 6
constant_names:
    upper: 22
true_false_null:
    lower: 19
    upper: 3
method_names:
    camel: 21
    lower_under: 1
method_brace_line:
    next: 15
    same: 7
control_brace_line:
    next: 4
    same: 18
control_space_after:
    no: 2
    yes: 20
always_use_control_braces:
    no: 3
    yes: 19
else_elseif_line:
    next: 6
    same: 16
case_break_indent_from_switch:
    0/1: 4
    1/1: 4
    1/2: 14
function_space_after:
    no: 22
closing_php_tag_required:
    no: 19
    yes: 3
line_endings:
    ?: 5
    LF: 17
static_or_visibility_first:
    ?: 5
    either: 7
    static: 4
    visibility: 6
control_space_parens:
    ?: 1
    no: 19
    yes: 2
blank_line_after_php:
    ?: 1
    no: 13
    yes: 8
class_method_control_brace:
    next/next/next: 4
    next/next/same: 11
    next/same/same: 1
    same/same/same: 6
CS Fixers out there
PSR-4: Autoloading Standard (again??)
Why bother?
  • PSR-0 was based on Horde/PEAR convention under the constraints of PHP 5.2 and previous
  • The PEAR installer moved source files from PEAR packages into 1 central directory
  • With Composer, package sources are used from their installed location, no moving around, no central directory
vendor/    # PSR-0
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php     # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php # Vendor_Name\Package_Name\ClassNameTest
vendor/    # PSR-4
    vendor_name/
        package_name/
            src/
                ClassName.php     # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php # Vendor_Name\Package_Name\ClassNameTest
Examples
PSR-7: HTTP Message Interface
The most exciting PSR!
The Future of PHP!

Credits to talk by Matthew Weier O'Phinney on
PSR-7 And Middleware

<?php // test.php?city=SG
if (isset($_POST['submit'])) {
    echo 'You typed: ' . $_POST['myText'];
}
?>
City: <?php echo ($_GET['city'] ?? 'None'); ?><br />
<form method="POST">
  Text: <input type="text" name="myText"><br />
  <input type="submit" />
</form>
<?php // localhost/city/SG
class TestController extends AbstractActionController
{
    public function indexAction()
    {
        $request = $this->getRequest();
        $usertext = $request->isPost() ? $request->getPost()['myText'] : '';
        return new ViewModel([
            'city' => $this->params('city', 'None'),
            'form' => new TestForm(),
            'usertext' => $usertext,
        ]);
    }
}



Guzzle, Buzz and more...
Abstract what?

  • Form post
  • Access $_SERVER variables
  • Extract request uri for routing purposes
  • Handle file uploads
  • Input and output streams
  • Returning of response

Credits: http://code.tutsplus.com/tutorials/http-the-protocol-every-web-developer-must-know-part-1--net-31177

Credits: http://wiki.hashphp.org/HttpPrimer
Every HTTP request message has a specific form:
POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat
HTTP response messages have a similar structure:
HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body
MessageInterface
$headers = $message->getHeaders(); // [<headerName> => <array>]

$headerValues = $message->getHeader('Cookie'); // array, may be more than 1

$headerLine = $message->getHeaderLine('Accept'); // string

$body = $message->getBody(); // StreamInterface

$newMessage = $message->withHeader('Content-Type', 'application/json');

$newerMessage = $newMessage->withBody($body->getContents() . 'appended content');
RequestInterface
$method = $request->getMethod(); // GET, POST, etc.

$uri = $request->getUri(); // UriInterface

$body = new Stream();
$body->write('{"id":1}');

$newRequest = $request
              ->withMethod('GET')
              ->withUri('http://example.com/api/v1/users')
              ->withHeader('Accept', 'application/json')
              ->withBody($body);
ResponseInterface
$status = $response->getStatusCode(); // 200, 404, 500

$reason = $response->getReasonPhrase(); // OK, Not Found, Server Error

$data = json_decode((string) $response->getBody(), true);
$data['timestamp'] = gmdate('c');

$newResponse = $response->withBody(json_encode($data));
$newerResponse = $newResponse->withStatus(200);
ServerRequestInterface
$queryParams = $request->getQueryParams();

$parsedBody = $request->getParsedBody(); // deserialized body params

$uploadedFiles = $request->getUploadedFiles(); // array of UploadedFileInterface

// Get `id` passed from route, eg. localhost/user/100.
// Application must parse route and inject into request attributes
$id = $request->getAttribute('id'); // 100
StreamInterface
interface StreamInterface
{
    public function __toString(); // important
    public function close();
    public function detach();
    public function getSize();
    public function tell();
    public function eof();
    public function isSeekable();
    public function seek($offset, $whence = SEEK_SET);
    public function rewind();
    public function isWritable();
    public function write($string);
    public function isReadable();
    public function read($length);
    public function getContents();
    public function getMetadata($key = null);
}
File Uploads with $_FILES
[
    'files' => [
        0 => [
            'name' => 'file0.txt',
            'type' => 'text/plain',
            /* etc. */
        ],
        1 => [
            'name' => 'file1.html',
            'type' => 'text/html',
            /* etc. */
        ],
    ],
];
File Uploads with $_FILES
[ // This is what $_FILES in PHP gives us :P
    'files' => [
        'name' => [
            0 => 'file0.txt',
            1 => 'file1.html',
        ],
        'type' => [
            0 => 'text/plain',
            1 => 'text/html',
        ],
        /* etc. */
    ],
];
UploadedFileInterface
$uploadedFiles = $request->getUploadedFiles(); // array of UploadedFileInterface

foreach ($uploadedFiles as $file) {
    echo $file->getClientFilename() . ', ' . $file->getClientMediaType();
    $size = $file->getSize();
    $contents = $file->getStream(); // StreamInterface
    $file->moveTo($newFilePath);
}

// Printout:
//   file0.txt, text/plain
//   file1.txt, text/html
What's in an URI?
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
 http://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
 └─┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘
scheme  user information     host     port                  query         fragment
Credits: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Examples
Trying to compose a URI via $_SERVER
array( // not all variables shown here
  'HTTP_HOST' => 'localhost',
  'HTTP_ACCEPT' => 'text/html,application/xhtml+xml',
  'SERVER_NAME' => 'localhost',
  'SERVER_ADDR' => '127.0.0.1',
  'SERVER_PORT' => '80',
  'REMOTE_ADDR' => '127.0.0.1',
  'REQUEST_SCHEME' => 'http',
  'REMOTE_PORT' => '53746',
  'SERVER_PROTOCOL' => 'HTTP/1.1',
  'REQUEST_METHOD' => 'GET',
  'QUERY_STRING' => 'key1=value1',
  'REQUEST_URI' => '/test.php?key1=value1',
  'SCRIPT_NAME' => '/test.php',
  'PHP_SELF' => '/test.php',
  'REQUEST_TIME' => 1470751673,
)
UriInterface
$uri = $request->getUri();
$scheme   = $uri->getScheme();
$userInfo = $uri->getUserInfo();
$host     = $uri->getHost();
$port     = $uri->getPort();
$path     = $uri->getPath();
$query    = $uri->getQuery();
$fragment = $uri->getFragment();
$newUri   = $uri->withHost('example.com');
Sample Lab

Middleware
Zend Expressive

PSR-7 Middleware in Minutes
Try it today!