This commit is contained in:
2026-04-26 23:47:49 +02:00
parent 1b95f03d1e
commit b073e009d8
5288 changed files with 1112699 additions and 55536 deletions

View File

@@ -0,0 +1,106 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker;
use Closure;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\NotCallableException;
use SmashBalloon\YoutubeFeed\Vendor\Psr\Container\ContainerInterface;
use SmashBalloon\YoutubeFeed\Vendor\Psr\Container\NotFoundExceptionInterface;
use ReflectionException;
use ReflectionMethod;
/**
* Resolves a callable from a container.
* @internal
*/
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,12 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception;
/**
* Impossible to invoke the callable.
* @internal
*/
class InvocationException extends \Exception
{
}

View File

@@ -0,0 +1,30 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception;
/**
* The given callable is not actually callable.
* @internal
*/
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,12 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception;
/**
* Not enough parameters could be resolved to invoke the callable.
* @internal
*/
class NotEnoughParametersException extends InvocationException
{
}

View File

@@ -0,0 +1,84 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\NotCallableException;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\NotEnoughParametersException;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\AssociativeArrayResolver;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\DefaultValueResolver;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\NumericArrayResolver;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\ParameterResolver;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\ResolverChain;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Reflection\CallableReflection;
use SmashBalloon\YoutubeFeed\Vendor\Psr\Container\ContainerInterface;
use ReflectionParameter;
/**
* Invoke a callable.
* @internal
*/
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,26 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\InvocationException;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\NotCallableException;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\Exception\NotEnoughParametersException;
/**
* Invoke a callable.
* @internal
*/
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,32 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\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.
* @internal
*/
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,39 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\Container;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\ParameterResolver;
use SmashBalloon\YoutubeFeed\Vendor\Psr\Container\ContainerInterface;
use ReflectionFunctionAbstract;
/**
* Inject entries from a DI container using the parameter names.
* @internal
*/
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,56 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\Container;
use SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver\ParameterResolver;
use SmashBalloon\YoutubeFeed\Vendor\Psr\Container\ContainerInterface;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
/**
* Inject entries from a DI container using the type-hints.
* @internal
*/
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,38 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver;
use ReflectionException;
use ReflectionFunctionAbstract;
/**
* Finds the default value for a parameter, *if it exists*.
* @internal
*/
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,33 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\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.
* @internal
*/
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,27 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Resolves the parameters to use to call the callable.
* @internal
*/
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,48 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
/**
* Dispatches the call to other resolvers until all parameters are resolved.
*
* Chain of responsibility pattern.
* @internal
*/
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,47 @@
<?php
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\ParameterResolver;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
/**
* Inject entries using type-hints.
*
* Tries to match type-hints with the parameters provided.
* @internal
*/
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
namespace SmashBalloon\YoutubeFeed\Vendor\Invoker\Reflection;
use Closure;
use SmashBalloon\YoutubeFeed\Vendor\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)));
}
}