InvokableCommand.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Command;
  11. use Symfony\Component\Console\Application;
  12. use Symfony\Component\Console\Attribute\Argument;
  13. use Symfony\Component\Console\Attribute\Option;
  14. use Symfony\Component\Console\Exception\LogicException;
  15. use Symfony\Component\Console\Exception\RuntimeException;
  16. use Symfony\Component\Console\Input\InputDefinition;
  17. use Symfony\Component\Console\Input\InputInterface;
  18. use Symfony\Component\Console\Output\OutputInterface;
  19. use Symfony\Component\Console\Style\SymfonyStyle;
  20. /**
  21. * Represents an invokable command.
  22. *
  23. * @author Yonel Ceruto <open@yceruto.dev>
  24. *
  25. * @internal
  26. */
  27. class InvokableCommand implements SignalableCommandInterface
  28. {
  29. private readonly \Closure $code;
  30. private readonly ?SignalableCommandInterface $signalableCommand;
  31. private readonly \ReflectionFunction $reflection;
  32. private bool $triggerDeprecations = false;
  33. public function __construct(
  34. private readonly Command $command,
  35. callable $code,
  36. ) {
  37. $this->code = $this->getClosure($code);
  38. $this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null;
  39. $this->reflection = new \ReflectionFunction($this->code);
  40. }
  41. /**
  42. * Invokes a callable with parameters generated from the input interface.
  43. */
  44. public function __invoke(InputInterface $input, OutputInterface $output): int
  45. {
  46. $statusCode = ($this->code)(...$this->getParameters($input, $output));
  47. if (!\is_int($statusCode)) {
  48. if ($this->triggerDeprecations) {
  49. trigger_deprecation('symfony/console', '7.3', \sprintf('Returning a non-integer value from the command "%s" is deprecated and will throw an exception in Symfony 8.0.', $this->command->getName()));
  50. return 0;
  51. }
  52. throw new \TypeError(\sprintf('The command "%s" must return an integer value in the "%s" method, but "%s" was returned.', $this->command->getName(), $this->reflection->getName(), get_debug_type($statusCode)));
  53. }
  54. return $statusCode;
  55. }
  56. /**
  57. * Configures the input definition from an invokable-defined function.
  58. *
  59. * Processes the parameters of the reflection function to extract and
  60. * add arguments or options to the provided input definition.
  61. */
  62. public function configure(InputDefinition $definition): void
  63. {
  64. foreach ($this->reflection->getParameters() as $parameter) {
  65. if ($argument = Argument::tryFrom($parameter)) {
  66. $definition->addArgument($argument->toInputArgument());
  67. } elseif ($option = Option::tryFrom($parameter)) {
  68. $definition->addOption($option->toInputOption());
  69. }
  70. }
  71. }
  72. private function getClosure(callable $code): \Closure
  73. {
  74. if (!$code instanceof \Closure) {
  75. return $code(...);
  76. }
  77. $this->triggerDeprecations = true;
  78. if (null !== (new \ReflectionFunction($code))->getClosureThis()) {
  79. return $code;
  80. }
  81. set_error_handler(static function () {});
  82. try {
  83. if ($c = \Closure::bind($code, $this->command)) {
  84. $code = $c;
  85. }
  86. } finally {
  87. restore_error_handler();
  88. }
  89. return $code;
  90. }
  91. private function getParameters(InputInterface $input, OutputInterface $output): array
  92. {
  93. $parameters = [];
  94. foreach ($this->reflection->getParameters() as $parameter) {
  95. if ($argument = Argument::tryFrom($parameter)) {
  96. $parameters[] = $argument->resolveValue($input);
  97. continue;
  98. }
  99. if ($option = Option::tryFrom($parameter)) {
  100. $parameters[] = $option->resolveValue($input);
  101. continue;
  102. }
  103. $type = $parameter->getType();
  104. if (!$type instanceof \ReflectionNamedType) {
  105. if ($this->triggerDeprecations) {
  106. trigger_deprecation('symfony/console', '7.3', \sprintf('Omitting the type declaration for the parameter "$%s" is deprecated and will throw an exception in Symfony 8.0.', $parameter->getName()));
  107. continue;
  108. }
  109. throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported.', $parameter->getName()));
  110. }
  111. $parameters[] = match ($type->getName()) {
  112. InputInterface::class => $input,
  113. OutputInterface::class => $output,
  114. SymfonyStyle::class => new SymfonyStyle($input, $output),
  115. Application::class => $this->command->getApplication(),
  116. default => throw new RuntimeException(\sprintf('Unsupported type "%s" for parameter "$%s".', $type->getName(), $parameter->getName())),
  117. };
  118. }
  119. return $parameters ?: [$input, $output];
  120. }
  121. public function getSubscribedSignals(): array
  122. {
  123. return $this->signalableCommand?->getSubscribedSignals() ?? [];
  124. }
  125. public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
  126. {
  127. return $this->signalableCommand?->handleSignal($signal, $previousExitCode) ?? false;
  128. }
  129. }