Skip to content

Commit

Permalink
Backports middleware decorators from zendframework#134
Browse files Browse the repository at this point in the history
This patch backports the following classes from the release-3.0.0
development series:

- `Zend\Stratigility\Middleware\CallableMiddlewareDecorator`, for
  decorating middleware that follows the PSR-15 signature.
- `Zend\Stratigility\Middleware\DoublePassMiddlewareDecorator`, for
  decorating callable middleware that follows the double pass middleware
  signature.
- `Zend\Stratigility\Middleware\PathMiddlewareDecorator`, for providing
  path-segregated middleware. This required also extracting the class
  `Zend\Stratigility\Middleware\PathRequestHandlerDecorator`, which is
  used internally in order to decorate the request handler passed to
  middleware it decorates.

All classes and their were updated to adapt to the
http-middleware-compatibility shims.
  • Loading branch information
weierophinney committed Jan 11, 2018
1 parent a5b6608 commit f8d2edd
Show file tree
Hide file tree
Showing 12 changed files with 1,099 additions and 247 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"require": {
"php": "^5.6 || ^7.0",
"psr/http-message": "^1.0",
"webimpress/http-middleware-compatibility": "^0.1.3",
"webimpress/http-middleware-compatibility": "^0.1.4",
"zendframework/zend-escaper": "^2.3"
},
"require-dev": {
Expand Down
403 changes: 157 additions & 246 deletions composer.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Stratigility\Exception;

/**
* @deprecated since 2.2.0; to be removed in 3.0.0. The need for this class
* disappears with strict types in PHP 7.
*/
class InvalidArgumentException extends \InvalidArgumentException
{
}
10 changes: 10 additions & 0 deletions src/Exception/MissingResponseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@
*/
class MissingResponseException extends OutOfBoundsException
{
public static function forCallableMiddleware(callable $middleware)
{
$type = is_object($middleware)
? get_class($middleware)
: gettype($middleware);
return new self(sprintf(
'Decorated callable middleware of type %s failed to produce a response.',
$type
));
}
}
28 changes: 28 additions & 0 deletions src/Exception/PathOutOfSyncException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Stratigility\Exception;

use RuntimeException;

class PathOutOfSyncException extends RuntimeException
{
/**
* @param string $pathPrefix
* @param string $path
* @return self
*/
public static function forPath($pathPrefix, $path)
{
return new self(sprintf(
'Layer path "%s" and request path "%s" are out of sync; cannot dispatch'
. ' middleware layer',
$pathPrefix,
$path
));
}
}
58 changes: 58 additions & 0 deletions src/Middleware/CallableMiddlewareDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Stratigility\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface as RequestHandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;
use Zend\Stratigility\Exception;

/**
* Decorate callable middleware as PSR-15 middleware.
*
* Decorates middleware with the following signature:
*
* <code>
* function (
* ServerRequestInterface $request,
* RequestHandlerInterface $handler
* ) : ResponseInterface
* </code>
*
* such that it will operate as PSR-15 middleware.
*
* Neither the arguments nor the return value need be typehinted; however, if
* the signature is incompatible, a PHP Error will likely be thrown.
*/
class CallableMiddlewareDecorator implements MiddlewareInterface
{
/**
* @var callable
*/
private $middleware;

public function __construct(callable $middleware)
{
$this->middleware = $middleware;
}

/**
* {@inheritDoc}
* @throws Exception\MissingResponseException if the decorated middleware
* fails to produce a response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler)
{
$response = ($this->middleware)($request, $handler);
if (! $response instanceof ResponseInterface) {
throw Exception\MissingResponseException::forCallableMiddleware($this->middleware);
}
return $response;
}
}
93 changes: 93 additions & 0 deletions src/Middleware/DoublePassMiddlewareDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Stratigility\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface as RequestHandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;
use Zend\Diactoros\Response;
use Zend\Stratigility\Exception;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

/**
* Decorate double-pass middleware as PSR-15 middleware.
*
* Decorates middleware with the following signature:
*
* <code>
* function (
* ServerRequestInterface $request,
* ResponseInterface $response,
* callable $next
* ) : ResponseInterface
* </code>
*
* such that it will operate as PSR-15 middleware.
*
* Neither the arguments nor the return value need be typehinted; however, if
* the signature is incompatible, a PHP Error will likely be thrown.
*/
class DoublePassMiddlewareDecorator implements MiddlewareInterface
{
/**
* @var callable
*/
private $middleware;

/**
* @var ResponseInterface
*/
private $responsePrototype;

/**
* @throws Exception\MissingResponsePrototypeException if no response
* prototype is present, and zend-diactoros is not installed.
*/
public function __construct(callable $middleware, ResponseInterface $responsePrototype = null)
{
$this->middleware = $middleware;

if (! $responsePrototype && ! class_exists(Response::class)) {
throw Exception\MissingResponsePrototypeException::create();
}

$this->responsePrototype = $responsePrototype ?: new Response();
}

/**
* {@inheritDoc}
* @throws Exception\MissingResponseException if the decorated middleware
* fails to produce a response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler)
{
$response = ($this->middleware)(
$request,
$this->responsePrototype,
$this->decorateHandler($handler)
);

if (! $response instanceof ResponseInterface) {
throw Exception\MissingResponseException::forCallableMiddleware($this->middleware);
}

return $response;
}

/**
* @return callable
*/
private function decorateHandler(RequestHandlerInterface $handler)
{
return function ($request, $response) use ($handler) {
return $handler->{HANDLER_METHOD}($request);
};
}
}
144 changes: 144 additions & 0 deletions src/Middleware/PathMiddlewareDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Stratigility\Middleware;

use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface as RequestHandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;
use Zend\Stratigility\Exception;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

class PathMiddlewareDecorator implements MiddlewareInterface
{
/** @var MiddlewareInterface */
private $middleware;

/** @var string Path prefix under which the middleware is segregated. */
private $prefix;

/**
* @param string $prefix
*/
public function __construct($prefix, MiddlewareInterface $middleware)
{
if (! is_string($prefix)) {
throw new Exception\InvalidArgumentException(sprintf(
'$prefix argument to %s must be a string; received %s',
__CLASS__,
is_object($prefix) ? get_class($prefix) : gettype($prefix)
));
}
$this->prefix = $this->normalizePrefix($prefix);
$this->middleware = $middleware;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler)
{
$path = $request->getUri()->getPath();
$path = $path ?: '/';

// Current path is shorter than decorator path
if (strlen($path) < strlen($this->prefix)) {
return $handler->{HANDLER_METHOD}($request);
}

// Current path does not match decorator path
if (substr(strtolower($path), 0, strlen($this->prefix)) !== strtolower($this->prefix)) {
return $handler->{HANDLER_METHOD}($request);
}

// Skip if match is not at a border ('/', '.', or end)
$border = $this->getBorder($path);
if ($border && '/' !== $border && '.' !== $border) {
return $handler->{HANDLER_METHOD}($request);
}

// Trim off the part of the url that matches the prefix if it is non-empty
$requestToProcess = (! empty($this->prefix) && $this->prefix !== '/')
? $this->prepareRequestWithTruncatedPrefix($request)
: $request;

// Process our middleware.
// If the middleware calls on the handler, the handler should be provided
// the original request, as this indicates we've left the path-segregated
// layer.
return $this->middleware->process(
$requestToProcess,
new PathRequestHandlerDecorator($handler, $request)
);
}

/**
* @param string $path
* @return string
*/
private function getBorder($path)
{
if ($this->prefix === '/') {
return '/';
}

$length = strlen($this->prefix);
return strlen($path) > $length ? $path[$length] : '';
}

/**
* @return ServerRequestInterface
*/
private function prepareRequestWithTruncatedPrefix(ServerRequestInterface $request)
{
$uri = $request->getUri();
$path = $this->getTruncatedPath($this->prefix, $uri->getPath());
$new = $uri->withPath($path);
return $request->withUri($new);
}

/**
* @param string $segment
* @param string $path
* @return string
*/
private function getTruncatedPath($segment, $path)
{
if ($segment === $path) {
// Decorated path and current path are the same; return empty string
return '';
}

$length = strlen($segment);
if (strlen($path) > $length) {
// Strip decorated path from start of current path
return substr($path, $length);
}

if ('/' === substr($segment, -1)) {
// Re-try by submitting with / stripped from end of segment
return $this->getTruncatedPath(rtrim($segment, '/'), $path);
}

// Segment is longer than path; this is a problem.
throw Exception\PathOutOfSyncException::forPath($this->prefix, $path);
}

/**
* Ensures that the right-most slash is trimmed for prefixes of more than
* one character, and that the prefix begins with a slash.
*
* @param string $prefix
* @return string
*/
private function normalizePrefix($prefix)
{
$prefix = strlen($prefix) > 1 ? rtrim($prefix, '/') : $prefix;
if ('/' !== substr($prefix, 0, 1)) {
$prefix = '/' . $prefix;
}
return $prefix;
}
}
Loading

0 comments on commit f8d2edd

Please sign in to comment.