Using PSR-7 Middleware
in Your RESTful WordPress Projects


A talk by @thorstenfrommen

Who’s That Guy?

Thorsten Frommen

Thorsten Frommen

  • Certified PHP engineer, web development professional, and tester.
  • WordPress engineer and technical project lead at Inpsyde
  • Lead developer of MultilingualPress.
  • Maintainer of WP REST Starter.

Using PSR-7 Middleware
in Your RESTful WordPress Projects

Excursus: PSR

PHP Standard Recommendations

  • PSR stands for PHP Standard Recommendations.
  • Maintained by the PHP Framework Interop Group.
  • Well-known PSRs:
    • PSR-2: Coding Style Guide;
    • PSR-3: Logger Interface;
    • PSR-4: Autoloading Standard.

PSR-7: HTTP Message Interfaces

HTTP Message Interfaces

  • PSR-7 concerns itself with HTTP messages such as requests and responses.
  • Package contains interfaces for HTTP messages and related structures (e.g., URI).
  • There are quite a few PSR-7 implementations.
  • There is lots of middleware designed to work with implementors.

Why Implement PSR-7?

(For the same reasons as almost any other PSR.)

  • WordPress can be used to build PHP-based web applications.
  • There are lots of other systems or frameworks.
  • Maybe your problem already has a (thoroughly tested) solution.
  • More consumers of interoperable pieces lead to more producers.

Example PSR-7 Middleware

  • AccessLog: Generate access logs for each request.
  • BasicAuthentication: Implement basic HTTP authentication.
  • DetectDevice: Detect the client device.
  • Firewall: Provide IP filtering.
  • Geolocate: Geolocate the client using the IP.
  • ...

Implementing PSR-7 in WordPress


“Can’t be that hard, right?”


Well, … unfortunately, it is.

PSR-7 Request and Response


					namespace Psr\Http\Message;

					interface MessageInterface { /* ... */ }

					interface RequestInterface extends MessageInterface { /* ... */ }
					interface ServerRequestInterface extends RequestInterface { /* ... */ }

					interface ResponseInterface extends MessageInterface { /* ... */ }
				

WordPress (REST) Request and Response


					class WP_REST_Request implements \ArrayAccess { /* ... */ }

					class WP_HTTP_Response { /* ... */ }
					class WP_REST_Response extends \WP_HTTP_Response { /* ... */ }
				

¯\_(ツ)_/¯

PSR-7 getHeaders() Method


					/**
					 * Retrieves all message header values.
					 *
					 * ...
					 *
					 * @return string[][] Returns an associative array of the message's headers. Each
					 *     key MUST be a header name, and each value MUST be an array of strings
					 *     for that header.
					 */
					public function getHeaders();
				

WordPress get_headers() Method

Request:

						/** @return string[][] */
						public function get_headers() {

							return [ 'k' => [ 'v1', 'v2' ] ];
						}
					
Response:

						/** @return string[] */
						public function get_headers() {

							return [ 'k' => 'v1, v2' ];
						}
					

(╯°□°)╯︵ ┻━┻

Goal


					use Psr\Http\Message\ServerRequestInterface;

					class Request extends \WP_REST_Request implements ServerRequestInterface {

						// ...
					}
				

					use Psr\Http\Message\ResponseInterface;

					class Response extends \WP_REST_Response implements ResponseInterface {

						// ...
					}
				
WP REST Starter
  • Composer package (i.e., no plugin).
  • Provides several interfaces for both data types and business logic.
  • Comes with straightforward implementations for the common needs.
  • Allows for proper object-oriented programming.
  • Provides PSR-7-compliant request and response implementations.

Creating a PSR-7-compliant REST Request

  • Instantiate a new object yourself. Unlikely, though.
  • More likely, make an existing request PSR-7-compliant:

						use Inpsyde\WPRESTStarter\Core\Request\Request;

						$request = Request::from_wp_request( $request );
					

Creating a PSR-7-compliant REST Response

  • Instantiate a new object yourself.
  • Make an existing response PSR-7-compliant:

						use Inpsyde\WPRESTStarter\Core\Response\Response;

						$response = Response::from_wp_response( $response );
					

How Does This Work?

Behind the Scenes

  • The new classes…
    • …extend the according WordPress class;
    • …implement the according PSR-7 interface.
  • WordPress methods proxy to the according parent method.
  • PSR-7 methods proxy to an internal PSR-7 HTTP message object.
  • Always keep the two proxies in sync.

Looking at the Code: PSR-7 Getter


					public function getHeaders() {

						return $this->message->getHeaders();
					}
				

Looking at the Code: WordPress Getter

No need to do anything at all.

You actually call the method on the parent (i.e., WordPress) class.

Looking at the Code: PSR-7 Factory Method (I)


					public function withCookieParams( array $cookies ) {

						$clone = clone $this;

						$clone->message = $this->message->withCookieParams( $cookies );

						return $clone;
					}
				

Looking at the Code: PSR-7 Factory Method (II)


					public function withQueryParams( array $query ) {

						$clone = clone $this;

						$clone->message = $this->message->withQueryParams( $query );

						$clone->set_wp_query_params_from_http_message();

						return $clone;
					}

					private function set_wp_query_params_from_http_message() {

						parent::set_query_params( $this->message->getQueryParams() );
					}
				

Looking at the Code: PSR-7 Factory Method (III)


					public function withHeader( $name, $value ) {

						// Similar to withQueryParams()...
					}

					private function set_wp_headers_from_http_message() {

						$headers = $this->message->getHeaders();
						$headers = array_map( [ $this, 'header_as_string' ], $headers );

						parent::set_headers( $headers );
					}

					private function header_as_string( array $values ) {

						return implode( ', ', $values );
					}
				

Looking at the Code: WordPress Setter (I)


					public function set_method( $method ) {

						parent::set_method( $method );

						$this->message = $this->message->withMethod( $this->get_method() );
					}
				

Looking at the Code: WordPress Setter (II)


					public function set_header( $name, $value ) {

						parent::set_header( $name, $value );

						$this->set_http_message_with_wp_headers();
					}

					private function set_http_message_with_wp_headers() {

						$message = $this->message;

						// No withHeaders() in PSR-7, so create a new HTTP message.
						$this->message = new PSR7Request(
							$message->getMethod(),
							$message->getUri(),
							(array) ( $this->get_headers() ?? [] ),       // <- WordPress data.
							$message->getBody(),
							$message->getProtocolVersion(),
							(array) ( $message->getServerParams() ?? [] )
						);
					}
				

Using the New Structures

Using the New Structures

Well, this is no big deal:

  • first, make the WordPress objects PSR-7-compliant;
  • then, pass them on to your desired PSR-7 middleware (stack).

Possible Access Points

  • WordPress (i.e., \WP_REST_Server) offers several filters to use:
    • rest_pre_dispatch;
    • rest_request_before_callbacks;
    • rest_dispatch_request;
    • rest_request_after_callbacks;
    • rest_post_dispatch;
    • rest_envelope_response;
    • rest_pre_serve_request;
    • rest_pre_echo_response.
  • Choose the one(s) that fit(s) your needs.
  • But mind the data you get!

Usage Example

Using Your PSR-7 Middlewares

What you need to do:

  • hook into your desired filter;
  • set up your middleware stack;
  • set up a middleware dispatcher;
  • dispatch the request.

Hook Into Your Desired Filter


					add_filter( 'rest_post_dispatch', function (
						\WP_HTTP_Response $response,
						\WP_REST_Server $server,
						\WP_REST_Request $request
					) {

						// ...
					}, 0, 3 );
				

Set Up Your Middleware Stack


					// ...

					$logger = ( new Logger( 'access' ) )->pushHandler( new ErrorLogHandler() );

					$middlewares = [
						Middleware::ResponseTime(),
						Middleware::ClientIp()->remote(),
						Middleware::Uuid(),
						Middleware::AccessLog( $logger )->combined(),
					];

					// ...
				

Set Up a Middleware Dispatcher


					// ...

					$dispatcher = ( new RelayBuilder() )->newInstance( $middlewares );

					// ...
				

Dispatch The Request


					// ...

					return $dispatcher(
						Request::from_wp_request( $request ),
						Response::from_wp_response( $response )
					);

					// ...
				

Putting It All Together


					add_filter( 'rest_post_dispatch', function (
						\WP_HTTP_Response $response,
						\WP_REST_Server $server,
						\WP_REST_Request $request
					) {

						$logger = ( new Logger( 'access' ) )->pushHandler( new ErrorLogHandler() );

						$middlewares = [
							Middleware::ResponseTime(),
							Middleware::ClientIp()->remote(),
							Middleware::Uuid(),
							Middleware::AccessLog( $logger )->combined(),
						];

						$dispatcher = ( new RelayBuilder() )->newInstance( $middlewares );

						return $dispatcher(
							Request::from_wp_request( $request ),
							Response::from_wp_response( $response )
						);
					}, 0, 3 );
				

Take-aways

  • PSR-7 is a standard (recommendation) aimed at interoperability.
  • PSR-7 is applicable to WordPress.
  • There is lots of ready-made PSR-7 middleware.
  • Use what works for you, and maybe implement (and share) your own.
  • Use WP REST Starter for proper OOP, and maybe even contribute to it.

References and Further Reading

Thanks!


@thorstenfrommen

slides.tfrommen.de/psr-7