PHP Speaks HTTP?

(or does it?)

Matthew Weier O'Phinney / @mwop

PHP was built for the web

PHP targets
Common Gateway Interface
(CGI)

But does it reflect how we use HTTP today?

  • Non-form payloads: XML! JSON!
  • APIs: PATCH! PUT! DELETE!
  • Quality assurance: offline testing!

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>://(<user-info>@)<host>(:<port>)(/<path>)(?<query>)
          

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 message types?

$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

WSGI: environ

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

path = environ.get("PATH_INFO", "")
query = parse_qs(environ.get("QUERY_STRING", ""))
accept = environ.get("HTTP_ACCEPT", "text/html")
content_length = int(environ.get("CONTENT_LENGTH", 0))
body = environ["wsgi.input"].read(content_length)
          

WSGI: start_response

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

headers = [("Content-Type", "application/json",
  ("X-Foo", "Bar")]
start_response("200 OK", headers)
return stream_representing_body
          

Ruby: Rack

Rack: env + Request

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

request = Rack::Request.new env
uri = request.url
accept = request.env['HTTP_ACCEPT']
data = JSON.parse( request.body.read )
          

Rack: Response

  • Encapsulates status and headers.
  • Body is iterable.

response = Rack::Response.new
response.status = 200
response['Content-Type'] = 'application/json'
response.write '{"message": "Content for message"}'
response.finish
          

Node

http.IncomingMessage

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

var method = req.method;
var path = require('url').parse(req.url).pathname;
var query = require('url').parse(req.url, true).query;
var accept = req.headers.accept;
var body;
req.on('data', function(chunk) { body += chunk.toString(); });
req.on('end', function() { body = JSON.parse(body); });
          

http.ServerResponse

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

res.statusCode = 200;
res.writeHead(200, 'OK!');
res.setHeader('Content-Type', 'application/json');
res.write('{"message": "Message for the body"}');
res.end();
          

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

RequestInterface


$body = new Stream();
$stream->write('{"foo":"bar"}');
$request = (new Request())
    ->withMethod('GET')
    ->withUri(new Uri('https://api.example.com/'))
    ->withHeader('Accept', 'application/json')
    ->withBody($stream);
          

ServerRequestInterface


$request = ServerRequestFactory::fromGlobals();
$method  = $request->getMethod();
$path    = $request->getUri()->getPath();
$accept  = $request->getHeader('Accept');
$data    = json_decode((string) $request->getBody());
$query   = $request->getQueryParams();
$cookies = $request->getCookieParams();
          

ResponseInterface


$body = new Stream();
$stream->write(json_encode(['foo' => 'bar']));
$response = (new Response())
    ->withStatus(200, 'OK!')
    ->withHeader('Accept', 'application/json')
    ->withBody($stream);
          

ResponseInterface


$status      = $response->getStatusCode();
$reason      = $response->getReasonPhrase();
$contentType = $response->getHeader('Content-Type');
$data        = json_decode((string) $response->getBody());
          

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\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface 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 lining up to write 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