Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PSR-15 middlewares #2379

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -22,7 +22,7 @@ This will install Slim and all required dependencies. Slim requires PHP 7.1 or n
Before you can get up and running with Slim you will need to choose a PSR-7 implementation that best fits your application. A few notable ones:
- [Nyholm/psr7](https://github.com/Nyholm/psr7) - This is the fastest, strictest and most lightweight implementation at the moment
- [Guzzle/psr7](https://github.com/guzzle/psr7) - This is the implementation used by the Guzzle Client. It is not as strict but adds some nice functionality for Streams and file handling. It is the second fastest implementation but is a bit bulkier
- [zend-diactoros](https://github.com/zendframework/zend-diactoros) - This is the Zend implementation. It is the slowest implementation of the 3.
- [zend-diactoros](https://github.com/zendframework/zend-diactoros) - This is the Zend implementation. It is the slowest implementation of the 3.
- [Slim-Psr7](https://github.com/slimphp/Slim-Psr7) - This is the Slim Framework projects PSR-7 implementation.


Expand Down
69 changes: 69 additions & 0 deletions Slim/Adapter/PsrMiddleware.php
@@ -0,0 +1,69 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @link https://github.com/slimphp/Slim
* @copyright Copyright (c) 2011-2018 Josh Lockhart
* @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License)
*/

declare(strict_types=1);

namespace Slim\Adapter;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* This class wraps a PSR-15 style single pass middleware,
* into an invokable double pass middleware.
*
* This is an internal class. This class is an implementation detail
* and is used only inside of the Slim application; it is not visible
* to—and should not be used by—end users.
*
* @link https://github.com/php-fig/http-server-middleware/blob/master/src/MiddlewareInterface.php
* @link https://github.com/php-fig/http-server-handler/blob/master/src/RequestHandlerInterface.php
*/
final class PsrMiddleware implements RequestHandlerInterface
{
/**
* @var MiddlewareInterface
*/
private $middleware;

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

/**
* @var callable
*/
private $next;

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

public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
): ResponseInterface {
$this->response = $response;
$this->next = $next;

/* Call the PSR-15 middleware and let it return to our handle()
* method by passing `$this` as RequestHandler. */
return $this->middleware->process($request, $this);
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return ($this->next)($request, $this->response);
}
}
7 changes: 4 additions & 3 deletions Slim/App.php
Expand Up @@ -16,6 +16,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
Expand Down Expand Up @@ -119,14 +120,14 @@ public function setContainer(ContainerInterface $container)
*
* This method prepends new middleware to the app's middleware stack.
*
* @param callable|string $callable The callback routine
* @param MiddlewareInterface|callable|string $middleware The middleware routine
*
* @return static
*/
public function add($callable)
public function add($middleware)
{
return $this->addMiddleware(
new DeferredCallable($callable, $this->getCallableResolver())
new DeferredCallable($middleware, $this->getCallableResolver(), true)
);
}

Expand Down
49 changes: 33 additions & 16 deletions Slim/CallableResolver.php
Expand Up @@ -12,8 +12,10 @@
namespace Slim;

use Psr\Container\ContainerInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Slim\Adapter\PsrMiddleware;
use Slim\Interfaces\CallableResolverInterface;

/**
Expand All @@ -36,25 +38,41 @@ public function __construct(ContainerInterface $container = null)
}

/**
* Resolve toResolve into a closure that that the router can dispatch.
* Resolve toResolve into a callable that that the router can dispatch.
*
* If toResolve is of the format 'class:method', then try to extract 'class'
* from the container otherwise instantiate it and then dispatch 'method'.
*
* @param mixed $toResolve
* @param bool $resolveMiddleware
*
* @return callable
*
* @throws RuntimeException if the callable does not exist
* @throws RuntimeException if the callable is not resolvable
*/
public function resolve($toResolve): callable
public function resolve($toResolve, $resolveMiddleware = false): callable
{
$resolved = $toResolve;
if ($resolveMiddleware && $toResolve instanceof MiddlewareInterface) {
return new PsrMiddleware($toResolve);
}

if ($toResolve instanceof RequestHandlerInterface) {
return [$toResolve, 'handle'];
}

if (is_callable($toResolve)) {
if ($toResolve instanceof \Closure && $this->container instanceof ContainerInterface) {
return $toResolve->bindTo($this->container);
}
return $toResolve;
}

if (!is_callable($toResolve) && is_string($toResolve)) {
$resolved = $toResolve;
if (is_string($toResolve)) {
$class = $toResolve;
$method = '__invoke';
$instance = null;
$method = null;

// check for slim callable as "class:method"
$callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
Expand All @@ -64,22 +82,25 @@ public function resolve($toResolve): callable
}

if ($this->container instanceof ContainerInterface && $this->container->has($class)) {
$resolved = [$this->container->get($class), $method];
$instance = $this->container->get($class);
} else {
if (!class_exists($class)) {
throw new RuntimeException(sprintf('Callable %s does not exist', $class));
}
$resolved = [new $class($this->container), $method];
$instance = new $class($this->container);
}

if ($resolveMiddleware && $instance instanceof MiddlewareInterface && $method === null) {
return new PsrMiddleware($instance);
}

// For a class that implements RequestHandlerInterface, we will call handle()
if ($resolved[0] instanceof RequestHandlerInterface) {
$resolved[1] = 'handle';
// if no method has been specified
if ($instance instanceof RequestHandlerInterface && $method === null) {
return [$instance, 'handle'];
}
}

if ($resolved instanceof RequestHandlerInterface) {
$resolved = [$resolved, 'handle'];
$resolved = [$instance, $method ?? '__invoke'];
}

if (!is_callable($resolved)) {
Expand All @@ -89,10 +110,6 @@ public function resolve($toResolve): callable
));
}

if ($this->container instanceof ContainerInterface && $resolved instanceof \Closure) {
$resolved = $resolved->bindTo($this->container);
}

return $resolved;
}
}
21 changes: 14 additions & 7 deletions Slim/DeferredCallable.php
Expand Up @@ -11,12 +11,13 @@

namespace Slim;

use Psr\Http\Server\MiddlewareInterface;
use Slim\Interfaces\CallableResolverInterface;

class DeferredCallable
{
/**
* @var callable|string
* @var MiddlewareInterface|callable|string
*/
protected $callable;

Expand All @@ -25,27 +26,33 @@ class DeferredCallable
*/
protected $callableResolver;

/**
* @var bool
*/
protected $resolveMiddleware;

/**
* DeferredMiddleware constructor.
*
* @param callable|string $callable
* @param MiddlewareInterface|callable|string $callable
* @param CallableResolverInterface|null $resolver
* @param bool $resolveMiddleware
*/
public function __construct($callable, CallableResolverInterface $resolver = null)
public function __construct($callable, CallableResolverInterface $resolver = null, $resolveMiddleware = false)
{
$this->callable = $callable;
$this->callableResolver = $resolver;
$this->resolveMiddleware = $resolveMiddleware;
}

public function __invoke()
public function __invoke(...$args)
{
/** @var callable $callable */
$callable = $this->callable;
if ($this->callableResolver) {
$callable = $this->callableResolver->resolve($callable);
$callable = $this->callableResolver->resolve($callable, $this->resolveMiddleware);
}
$args = func_get_args();

return call_user_func_array($callable, $args);
return $callable(...$args);
}
}
2 changes: 1 addition & 1 deletion Slim/Handlers/Strategies/RequestResponse.php
Expand Up @@ -41,6 +41,6 @@ public function __invoke(
$request = $request->withAttribute($k, $v);
}

return call_user_func($callable, $request, $response, $routeArguments);
return $callable($request, $response, $routeArguments);
}
}
4 changes: 1 addition & 3 deletions Slim/Handlers/Strategies/RequestResponseArgs.php
Expand Up @@ -38,8 +38,6 @@ public function __invoke(
ResponseInterface $response,
array $routeArguments
): ResponseInterface {
array_unshift($routeArguments, $request, $response);

return call_user_func_array($callable, $routeArguments);
return $callable($request, $response, ...array_values($routeArguments));
}
}
5 changes: 3 additions & 2 deletions Slim/Interfaces/CallableResolverInterface.php
Expand Up @@ -20,11 +20,12 @@
interface CallableResolverInterface
{
/**
* Invoke the resolved callable.
* Resolve $toResolve into a callable
*
* @param mixed $toResolve
* @param bool $resolveMiddleware
*
* @return callable
*/
public function resolve($toResolve): callable;
public function resolve($toResolve, $resolveMiddleware = false): callable;
}
2 changes: 1 addition & 1 deletion Slim/MiddlewareAwareTrait.php
Expand Up @@ -70,7 +70,7 @@ protected function addMiddleware(callable $callable): self
$callable,
$next
) {
$result = call_user_func($callable, $request, $response, $next);
$result = $callable($request, $response, $next);
if ($result instanceof ResponseInterface === false) {
throw new UnexpectedValueException(
'Middleware must return instance of \Psr\Http\Message\ResponseInterface'
Expand Down
7 changes: 4 additions & 3 deletions Slim/Routable.php
Expand Up @@ -11,6 +11,7 @@

namespace Slim;

use Psr\Http\Server\MiddlewareInterface;
use Slim\Interfaces\CallableResolverInterface;

/**
Expand Down Expand Up @@ -90,13 +91,13 @@ public function getCallableResolver()
/**
* Prepend middleware to the middleware collection
*
* @param callable|string $callable The callback routine
* @param MiddlewareInterface|callable|string $middleware The middleware routine

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you imported this interface within this class file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that one, will fix.

*
* @return static
*/
public function add($callable)
public function add($middleware)
{
$this->middleware[] = new DeferredCallable($callable, $this->callableResolver);
$this->middleware[] = new DeferredCallable($middleware, $this->callableResolver, true);
return $this;
}

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -34,7 +34,8 @@
"psr/container": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0"
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"ext-simplexml": "*",
Expand Down
21 changes: 21 additions & 0 deletions tests/AppTest.php
Expand Up @@ -20,6 +20,7 @@
use Slim\Handlers\Strategies\RequestResponseArgs;
use Slim\Router;
use Slim\Tests\Mocks\MockAction;
use Slim\Tests\Mocks\Psr15MiddlewareTest;

class AppTest extends TestCase
{
Expand Down Expand Up @@ -974,6 +975,26 @@ public function testAddMiddleware()
$this->assertSame($called, 1);
}

public function testAddPsr15Middleware()
{
$responseFactory = $this->getResponseFactory();
$app = new App($responseFactory);
$method = new \ReflectionMethod('Slim\App', 'seedMiddlewareStack');
$method->setAccessible(true);
$method->invoke($app, function ($req, $res) {
return $res;
});

$app->add(new Psr15MiddlewareTest);

$app->callMiddlewareStack(
$this->getMockBuilder('Psr\Http\Message\ServerRequestInterface')->disableOriginalConstructor()->getMock(),
$this->getMockBuilder('Psr\Http\Message\ResponseInterface')->disableOriginalConstructor()->getMock()
);

$this->assertSame(Psr15MiddlewareTest::$CalledCount, 1);
}

public function testAddMiddlewareOnRoute()
{
$responseFactory = $this->getResponseFactory();
Expand Down