PHP Speaks HTTP?

(or does it?)

Matthew Weier O'Phinney / @mwop

PHP was built for the web

In the 90s.

PHP targets
Common Gateway Interface
(CGI)

But does it reflect how we use HTTP today?

  • APIs
  • Quality assurance (testing!)
  • Non-form payloads (XML! JSON!)

HTTP Primer

Request


{REQUEST_METHOD} {REQUEST_URI} HTTP/{PROTOCOL_VERSION}
Header: value
Another-Header: value

Message body
          

Response


HTTP/{PROTOCOL_VERSION} {STATUS_CODE} {REASON_PHRASE}
Header: value
Another-Header: value

Message body
          

URI


<scheme>://<host>(:<port>)?/<path>?<query string arguments>
          

Query string arguments


name=value&name2=value2&etc
          

PHP's Role

  • Accept an incoming Request
  • Create and return a Response

Request considerations

  • We MAY need to act on the request method
  • We MAY need to act on the request URI, including query string arguments
  • We MAY need to act on one or more headers
  • We MAY need to parse the request message body
  • How do we interact with a request?

Response considerations

  • We MAY need to set the status code
  • We MAY need to specify one or more headers
  • We MAY need to provide message body content
  • How do we create the response?

The Past

PHP v2 — v4.0.6

Request

Protocol version and request method


$version = $HTTP_SERVER_VARS['PROTOCOL_VERSION']
$method  = $HTTP_SERVER_VARS['REQUEST_METHOD']
          

URL


$url = $HTTP_SERVER_VARS['REQUEST_URI'];
// except when it isn't...
          

URL sources

  • $HTTP_SERVER_VARS['SCHEME'] or $HTTP_SERVER_VARS['HTTP_X_FORWARDED_PROTO']?
  • $HTTP_SERVER_VARS['HOST'] or $HTTP_SERVER_VARS['SERVER_NAME'] or $HTTP_SERVER_VARS['SERVER_ADDR']?
  • $HTTP_SERVER_VARS['REQUEST_URI'] or $HTTP_SERVER_VARS['UNENCODED_URL'] or $HTTP_SERVER_VARS['HTTP_X_ORIGINAL_URLSERVER_ADDR'] or $HTTP_SERVER_VARS['ORIG_PATH_INFO']?

And...

  • $HTTP_SERVER_VARS is only available in the global scope.

Getting headers


// Most headers, e.g. Accept:
$accept = $HTTP_SERVER_VARS['HTTP_ACCEPT'];

// Some headers, e.g. Content-Type:
$contentType = $HTTP_SERVER_VARS['CONTENT_TYPE'];
          

Slightly easier

If you used Apache:

$headers = apache_request_headers();

// or its alias

$headers = getallheaders();

$contentType = $headers['Content-Type'];
          

But...

  • Apache-only (until 4.3.3, when NSAPI support was added)
  • Some headers are not returned (If-*)

Input: register_globals


// Assume /foo?duck=goose
echo $duck; // "goose"

// Also works for POST: assume cat=dog
echo $cat; // "dog"

// And cookies: assume bird=bat
echo $bird; // "bat"
          

But what if...?


// What if /foo?duck=goose
// AND POST duck=bluebird
// AND cookie duck=eagle ?
echo $duck; // ?
          

Introducing variables_order


; Get -> Post -> Cookie
variables_order = "EGPCS"
          

So...


// What if /foo?duck=goose
// AND POST duck=bluebird
// AND cookie duck=eagle ?
echo $duck; // "eagle"
          

But I wanted POST!

And thus we have $HTTP_POST_VARS:

$duck = $HTTP_POST_VARS['duck'];
echo $duck; // "bluebird"
          

"Explicit" Access

  • $HTTP_GET_VARS: Query string arguments
  • $HTTP_POST_VARS: POST form-encoded data
  • $HTTP_COOKIE_VARS: Cookies

What about alternate messages?

$HTTP_RAW_POST_DATA to the rescue!

$data = parse($HTTP_RAW_POST_DATA); // parse somehow...
$profit($data);
          

But...

  • Until 4.1, you had to enable track_vars for these to be visible!
  • Only available in the global scope - not within functions/methods!
  • $HTTP_RAW_POST_DATA will not be present for form-encoded data, unless always_populate_raw_post_data is enabled.

Response

Status Code/Reason Phrase


header('HTTP/1.0 404 Not Found'); // Send a status line
header('Location: /foo'); // Implicit 302
header('Location: /foo', true, 301); // Status code as argument
          

Headers


header('X-Foo: Bar');
header('X-Foo: Baz', false); // Emit an additional header
header('X-Foo: Bat');        // Replace any previous values
          

But...

echo(), print(), or emit anything to the output buffer, and no more headers can be sent!

Message bodies


echo "Foo!";
          

HTML is easy!

Just mix it in with your PHP!

<?php
$url  = 'http://http://www.phpconference.com.br/';
$text = 'PHP Conference Brasil';
?>
<a href="<?php echo $url ?>"><?php echo $text ?></a>
          

Mixing headers and content

  • Aggregate body in variables
  • Or get comfortable with PHP's ob*() API...

Ouch!

The Present

PHP v4.1 — Now (5.6.3)

Request

Goodbye, register_globals!

Superglobals

  • Available in any scope
  • Prefixed with an underscore
  • $_SERVER: Server configuration, HTTP metadata, etc.
  • $_GET: Query string arguments
  • $_POST: Deserialized form-encoded POST data
  • $_COOKIE: Cookie values

But...

  • $_SERVER is still in the same format.
  • Testing + superglobals == recipe for disaster.
  • Superglobals are mutable.

$_REQUEST

  • Merges GET, POST, and Cookie variables, according to variables_order or request_order_string.
  • Just. Don't.

Message Bodies

php://input

  • Always available
  • Read-only
  • Uses streams: more performant, less memory intensive.

But...

  • Read-once until 5.6

Response

Nothing changes

Argh!

How do other languages hold up?

Python: WSGI

"environ" (request)

  • Encapsulates the request method, URI information, and headers.
  • Body is a stream.

"start_response" callback

  • Encapsulates status and headers.
  • Body is a stream.

Ruby: Rack

Environment (request)

  • Encapsulates the request method, URI information, and headers.
  • Body is a stream.

Response

  • Encapsulates status and headers.
  • Body is iterable.

Node

http.IncomingMessage

  • Encapsulates the request method, URL, and headers.
  • Body is a stream.

http.ServerResponse

  • Encapsulates status and headers.
  • Body is a stream.

Commonalities

  • Standard access to the request URI
  • Standard access to request headers
  • Standard way to provide response status
  • Standard way to provide response headers
  • Message bodies are treated as streams/iterators

Users of these languages have
HTTP message abstractions

If Python, Ruby, and Node can solve this problem, why can't PHP?

The Future

Framework Interop Group
(FIG)

Promote reuse of code across projects,
via "standards recommendations":

  • Autoloading
  • Shared, independent coding standards
  • Logging
  • and more to come...

PSR-7

HTTP Message Interfaces

Client-side

  • OutgoingRequestInterface
  • IncomingResponseInterface

Server-side

  • IncomingRequestInterface
  • OutgoingResponseInterface

So?

Request

Request Metadata


// Protocol
$request->getProtocolVersion();

// Method
$request->getMethod();

// Url
$request->getUrl();
          

Request Headers


foreach ($request->getHeaders() as $header => $values) {
    printf("%s: %s\n", $header, implode(', ', $values);
}
          

Message Body


$body = $request->getBody();

// Treat it as a string
echo $body;

// Or as a stream-like implementation
while (! $body->eof()) {
  echo $body->read(4096);
}
          

Incoming Request Data


$server = $request->getServerParams();
$cookie = $request->getCookieParams();
$query  = $request->getQueryParams();
$data   = $request->getBodyParams();
$files  = $request->getFileParams();
          

Response

Response Metadata


// Protocol
$response->setProtocolVersion('1.0');

// Status
$response->setStatus(404, 'Not Found');

header(sprintf(
    'HTTP/%s %d %s',
    $response->getProtocolVersion(),
    $response->getStatusCode(),
    $response->getReasonPhrase()
));
          

Response Headers


$response->setHeader('X-Foo', 'Bar');
$response->addHeader('X-Foo', 'Baz'); // add another value!
$response->setHeader('X-Foo', 'Bat'); // overwrite!

foreach ($request->getHeaders() as $header => $values) {
    foreach ($values as $value) {
      header(sprintf(
          '%s: %s',
          $header,
          $value
      ), false);
    }
}
          

Response Body


$body = $response->getBody();
$body->write('Foo! Bar!');
$body->write(' More!');

echo $body;
          

PSR-7 in a nutshell

Uniform access to HTTP messages

An end to framework silos

Target PSR-7

More specifically, start thinking in terms of middleware:


use Psr\Http\Message\IncomingRequestInterface as Request;
use Psr\Http\Message\OutgoingResponseInterface as Response;

$handler = function (Request $request, Response $response) {
    // Grab input from the request
    // Update the response
};
          

Make frameworks consume middleware


class ContactController
{
    private $contact;
    public function __construct(Contact $contact)
    {
        $this->contact = $contact;
    }

    public function dispatch(Request $req, Response $res)
    {
        return call_user_func($this->contact, $req, $res);
    }
}
          

And in the future...

Developers are already working on a native PHP extension!

Resources

The future of HTTP in PHP is bright

Thank You

Matthew Weier O'Phinney

https://mwop.net/
https://apigility.org
http://framework.zend.com
@mwop