mirror of
https://github.com/symfony/console.git
synced 2026-03-24 01:12:13 +01:00
[Console] Add argument resolvers
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolverInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\CompleteCommand;
|
||||
use Symfony\Component\Console\Command\DumpCompletionCommand;
|
||||
@@ -82,6 +83,7 @@ class Application implements ResetInterface
|
||||
private InputDefinition $definition;
|
||||
private HelperSet $helperSet;
|
||||
private ?EventDispatcherInterface $dispatcher = null;
|
||||
private ?ArgumentResolverInterface $argumentResolver = null;
|
||||
private Terminal $terminal;
|
||||
private string $defaultCommand;
|
||||
private bool $singleCommand = false;
|
||||
@@ -110,6 +112,19 @@ class Application implements ResetInterface
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
public function setArgumentResolver(ArgumentResolverInterface $argumentResolver): void
|
||||
{
|
||||
$this->argumentResolver = $argumentResolver;
|
||||
}
|
||||
|
||||
public function getArgumentResolver(): ?ArgumentResolverInterface
|
||||
{
|
||||
return $this->argumentResolver;
|
||||
}
|
||||
|
||||
public function setCommandLoader(CommandLoaderInterface $commandLoader): void
|
||||
{
|
||||
$this->commandLoader = $commandLoader;
|
||||
|
||||
159
ArgumentResolver/ArgumentResolver.php
Normal file
159
ArgumentResolver/ArgumentResolver.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\NearMissValueResolverException;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\ResolverNotFoundException;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver as Resolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Attribute\ValueResolver;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\Service\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* Resolves the arguments passed to a console command.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class ArgumentResolver implements ArgumentResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param iterable<mixed, ValueResolverInterface> $argumentValueResolvers
|
||||
*/
|
||||
public function __construct(
|
||||
private iterable $argumentValueResolvers = [],
|
||||
private ?ContainerInterface $namedResolvers = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getArguments(InputInterface $input, callable $command, ?\ReflectionFunctionAbstract $reflector = null): array
|
||||
{
|
||||
$reflector ??= new \ReflectionFunction($command(...));
|
||||
|
||||
$argumentReflectors = [];
|
||||
foreach ($reflector->getParameters() as $param) {
|
||||
$argumentReflectors[$param->getName()] = new ReflectionMember($param);
|
||||
}
|
||||
|
||||
$arguments = [];
|
||||
|
||||
foreach ($argumentReflectors as $argumentName => $member) {
|
||||
$argumentValueResolvers = $this->argumentValueResolvers;
|
||||
$disabledResolvers = [];
|
||||
|
||||
if ($this->namedResolvers && $attributes = $member->getAttributes(ValueResolver::class)) {
|
||||
$resolverName = null;
|
||||
foreach ($attributes as $attribute) {
|
||||
if ($attribute->disabled) {
|
||||
$disabledResolvers[$attribute->resolver] = true;
|
||||
} elseif ($resolverName) {
|
||||
throw new \LogicException(\sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $member->getName(), $member->getSourceName()));
|
||||
} else {
|
||||
$resolverName = $attribute->resolver;
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolverName) {
|
||||
if (!$this->namedResolvers->has($resolverName)) {
|
||||
throw new ResolverNotFoundException($resolverName, $this->namedResolvers instanceof ServiceProviderInterface ? array_keys($this->namedResolvers->getProvidedServices()) : []);
|
||||
}
|
||||
|
||||
$argumentValueResolvers = [
|
||||
$this->namedResolvers->get($resolverName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$valueResolverExceptions = [];
|
||||
foreach ($argumentValueResolvers as $name => $resolver) {
|
||||
if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$count = 0;
|
||||
foreach ($resolver->resolve($argumentName, $input, $member) as $argument) {
|
||||
++$count;
|
||||
$arguments[] = $argument;
|
||||
}
|
||||
} catch (NearMissValueResolverException $e) {
|
||||
$valueResolverExceptions[] = $e;
|
||||
}
|
||||
|
||||
if (1 < $count && !$member->isVariadic()) {
|
||||
throw new \InvalidArgumentException(\sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver)));
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// For variadic parameters with explicit input mapping, 0 values is valid
|
||||
if ($member->isVariadic() && (Argument::tryFrom($member->getMember()) || Option::tryFrom($member->getMember()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $member->getType();
|
||||
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null;
|
||||
|
||||
if ($typeName && \in_array($typeName, [
|
||||
InputInterface::class,
|
||||
OutputInterface::class,
|
||||
SymfonyStyle::class,
|
||||
Cursor::class,
|
||||
\Symfony\Component\Console\Application::class,
|
||||
Command::class,
|
||||
], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reasons = array_map(static fn (NearMissValueResolverException $e) => $e->getMessage(), $valueResolverExceptions);
|
||||
if (!$reasons) {
|
||||
$reasons[] = \sprintf('The parameter has no #[Argument], #[Option], or #[MapInput] attribute, and its type "%s" cannot be auto-resolved.', $typeName ?? 'unknown');
|
||||
$reasons[] = 'Add an attribute to map this parameter to command input.';
|
||||
}
|
||||
|
||||
throw new \RuntimeException(\sprintf('Could not resolve parameter "$%s" of command "%s".'."\n\n".'Possible reasons:'."\n".' • '.implode("\n • ", $reasons), $member->getName(), $member->getSourceName()));
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<int, ValueResolverInterface>
|
||||
*/
|
||||
public static function getDefaultArgumentValueResolvers(): iterable
|
||||
{
|
||||
$builtinTypeResolver = new Resolver\BuiltinTypeValueResolver();
|
||||
$backedEnumResolver = new Resolver\BackedEnumValueResolver();
|
||||
$dateTimeResolver = new Resolver\DateTimeValueResolver();
|
||||
|
||||
return [
|
||||
$backedEnumResolver,
|
||||
new Resolver\UidValueResolver(),
|
||||
$builtinTypeResolver,
|
||||
new Resolver\MapInputValueResolver($builtinTypeResolver, $backedEnumResolver, $dateTimeResolver),
|
||||
$dateTimeResolver,
|
||||
new Resolver\DefaultValueResolver(),
|
||||
new Resolver\VariadicValueResolver(),
|
||||
];
|
||||
}
|
||||
}
|
||||
33
ArgumentResolver/ArgumentResolverInterface.php
Normal file
33
ArgumentResolver/ArgumentResolverInterface.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\ResolverNotFoundException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Determines the arguments for a specific Console Command.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface ArgumentResolverInterface
|
||||
{
|
||||
/**
|
||||
* Returns the arguments to pass to the Console Command after resolution.
|
||||
*
|
||||
* @throws \RuntimeException When no value could be provided for a required argument
|
||||
* @throws ResolverNotFoundException
|
||||
*/
|
||||
public function getArguments(InputInterface $input, callable $command, ?\ReflectionFunctionAbstract $reflector = null): array;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\Exception;
|
||||
|
||||
/**
|
||||
* Lets value resolvers tell when an argument could be under their watch but failed to be resolved.
|
||||
*
|
||||
* Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain.
|
||||
*/
|
||||
final class NearMissValueResolverException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
33
ArgumentResolver/Exception/ResolverNotFoundException.php
Normal file
33
ArgumentResolver/Exception/ResolverNotFoundException.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\Exception;
|
||||
|
||||
final class ResolverNotFoundException extends \RuntimeException
|
||||
{
|
||||
/**
|
||||
* @param string[] $alternatives
|
||||
*/
|
||||
public function __construct(string $name, array $alternatives = [])
|
||||
{
|
||||
$msg = \sprintf('You have requested a non-existent resolver "%s".', $name);
|
||||
if ($alternatives) {
|
||||
if (1 === \count($alternatives)) {
|
||||
$msg .= ' Did you mean this: "';
|
||||
} else {
|
||||
$msg .= ' Did you mean one of these: "';
|
||||
}
|
||||
$msg .= implode('", "', $alternatives).'"?';
|
||||
}
|
||||
|
||||
parent::__construct($msg);
|
||||
}
|
||||
}
|
||||
90
ArgumentResolver/ValueResolver/BackedEnumValueResolver.php
Normal file
90
ArgumentResolver/ValueResolver/BackedEnumValueResolver.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves a BackedEnum instance from a Command argument or option.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*/
|
||||
final class BackedEnumValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveArgument($argument, $input)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveArgument(Argument $argument, InputInterface $input): ?\BackedEnum
|
||||
{
|
||||
$value = $input->getArgument($argument->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $argument->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) && !\is_int($value)) {
|
||||
throw InvalidArgumentException::fromEnumValue($argument->name, get_debug_type($value), $argument->suggestedValues);
|
||||
}
|
||||
|
||||
return $argument->typeName::tryFrom($value)
|
||||
?? throw InvalidArgumentException::fromEnumValue($argument->name, $value, $argument->suggestedValues);
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): ?\BackedEnum
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $option->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) && !\is_int($value)) {
|
||||
throw InvalidOptionException::fromEnumValue($option->name, get_debug_type($value), $option->suggestedValues);
|
||||
}
|
||||
|
||||
return $option->typeName::tryFrom($value)
|
||||
?? throw InvalidOptionException::fromEnumValue($option->name, $value, $option->suggestedValues);
|
||||
}
|
||||
}
|
||||
75
ArgumentResolver/ValueResolver/BuiltinTypeValueResolver.php
Normal file
75
ArgumentResolver/ValueResolver/BuiltinTypeValueResolver.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves values from #[Argument] or #[Option] attributes for built-in PHP types.
|
||||
*
|
||||
* Handles: string, bool, int, float, array
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class BuiltinTypeValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($member->isVariadic()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$input->getArgument($argument->name)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): mixed
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value && \in_array($option->typeName, Option::ALLOWED_UNION_TYPES, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('array' === $option->typeName && $option->allowNull && [] === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('bool' === $option->typeName) {
|
||||
if ($option->allowNull && null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value ?? $option->default;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
102
ArgumentResolver/ValueResolver/DateTimeValueResolver.php
Normal file
102
ArgumentResolver/ValueResolver/DateTimeValueResolver.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\Console\Attribute\MapDateTime;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves a \DateTime* instance as a command input argument or option.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Tim Goudriaan <tim@codedmonkey.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class DateTimeValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?ClockInterface $clock = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || !is_a($type->getName(), \DateTimeInterface::class, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$attribute = $member->getAttribute(MapDateTime::class);
|
||||
|
||||
$inputName = $attribute?->argument ?? $attribute?->option ?? $member->getInputName();
|
||||
|
||||
// Try to get value from argument or option
|
||||
$value = null;
|
||||
if ($input->hasArgument($inputName)) {
|
||||
$value = $input->getArgument($inputName);
|
||||
} elseif ($input->hasOption($inputName)) {
|
||||
$value = $input->getOption($inputName);
|
||||
}
|
||||
|
||||
$class = \DateTimeInterface::class === $type->getName() ? \DateTimeImmutable::class : $type->getName();
|
||||
|
||||
if (!$value) {
|
||||
if ($member->isNullable()) {
|
||||
return [null];
|
||||
}
|
||||
if (!$this->clock) {
|
||||
return [new $class()];
|
||||
}
|
||||
$value = $this->clock->now();
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
/** @var class-string<\DateTimeImmutable>|class-string<\DateTime> $class */
|
||||
return [$value instanceof $class ? $value : $class::createFromInterface($value)];
|
||||
}
|
||||
|
||||
$format = $attribute?->format;
|
||||
|
||||
/** @var class-string<\DateTimeImmutable>|class-string<\DateTime> $class */
|
||||
if (null !== $format) {
|
||||
$date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone());
|
||||
|
||||
if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) {
|
||||
$date = false;
|
||||
}
|
||||
} else {
|
||||
if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
|
||||
$value = '@'.$value;
|
||||
}
|
||||
try {
|
||||
$date = new $class($value, $this->clock?->now()->getTimeZone());
|
||||
} catch (\Exception) {
|
||||
$date = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$message = \sprintf('Invalid date given for parameter "$%s".', $argumentName);
|
||||
if ($format) {
|
||||
$message .= \sprintf(' Expected format: "%s".', $format);
|
||||
}
|
||||
$message .= ' Use #[MapDateTime(format: \'your-format\')] to specify a custom format.';
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
return [$date];
|
||||
}
|
||||
}
|
||||
37
ArgumentResolver/ValueResolver/DefaultValueResolver.php
Normal file
37
ArgumentResolver/ValueResolver/DefaultValueResolver.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Yields the default value defined in the command signature when no input value has been explicitly passed.
|
||||
*
|
||||
* @author Iltar van der Berg <kjarli@gmail.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class DefaultValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($member->hasDefaultValue()) {
|
||||
return [$member->getDefaultValue()];
|
||||
}
|
||||
|
||||
if ($member->isNullable() && !$member->isVariadic()) {
|
||||
return [null];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
89
ArgumentResolver/ValueResolver/MapInputValueResolver.php
Normal file
89
ArgumentResolver/ValueResolver/MapInputValueResolver.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves the value of a input argument/option to an object holding the #[MapInput] attribute.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class MapInputValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ValueResolverInterface $builtinTypeResolver,
|
||||
private readonly ValueResolverInterface $backedEnumResolver,
|
||||
private readonly ValueResolverInterface $dateTimeResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if (!$attribute = MapInput::tryFrom($member->getMember())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveMapInput($attribute, $input)];
|
||||
}
|
||||
|
||||
private function resolveMapInput(MapInput $mapInput, InputInterface $input): object
|
||||
{
|
||||
$instance = $mapInput->getClass()->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ($mapInput->getDefinition() as $name => $spec) {
|
||||
// ignore required arguments that are not set yet (may happen in interactive mode)
|
||||
if ($spec instanceof Argument && $spec->isRequired() && \in_array($input->getArgument($spec->name), [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$instance->$name = match (true) {
|
||||
$spec instanceof Argument => $this->resolveArgumentSpec($spec, $mapInput->getClass()->getProperty($name), $input),
|
||||
$spec instanceof Option => $this->resolveOptionSpec($spec, $mapInput->getClass()->getProperty($name), $input),
|
||||
$spec instanceof MapInput => $this->resolveMapInput($spec, $input),
|
||||
};
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
private function resolveArgumentSpec(Argument $argument, \ReflectionProperty $property, InputInterface $input): mixed
|
||||
{
|
||||
if (is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return iterator_to_array($this->backedEnumResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
if (is_a($argument->typeName, \DateTimeInterface::class, true)) {
|
||||
return iterator_to_array($this->dateTimeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
return iterator_to_array($this->builtinTypeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
private function resolveOptionSpec(Option $option, \ReflectionProperty $property, InputInterface $input): mixed
|
||||
{
|
||||
if (is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return iterator_to_array($this->backedEnumResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
if (is_a($option->typeName, \DateTimeInterface::class, true)) {
|
||||
return iterator_to_array($this->dateTimeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
return iterator_to_array($this->builtinTypeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
}
|
||||
81
ArgumentResolver/ValueResolver/ServiceValueResolver.php
Normal file
81
ArgumentResolver/ValueResolver/ServiceValueResolver.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\NearMissValueResolverException;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Yields a service from a service locator keyed by command and argument name.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class ServiceValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContainerInterface $container,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$command = $input->getFirstArgument();
|
||||
|
||||
if ($command && $this->container->has($command)) {
|
||||
$locator = $this->container->get($command);
|
||||
if ($locator instanceof ContainerInterface && $locator->has($argumentName)) {
|
||||
try {
|
||||
return [$locator->get($argumentName)];
|
||||
} catch (RuntimeException|\Throwable $e) {
|
||||
$what = \sprintf('argument $%s', $argumentName);
|
||||
$message = str_replace(\sprintf('service "%s"', $argumentName), $what, $e->getMessage());
|
||||
$what .= \sprintf(' of command "%s"', $command);
|
||||
$message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message);
|
||||
|
||||
if ($e->getMessage() === $message) {
|
||||
$message = \sprintf('Cannot resolve %s: %s', $what, $message);
|
||||
}
|
||||
|
||||
throw new NearMissValueResolverException($message, $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$typeName = $type->getName();
|
||||
|
||||
if (!$this->container->has($typeName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$service = $this->container->get($typeName);
|
||||
|
||||
if (!$service instanceof $typeName) {
|
||||
throw new NearMissValueResolverException(\sprintf('Service "%s" exists in the container but is not an instance of "%s".', $typeName, $typeName));
|
||||
}
|
||||
|
||||
return [$service];
|
||||
} catch (\Throwable $e) {
|
||||
throw new NearMissValueResolverException(\sprintf('Cannot resolve parameter "$%s" of type "%s": %s', $argumentName, $typeName, $e->getMessage()), previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
ArgumentResolver/ValueResolver/UidValueResolver.php
Normal file
87
ArgumentResolver/ValueResolver/UidValueResolver.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
|
||||
/**
|
||||
* Resolves an AbstractUid instance from a Command argument or option.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class UidValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($argument->typeName, AbstractUid::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveArgument($argument, $input)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($option->typeName, AbstractUid::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveArgument(Argument $argument, InputInterface $input): ?AbstractUid
|
||||
{
|
||||
$value = $input->getArgument($argument->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $argument->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) || !$argument->typeName::isValid($value)) {
|
||||
throw new InvalidArgumentException(\sprintf('The uid for the "%s" argument is invalid.', $argument->name));
|
||||
}
|
||||
|
||||
return $argument->typeName::fromString($value);
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): ?AbstractUid
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $option->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) || !$option->typeName::isValid($value)) {
|
||||
throw new InvalidOptionException(\sprintf('The uid for the "--%s" option is invalid.', $option->name));
|
||||
}
|
||||
|
||||
return $option->typeName::fromString($value);
|
||||
}
|
||||
}
|
||||
30
ArgumentResolver/ValueResolver/ValueResolverInterface.php
Normal file
30
ArgumentResolver/ValueResolver/ValueResolverInterface.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Responsible for resolving the value of a Command argument based on its
|
||||
* parameter metadata and the Command MapInput.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
interface ValueResolverInterface
|
||||
{
|
||||
/**
|
||||
* Returns the possible value(s) for the argument.
|
||||
*/
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable;
|
||||
}
|
||||
54
ArgumentResolver/ValueResolver/VariadicValueResolver.php
Normal file
54
ArgumentResolver/ValueResolver/VariadicValueResolver.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Yields a variadic argument's values from the input.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class VariadicValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if (!$member->isVariadic()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
$values = $input->getArgument($argument->name);
|
||||
|
||||
if (!\is_array($values)) {
|
||||
throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the input argument "%1$s" contains a type of "%2$s" instead.', $argument->name, get_debug_type($values)));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
$values = $input->getOption($option->name);
|
||||
|
||||
if (!\is_array($values)) {
|
||||
throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the input option "--%1$s" contains a type of "%2$s" instead.', $option->name, get_debug_type($values)));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -14,24 +14,23 @@ namespace Symfony\Component\Console\Attribute;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class Argument
|
||||
{
|
||||
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
|
||||
public string|bool|int|float|array|null $default = null;
|
||||
public array|\Closure $suggestedValues;
|
||||
|
||||
private string|bool|int|float|array|null $default = null;
|
||||
private array|\Closure $suggestedValues;
|
||||
private ?int $mode = null;
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @var string|class-string<\BackedEnum>
|
||||
*/
|
||||
private string $typeName = '';
|
||||
public string $typeName = '';
|
||||
private ?int $mode = null;
|
||||
private ?InteractiveAttributeInterface $interactiveAttribute = null;
|
||||
|
||||
/**
|
||||
@@ -68,11 +67,6 @@ class Argument
|
||||
}
|
||||
|
||||
$self->typeName = $type->getName();
|
||||
$isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
|
||||
|
||||
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
|
||||
throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $reflection->getMemberName(), $name, $reflection->getSourceName(), implode('", "', self::ALLOWED_TYPES)));
|
||||
}
|
||||
|
||||
if (!$self->name) {
|
||||
$self->name = (new UnicodeString($name))->kebab();
|
||||
@@ -80,9 +74,9 @@ class Argument
|
||||
|
||||
$self->default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null;
|
||||
|
||||
$isOptional = $reflection->hasDefaultValue() || $reflection->isNullable();
|
||||
$isOptional = $reflection->hasDefaultValue() || $reflection->isNullable() || $reflection->isVariadic();
|
||||
$self->mode = $isOptional ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
|
||||
if ('array' === $self->typeName) {
|
||||
if ('array' === $self->typeName || $reflection->isVariadic()) {
|
||||
$self->mode |= InputArgument::IS_ARRAY;
|
||||
}
|
||||
|
||||
@@ -92,7 +86,7 @@ class Argument
|
||||
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
|
||||
}
|
||||
|
||||
if ($isBackedEnum && !$self->suggestedValues) {
|
||||
if (is_subclass_of($self->typeName, \BackedEnum::class) && !$self->suggestedValues) {
|
||||
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
|
||||
}
|
||||
|
||||
@@ -115,20 +109,6 @@ class Argument
|
||||
return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function resolveValue(InputInterface $input): mixed
|
||||
{
|
||||
$value = $input->getArgument($this->name);
|
||||
|
||||
if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) {
|
||||
return $this->typeName::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
26
Attribute/AsTargetedValueResolver.php
Normal file
26
Attribute/AsTargetedValueResolver.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure targeted value resolvers.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class AsTargetedValueResolver
|
||||
{
|
||||
/**
|
||||
* @param string|null $name The name with which the resolver can be targeted
|
||||
*/
|
||||
public function __construct(public readonly ?string $name = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
36
Attribute/MapDateTime.php
Normal file
36
Attribute/MapDateTime.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* Defines how a DateTime parameter should be resolved from command input.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class MapDateTime
|
||||
{
|
||||
/**
|
||||
* @param string|null $format The DateTime format (@see https://php.net/datetime.format)
|
||||
* @param string|null $argument The argument name to read from (defaults to parameter name)
|
||||
* @param string|null $option The option name to read from (mutually exclusive with $argument)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?string $format = null,
|
||||
public readonly ?string $argument = null,
|
||||
public readonly ?string $option = null,
|
||||
) {
|
||||
if ($argument && $option) {
|
||||
throw new \LogicException('MapDateTime cannot specify both argument and option.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,25 +84,6 @@ final class MapInput
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function resolveValue(InputInterface $input): object
|
||||
{
|
||||
$instance = $this->class->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ($this->definition as $name => $spec) {
|
||||
// ignore required arguments that are not set yet (may happen in interactive mode)
|
||||
if ($spec instanceof Argument && $spec->isRequired() && \in_array($input->getArgument($spec->name), [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$instance->$name = $spec->resolveValue($input);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -152,6 +133,79 @@ final class MapInput
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return \ReflectionClass<object>
|
||||
*/
|
||||
public function getClass(): \ReflectionClass
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, Argument|Option|self>
|
||||
*/
|
||||
public function getDefinition(): array
|
||||
{
|
||||
return $this->definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a populated instance of the DTO from command input.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function createInstance(InputInterface $input): object
|
||||
{
|
||||
$instance = $this->class->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ($this->definition as $name => $spec) {
|
||||
if ($spec instanceof Argument) {
|
||||
$value = $input->getArgument($spec->name);
|
||||
if ($spec->isRequired() && \in_array($value, [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
$instance->$name = $this->resolveValue($spec->typeName, $value, $spec->default);
|
||||
} elseif ($spec instanceof Option) {
|
||||
$value = $input->getOption($spec->name);
|
||||
$instance->$name = $this->resolveValue($spec->typeName, $value, $spec->default);
|
||||
} elseif ($spec instanceof self) {
|
||||
$instance->$name = $spec->createInstance($input);
|
||||
}
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
private function resolveValue(string $typeName, mixed $value, mixed $default): mixed
|
||||
{
|
||||
if (null === $value) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ('' === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_subclass_of($typeName, \BackedEnum::class)) {
|
||||
return $value instanceof $typeName ? $value : $typeName::tryFrom($value);
|
||||
}
|
||||
|
||||
if (is_a($typeName, \DateTimeInterface::class, true)) {
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value;
|
||||
}
|
||||
$class = \DateTimeInterface::class === $typeName ? \DateTimeImmutable::class : $typeName;
|
||||
|
||||
return new $class($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
|
||||
@@ -14,26 +14,26 @@ namespace Symfony\Component\Console\Attribute;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class Option
|
||||
{
|
||||
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
|
||||
private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
|
||||
public const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
|
||||
public string|bool|int|float|array|null $default = null;
|
||||
public array|\Closure $suggestedValues;
|
||||
|
||||
private string|bool|int|float|array|null $default = null;
|
||||
private array|\Closure $suggestedValues;
|
||||
private ?int $mode = null;
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @var string|class-string<\BackedEnum>
|
||||
*/
|
||||
private string $typeName = '';
|
||||
private bool $allowNull = false;
|
||||
public string $typeName = '';
|
||||
/** @internal */
|
||||
public bool $allowNull = false;
|
||||
private ?int $mode = null;
|
||||
private string $memberName = '';
|
||||
private string $sourceName = '';
|
||||
|
||||
@@ -71,7 +71,8 @@ class Option
|
||||
$name = $reflection->getName();
|
||||
$type = $reflection->getType();
|
||||
|
||||
if (!$reflection->hasDefaultValue()) {
|
||||
// Variadic parameters implicitly default to an empty array
|
||||
if (!$reflection->isVariadic() && !$reflection->hasDefaultValue()) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must declare a default value.', $self->memberName, $name, $self->sourceName));
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ class Option
|
||||
$self->name = (new UnicodeString($name))->kebab();
|
||||
}
|
||||
|
||||
$self->default = $reflection->getDefaultValue();
|
||||
$self->default = $reflection->isVariadic() ? [] : $reflection->getDefaultValue();
|
||||
$self->allowNull = $reflection->isNullable();
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
@@ -91,11 +92,6 @@ class Option
|
||||
}
|
||||
|
||||
$self->typeName = $type->getName();
|
||||
$isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
|
||||
|
||||
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
|
||||
throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command option. Only "%s" types and BackedEnum are allowed.', $self->typeName, $self->memberName, $name, $self->sourceName, implode('", "', self::ALLOWED_TYPES)));
|
||||
}
|
||||
|
||||
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must not be nullable when it has a default boolean value.', $self->memberName, $name, $self->sourceName));
|
||||
@@ -110,7 +106,7 @@ class Option
|
||||
if (false !== $self->default) {
|
||||
$self->mode |= InputOption::VALUE_NEGATABLE;
|
||||
}
|
||||
} elseif ('array' === $self->typeName) {
|
||||
} elseif ('array' === $self->typeName || $reflection->isVariadic()) {
|
||||
$self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
|
||||
} else {
|
||||
$self->mode = InputOption::VALUE_REQUIRED;
|
||||
@@ -120,7 +116,7 @@ class Option
|
||||
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
|
||||
}
|
||||
|
||||
if ($isBackedEnum && !$self->suggestedValues) {
|
||||
if (is_subclass_of($self->typeName, \BackedEnum::class) && !$self->suggestedValues) {
|
||||
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
|
||||
}
|
||||
|
||||
@@ -138,36 +134,6 @@ class Option
|
||||
return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $default, $suggestedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function resolveValue(InputInterface $input): mixed
|
||||
{
|
||||
$value = $input->getOption($this->name);
|
||||
|
||||
if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) {
|
||||
return $this->typeName::tryFrom($value) ?? throw InvalidOptionException::fromEnumValue($this->name, $value, $this->suggestedValues);
|
||||
}
|
||||
|
||||
if ('array' === $this->typeName && $this->allowNull && [] === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('bool' !== $this->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($this->allowNull && null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value ?? $this->default;
|
||||
}
|
||||
|
||||
private function handleUnion(\ReflectionUnionType $type): self
|
||||
{
|
||||
$types = array_map(
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Symfony\Component\Console\Attribute\Reflection;
|
||||
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -33,6 +35,21 @@ class ReflectionMember
|
||||
return ($this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T> $class
|
||||
*
|
||||
* @return list<T>
|
||||
*/
|
||||
public function getAttributes(string $class): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (\ReflectionAttribute $attribute) => $attribute->newInstance(),
|
||||
$this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)
|
||||
);
|
||||
}
|
||||
|
||||
public function getSourceName(): string
|
||||
{
|
||||
if ($this->member instanceof \ReflectionProperty) {
|
||||
@@ -102,8 +119,23 @@ class ReflectionMember
|
||||
return $this->member instanceof \ReflectionParameter;
|
||||
}
|
||||
|
||||
public function isVariadic(): bool
|
||||
{
|
||||
return $this->member instanceof \ReflectionParameter && $this->member->isVariadic();
|
||||
}
|
||||
|
||||
public function isProperty(): bool
|
||||
{
|
||||
return $this->member instanceof \ReflectionProperty;
|
||||
}
|
||||
|
||||
public function getMember(): \ReflectionParameter|\ReflectionProperty
|
||||
{
|
||||
return $this->member;
|
||||
}
|
||||
|
||||
public function getInputName(): string
|
||||
{
|
||||
return (new UnicodeString($this->member->getName()))->kebab()->toString();
|
||||
}
|
||||
}
|
||||
|
||||
31
Attribute/ValueResolver.php
Normal file
31
Attribute/ValueResolver.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
|
||||
/**
|
||||
* Defines which value resolver should be used for a given parameter.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
|
||||
class ValueResolver
|
||||
{
|
||||
/**
|
||||
* @param class-string<ValueResolverInterface>|string $resolver The class name of the resolver to use
|
||||
* @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases
|
||||
*/
|
||||
public function __construct(
|
||||
public string $resolver,
|
||||
public bool $disabled = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ CHANGELOG
|
||||
---
|
||||
|
||||
* Add support for method-based commands with `#[AsCommand]` attribute
|
||||
* Add argument resolver support
|
||||
* Add `BackedEnum` and `DateTimeInterface` support to `#[MapInput]`
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Interact;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -46,6 +46,7 @@ class InvokableCommand implements SignalableCommandInterface
|
||||
public function __construct(
|
||||
private readonly Command $command,
|
||||
callable $code,
|
||||
private ?ArgumentResolverInterface $argumentResolver = null,
|
||||
) {
|
||||
$this->code = $code;
|
||||
$this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null;
|
||||
@@ -131,44 +132,61 @@ class InvokableCommand implements SignalableCommandInterface
|
||||
|
||||
private function getParameters(\ReflectionFunction $function, InputInterface $input, OutputInterface $output): array
|
||||
{
|
||||
$parameters = [];
|
||||
foreach ($function->getParameters() as $parameter) {
|
||||
if ($argument = Argument::tryFrom($parameter)) {
|
||||
$parameters[] = $argument->resolveValue($input);
|
||||
$coreUtilities = [];
|
||||
$needsArgumentResolver = false;
|
||||
|
||||
continue;
|
||||
foreach ($function->getParameters() as $index => $param) {
|
||||
$type = $param->getType();
|
||||
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$argument = match ($type->getName()) {
|
||||
InputInterface::class => $input,
|
||||
OutputInterface::class => $output,
|
||||
SymfonyStyle::class => new SymfonyStyle($input, $output),
|
||||
Cursor::class => new Cursor($output),
|
||||
Application::class => $this->command->getApplication(),
|
||||
Command::class, self::class => $this->command,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (null !== $argument) {
|
||||
$coreUtilities[$index] = $argument;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($parameter)) {
|
||||
$parameters[] = $option->resolveValue($input);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($in = MapInput::tryFrom($parameter)) {
|
||||
$parameters[] = $in->resolveValue($input);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported.', $parameter->getName()));
|
||||
}
|
||||
|
||||
$parameters[] = match ($type->getName()) {
|
||||
InputInterface::class => $input,
|
||||
OutputInterface::class => $output,
|
||||
Cursor::class => new Cursor($output),
|
||||
SymfonyStyle::class => new SymfonyStyle($input, $output),
|
||||
Command::class => $this->command,
|
||||
Application::class => $this->command->getApplication(),
|
||||
default => throw new RuntimeException(\sprintf('Unsupported type "%s" for parameter "$%s".', $type->getName(), $parameter->getName())),
|
||||
};
|
||||
$needsArgumentResolver = true;
|
||||
}
|
||||
|
||||
return $parameters ?: [$input, $output];
|
||||
if (!$needsArgumentResolver) {
|
||||
return $coreUtilities;
|
||||
}
|
||||
|
||||
if (null === $this->argumentResolver) {
|
||||
$this->argumentResolver = $this->command->getApplication()?->getArgumentResolver() ?? new ArgumentResolver(
|
||||
ArgumentResolver::getDefaultArgumentValueResolvers()
|
||||
);
|
||||
}
|
||||
|
||||
$closure = $function->getClosure();
|
||||
$resolvedArgs = $this->argumentResolver->getArguments($input, $closure, $function);
|
||||
|
||||
$parameters = [];
|
||||
$resolvedIndex = 0;
|
||||
|
||||
foreach ($function->getParameters() as $index => $param) {
|
||||
if (isset($coreUtilities[$index])) {
|
||||
$parameters[] = $coreUtilities[$index];
|
||||
} elseif ($param->isVariadic()) {
|
||||
// Variadic parameters consume all remaining resolved arguments
|
||||
$parameters = [...$parameters, ...\array_slice($resolvedArgs, $resolvedIndex)];
|
||||
break;
|
||||
} else {
|
||||
$parameters[] = $resolvedArgs[$resolvedIndex++] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public function getSubscribedSignals(): array
|
||||
|
||||
189
DependencyInjection/RegisterCommandArgumentLocatorsPass.php
Normal file
189
DependencyInjection/RegisterCommandArgumentLocatorsPass.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
use Symfony\Component\VarExporter\ProxyHelper;
|
||||
|
||||
/**
|
||||
* Creates the service-locators required by ServiceValueResolver for commands.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class RegisterCommandArgumentLocatorsPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('console.argument_resolver.service')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parameterBag = $container->getParameterBag();
|
||||
$serviceLocators = [];
|
||||
|
||||
foreach ($container->findTaggedServiceIds('console.command.service_arguments', true) as $id => $tags) {
|
||||
$def = $container->getDefinition($id);
|
||||
$class = $def->getClass();
|
||||
$autowire = $def->isAutowired();
|
||||
$bindings = $def->getBindings();
|
||||
|
||||
// Resolve service class, taking parent definitions into account
|
||||
while ($def instanceof ChildDefinition) {
|
||||
$def = $container->findDefinition($def->getParent());
|
||||
$class = $class ?: $def->getClass();
|
||||
$bindings += $def->getBindings();
|
||||
}
|
||||
$class = $parameterBag->resolveValue($class);
|
||||
|
||||
if (!$r = $container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(\sprintf('Class "%s" used for command "%s" cannot be found.', $class, $id));
|
||||
}
|
||||
|
||||
// Get all console.command tags to find command names and their methods
|
||||
$commandTags = $container->getDefinition($id)->getTag('console.command');
|
||||
$manualArguments = [];
|
||||
|
||||
// Validate and collect explicit per-arguments service references
|
||||
foreach ($tags as $attributes) {
|
||||
if (!isset($attributes['argument']) && !isset($attributes['id'])) {
|
||||
$autowire = true;
|
||||
continue;
|
||||
}
|
||||
foreach (['argument', 'id'] as $k) {
|
||||
if (!isset($attributes[$k][0])) {
|
||||
throw new InvalidArgumentException(\sprintf('Missing "%s" attribute on tag "console.command.service_arguments" %s for service "%s".', $k, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id));
|
||||
}
|
||||
}
|
||||
|
||||
$manualArguments[$attributes['argument']] = $attributes['id'];
|
||||
}
|
||||
|
||||
foreach ($commandTags as $commandTag) {
|
||||
$commandName = $commandTag['command'] ?? null;
|
||||
|
||||
if (!$commandName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $commandTag['method'] ?? '__invoke';
|
||||
|
||||
if (!$r->hasMethod($methodName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = $r->getMethod($methodName);
|
||||
$arguments = [];
|
||||
$erroredIds = 0;
|
||||
|
||||
foreach ($method->getParameters() as $p) {
|
||||
$type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?'));
|
||||
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
|
||||
$autowireAttributes = null;
|
||||
$parsedName = $p->name;
|
||||
$k = null;
|
||||
|
||||
if (isset($manualArguments[$p->name])) {
|
||||
$target = $manualArguments[$p->name];
|
||||
if ('?' !== $target[0]) {
|
||||
$invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
|
||||
} elseif ('' === $target = substr($target, 1)) {
|
||||
throw new InvalidArgumentException(\sprintf('A "console.command.service_arguments" tag must have non-empty "id" attributes for service "%s".', $id));
|
||||
} elseif ($p->allowsNull() && !$p->isOptional()) {
|
||||
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
|
||||
}
|
||||
} elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p, $k, $parsedName)])
|
||||
|| isset($bindings[$bindingName = $type.' $'.$parsedName])
|
||||
|| isset($bindings[$bindingName = '$'.$name])
|
||||
|| isset($bindings[$bindingName = $type])
|
||||
) {
|
||||
$binding = $bindings[$bindingName];
|
||||
|
||||
[$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues();
|
||||
$binding->setValues([$bindingValue, $bindingId, true, $bindingType, $bindingFile]);
|
||||
|
||||
$arguments[$p->name] = $bindingValue;
|
||||
|
||||
continue;
|
||||
} elseif (!$autowire || (!($autowireAttributes = $p->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)) && (!$type || '\\' !== $target[0]))) {
|
||||
continue;
|
||||
} elseif (!$autowireAttributes && is_subclass_of($type, \UnitEnum::class)) {
|
||||
// Do not attempt to register enum typed arguments if not already present in bindings
|
||||
continue;
|
||||
} elseif (!$p->allowsNull()) {
|
||||
$invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
|
||||
}
|
||||
|
||||
// Skip console-specific types that are resolved by other resolvers
|
||||
if (InputInterface::class === $type || OutputInterface::class === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($autowireAttributes) {
|
||||
$attribute = $autowireAttributes[0]->newInstance();
|
||||
$value = $parameterBag->resolveValue($attribute->value);
|
||||
|
||||
if ($attribute instanceof AutowireCallable) {
|
||||
$arguments[$p->name] = $attribute->buildDefinition($value, $type, $p);
|
||||
} elseif ($value instanceof Reference) {
|
||||
$arguments[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior);
|
||||
} else {
|
||||
$arguments[$p->name] = new Reference('.value.'.$container->hash($value));
|
||||
$container->register((string) $arguments[$p->name], 'mixed')
|
||||
->setFactory('current')
|
||||
->addArgument([$value]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) {
|
||||
$message = \sprintf('Cannot determine command argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $method->name, $p->name, $type);
|
||||
|
||||
// See if the type-hint lives in the same namespace as the command
|
||||
if (0 === strncmp($type, $class, strrpos($class, '\\'))) {
|
||||
$message .= ' Did you forget to add a use statement?';
|
||||
}
|
||||
|
||||
$container->register($erroredId = '.errored.'.$container->hash($message), $type)
|
||||
->addError($message);
|
||||
|
||||
$arguments[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE);
|
||||
++$erroredIds;
|
||||
} else {
|
||||
$target = preg_replace('/(^|[(|&])\\\\/', '\1', $target);
|
||||
$arguments[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
if ($arguments) {
|
||||
$serviceLocators[$commandName] = ServiceLocatorTagPass::register($container, $arguments, \count($arguments) !== $erroredIds ? $commandName : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$container->getDefinition('console.argument_resolver.service')
|
||||
->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceLocators));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Removes empty service-locators registered for ServiceValueResolver for commands.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class RemoveEmptyCommandArgumentLocatorsPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('console.argument_resolver.service')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
if (!$commandLocatorRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
|
||||
if ($commandLocator->getFactory()) {
|
||||
$commandLocator = $container->getDefinition($commandLocator->getFactory()[0]);
|
||||
}
|
||||
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
|
||||
foreach ($commands as $commandName => $argumentRef) {
|
||||
$argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]);
|
||||
|
||||
if ($argumentLocator->getFactory()) {
|
||||
$argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]);
|
||||
}
|
||||
|
||||
if (!$argumentLocator->getArgument(0)) {
|
||||
$reason = \sprintf('Removing service-argument resolver for command "%s": no corresponding services exist for the referenced types.', $commandName);
|
||||
unset($commands[$commandName]);
|
||||
$container->log($this, $reason);
|
||||
}
|
||||
}
|
||||
|
||||
$commandLocator->replaceArgument(0, $commands);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ final class Interaction
|
||||
public function interact(InputInterface $input, OutputInterface $output, \Closure $parameterResolver): void
|
||||
{
|
||||
if ($this->owner instanceof MapInput) {
|
||||
$function = $this->attribute->getFunction($this->owner->resolveValue($input));
|
||||
$function = $this->attribute->getFunction($this->owner->createInstance($input));
|
||||
$function->invoke(...$parameterResolver($function, $input, $output));
|
||||
$this->owner->setValue($input, $function->getClosureThis());
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\BackedEnumValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class BackedEnumValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveBackedEnumArgument()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['status' => 'pending'], new InputDefinition([
|
||||
new InputArgument('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
BackedEnumTestStatus $status,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('status', $input, $member));
|
||||
|
||||
$this->assertSame([BackedEnumTestStatus::Pending], $result);
|
||||
}
|
||||
|
||||
public function testResolveBackedEnumOption()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--status' => 'completed'], new InputDefinition([
|
||||
new InputOption('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
BackedEnumTestStatus $status = BackedEnumTestStatus::Pending,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('status', $input, $member));
|
||||
|
||||
$this->assertSame([BackedEnumTestStatus::Completed], $result);
|
||||
}
|
||||
|
||||
public function testBackedEnumArgumentThrowsOnInvalidValue()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['status' => 'invalid'], new InputDefinition([
|
||||
new InputArgument('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
BackedEnumTestStatus $status,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('status', $input, $member));
|
||||
}
|
||||
|
||||
public function testBackedEnumOptionThrowsOnInvalidValue()
|
||||
{
|
||||
$this->expectException(InvalidOptionException::class);
|
||||
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--status' => 'invalid'], new InputDefinition([
|
||||
new InputOption('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
BackedEnumTestStatus $status = BackedEnumTestStatus::Pending,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('status', $input, $member));
|
||||
}
|
||||
|
||||
public function testDoesNotResolveNonEnumArgument()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['username' => 'john'], new InputDefinition([
|
||||
new InputArgument('username'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $username,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('username', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveNonEnumOption()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--name' => 'john'], new InputDefinition([
|
||||
new InputOption('name'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
string $name = '',
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('name', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWithoutAttribute()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['status' => 'pending'], new InputDefinition([
|
||||
new InputArgument('status'),
|
||||
]));
|
||||
|
||||
$function = static fn (BackedEnumTestStatus $status) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('status', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolveIntBackedEnumArgument()
|
||||
{
|
||||
$resolver = new BackedEnumValueResolver();
|
||||
|
||||
$input = new ArrayInput(['priority' => 1], new InputDefinition([
|
||||
new InputArgument('priority'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
BackedEnumTestPriority $priority,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('priority', $input, $member));
|
||||
|
||||
$this->assertSame([BackedEnumTestPriority::High], $result);
|
||||
}
|
||||
}
|
||||
|
||||
enum BackedEnumTestStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Completed = 'completed';
|
||||
case Failed = 'failed';
|
||||
}
|
||||
|
||||
enum BackedEnumTestPriority: int
|
||||
{
|
||||
case Low = 0;
|
||||
case High = 1;
|
||||
case Critical = 2;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\BuiltinTypeValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class BuiltinTypeValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveStringArgument()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['username' => 'john'], new InputDefinition([
|
||||
new InputArgument('username'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $username,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('username', $input, $member);
|
||||
|
||||
$this->assertSame(['john'], $result);
|
||||
}
|
||||
|
||||
public function testResolveStringOption()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--name' => 'john'], new InputDefinition([
|
||||
new InputOption('name'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
string $name = '',
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('name', $input, $member);
|
||||
|
||||
$this->assertSame(['john'], $result);
|
||||
}
|
||||
|
||||
public function testDelegatesToBackedEnumValueResolverForEnumArgument()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['status' => 'pending'], new InputDefinition([
|
||||
new InputArgument('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
DummyBackedEnum $status,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('status', $input, $member);
|
||||
|
||||
// BuiltinTypeValueResolver returns empty for enums - BackedEnumValueResolver handles them
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDelegatesToBackedEnumValueResolverForEnumOption()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--status' => 'completed'], new InputDefinition([
|
||||
new InputOption('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
DummyBackedEnum $status = DummyBackedEnum::Pending,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('status', $input, $member);
|
||||
|
||||
// BuiltinTypeValueResolver returns empty for enums - BackedEnumValueResolver handles them
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolveBoolOption()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--force' => true], new InputDefinition([
|
||||
new InputOption('force'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
bool $force = false,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('force', $input, $member);
|
||||
|
||||
$this->assertSame([true], $result);
|
||||
}
|
||||
|
||||
public function testResolveNullableBoolOptionWithNullValue()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput([], new InputDefinition([
|
||||
new InputOption('force'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
?bool $force = null,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('force', $input, $member);
|
||||
|
||||
$this->assertSame([false], $result);
|
||||
}
|
||||
|
||||
public function testResolveArrayOption()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--tags' => ['foo', 'bar']], new InputDefinition([
|
||||
new InputOption('tags', mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
array $tags = [],
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('tags', $input, $member);
|
||||
|
||||
$this->assertSame([['foo', 'bar']], $result);
|
||||
}
|
||||
|
||||
public function testResolveNullableArrayOptionWithEmptyValue()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput([], new InputDefinition([
|
||||
new InputOption('tags', mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
?array $tags = null,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('tags', $input, $member);
|
||||
|
||||
$this->assertSame([null], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWithoutAttribute()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['username' => 'john'], new InputDefinition([
|
||||
new InputArgument('username'),
|
||||
]));
|
||||
|
||||
$function = static fn (string $username) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('username', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolveIntegerArgument()
|
||||
{
|
||||
$resolver = new BuiltinTypeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['count' => 42], new InputDefinition([
|
||||
new InputArgument('count'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
int $count,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('count', $input, $member);
|
||||
|
||||
$this->assertSame([42], $result);
|
||||
}
|
||||
}
|
||||
|
||||
enum DummyBackedEnum: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Completed = 'completed';
|
||||
case Failed = 'failed';
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\DateTimeValueResolver;
|
||||
use Symfony\Component\Console\Attribute\MapDateTime;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class DateTimeValueResolverTest extends TestCase
|
||||
{
|
||||
private readonly string $defaultTimezone;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->defaultTimezone = date_default_timezone_get();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
date_default_timezone_set($this->defaultTimezone);
|
||||
}
|
||||
|
||||
public static function getTimeZones()
|
||||
{
|
||||
yield ['UTC', false];
|
||||
yield ['Pacific/Honolulu', false];
|
||||
yield ['America/Toronto', false];
|
||||
yield ['UTC', true];
|
||||
yield ['Pacific/Honolulu', true];
|
||||
yield ['America/Toronto', true];
|
||||
}
|
||||
|
||||
public static function getClasses()
|
||||
{
|
||||
yield [\DateTimeInterface::class];
|
||||
yield [\DateTime::class];
|
||||
yield [\DateTimeImmutable::class];
|
||||
yield [FooDateTime::class];
|
||||
}
|
||||
|
||||
public function testUnsupportedArgument()
|
||||
{
|
||||
$resolver = new DateTimeValueResolver();
|
||||
$input = new ArrayInput(['created-at' => 'now'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (string $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$this->assertSame([], $resolver->resolve('createdAt', $input, $member));
|
||||
}
|
||||
|
||||
#[DataProvider('getTimeZones')]
|
||||
public function testFullDate(string $timezone, bool $withClock)
|
||||
{
|
||||
date_default_timezone_set($withClock ? 'UTC' : $timezone);
|
||||
$resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null);
|
||||
|
||||
$input = new ArrayInput(['created-at' => '2012-07-21 00:00:00'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (\DateTimeImmutable $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
|
||||
$this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone');
|
||||
$this->assertEquals('2012-07-21 00:00:00', $results[0]->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
#[DataProvider('getTimeZones')]
|
||||
public function testUnixTimestamp(string $timezone, bool $withClock)
|
||||
{
|
||||
date_default_timezone_set($withClock ? 'UTC' : $timezone);
|
||||
$resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null);
|
||||
|
||||
$input = new ArrayInput(['created-at' => '989541720'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (\DateTimeImmutable $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
|
||||
$this->assertSame('+00:00', $results[0]->getTimezone()->getName(), 'Timestamps are UTC');
|
||||
$this->assertEquals('2001-05-11 00:42:00', $results[0]->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testNullableWithEmptyArgument()
|
||||
{
|
||||
$resolver = new DateTimeValueResolver();
|
||||
$input = new ArrayInput(['created-at' => ''], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (?\DateTimeImmutable $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertNull($results[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<\DateTimeInterface> $class
|
||||
*/
|
||||
#[DataProvider('getClasses')]
|
||||
public function testNow(string $class)
|
||||
{
|
||||
date_default_timezone_set($timezone = 'Pacific/Honolulu');
|
||||
$resolver = new DateTimeValueResolver();
|
||||
|
||||
$input = new ArrayInput([], new InputDefinition([
|
||||
new InputArgument('created-at', InputArgument::OPTIONAL),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(\DateTimeInterface $createdAt)
|
||||
{
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
|
||||
// Replace type with the class we're testing
|
||||
$function = eval(\sprintf('return fn (%s $createdAt) => null;', $class));
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf($class, $results[0]);
|
||||
$this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone');
|
||||
$this->assertEquals('0', $results[0]->diff(new \DateTimeImmutable())->format('%s'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<\DateTimeInterface> $class
|
||||
*/
|
||||
#[DataProvider('getClasses')]
|
||||
public function testNowWithClock(string $class)
|
||||
{
|
||||
date_default_timezone_set('Pacific/Honolulu');
|
||||
$clock = new MockClock('2022-02-20 22:20:02');
|
||||
$resolver = new DateTimeValueResolver($clock);
|
||||
|
||||
$input = new ArrayInput([], new InputDefinition([
|
||||
new InputArgument('created-at', InputArgument::OPTIONAL),
|
||||
]));
|
||||
|
||||
$function = eval(\sprintf('return fn (%s $createdAt) => null;', $class));
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf($class, $results[0]);
|
||||
$this->assertSame('UTC', $results[0]->getTimezone()->getName(), 'Default timezone');
|
||||
$this->assertEquals($clock->now(), $results[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<\DateTimeInterface> $class
|
||||
*/
|
||||
#[DataProvider('getClasses')]
|
||||
public function testPreviouslyConvertedArgument(string $class)
|
||||
{
|
||||
$resolver = new DateTimeValueResolver();
|
||||
$datetime = new \DateTimeImmutable();
|
||||
$input = new ArrayInput(['created-at' => $datetime], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = eval(\sprintf('return fn (%s $createdAt) => null;', $class));
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertEquals($datetime, $results[0], 'The value is the same, but the class can be modified.');
|
||||
$this->assertInstanceOf($class, $results[0]);
|
||||
}
|
||||
|
||||
public function testCustomClass()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
$resolver = new DateTimeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['created-at' => '2016-09-08 00:00:00'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (FooDateTime $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(FooDateTime::class, $results[0]);
|
||||
$this->assertEquals('2016-09-08 00:00:00+00:00', $results[0]->format('Y-m-d H:i:sP'));
|
||||
}
|
||||
|
||||
#[DataProvider('getTimeZones')]
|
||||
public function testDateTimeImmutable(string $timezone, bool $withClock)
|
||||
{
|
||||
date_default_timezone_set($withClock ? 'UTC' : $timezone);
|
||||
$resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null);
|
||||
|
||||
$input = new ArrayInput(['created-at' => '2016-09-08 00:00:00 +05:00'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$function = static fn (\DateTimeImmutable $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
|
||||
$this->assertSame('+05:00', $results[0]->getTimezone()->getName(), 'Input timezone');
|
||||
$this->assertEquals('2016-09-08 00:00:00', $results[0]->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
#[DataProvider('getTimeZones')]
|
||||
public function testWithFormat(string $timezone, bool $withClock)
|
||||
{
|
||||
date_default_timezone_set($withClock ? 'UTC' : $timezone);
|
||||
$resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null);
|
||||
|
||||
$input = new ArrayInput(['created-at' => '09-08-16 12:34:56'], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[MapDateTime(format: 'm-d-y H:i:s')]
|
||||
\DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
|
||||
$this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone');
|
||||
$this->assertEquals('2016-09-08 12:34:56', $results[0]->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testWithOption()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
$resolver = new DateTimeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--created-at' => '2016-09-08 00:00:00'], new InputDefinition([
|
||||
new InputOption('created-at'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[MapDateTime(option: 'created-at')]
|
||||
\DateTimeImmutable $createdAt,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$results = $resolver->resolve('createdAt', $input, $member);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
|
||||
$this->assertEquals('2016-09-08 00:00:00', $results[0]->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public static function provideInvalidDates()
|
||||
{
|
||||
return [
|
||||
'invalid date' => ['Invalid DateTime Format'],
|
||||
'invalid format' => ['2012-07-21', 'd.m.Y'],
|
||||
'invalid ymd format' => ['2012-21-07', 'Y-m-d'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideInvalidDates')]
|
||||
public function testRuntimeException(string $value, ?string $format = null)
|
||||
{
|
||||
$resolver = new DateTimeValueResolver();
|
||||
|
||||
$input = new ArrayInput(['created-at' => $value], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
]));
|
||||
|
||||
if ($format) {
|
||||
$command = eval(\sprintf('return new class {
|
||||
public function __invoke(
|
||||
#[\\Symfony\\Component\\Console\\Attribute\\MapDateTime(format: "%s")]
|
||||
\\DateTimeImmutable $createdAt
|
||||
) {}
|
||||
};', $format));
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
} else {
|
||||
$function = static fn (\DateTimeImmutable $createdAt) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
}
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Invalid date given for parameter "$createdAt".');
|
||||
|
||||
$resolver->resolve('createdAt', $input, $member);
|
||||
}
|
||||
}
|
||||
|
||||
class FooDateTime extends \DateTimeImmutable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\DefaultValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
|
||||
class DefaultValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveParameterWithDefaultValue()
|
||||
{
|
||||
$resolver = new DefaultValueResolver();
|
||||
$input = new ArrayInput([]);
|
||||
|
||||
$function = static fn (string $name = 'default') => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('name', $input, $member);
|
||||
|
||||
$this->assertSame(['default'], $result);
|
||||
}
|
||||
|
||||
public function testResolveNullableParameterWithoutDefaultValue()
|
||||
{
|
||||
$resolver = new DefaultValueResolver();
|
||||
$input = new ArrayInput([]);
|
||||
|
||||
$function = static fn (?string $name) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('name', $input, $member);
|
||||
|
||||
$this->assertSame([null], $result);
|
||||
}
|
||||
|
||||
public function testResolveVariadicParameterReturnsEmpty()
|
||||
{
|
||||
$resolver = new DefaultValueResolver();
|
||||
$input = new ArrayInput([]);
|
||||
|
||||
$function = static fn (string ...$names) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('names', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolveRequiredParameterWithoutDefaultReturnsEmpty()
|
||||
{
|
||||
$resolver = new DefaultValueResolver();
|
||||
$input = new ArrayInput([]);
|
||||
|
||||
$function = static fn (string $name) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('name', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\BackedEnumValueResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\BuiltinTypeValueResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\DateTimeValueResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\MapInputValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class MapInputValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveMapInput()
|
||||
{
|
||||
$resolver = new MapInputValueResolver(new BuiltinTypeValueResolver(), new BackedEnumValueResolver(), new DateTimeValueResolver());
|
||||
|
||||
$input = new ArrayInput(['username' => 'john', '--email' => 'john@example.com'], new InputDefinition([
|
||||
new InputArgument('username'),
|
||||
new InputOption('email'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[MapInput]
|
||||
DummyInput $input,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('input', $input, $member);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(DummyInput::class, $result[0]);
|
||||
$this->assertSame('john', $result[0]->username);
|
||||
$this->assertSame('john@example.com', $result[0]->email);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWithoutAttribute()
|
||||
{
|
||||
$resolver = new MapInputValueResolver(new BuiltinTypeValueResolver(), new BackedEnumValueResolver(), new DateTimeValueResolver());
|
||||
|
||||
$input = new ArrayInput(['username' => 'john'], new InputDefinition([
|
||||
new InputArgument('username'),
|
||||
]));
|
||||
|
||||
$function = static fn (string $username) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('username', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveBuiltinTypes()
|
||||
{
|
||||
$resolver = new MapInputValueResolver(new BuiltinTypeValueResolver(), new BackedEnumValueResolver(), new DateTimeValueResolver());
|
||||
|
||||
$input = new ArrayInput(['count' => '5'], new InputDefinition([
|
||||
new InputArgument('count'),
|
||||
]));
|
||||
|
||||
$function = static fn (int $count) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('count', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolvesDateTimeAndBackedEnum()
|
||||
{
|
||||
$resolver = new MapInputValueResolver(new BuiltinTypeValueResolver(), new BackedEnumValueResolver(), new DateTimeValueResolver());
|
||||
|
||||
$input = new ArrayInput([
|
||||
'created-at' => '2024-01-15',
|
||||
'--status' => 'active',
|
||||
], new InputDefinition([
|
||||
new InputArgument('created-at'),
|
||||
new InputOption('status'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[MapInput]
|
||||
DummyInputWithDateTimeAndEnum $input,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('input', $input, $member);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(DummyInputWithDateTimeAndEnum::class, $result[0]);
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $result[0]->createdAt);
|
||||
$this->assertSame('2024-01-15', $result[0]->createdAt->format('Y-m-d'));
|
||||
$this->assertSame(DummyStatus::Active, $result[0]->status);
|
||||
}
|
||||
}
|
||||
|
||||
class DummyInput
|
||||
{
|
||||
#[Argument]
|
||||
public string $username;
|
||||
|
||||
#[Option]
|
||||
public ?string $email = null;
|
||||
}
|
||||
|
||||
class DummyInputWithDateTimeAndEnum
|
||||
{
|
||||
#[Argument]
|
||||
public \DateTimeImmutable $createdAt;
|
||||
|
||||
#[Option]
|
||||
public DummyStatus $status = DummyStatus::Pending;
|
||||
}
|
||||
|
||||
enum DummyStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Active = 'active';
|
||||
case Inactive = 'inactive';
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\NearMissValueResolverException;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ServiceValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
class ServiceValueResolverTest extends TestCase
|
||||
{
|
||||
public function testDoNotSupportWhenCommandDoesNotExist()
|
||||
{
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([]));
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$this->assertSame([], $resolver->resolve('dummy', $input, $member));
|
||||
}
|
||||
|
||||
public function testExistingCommand()
|
||||
{
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
'app:test' => static fn () => new ServiceLocator([
|
||||
'dummy' => static fn () => new DummyService(),
|
||||
]),
|
||||
]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('dummy', $input, $member);
|
||||
|
||||
$this->assertEquals([new DummyService()], $result);
|
||||
}
|
||||
|
||||
public function testServiceLocatorPatternTakesPriorityOverTypeResolution()
|
||||
{
|
||||
$serviceA = new DummyService();
|
||||
$serviceB = new DummyService();
|
||||
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
'app:test' => static fn () => new ServiceLocator([
|
||||
'dummy' => static fn () => $serviceA,
|
||||
]),
|
||||
DummyService::class => static fn () => $serviceB,
|
||||
]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('dummy', $input, $member);
|
||||
|
||||
$this->assertSame([$serviceA], $result);
|
||||
}
|
||||
|
||||
public function testFallbackToTypeBasedResolution()
|
||||
{
|
||||
$service = new DummyService();
|
||||
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
DummyService::class => static fn () => $service,
|
||||
]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('dummy', $input, $member);
|
||||
|
||||
$this->assertSame([$service], $result);
|
||||
}
|
||||
|
||||
public function testTypeResolutionReturnsEmptyForBuiltinTypes()
|
||||
{
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (string $name) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('name', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testTypeResolutionReturnsEmptyWhenServiceDoesNotExist()
|
||||
{
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('dummy', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testThrowsNearMissExceptionWhenServiceExistsButWrongType()
|
||||
{
|
||||
$this->expectException(NearMissValueResolverException::class);
|
||||
$this->expectExceptionMessage('Service "Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver\DummyService" exists in the container but is not an instance of "Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver\DummyService".');
|
||||
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
DummyService::class => static fn () => new \stdClass(),
|
||||
]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('dummy', $input, $member));
|
||||
}
|
||||
|
||||
public function testThrowsNearMissExceptionOnServiceLocatorError()
|
||||
{
|
||||
$this->expectException(NearMissValueResolverException::class);
|
||||
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
'app:test' => static fn () => new ServiceLocator([
|
||||
'dummy' => static fn () => throw new \RuntimeException('Service initialization failed'),
|
||||
]),
|
||||
]));
|
||||
|
||||
$input = new ArrayInput(['app:test'], new InputDefinition([
|
||||
new InputArgument('command'),
|
||||
]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('dummy', $input, $member));
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWhenNoCommandArgument()
|
||||
{
|
||||
$resolver = new ServiceValueResolver(new ServiceLocator([
|
||||
'app:test' => static fn () => new ServiceLocator([
|
||||
'dummy' => static fn () => new DummyService(),
|
||||
]),
|
||||
]));
|
||||
|
||||
$input = new ArrayInput([], new InputDefinition([]));
|
||||
|
||||
$function = static fn (DummyService $dummy) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = $resolver->resolve('dummy', $input, $member);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
|
||||
class DummyService
|
||||
{
|
||||
}
|
||||
253
Tests/ArgumentResolver/ValueResolver/UidValueResolverTest.php
Normal file
253
Tests/ArgumentResolver/ValueResolver/UidValueResolverTest.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\UidValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Component\Uid\UuidV4;
|
||||
|
||||
class UidValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveUuidArgument()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
$input = new ArrayInput(['id' => $uuid], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
Uuid $id,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(Uuid::class, $result[0]);
|
||||
$this->assertSame($uuid, (string) $result[0]);
|
||||
}
|
||||
|
||||
public function testResolveUlidArgument()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
$ulid = '01ARZ3NDEKTSV4RRFFQ69G5FAV';
|
||||
|
||||
$input = new ArrayInput(['id' => $ulid], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
Ulid $id,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(Ulid::class, $result[0]);
|
||||
$this->assertSame($ulid, (string) $result[0]);
|
||||
}
|
||||
|
||||
public function testResolveUuidOption()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
$input = new ArrayInput(['--id' => $uuid], new InputDefinition([
|
||||
new InputOption('id', null, InputOption::VALUE_REQUIRED),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
?Uuid $id = null,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(Uuid::class, $result[0]);
|
||||
$this->assertSame($uuid, (string) $result[0]);
|
||||
}
|
||||
|
||||
public function testArgumentThrowsOnInvalidUid()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The uid for the "id" argument is invalid.');
|
||||
|
||||
$resolver = new UidValueResolver();
|
||||
|
||||
$input = new ArrayInput(['id' => 'not-a-valid-uuid'], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
Uuid $id,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
}
|
||||
|
||||
public function testOptionThrowsOnInvalidUid()
|
||||
{
|
||||
$this->expectException(InvalidOptionException::class);
|
||||
$this->expectExceptionMessage('The uid for the "--id" option is invalid.');
|
||||
|
||||
$resolver = new UidValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--id' => 'not-a-valid-uuid'], new InputDefinition([
|
||||
new InputOption('id', null, InputOption::VALUE_REQUIRED),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
?Uuid $id = null,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
}
|
||||
|
||||
public function testReturnsNullWhenArgumentIsNull()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
|
||||
$input = new ArrayInput(['id' => null], new InputDefinition([
|
||||
new InputArgument('id', InputArgument::OPTIONAL),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
?Uuid $id = null,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertSame([null], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveNonUidType()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
|
||||
$input = new ArrayInput(['id' => '123'], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $id,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWithoutAttribute()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
|
||||
$input = new ArrayInput(['id' => '550e8400-e29b-41d4-a716-446655440000'], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$function = static fn (Uuid $id) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testResolveSpecificUuidVersion()
|
||||
{
|
||||
$resolver = new UidValueResolver();
|
||||
$uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
$input = new ArrayInput(['id' => $uuid], new InputDefinition([
|
||||
new InputArgument('id'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
UuidV4 $id,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('id', $input, $member));
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(UuidV4::class, $result[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\ArgumentResolver\ValueResolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\VariadicValueResolver;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class VariadicValueResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveVariadicArgument()
|
||||
{
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['files' => ['file1.txt', 'file2.txt', 'file3.txt']], new InputDefinition([
|
||||
new InputArgument('files', InputArgument::IS_ARRAY),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string ...$files,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('files', $input, $member));
|
||||
|
||||
$this->assertSame(['file1.txt', 'file2.txt', 'file3.txt'], $result);
|
||||
}
|
||||
|
||||
public function testResolveVariadicOption()
|
||||
{
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--tags' => ['foo', 'bar', 'baz']], new InputDefinition([
|
||||
new InputOption('tags', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
string ...$tags,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('tags', $input, $member));
|
||||
|
||||
$this->assertSame(['foo', 'bar', 'baz'], $result);
|
||||
}
|
||||
|
||||
public function testResolveEmptyVariadicArgument()
|
||||
{
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['files' => []], new InputDefinition([
|
||||
new InputArgument('files', InputArgument::IS_ARRAY | InputArgument::OPTIONAL),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string ...$files,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('files', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveNonVariadicParameter()
|
||||
{
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['name' => 'john'], new InputDefinition([
|
||||
new InputArgument('name'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string $name,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('name', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotResolveWithoutAttribute()
|
||||
{
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['files' => ['file1.txt', 'file2.txt']], new InputDefinition([
|
||||
new InputArgument('files', InputArgument::IS_ARRAY),
|
||||
]));
|
||||
|
||||
$function = static fn (string ...$files) => null;
|
||||
$reflection = new \ReflectionFunction($function);
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
$result = iterator_to_array($resolver->resolve('files', $input, $member));
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testThrowsWhenArgumentValueIsNotArray()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The action argument "...$files" is required to be an array');
|
||||
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['files' => 'single-value'], new InputDefinition([
|
||||
new InputArgument('files'),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Argument]
|
||||
string ...$files,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('files', $input, $member));
|
||||
}
|
||||
|
||||
public function testThrowsWhenOptionValueIsNotArray()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The action argument "...$tags" is required to be an array');
|
||||
|
||||
$resolver = new VariadicValueResolver();
|
||||
|
||||
$input = new ArrayInput(['--tags' => 'single-value'], new InputDefinition([
|
||||
new InputOption('tags', null, InputOption::VALUE_REQUIRED),
|
||||
]));
|
||||
|
||||
$command = new class {
|
||||
public function __invoke(
|
||||
#[Option]
|
||||
string ...$tags,
|
||||
) {
|
||||
}
|
||||
};
|
||||
$reflection = new \ReflectionMethod($command, '__invoke');
|
||||
$parameter = $reflection->getParameters()[0];
|
||||
$member = new ReflectionMember($parameter);
|
||||
|
||||
iterator_to_array($resolver->resolve('tags', $input, $member));
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,11 @@ use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
@@ -30,6 +33,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand;
|
||||
|
||||
class InvokableCommandTest extends TestCase
|
||||
@@ -230,26 +234,6 @@ class InvokableCommandTest extends TestCase
|
||||
$command->run(new ArrayInput(['--enum' => 'incorrect']), new NullOutput());
|
||||
}
|
||||
|
||||
public function testInvalidArgumentType()
|
||||
{
|
||||
$command = new Command('foo');
|
||||
$command->setCode(static function (#[Argument] object $any) {});
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
$command->getDefinition();
|
||||
}
|
||||
|
||||
public function testInvalidOptionType()
|
||||
{
|
||||
$command = new Command('foo');
|
||||
$command->setCode(static function (#[Option] ?object $any = null) {});
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
$command->getDefinition();
|
||||
}
|
||||
|
||||
public function testExecuteHasPriorityOverInvokeMethod()
|
||||
{
|
||||
$command = new class extends Command {
|
||||
@@ -494,6 +478,79 @@ class InvokableCommandTest extends TestCase
|
||||
$command->run(new ArrayInput([]), new NullOutput());
|
||||
}
|
||||
|
||||
public function testDefaultArgumentResolversWithoutApplication()
|
||||
{
|
||||
$command = new Command('foo');
|
||||
$command->setCode(static function (
|
||||
\DateTime $date,
|
||||
#[Argument] string $name = 'default',
|
||||
): int {
|
||||
Assert::assertInstanceOf(\DateTime::class, $date);
|
||||
Assert::assertSame('test', $name);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
$tester->execute(['name' => 'test']);
|
||||
|
||||
$tester->assertCommandIsSuccessful();
|
||||
}
|
||||
|
||||
public function testCustomArgumentResolverViaApplication()
|
||||
{
|
||||
$customArgumentResolver = new ArgumentResolver([
|
||||
new CustomTypeValueResolver(),
|
||||
...ArgumentResolver::getDefaultArgumentValueResolvers(),
|
||||
]);
|
||||
|
||||
$application = new Application();
|
||||
$application->setArgumentResolver($customArgumentResolver);
|
||||
|
||||
$command = new Command('foo');
|
||||
$command->setCode(static function (
|
||||
CustomType $custom,
|
||||
#[Argument] string $name = 'default',
|
||||
): int {
|
||||
Assert::assertInstanceOf(CustomType::class, $custom);
|
||||
Assert::assertSame('resolved:from-app-test', $custom->value);
|
||||
Assert::assertSame('app-test', $name);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
$application->addCommand($command);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
$tester->execute(['name' => 'app-test']);
|
||||
|
||||
$tester->assertCommandIsSuccessful();
|
||||
}
|
||||
|
||||
public function testCommandInjection()
|
||||
{
|
||||
$application = new Application();
|
||||
|
||||
$command = new Command('test-cmd');
|
||||
$command->setCode(static function (
|
||||
Command $cmd,
|
||||
#[Argument] string $arg = 'default',
|
||||
): int {
|
||||
Assert::assertInstanceOf(Command::class, $cmd);
|
||||
Assert::assertSame('test-cmd', $cmd->getName());
|
||||
Assert::assertSame('value', $arg);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
$application->addCommand($command);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
$tester->execute(['arg' => 'value']);
|
||||
|
||||
$tester->assertCommandIsSuccessful();
|
||||
}
|
||||
|
||||
public function getSuggestedRoles(CompletionInput $input): array
|
||||
{
|
||||
return ['ROLE_ADMIN', 'ROLE_USER'];
|
||||
@@ -505,3 +562,26 @@ enum StringEnum: string
|
||||
case Image = 'image';
|
||||
case Video = 'video';
|
||||
}
|
||||
|
||||
class CustomType
|
||||
{
|
||||
public function __construct(public string $value)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTypeValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || CustomType::class !== $type->getName()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$name = $input->hasArgument('name') ? $input->getArgument('name') : 'default';
|
||||
|
||||
yield new CustomType('resolved:from-'.$name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\DependencyInjection;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\DependencyInjection\RegisterCommandArgumentLocatorsPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
class RegisterCommandArgumentLocatorsPassTest extends TestCase
|
||||
{
|
||||
public function testProcessWithoutServiceResolver()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testProcessWithServiceArguments()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('logger', LoggerInterface::class);
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(CommandWithServiceArguments::class);
|
||||
$command->setAutowired(true);
|
||||
$command->addTag('console.command', ['command' => 'test:command']);
|
||||
$command->addTag('console.command.service_arguments');
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
$this->assertInstanceOf(Reference::class, $commandLocatorRef);
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
$this->assertSame(ServiceLocator::class, $commandLocator->getClass());
|
||||
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
$this->assertArrayHasKey('test:command', $commands);
|
||||
}
|
||||
|
||||
public function testProcessWithManualArgumentMapping()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('my.logger', LoggerInterface::class);
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(CommandWithServiceArguments::class);
|
||||
$command->addTag('console.command', ['command' => 'test:command']);
|
||||
$command->addTag('console.command.service_arguments', [
|
||||
'argument' => 'logger',
|
||||
'id' => 'my.logger',
|
||||
]);
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
|
||||
$this->assertArrayHasKey('test:command', $commands);
|
||||
}
|
||||
|
||||
public function testProcessSkipsInputOutputParameters()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(CommandWithInputOutput::class);
|
||||
$command->setAutowired(true);
|
||||
$command->addTag('console.command', ['command' => 'test:command']);
|
||||
$command->addTag('console.command.service_arguments');
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
|
||||
$this->assertArrayNotHasKey('test:command', $commands);
|
||||
}
|
||||
|
||||
public function testProcessWithMultipleMethods()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('logger', LoggerInterface::class);
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(MultiMethodCommand::class);
|
||||
$command->setAutowired(true);
|
||||
$command->addTag('console.command', ['command' => 'test:cmd1', 'method' => 'cmd1']);
|
||||
$command->addTag('console.command', ['command' => 'test:cmd2', 'method' => 'cmd2']);
|
||||
$command->addTag('console.command.service_arguments');
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
|
||||
$this->assertArrayHasKey('test:cmd1', $commands);
|
||||
$this->assertArrayHasKey('test:cmd2', $commands);
|
||||
}
|
||||
|
||||
public function testProcessThrowsOnMissingArgumentAttribute()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(CommandWithServiceArguments::class);
|
||||
$command->addTag('console.command', ['command' => 'test:command']);
|
||||
$command->addTag('console.command.service_arguments', ['argument' => 'logger']);
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Missing "id" attribute');
|
||||
|
||||
$pass->process($container);
|
||||
}
|
||||
|
||||
public function testProcessThrowsOnMissingIdAttribute()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$command = new Definition(CommandWithServiceArguments::class);
|
||||
$command->addTag('console.command', ['command' => 'test:command']);
|
||||
$command->addTag('console.command.service_arguments', ['id' => 'my.logger']);
|
||||
$container->setDefinition('test.command', $command);
|
||||
|
||||
$pass = new RegisterCommandArgumentLocatorsPass();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Missing "argument" attribute');
|
||||
|
||||
$pass->process($container);
|
||||
}
|
||||
}
|
||||
|
||||
class CommandWithServiceArguments
|
||||
{
|
||||
public function __invoke(LoggerInterface $logger): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class CommandWithInputOutput
|
||||
{
|
||||
public function __invoke(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class MultiMethodCommand
|
||||
{
|
||||
public function cmd1(LoggerInterface $logger): void
|
||||
{
|
||||
}
|
||||
|
||||
public function cmd2(LoggerInterface $logger): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Tests\DependencyInjection;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\DependencyInjection\RemoveEmptyCommandArgumentLocatorsPass;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
class RemoveEmptyCommandArgumentLocatorsPassTest extends TestCase
|
||||
{
|
||||
public function testProcessWithoutServiceResolver()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$pass = new RemoveEmptyCommandArgumentLocatorsPass();
|
||||
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testProcessWithNullArgument()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('console.argument_resolver.service')->addArgument(null);
|
||||
|
||||
$pass = new RemoveEmptyCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testProcessRemovesEmptyLocators()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$emptyArgumentLocator = new Definition(ServiceLocator::class, [[]]);
|
||||
$container->setDefinition('empty_argument_locator', $emptyArgumentLocator);
|
||||
|
||||
$nonEmptyArgumentLocator = new Definition(ServiceLocator::class, [['service' => new Reference('some.service')]]);
|
||||
$container->setDefinition('non_empty_argument_locator', $nonEmptyArgumentLocator);
|
||||
|
||||
$commandLocator = new Definition(ServiceLocator::class, [[
|
||||
'empty:command' => new ServiceClosureArgument(new Reference('empty_argument_locator')),
|
||||
'non-empty:command' => new ServiceClosureArgument(new Reference('non_empty_argument_locator')),
|
||||
]]);
|
||||
$container->setDefinition('command_locator', $commandLocator);
|
||||
|
||||
$serviceResolver = new Definition('stdClass', [new Reference('command_locator')]);
|
||||
$container->setDefinition('console.argument_resolver.service', $serviceResolver);
|
||||
|
||||
$pass = new RemoveEmptyCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$commandLocatorDef = $container->getDefinition('command_locator');
|
||||
$commands = $commandLocatorDef->getArgument(0);
|
||||
|
||||
$this->assertArrayNotHasKey('empty:command', $commands);
|
||||
$this->assertArrayHasKey('non-empty:command', $commands);
|
||||
}
|
||||
|
||||
public function testProcessWithFactory()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$emptyArgumentLocator = new Definition(ServiceLocator::class, [[]]);
|
||||
$container->setDefinition('empty_argument_locator_inner', $emptyArgumentLocator);
|
||||
|
||||
$emptyArgumentLocatorWrapper = new Definition(ServiceLocator::class);
|
||||
$emptyArgumentLocatorWrapper->setFactory([new Reference('empty_argument_locator_inner'), 'getInstance']);
|
||||
$container->setDefinition('empty_argument_locator', $emptyArgumentLocatorWrapper);
|
||||
|
||||
$commandLocator = new Definition(ServiceLocator::class, [[
|
||||
'empty:command' => new ServiceClosureArgument(new Reference('empty_argument_locator')),
|
||||
]]);
|
||||
$container->setDefinition('command_locator_inner', $commandLocator);
|
||||
|
||||
$commandLocatorWrapper = new Definition(ServiceLocator::class);
|
||||
$commandLocatorWrapper->setFactory([new Reference('command_locator_inner'), 'getInstance']);
|
||||
$container->setDefinition('command_locator', $commandLocatorWrapper);
|
||||
|
||||
$serviceResolver = new Definition('stdClass', [new Reference('command_locator')]);
|
||||
$container->setDefinition('console.argument_resolver.service', $serviceResolver);
|
||||
|
||||
$pass = new RemoveEmptyCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$commandLocatorDef = $container->getDefinition('command_locator_inner');
|
||||
$commands = $commandLocatorDef->getArgument(0);
|
||||
|
||||
$this->assertArrayNotHasKey('empty:command', $commands);
|
||||
}
|
||||
|
||||
public function testProcessPreservesNonEmptyLocators()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$argumentLocator = new Definition(ServiceLocator::class, [['logger' => new Reference('logger')]]);
|
||||
$container->setDefinition('argument_locator', $argumentLocator);
|
||||
|
||||
$commandLocator = new Definition(ServiceLocator::class, [[
|
||||
'test:command' => new ServiceClosureArgument(new Reference('argument_locator')),
|
||||
]]);
|
||||
$container->setDefinition('command_locator', $commandLocator);
|
||||
|
||||
$serviceResolver = new Definition('stdClass', [new Reference('command_locator')]);
|
||||
$container->setDefinition('console.argument_resolver.service', $serviceResolver);
|
||||
|
||||
$pass = new RemoveEmptyCommandArgumentLocatorsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$commandLocatorDef = $container->getDefinition('command_locator');
|
||||
$commands = $commandLocatorDef->getArgument(0);
|
||||
|
||||
$this->assertArrayHasKey('test:command', $commands);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"symfony/messenger": "^7.4|^8.0",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/stopwatch": "^7.4|^8.0",
|
||||
"symfony/uid": "^7.4|^8.0",
|
||||
"symfony/var-dumper": "^7.4|^8.0"
|
||||
},
|
||||
"provide": {
|
||||
|
||||
Reference in New Issue
Block a user