194 lines
5.7 KiB
PHP
194 lines
5.7 KiB
PHP
<?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;
|
|
|
|
use Closure;
|
|
use Psr\Container\ContainerInterface;
|
|
use Psr\Http\Server\MiddlewareInterface;
|
|
use Psr\Http\Server\RequestHandlerInterface;
|
|
use RuntimeException;
|
|
use Slim\Interfaces\AdvancedCallableResolverInterface;
|
|
|
|
use function class_exists;
|
|
use function is_array;
|
|
use function is_callable;
|
|
use function is_object;
|
|
use function is_string;
|
|
use function json_encode;
|
|
use function preg_match;
|
|
use function sprintf;
|
|
|
|
final class CallableResolver implements AdvancedCallableResolverInterface
|
|
{
|
|
public static string $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
|
|
|
|
private ?ContainerInterface $container;
|
|
|
|
public function __construct(?ContainerInterface $container = null)
|
|
{
|
|
$this->container = $container;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function resolve($toResolve): callable
|
|
{
|
|
$toResolve = $this->prepareToResolve($toResolve);
|
|
if (is_callable($toResolve)) {
|
|
return $this->bindToContainer($toResolve);
|
|
}
|
|
$resolved = $toResolve;
|
|
if (is_string($toResolve)) {
|
|
$resolved = $this->resolveSlimNotation($toResolve);
|
|
$resolved[1] ??= '__invoke';
|
|
}
|
|
$callable = $this->assertCallable($resolved, $toResolve);
|
|
return $this->bindToContainer($callable);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function resolveRoute($toResolve): callable
|
|
{
|
|
return $this->resolveByPredicate($toResolve, [$this, 'isRoute'], 'handle');
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function resolveMiddleware($toResolve): callable
|
|
{
|
|
return $this->resolveByPredicate($toResolve, [$this, 'isMiddleware'], 'process');
|
|
}
|
|
|
|
/**
|
|
* @param string|callable $toResolve
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
private function resolveByPredicate($toResolve, callable $predicate, string $defaultMethod): callable
|
|
{
|
|
$toResolve = $this->prepareToResolve($toResolve);
|
|
if (is_callable($toResolve)) {
|
|
return $this->bindToContainer($toResolve);
|
|
}
|
|
$resolved = $toResolve;
|
|
if ($predicate($toResolve)) {
|
|
$resolved = [$toResolve, $defaultMethod];
|
|
}
|
|
if (is_string($toResolve)) {
|
|
[$instance, $method] = $this->resolveSlimNotation($toResolve);
|
|
if ($method === null && $predicate($instance)) {
|
|
$method = $defaultMethod;
|
|
}
|
|
$resolved = [$instance, $method ?? '__invoke'];
|
|
}
|
|
$callable = $this->assertCallable($resolved, $toResolve);
|
|
return $this->bindToContainer($callable);
|
|
}
|
|
|
|
/**
|
|
* @param mixed $toResolve
|
|
*/
|
|
private function isRoute($toResolve): bool
|
|
{
|
|
return $toResolve instanceof RequestHandlerInterface;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $toResolve
|
|
*/
|
|
private function isMiddleware($toResolve): bool
|
|
{
|
|
return $toResolve instanceof MiddlewareInterface;
|
|
}
|
|
|
|
/**
|
|
* @throws RuntimeException
|
|
*
|
|
* @return array{object, string|null} [Instance, Method Name]
|
|
*/
|
|
private function resolveSlimNotation(string $toResolve): array
|
|
{
|
|
preg_match(CallableResolver::$callablePattern, $toResolve, $matches);
|
|
[$class, $method] = $matches ? [$matches[1], $matches[2]] : [$toResolve, null];
|
|
|
|
/** @var string $class */
|
|
/** @var string|null $method */
|
|
if ($this->container && $this->container->has($class)) {
|
|
$instance = $this->container->get($class);
|
|
if (!is_object($instance)) {
|
|
throw new RuntimeException(sprintf('%s container entry is not an object', $class));
|
|
}
|
|
} else {
|
|
if (!class_exists($class)) {
|
|
if ($method) {
|
|
$class .= '::' . $method . '()';
|
|
}
|
|
throw new RuntimeException(sprintf('Callable %s does not exist', $class));
|
|
}
|
|
$instance = new $class($this->container);
|
|
}
|
|
return [$instance, $method];
|
|
}
|
|
|
|
/**
|
|
* @param mixed $resolved
|
|
* @param mixed $toResolve
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
private function assertCallable($resolved, $toResolve): callable
|
|
{
|
|
if (!is_callable($resolved)) {
|
|
if (is_callable($toResolve) || is_object($toResolve) || is_array($toResolve)) {
|
|
$formatedToResolve = ($toResolveJson = json_encode($toResolve)) !== false ? $toResolveJson : '';
|
|
} else {
|
|
$formatedToResolve = is_string($toResolve) ? $toResolve : '';
|
|
}
|
|
throw new RuntimeException(sprintf('%s is not resolvable', $formatedToResolve));
|
|
}
|
|
return $resolved;
|
|
}
|
|
|
|
private function bindToContainer(callable $callable): callable
|
|
{
|
|
if (is_array($callable) && $callable[0] instanceof Closure) {
|
|
$callable = $callable[0];
|
|
}
|
|
if ($this->container && $callable instanceof Closure) {
|
|
/** @var Closure $callable */
|
|
$callable = $callable->bindTo($this->container);
|
|
}
|
|
return $callable;
|
|
}
|
|
|
|
/**
|
|
* @param string|callable $toResolve
|
|
* @return string|callable
|
|
*/
|
|
private function prepareToResolve($toResolve)
|
|
{
|
|
if (!is_array($toResolve)) {
|
|
return $toResolve;
|
|
}
|
|
$candidate = $toResolve;
|
|
$class = array_shift($candidate);
|
|
$method = array_shift($candidate);
|
|
if (is_string($class) && is_string($method)) {
|
|
return $class . ':' . $method;
|
|
}
|
|
return $toResolve;
|
|
}
|
|
}
|