Skip to content

Commit

Permalink
Merge pull request #2787 from adriansuter/patch-callable-resolver
Browse files Browse the repository at this point in the history
4.x - Add an advanced callable resolver
  • Loading branch information
l0gicgate committed Aug 11, 2019
2 parents b8d2006 + 1386f93 commit 88187e7
Show file tree
Hide file tree
Showing 15 changed files with 880 additions and 164 deletions.
2 changes: 1 addition & 1 deletion Slim/App.php
Expand Up @@ -68,7 +68,7 @@ public function __construct(
$this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector);
$routeRunner = new RouteRunner($this->routeResolver, $this->routeCollector->getRouteParser());

$this->middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $container);
$this->middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $this->callableResolver, $container);
}

/**
Expand Down
128 changes: 95 additions & 33 deletions Slim/CallableResolver.php
Expand Up @@ -11,15 +11,12 @@

use Closure;
use Psr\Container\ContainerInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Slim\Interfaces\CallableResolverInterface;
use Slim\Interfaces\AdvancedCallableResolverInterface;

/**
* This class resolves a string of the format 'class:method' into a closure
* that can be dispatched.
*/
final class CallableResolver implements CallableResolverInterface
final class CallableResolver implements AdvancedCallableResolverInterface
{
/**
* @var string
Expand All @@ -40,40 +37,31 @@ public function __construct(?ContainerInterface $container = null)
}

/**
* Resolve toResolve into a callable 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
* @return callable
*
* @throws RuntimeException if the callable does not exist
* @throws RuntimeException if the callable is not resolvable
* {@inheritdoc}
*/
public function resolve($toResolve): callable
{
$resolved = $toResolve;

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

// Check for Slim callable as `class:method`
if (preg_match(self::$callablePattern, $toResolve, $matches)) {
$class = $matches[1];
$method = $matches[2];
$resolved = $this->resolveInstanceAndMethod($toResolve);
if ($resolved[1] === null) {
$resolved[1] = '__invoke';
}
}

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

/**
* {@inheritdoc}
*/
public function resolveRoute($toResolve): callable
{
$resolved = $toResolve;

if (!is_callable($toResolve) && is_string($toResolve)) {
[$instance, $method] = $this->resolveInstanceAndMethod($toResolve);

// For a class that implements RequestHandlerInterface, we will call handle()
// if no method has been specified explicitly
Expand All @@ -88,13 +76,87 @@ public function resolve($toResolve): callable
$resolved = [$resolved, 'handle'];
}

return $this->assertCallableAndBindClosureToContainer($resolved, $toResolve);
}

/**
* {@inheritdoc}
*/
public function resolveMiddleware($toResolve): callable
{
$resolved = $toResolve;

if (!is_callable($toResolve) && is_string($toResolve)) {
[$instance, $method] = $this->resolveInstanceAndMethod($toResolve);

// For a class that implements MiddlewareInterface, we will call process()
// if no method has been specified explicitly
if ($instance instanceof MiddlewareInterface && $method === null) {
$method = 'process';
}

$resolved = [$instance, $method ?? '__invoke'];
}

if ($resolved instanceof MiddlewareInterface) {
$resolved = [$resolved, 'process'];
}

return $this->assertCallableAndBindClosureToContainer($resolved, $toResolve);
}

/**
* Resolves the given param and if successful returns an instance as well
* as a method name.
*
* @param string $toResolve
*
* @return array [Instance, Method Name]
*/
private function resolveInstanceAndMethod(string $toResolve): array
{
$class = $toResolve;
$instance = null;
$method = null;

// Check for Slim callable as `class:method`
if (preg_match(CallableResolver::$callablePattern, $toResolve, $matches)) {
$class = $matches[1];
$method = $matches[2];
}

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

return [$instance, $method];
}

/**
* @param mixed $resolved
* @param string|object|array|callable $toResolve
*
* @return callable
*/
private function assertCallableAndBindClosureToContainer($resolved, $toResolve): callable
{
if (!is_callable($resolved)) {
throw new RuntimeException(sprintf(
'%s is not resolvable',
is_array($toResolve) || is_object($toResolve) ? json_encode($toResolve) : $toResolve
is_callable($toResolve) || is_object($toResolve) || is_array($toResolve) ?
json_encode($toResolve) : $toResolve
));
}

if (is_array($resolved) && $resolved[0] instanceof Closure) {
$resolved = $resolved[0];
}

if ($this->container && $resolved instanceof Closure) {
$resolved = $resolved->bindTo($this->container);
}
Expand Down
31 changes: 31 additions & 0 deletions Slim/Interfaces/AdvancedCallableResolverInterface.php
@@ -0,0 +1,31 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
*/

declare(strict_types=1);

namespace Slim\Interfaces;

interface AdvancedCallableResolverInterface extends CallableResolverInterface
{
/**
* Resolve $toResolve into a callable
*
* @param string|callable $toResolve
*
* @return callable
*/
public function resolveRoute($toResolve): callable;

/**
* Resolve $toResolve into a callable
*
* @param string|callable $toResolve
*
* @return callable
*/
public function resolveMiddleware($toResolve): callable;
}
97 changes: 66 additions & 31 deletions Slim/MiddlewareDispatcher.php
Expand Up @@ -16,6 +16,8 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Slim\Interfaces\AdvancedCallableResolverInterface;
use Slim\Interfaces\CallableResolverInterface;

class MiddlewareDispatcher implements RequestHandlerInterface
{
Expand All @@ -26,20 +28,28 @@ class MiddlewareDispatcher implements RequestHandlerInterface
*/
protected $tip;

/**
* @var CallableResolverInterface
*/
protected $callableResolver;

/**
* @var ContainerInterface|null
*/
protected $container;

/**
* @param RequestHandlerInterface $kernel
* @param ContainerInterface|null $container
* @param RequestHandlerInterface $kernel
* @param CallableResolverInterface $callableResolver
* @param ContainerInterface|null $container
*/
public function __construct(
RequestHandlerInterface $kernel,
CallableResolverInterface $callableResolver,
?ContainerInterface $container = null
) {
$this->seedMiddlewareStack($kernel);
$this->callableResolver = $callableResolver;
$this->container = $container;
}

Expand Down Expand Up @@ -141,53 +151,82 @@ public function handle(ServerRequestInterface $request): ResponseInterface
public function addDeferred(string $middleware): self
{
$next = $this->tip;
$this->tip = new class($middleware, $next, $this->container) implements RequestHandlerInterface
$this->tip = new class(
$middleware,
$next,
$this->container,
$this->callableResolver
) implements RequestHandlerInterface
{
private $middleware;
private $next;
private $container;
private $callableResolver;

public function __construct(
string $middleware,
RequestHandlerInterface $next,
?ContainerInterface $container = null
?ContainerInterface $container = null,
?CallableResolverInterface $callableResolver = null
) {
$this->middleware = $middleware;
$this->next = $next;
$this->container = $container;
$this->callableResolver = $callableResolver;
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$resolved = $this->middleware;
$instance = null;
$method = null;

// Check for Slim callable as `class:method`
if (preg_match(CallableResolver::$callablePattern, $resolved, $matches)) {
$resolved = $matches[1];
$method = $matches[2];
if ($this->callableResolver instanceof AdvancedCallableResolverInterface) {
$callable = $this->callableResolver->resolveMiddleware($this->middleware);
return $callable($request, $this->next);
}

if ($this->container && $this->container->has($resolved)) {
$instance = $this->container->get($resolved);
if ($instance instanceof MiddlewareInterface) {
return $instance->process($request, $this->next);
}
} elseif (!function_exists($resolved)) {
if (!class_exists($resolved)) {
throw new RuntimeException(sprintf('Middleware %s does not exist', $resolved));
$callable = null;

if ($this->callableResolver instanceof CallableResolverInterface) {
try {
$callable = $this->callableResolver->resolve($this->middleware);
} catch (RuntimeException $e) {
// Do Nothing
}
$instance = new $resolved($this->container);
}

if ($instance && $instance instanceof MiddlewareInterface) {
return $instance->process($request, $this->next);
}
if (!$callable) {
$resolved = $this->middleware;
$instance = null;
$method = null;

// Check for Slim callable as `class:method`
if (preg_match(CallableResolver::$callablePattern, $resolved, $matches)) {
$resolved = $matches[1];
$method = $matches[2];
}

$callable = $instance ?? $resolved;
if ($instance && $method) {
$callable = [$instance, $method];
if ($this->container && $this->container->has($resolved)) {
$instance = $this->container->get($resolved);
if ($instance instanceof MiddlewareInterface) {
return $instance->process($request, $this->next);
}
} elseif (!function_exists($resolved)) {
if (!class_exists($resolved)) {
throw new RuntimeException(sprintf('Middleware %s does not exist', $resolved));
}
$instance = new $resolved($this->container);
}

if ($instance && $instance instanceof MiddlewareInterface) {
return $instance->process($request, $this->next);
}

$callable = $instance ?? $resolved;
if ($instance && $method) {
$callable = [$instance, $method];
}

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

if (!is_callable($callable)) {
Expand All @@ -197,10 +236,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface
));
}

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

return $callable($request, $this->next);
}
};
Expand Down

0 comments on commit 88187e7

Please sign in to comment.