first commit

This commit is contained in:
2026-03-24 00:31:47 +01:00
commit 2506f6f9c7
3328 changed files with 1172155 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Matthieu Napoli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,39 @@
{
"name": "php-di\/invoker",
"description": "Generic and extensible callable invoker",
"keywords": [
"invoker",
"dependency-injection",
"dependency",
"injection",
"callable",
"invoke"
],
"homepage": "https:\/\/github.com\/PHP-DI\/Invoker",
"license": "MIT",
"type": "library",
"autoload": {
"psr-4": {
"ElementorProDeps\\Invoker\\": "src\/"
}
},
"autoload-dev": {
"psr-4": {
"ElementorProDeps\\Invoker\\Test\\": "tests\/"
}
},
"require": {
"php": ">=7.3",
"psr\/container": "^1.0|^2.0"
},
"require-dev": {
"phpunit\/phpunit": "^9.0",
"athletic\/athletic": "~0.1.8",
"mnapoli\/hard-mode": "~0.3.0"
},
"config": {
"allow-plugins": {
"dealerdirect\/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker;
use Closure;
use ElementorProDeps\Invoker\Exception\NotCallableException;
use ElementorProDeps\Psr\Container\ContainerInterface;
use ElementorProDeps\Psr\Container\NotFoundExceptionInterface;
use ReflectionException;
use ReflectionMethod;
/**
* Resolves a callable from a container.
*/
class CallableResolver
{
/** @var ContainerInterface */
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Resolve the given callable into a real PHP callable.
*
* @param callable|string|array $callable
* @return callable Real PHP callable.
* @throws NotCallableException|ReflectionException
*/
public function resolve($callable) : callable
{
if (\is_string($callable) && \strpos($callable, '::') !== \false) {
$callable = \explode('::', $callable, 2);
}
$callable = $this->resolveFromContainer($callable);
if (!\is_callable($callable)) {
throw NotCallableException::fromInvalidCallable($callable, \true);
}
return $callable;
}
/**
* @param callable|string|array $callable
* @return callable|mixed
* @throws NotCallableException|ReflectionException
*/
private function resolveFromContainer($callable)
{
// Shortcut for a very common use case
if ($callable instanceof Closure) {
return $callable;
}
// If it's already a callable there is nothing to do
if (\is_callable($callable)) {
// TODO with PHP 8 that should not be necessary to check this anymore
if (!$this->isStaticCallToNonStaticMethod($callable)) {
return $callable;
}
}
// The callable is a container entry name
if (\is_string($callable)) {
try {
return $this->container->get($callable);
} catch (NotFoundExceptionInterface $e) {
if ($this->container->has($callable)) {
throw $e;
}
throw NotCallableException::fromInvalidCallable($callable, \true);
}
}
// The callable is an array whose first item is a container entry name
// e.g. ['some-container-entry', 'methodToCall']
if (\is_array($callable) && \is_string($callable[0])) {
try {
// Replace the container entry name by the actual object
$callable[0] = $this->container->get($callable[0]);
return $callable;
} catch (NotFoundExceptionInterface $e) {
if ($this->container->has($callable[0])) {
throw $e;
}
throw new NotCallableException(\sprintf('Cannot call %s() on %s because it is not a class nor a valid container entry', $callable[1], $callable[0]));
}
}
// Unrecognized stuff, we let it fail later
return $callable;
}
/**
* Check if the callable represents a static call to a non-static method.
*
* @param mixed $callable
* @throws ReflectionException
*/
private function isStaticCallToNonStaticMethod($callable) : bool
{
if (\is_array($callable) && \is_string($callable[0])) {
[$class, $method] = $callable;
if (!\method_exists($class, $method)) {
return \false;
}
$reflection = new ReflectionMethod($class, $method);
return !$reflection->isStatic();
}
return \false;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\Exception;
/**
* Impossible to invoke the callable.
*/
class InvocationException extends \Exception
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\Exception;
/**
* The given callable is not actually callable.
*/
class NotCallableException extends InvocationException
{
/**
* @param mixed $value
*/
public static function fromInvalidCallable($value, bool $containerEntry = \false) : self
{
if (\is_object($value)) {
$message = \sprintf('Instance of %s is not a callable', \get_class($value));
} elseif (\is_array($value) && isset($value[0], $value[1])) {
$class = \is_object($value[0]) ? \get_class($value[0]) : $value[0];
$extra = \method_exists($class, '__call') || \method_exists($class, '__callStatic') ? ' A __call() or __callStatic() method exists but magic methods are not supported.' : '';
$message = \sprintf('%s::%s() is not a callable.%s', $class, $value[1], $extra);
} elseif ($containerEntry) {
$message = \var_export($value, \true) . ' is neither a callable nor a valid container entry';
} else {
$message = \var_export($value, \true) . ' is not a callable';
}
return new self($message);
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\Exception;
/**
* Not enough parameters could be resolved to invoke the callable.
*/
class NotEnoughParametersException extends InvocationException
{
}

View File

@@ -0,0 +1,83 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker;
use ElementorProDeps\Invoker\Exception\NotCallableException;
use ElementorProDeps\Invoker\Exception\NotEnoughParametersException;
use ElementorProDeps\Invoker\ParameterResolver\AssociativeArrayResolver;
use ElementorProDeps\Invoker\ParameterResolver\DefaultValueResolver;
use ElementorProDeps\Invoker\ParameterResolver\NumericArrayResolver;
use ElementorProDeps\Invoker\ParameterResolver\ParameterResolver;
use ElementorProDeps\Invoker\ParameterResolver\ResolverChain;
use ElementorProDeps\Invoker\Reflection\CallableReflection;
use ElementorProDeps\Psr\Container\ContainerInterface;
use ReflectionParameter;
/**
* Invoke a callable.
*/
class Invoker implements InvokerInterface
{
/** @var CallableResolver|null */
private $callableResolver;
/** @var ParameterResolver */
private $parameterResolver;
/** @var ContainerInterface|null */
private $container;
public function __construct(?ParameterResolver $parameterResolver = null, ?ContainerInterface $container = null)
{
$this->parameterResolver = $parameterResolver ?: $this->createParameterResolver();
$this->container = $container;
if ($container) {
$this->callableResolver = new CallableResolver($container);
}
}
/**
* {@inheritdoc}
*/
public function call($callable, array $parameters = [])
{
if ($this->callableResolver) {
$callable = $this->callableResolver->resolve($callable);
}
if (!\is_callable($callable)) {
throw new NotCallableException(\sprintf('%s is not a callable', \is_object($callable) ? 'Instance of ' . \get_class($callable) : \var_export($callable, \true)));
}
$callableReflection = CallableReflection::create($callable);
$args = $this->parameterResolver->getParameters($callableReflection, $parameters, []);
// Sort by array key because call_user_func_array ignores numeric keys
\ksort($args);
// Check all parameters are resolved
$diff = \array_diff_key($callableReflection->getParameters(), $args);
$parameter = \reset($diff);
if ($parameter && \assert($parameter instanceof ReflectionParameter) && !$parameter->isVariadic()) {
throw new NotEnoughParametersException(\sprintf('Unable to invoke the callable because no value was given for parameter %d ($%s)', $parameter->getPosition() + 1, $parameter->name));
}
return \call_user_func_array($callable, $args);
}
/**
* Create the default parameter resolver.
*/
private function createParameterResolver() : ParameterResolver
{
return new ResolverChain([new NumericArrayResolver(), new AssociativeArrayResolver(), new DefaultValueResolver()]);
}
/**
* @return ParameterResolver By default it's a ResolverChain
*/
public function getParameterResolver() : ParameterResolver
{
return $this->parameterResolver;
}
public function getContainer() : ?ContainerInterface
{
return $this->container;
}
/**
* @return CallableResolver|null Returns null if no container was given in the constructor.
*/
public function getCallableResolver() : ?CallableResolver
{
return $this->callableResolver;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker;
use ElementorProDeps\Invoker\Exception\InvocationException;
use ElementorProDeps\Invoker\Exception\NotCallableException;
use ElementorProDeps\Invoker\Exception\NotEnoughParametersException;
/**
* Invoke a callable.
*/
interface InvokerInterface
{
/**
* Call the given function using the given parameters.
*
* @param callable|array|string $callable Function to call.
* @param array $parameters Parameters to use.
* @return mixed Result of the function.
* @throws InvocationException Base exception class for all the sub-exceptions below.
* @throws NotCallableException
* @throws NotEnoughParametersException
*/
public function call($callable, array $parameters = []);
}

View File

@@ -0,0 +1,31 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Tries to map an associative array (string-indexed) to the parameter names.
*
* E.g. `->call($callable, ['foo' => 'bar'])` will inject the string `'bar'`
* in the parameter named `$foo`.
*
* Parameters that are not indexed by a string are ignored.
*/
class AssociativeArrayResolver implements ParameterResolver
{
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$parameters = $reflection->getParameters();
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$parameters = \array_diff_key($parameters, $resolvedParameters);
}
foreach ($parameters as $index => $parameter) {
if (\array_key_exists($parameter->name, $providedParameters)) {
$resolvedParameters[$index] = $providedParameters[$parameter->name];
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver\Container;
use ElementorProDeps\Invoker\ParameterResolver\ParameterResolver;
use ElementorProDeps\Psr\Container\ContainerInterface;
use ReflectionFunctionAbstract;
/**
* Inject entries from a DI container using the parameter names.
*/
class ParameterNameContainerResolver implements ParameterResolver
{
/** @var ContainerInterface */
private $container;
/**
* @param ContainerInterface $container The container to get entries from.
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$parameters = $reflection->getParameters();
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$parameters = \array_diff_key($parameters, $resolvedParameters);
}
foreach ($parameters as $index => $parameter) {
$name = $parameter->name;
if ($name && $this->container->has($name)) {
$resolvedParameters[$index] = $this->container->get($name);
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver\Container;
use ElementorProDeps\Invoker\ParameterResolver\ParameterResolver;
use ElementorProDeps\Psr\Container\ContainerInterface;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
/**
* Inject entries from a DI container using the type-hints.
*/
class TypeHintContainerResolver implements ParameterResolver
{
/** @var ContainerInterface */
private $container;
/**
* @param ContainerInterface $container The container to get entries from.
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$parameters = $reflection->getParameters();
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$parameters = \array_diff_key($parameters, $resolvedParameters);
}
foreach ($parameters as $index => $parameter) {
$parameterType = $parameter->getType();
if (!$parameterType) {
// No type
continue;
}
if (!$parameterType instanceof ReflectionNamedType) {
// Union types are not supported
continue;
}
if ($parameterType->isBuiltin()) {
// Primitive types are not supported
continue;
}
$parameterClass = $parameterType->getName();
if ($parameterClass === 'self') {
$parameterClass = $parameter->getDeclaringClass()->getName();
}
if ($this->container->has($parameterClass)) {
$resolvedParameters[$index] = $this->container->get($parameterClass);
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionException;
use ReflectionFunctionAbstract;
/**
* Finds the default value for a parameter, *if it exists*.
*/
class DefaultValueResolver implements ParameterResolver
{
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$parameters = $reflection->getParameters();
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$parameters = \array_diff_key($parameters, $resolvedParameters);
}
foreach ($parameters as $index => $parameter) {
\assert($parameter instanceof \ReflectionParameter);
if ($parameter->isDefaultValueAvailable()) {
try {
$resolvedParameters[$index] = $parameter->getDefaultValue();
} catch (ReflectionException $e) {
// Can't get default values from PHP internal classes and functions
}
} else {
$parameterType = $parameter->getType();
if ($parameterType && $parameterType->allowsNull()) {
$resolvedParameters[$index] = null;
}
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Simply returns all the values of the $providedParameters array that are
* indexed by the parameter position (i.e. a number).
*
* E.g. `->call($callable, ['foo', 'bar'])` will simply resolve the parameters
* to `['foo', 'bar']`.
*
* Parameters that are not indexed by a number (i.e. parameter position)
* will be ignored.
*/
class NumericArrayResolver implements ParameterResolver
{
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$providedParameters = \array_diff_key($providedParameters, $resolvedParameters);
}
foreach ($providedParameters as $key => $value) {
if (\is_int($key)) {
$resolvedParameters[$key] = $value;
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Resolves the parameters to use to call the callable.
*/
interface ParameterResolver
{
/**
* Resolves the parameters to use to call the callable.
*
* `$resolvedParameters` contains parameters that have already been resolved.
*
* Each ParameterResolver must resolve parameters that are not already
* in `$resolvedParameters`. That allows to chain multiple ParameterResolver.
*
* @param ReflectionFunctionAbstract $reflection Reflection object for the callable.
* @param array $providedParameters Parameters provided by the caller.
* @param array $resolvedParameters Parameters resolved (indexed by parameter position).
* @return array
*/
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters);
}

View File

@@ -0,0 +1,47 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Dispatches the call to other resolvers until all parameters are resolved.
*
* Chain of responsibility pattern.
*/
class ResolverChain implements ParameterResolver
{
/** @var ParameterResolver[] */
private $resolvers;
public function __construct(array $resolvers = [])
{
$this->resolvers = $resolvers;
}
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$reflectionParameters = $reflection->getParameters();
foreach ($this->resolvers as $resolver) {
$resolvedParameters = $resolver->getParameters($reflection, $providedParameters, $resolvedParameters);
$diff = \array_diff_key($reflectionParameters, $resolvedParameters);
if (empty($diff)) {
// Stop traversing: all parameters are resolved
return $resolvedParameters;
}
}
return $resolvedParameters;
}
/**
* Push a parameter resolver after the ones already registered.
*/
public function appendResolver(ParameterResolver $resolver) : void
{
$this->resolvers[] = $resolver;
}
/**
* Insert a parameter resolver before the ones already registered.
*/
public function prependResolver(ParameterResolver $resolver) : void
{
\array_unshift($this->resolvers, $resolver);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
/**
* Inject entries using type-hints.
*
* Tries to match type-hints with the parameters provided.
*/
class TypeHintResolver implements ParameterResolver
{
public function getParameters(ReflectionFunctionAbstract $reflection, array $providedParameters, array $resolvedParameters) : array
{
$parameters = $reflection->getParameters();
// Skip parameters already resolved
if (!empty($resolvedParameters)) {
$parameters = \array_diff_key($parameters, $resolvedParameters);
}
foreach ($parameters as $index => $parameter) {
$parameterType = $parameter->getType();
if (!$parameterType) {
// No type
continue;
}
if (!$parameterType instanceof ReflectionNamedType) {
// Union types are not supported
continue;
}
if ($parameterType->isBuiltin()) {
// Primitive types are not supported
continue;
}
$parameterClass = $parameterType->getName();
if ($parameterClass === 'self') {
$parameterClass = $parameter->getDeclaringClass()->getName();
}
if (\array_key_exists($parameterClass, $providedParameters)) {
$resolvedParameters[$index] = $providedParameters[$parameterClass];
}
}
return $resolvedParameters;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare (strict_types=1);
namespace ElementorProDeps\Invoker\Reflection;
use Closure;
use ElementorProDeps\Invoker\Exception\NotCallableException;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
/**
* Create a reflection object from a callable or a callable-like.
*
* @internal
*/
class CallableReflection
{
/**
* @param callable|array|string $callable Can be a callable or a callable-like.
* @throws NotCallableException|ReflectionException
*/
public static function create($callable) : ReflectionFunctionAbstract
{
// Closure
if ($callable instanceof Closure) {
return new ReflectionFunction($callable);
}
// Array callable
if (\is_array($callable)) {
[$class, $method] = $callable;
if (!\method_exists($class, $method)) {
throw NotCallableException::fromInvalidCallable($callable);
}
return new ReflectionMethod($class, $method);
}
// Callable object (i.e. implementing __invoke())
if (\is_object($callable) && \method_exists($callable, '__invoke')) {
return new ReflectionMethod($callable, '__invoke');
}
// Standard function
if (\is_string($callable) && \function_exists($callable)) {
return new ReflectionFunction($callable);
}
throw new NotCallableException(\sprintf('%s is not a callable', \is_string($callable) ? $callable : 'Instance of ' . \get_class($callable)));
}
}