File size: 5.68Kb
<?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;
}
}