summaryrefslogtreecommitdiff
path: root/vendor/symfony/console/Command
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/symfony/console/Command
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/symfony/console/Command')
-rw-r--r--vendor/symfony/console/Command/Command.php664
-rw-r--r--vendor/symfony/console/Command/CompleteCommand.php212
-rw-r--r--vendor/symfony/console/Command/DumpCompletionCommand.php151
-rw-r--r--vendor/symfony/console/Command/HelpCommand.php76
-rw-r--r--vendor/symfony/console/Command/LazyCommand.php206
-rw-r--r--vendor/symfony/console/Command/ListCommand.php72
-rw-r--r--vendor/symfony/console/Command/LockableTrait.php74
-rw-r--r--vendor/symfony/console/Command/SignalableCommandInterface.php32
-rw-r--r--vendor/symfony/console/Command/TraceableCommand.php356
9 files changed, 1843 insertions, 0 deletions
diff --git a/vendor/symfony/console/Command/Command.php b/vendor/symfony/console/Command/Command.php
new file mode 100644
index 0000000..03da6db
--- /dev/null
+++ b/vendor/symfony/console/Command/Command.php
@@ -0,0 +1,664 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Application;
15use Symfony\Component\Console\Attribute\AsCommand;
16use Symfony\Component\Console\Completion\CompletionInput;
17use Symfony\Component\Console\Completion\CompletionSuggestions;
18use Symfony\Component\Console\Completion\Suggestion;
19use Symfony\Component\Console\Exception\ExceptionInterface;
20use Symfony\Component\Console\Exception\InvalidArgumentException;
21use Symfony\Component\Console\Exception\LogicException;
22use Symfony\Component\Console\Helper\HelperInterface;
23use Symfony\Component\Console\Helper\HelperSet;
24use Symfony\Component\Console\Input\InputArgument;
25use Symfony\Component\Console\Input\InputDefinition;
26use Symfony\Component\Console\Input\InputInterface;
27use Symfony\Component\Console\Input\InputOption;
28use Symfony\Component\Console\Output\OutputInterface;
29
30/**
31 * Base class for all commands.
32 *
33 * @author Fabien Potencier <fabien@symfony.com>
34 */
35class Command
36{
37 // see https://tldp.org/LDP/abs/html/exitcodes.html
38 public const SUCCESS = 0;
39 public const FAILURE = 1;
40 public const INVALID = 2;
41
42 private ?Application $application = null;
43 private ?string $name = null;
44 private ?string $processTitle = null;
45 private array $aliases = [];
46 private InputDefinition $definition;
47 private bool $hidden = false;
48 private string $help = '';
49 private string $description = '';
50 private ?InputDefinition $fullDefinition = null;
51 private bool $ignoreValidationErrors = false;
52 private ?\Closure $code = null;
53 private array $synopsis = [];
54 private array $usages = [];
55 private ?HelperSet $helperSet = null;
56
57 public static function getDefaultName(): ?string
58 {
59 if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
60 return $attribute[0]->newInstance()->name;
61 }
62
63 return null;
64 }
65
66 public static function getDefaultDescription(): ?string
67 {
68 if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
69 return $attribute[0]->newInstance()->description;
70 }
71
72 return null;
73 }
74
75 /**
76 * @param string|null $name The name of the command; passing null means it must be set in configure()
77 *
78 * @throws LogicException When the command name is empty
79 */
80 public function __construct(?string $name = null)
81 {
82 $this->definition = new InputDefinition();
83
84 if (null === $name && null !== $name = static::getDefaultName()) {
85 $aliases = explode('|', $name);
86
87 if ('' === $name = array_shift($aliases)) {
88 $this->setHidden(true);
89 $name = array_shift($aliases);
90 }
91
92 $this->setAliases($aliases);
93 }
94
95 if (null !== $name) {
96 $this->setName($name);
97 }
98
99 if ('' === $this->description) {
100 $this->setDescription(static::getDefaultDescription() ?? '');
101 }
102
103 $this->configure();
104 }
105
106 /**
107 * Ignores validation errors.
108 *
109 * This is mainly useful for the help command.
110 */
111 public function ignoreValidationErrors(): void
112 {
113 $this->ignoreValidationErrors = true;
114 }
115
116 public function setApplication(?Application $application): void
117 {
118 $this->application = $application;
119 if ($application) {
120 $this->setHelperSet($application->getHelperSet());
121 } else {
122 $this->helperSet = null;
123 }
124
125 $this->fullDefinition = null;
126 }
127
128 public function setHelperSet(HelperSet $helperSet): void
129 {
130 $this->helperSet = $helperSet;
131 }
132
133 /**
134 * Gets the helper set.
135 */
136 public function getHelperSet(): ?HelperSet
137 {
138 return $this->helperSet;
139 }
140
141 /**
142 * Gets the application instance for this command.
143 */
144 public function getApplication(): ?Application
145 {
146 return $this->application;
147 }
148
149 /**
150 * Checks whether the command is enabled or not in the current environment.
151 *
152 * Override this to check for x or y and return false if the command cannot
153 * run properly under the current conditions.
154 */
155 public function isEnabled(): bool
156 {
157 return true;
158 }
159
160 /**
161 * Configures the current command.
162 *
163 * @return void
164 */
165 protected function configure()
166 {
167 }
168
169 /**
170 * Executes the current command.
171 *
172 * This method is not abstract because you can use this class
173 * as a concrete class. In this case, instead of defining the
174 * execute() method, you set the code to execute by passing
175 * a Closure to the setCode() method.
176 *
177 * @return int 0 if everything went fine, or an exit code
178 *
179 * @throws LogicException When this abstract method is not implemented
180 *
181 * @see setCode()
182 */
183 protected function execute(InputInterface $input, OutputInterface $output): int
184 {
185 throw new LogicException('You must override the execute() method in the concrete command class.');
186 }
187
188 /**
189 * Interacts with the user.
190 *
191 * This method is executed before the InputDefinition is validated.
192 * This means that this is the only place where the command can
193 * interactively ask for values of missing required arguments.
194 *
195 * @return void
196 */
197 protected function interact(InputInterface $input, OutputInterface $output)
198 {
199 }
200
201 /**
202 * Initializes the command after the input has been bound and before the input
203 * is validated.
204 *
205 * This is mainly useful when a lot of commands extends one main command
206 * where some things need to be initialized based on the input arguments and options.
207 *
208 * @see InputInterface::bind()
209 * @see InputInterface::validate()
210 *
211 * @return void
212 */
213 protected function initialize(InputInterface $input, OutputInterface $output)
214 {
215 }
216
217 /**
218 * Runs the command.
219 *
220 * The code to execute is either defined directly with the
221 * setCode() method or by overriding the execute() method
222 * in a sub-class.
223 *
224 * @return int The command exit code
225 *
226 * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
227 *
228 * @see setCode()
229 * @see execute()
230 */
231 public function run(InputInterface $input, OutputInterface $output): int
232 {
233 // add the application arguments and options
234 $this->mergeApplicationDefinition();
235
236 // bind the input against the command specific arguments/options
237 try {
238 $input->bind($this->getDefinition());
239 } catch (ExceptionInterface $e) {
240 if (!$this->ignoreValidationErrors) {
241 throw $e;
242 }
243 }
244
245 $this->initialize($input, $output);
246
247 if (null !== $this->processTitle) {
248 if (\function_exists('cli_set_process_title')) {
249 if (!@cli_set_process_title($this->processTitle)) {
250 if ('Darwin' === \PHP_OS) {
251 $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
252 } else {
253 cli_set_process_title($this->processTitle);
254 }
255 }
256 } elseif (\function_exists('setproctitle')) {
257 setproctitle($this->processTitle);
258 } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
259 $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
260 }
261 }
262
263 if ($input->isInteractive()) {
264 $this->interact($input, $output);
265 }
266
267 // The command name argument is often omitted when a command is executed directly with its run() method.
268 // It would fail the validation if we didn't make sure the command argument is present,
269 // since it's required by the application.
270 if ($input->hasArgument('command') && null === $input->getArgument('command')) {
271 $input->setArgument('command', $this->getName());
272 }
273
274 $input->validate();
275
276 if ($this->code) {
277 $statusCode = ($this->code)($input, $output);
278 } else {
279 $statusCode = $this->execute($input, $output);
280 }
281
282 return is_numeric($statusCode) ? (int) $statusCode : 0;
283 }
284
285 /**
286 * Supplies suggestions when resolving possible completion options for input (e.g. option or argument).
287 */
288 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
289 {
290 $definition = $this->getDefinition();
291 if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
292 $definition->getOption($input->getCompletionName())->complete($input, $suggestions);
293 } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
294 $definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
295 }
296 }
297
298 /**
299 * Sets the code to execute when running this command.
300 *
301 * If this method is used, it overrides the code defined
302 * in the execute() method.
303 *
304 * @param callable $code A callable(InputInterface $input, OutputInterface $output)
305 *
306 * @return $this
307 *
308 * @throws InvalidArgumentException
309 *
310 * @see execute()
311 */
312 public function setCode(callable $code): static
313 {
314 if ($code instanceof \Closure) {
315 $r = new \ReflectionFunction($code);
316 if (null === $r->getClosureThis()) {
317 set_error_handler(static function () {});
318 try {
319 if ($c = \Closure::bind($code, $this)) {
320 $code = $c;
321 }
322 } finally {
323 restore_error_handler();
324 }
325 }
326 } else {
327 $code = $code(...);
328 }
329
330 $this->code = $code;
331
332 return $this;
333 }
334
335 /**
336 * Merges the application definition with the command definition.
337 *
338 * This method is not part of public API and should not be used directly.
339 *
340 * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
341 *
342 * @internal
343 */
344 public function mergeApplicationDefinition(bool $mergeArgs = true): void
345 {
346 if (null === $this->application) {
347 return;
348 }
349
350 $this->fullDefinition = new InputDefinition();
351 $this->fullDefinition->setOptions($this->definition->getOptions());
352 $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
353
354 if ($mergeArgs) {
355 $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
356 $this->fullDefinition->addArguments($this->definition->getArguments());
357 } else {
358 $this->fullDefinition->setArguments($this->definition->getArguments());
359 }
360 }
361
362 /**
363 * Sets an array of argument and option instances.
364 *
365 * @return $this
366 */
367 public function setDefinition(array|InputDefinition $definition): static
368 {
369 if ($definition instanceof InputDefinition) {
370 $this->definition = $definition;
371 } else {
372 $this->definition->setDefinition($definition);
373 }
374
375 $this->fullDefinition = null;
376
377 return $this;
378 }
379
380 /**
381 * Gets the InputDefinition attached to this Command.
382 */
383 public function getDefinition(): InputDefinition
384 {
385 return $this->fullDefinition ?? $this->getNativeDefinition();
386 }
387
388 /**
389 * Gets the InputDefinition to be used to create representations of this Command.
390 *
391 * Can be overridden to provide the original command representation when it would otherwise
392 * be changed by merging with the application InputDefinition.
393 *
394 * This method is not part of public API and should not be used directly.
395 */
396 public function getNativeDefinition(): InputDefinition
397 {
398 return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
399 }
400
401 /**
402 * Adds an argument.
403 *
404 * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
405 * @param $default The default value (for InputArgument::OPTIONAL mode only)
406 * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
407 *
408 * @return $this
409 *
410 * @throws InvalidArgumentException When argument mode is not valid
411 */
412 public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
413 {
414 $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
415 $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
416
417 return $this;
418 }
419
420 /**
421 * Adds an option.
422 *
423 * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
424 * @param $mode The option mode: One of the InputOption::VALUE_* constants
425 * @param $default The default value (must be null for InputOption::VALUE_NONE)
426 * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
427 *
428 * @return $this
429 *
430 * @throws InvalidArgumentException If option mode is invalid or incompatible
431 */
432 public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
433 {
434 $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
435 $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
436
437 return $this;
438 }
439
440 /**
441 * Sets the name of the command.
442 *
443 * This method can set both the namespace and the name if
444 * you separate them by a colon (:)
445 *
446 * $command->setName('foo:bar');
447 *
448 * @return $this
449 *
450 * @throws InvalidArgumentException When the name is invalid
451 */
452 public function setName(string $name): static
453 {
454 $this->validateName($name);
455
456 $this->name = $name;
457
458 return $this;
459 }
460
461 /**
462 * Sets the process title of the command.
463 *
464 * This feature should be used only when creating a long process command,
465 * like a daemon.
466 *
467 * @return $this
468 */
469 public function setProcessTitle(string $title): static
470 {
471 $this->processTitle = $title;
472
473 return $this;
474 }
475
476 /**
477 * Returns the command name.
478 */
479 public function getName(): ?string
480 {
481 return $this->name;
482 }
483
484 /**
485 * @param bool $hidden Whether or not the command should be hidden from the list of commands
486 *
487 * @return $this
488 */
489 public function setHidden(bool $hidden = true): static
490 {
491 $this->hidden = $hidden;
492
493 return $this;
494 }
495
496 /**
497 * @return bool whether the command should be publicly shown or not
498 */
499 public function isHidden(): bool
500 {
501 return $this->hidden;
502 }
503
504 /**
505 * Sets the description for the command.
506 *
507 * @return $this
508 */
509 public function setDescription(string $description): static
510 {
511 $this->description = $description;
512
513 return $this;
514 }
515
516 /**
517 * Returns the description for the command.
518 */
519 public function getDescription(): string
520 {
521 return $this->description;
522 }
523
524 /**
525 * Sets the help for the command.
526 *
527 * @return $this
528 */
529 public function setHelp(string $help): static
530 {
531 $this->help = $help;
532
533 return $this;
534 }
535
536 /**
537 * Returns the help for the command.
538 */
539 public function getHelp(): string
540 {
541 return $this->help;
542 }
543
544 /**
545 * Returns the processed help for the command replacing the %command.name% and
546 * %command.full_name% patterns with the real values dynamically.
547 */
548 public function getProcessedHelp(): string
549 {
550 $name = $this->name;
551 $isSingleCommand = $this->application?->isSingleCommand();
552
553 $placeholders = [
554 '%command.name%',
555 '%command.full_name%',
556 ];
557 $replacements = [
558 $name,
559 $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
560 ];
561
562 return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
563 }
564
565 /**
566 * Sets the aliases for the command.
567 *
568 * @param string[] $aliases An array of aliases for the command
569 *
570 * @return $this
571 *
572 * @throws InvalidArgumentException When an alias is invalid
573 */
574 public function setAliases(iterable $aliases): static
575 {
576 $list = [];
577
578 foreach ($aliases as $alias) {
579 $this->validateName($alias);
580 $list[] = $alias;
581 }
582
583 $this->aliases = \is_array($aliases) ? $aliases : $list;
584
585 return $this;
586 }
587
588 /**
589 * Returns the aliases for the command.
590 */
591 public function getAliases(): array
592 {
593 return $this->aliases;
594 }
595
596 /**
597 * Returns the synopsis for the command.
598 *
599 * @param bool $short Whether to show the short version of the synopsis (with options folded) or not
600 */
601 public function getSynopsis(bool $short = false): string
602 {
603 $key = $short ? 'short' : 'long';
604
605 if (!isset($this->synopsis[$key])) {
606 $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
607 }
608
609 return $this->synopsis[$key];
610 }
611
612 /**
613 * Add a command usage example, it'll be prefixed with the command name.
614 *
615 * @return $this
616 */
617 public function addUsage(string $usage): static
618 {
619 if (!str_starts_with($usage, $this->name)) {
620 $usage = sprintf('%s %s', $this->name, $usage);
621 }
622
623 $this->usages[] = $usage;
624
625 return $this;
626 }
627
628 /**
629 * Returns alternative usages of the command.
630 */
631 public function getUsages(): array
632 {
633 return $this->usages;
634 }
635
636 /**
637 * Gets a helper instance by name.
638 *
639 * @throws LogicException if no HelperSet is defined
640 * @throws InvalidArgumentException if the helper is not defined
641 */
642 public function getHelper(string $name): HelperInterface
643 {
644 if (null === $this->helperSet) {
645 throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
646 }
647
648 return $this->helperSet->get($name);
649 }
650
651 /**
652 * Validates a command name.
653 *
654 * It must be non-empty and parts can optionally be separated by ":".
655 *
656 * @throws InvalidArgumentException When the name is invalid
657 */
658 private function validateName(string $name): void
659 {
660 if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
661 throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
662 }
663 }
664}
diff --git a/vendor/symfony/console/Command/CompleteCommand.php b/vendor/symfony/console/Command/CompleteCommand.php
new file mode 100644
index 0000000..38aa737
--- /dev/null
+++ b/vendor/symfony/console/Command/CompleteCommand.php
@@ -0,0 +1,212 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Attribute\AsCommand;
15use Symfony\Component\Console\Completion\CompletionInput;
16use Symfony\Component\Console\Completion\CompletionSuggestions;
17use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
18use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
19use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
20use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
21use Symfony\Component\Console\Exception\CommandNotFoundException;
22use Symfony\Component\Console\Exception\ExceptionInterface;
23use Symfony\Component\Console\Input\InputInterface;
24use Symfony\Component\Console\Input\InputOption;
25use Symfony\Component\Console\Output\OutputInterface;
26
27/**
28 * Responsible for providing the values to the shell completion.
29 *
30 * @author Wouter de Jong <wouter@wouterj.nl>
31 */
32#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
33final class CompleteCommand extends Command
34{
35 public const COMPLETION_API_VERSION = '1';
36
37 private array $completionOutputs;
38 private bool $isDebug = false;
39
40 /**
41 * @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
42 */
43 public function __construct(array $completionOutputs = [])
44 {
45 // must be set before the parent constructor, as the property value is used in configure()
46 $this->completionOutputs = $completionOutputs + [
47 'bash' => BashCompletionOutput::class,
48 'fish' => FishCompletionOutput::class,
49 'zsh' => ZshCompletionOutput::class,
50 ];
51
52 parent::__construct();
53 }
54
55 protected function configure(): void
56 {
57 $this
58 ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
59 ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
60 ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
61 ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script')
62 ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated')
63 ;
64 }
65
66 protected function initialize(InputInterface $input, OutputInterface $output): void
67 {
68 $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
69 }
70
71 protected function execute(InputInterface $input, OutputInterface $output): int
72 {
73 try {
74 // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
75 $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version');
76 if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) {
77 $message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION);
78 $this->log($message);
79
80 $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
81
82 return 126;
83 }
84
85 $shell = $input->getOption('shell');
86 if (!$shell) {
87 throw new \RuntimeException('The "--shell" option must be set.');
88 }
89
90 if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
91 throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
92 }
93
94 $completionInput = $this->createCompletionInput($input);
95 $suggestions = new CompletionSuggestions();
96
97 $this->log([
98 '',
99 '<comment>'.date('Y-m-d H:i:s').'</>',
100 '<info>Input:</> <comment>("|" indicates the cursor position)</>',
101 ' '.(string) $completionInput,
102 '<info>Command:</>',
103 ' '.(string) implode(' ', $_SERVER['argv']),
104 '<info>Messages:</>',
105 ]);
106
107 $command = $this->findCommand($completionInput, $output);
108 if (null === $command) {
109 $this->log(' No command found, completing using the Application class.');
110
111 $this->getApplication()->complete($completionInput, $suggestions);
112 } elseif (
113 $completionInput->mustSuggestArgumentValuesFor('command')
114 && $command->getName() !== $completionInput->getCompletionValue()
115 && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true)
116 ) {
117 $this->log(' No command found, completing using the Application class.');
118
119 // expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
120 $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases())));
121 } else {
122 $command->mergeApplicationDefinition();
123 $completionInput->bind($command->getDefinition());
124
125 if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
126 $this->log(' Completing option names for the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> command.');
127
128 $suggestions->suggestOptions($command->getDefinition()->getOptions());
129 } else {
130 $this->log([
131 ' Completing using the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.',
132 ' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
133 ]);
134 if (null !== $compval = $completionInput->getCompletionValue()) {
135 $this->log(' Current value: <comment>'.$compval.'</>');
136 }
137
138 $command->complete($completionInput, $suggestions);
139 }
140 }
141
142 /** @var CompletionOutputInterface $completionOutput */
143 $completionOutput = new $completionOutput();
144
145 $this->log('<info>Suggestions:</>');
146 if ($options = $suggestions->getOptionSuggestions()) {
147 $this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options)));
148 } elseif ($values = $suggestions->getValueSuggestions()) {
149 $this->log(' '.implode(' ', $values));
150 } else {
151 $this->log(' <comment>No suggestions were provided</>');
152 }
153
154 $completionOutput->write($suggestions, $output);
155 } catch (\Throwable $e) {
156 $this->log([
157 '<error>Error!</error>',
158 (string) $e,
159 ]);
160
161 if ($output->isDebug()) {
162 throw $e;
163 }
164
165 return 2;
166 }
167
168 return 0;
169 }
170
171 private function createCompletionInput(InputInterface $input): CompletionInput
172 {
173 $currentIndex = $input->getOption('current');
174 if (!$currentIndex || !ctype_digit($currentIndex)) {
175 throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
176 }
177
178 $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
179
180 try {
181 $completionInput->bind($this->getApplication()->getDefinition());
182 } catch (ExceptionInterface) {
183 }
184
185 return $completionInput;
186 }
187
188 private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
189 {
190 try {
191 $inputName = $completionInput->getFirstArgument();
192 if (null === $inputName) {
193 return null;
194 }
195
196 return $this->getApplication()->find($inputName);
197 } catch (CommandNotFoundException) {
198 }
199
200 return null;
201 }
202
203 private function log($messages): void
204 {
205 if (!$this->isDebug) {
206 return;
207 }
208
209 $commandName = basename($_SERVER['argv'][0]);
210 file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND);
211 }
212}
diff --git a/vendor/symfony/console/Command/DumpCompletionCommand.php b/vendor/symfony/console/Command/DumpCompletionCommand.php
new file mode 100644
index 0000000..be6f545
--- /dev/null
+++ b/vendor/symfony/console/Command/DumpCompletionCommand.php
@@ -0,0 +1,151 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Attribute\AsCommand;
15use Symfony\Component\Console\Input\InputArgument;
16use Symfony\Component\Console\Input\InputInterface;
17use Symfony\Component\Console\Input\InputOption;
18use Symfony\Component\Console\Output\ConsoleOutputInterface;
19use Symfony\Component\Console\Output\OutputInterface;
20use Symfony\Component\Process\Process;
21
22/**
23 * Dumps the completion script for the current shell.
24 *
25 * @author Wouter de Jong <wouter@wouterj.nl>
26 */
27#[AsCommand(name: 'completion', description: 'Dump the shell completion script')]
28final class DumpCompletionCommand extends Command
29{
30 private array $supportedShells;
31
32 protected function configure(): void
33 {
34 $fullCommand = $_SERVER['PHP_SELF'];
35 $commandName = basename($fullCommand);
36 $fullCommand = @realpath($fullCommand) ?: $fullCommand;
37
38 $shell = $this->guessShell();
39 [$rcFile, $completionFile] = match ($shell) {
40 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
41 'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
42 default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
43 };
44
45 $supportedShells = implode(', ', $this->getSupportedShells());
46
47 $this
48 ->setHelp(<<<EOH
49The <info>%command.name%</> command dumps the shell completion script required
50to use shell autocompletion (currently, {$supportedShells} completion are supported).
51
52<comment>Static installation
53-------------------</>
54
55Dump the script to a global completion file and restart your shell:
56
57 <info>%command.full_name% {$shell} | sudo tee {$completionFile}</>
58
59Or dump the script to a local file and source it:
60
61 <info>%command.full_name% {$shell} > completion.sh</>
62
63 <comment># source the file whenever you use the project</>
64 <info>source completion.sh</>
65
66 <comment># or add this line at the end of your "{$rcFile}" file:</>
67 <info>source /path/to/completion.sh</>
68
69<comment>Dynamic installation
70--------------------</>
71
72Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):
73
74 <info>eval "$({$fullCommand} completion {$shell})"</>
75EOH
76 )
77 ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...))
78 ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
79 ;
80 }
81
82 protected function execute(InputInterface $input, OutputInterface $output): int
83 {
84 $commandName = basename($_SERVER['argv'][0]);
85
86 if ($input->getOption('debug')) {
87 $this->tailDebugLog($commandName, $output);
88
89 return 0;
90 }
91
92 $shell = $input->getArgument('shell') ?? self::guessShell();
93 $completionFile = __DIR__.'/../Resources/completion.'.$shell;
94 if (!file_exists($completionFile)) {
95 $supportedShells = $this->getSupportedShells();
96
97 if ($output instanceof ConsoleOutputInterface) {
98 $output = $output->getErrorOutput();
99 }
100 if ($shell) {
101 $output->writeln(sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
102 } else {
103 $output->writeln(sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
104 }
105
106 return 2;
107 }
108
109 $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));
110
111 return 0;
112 }
113
114 private static function guessShell(): string
115 {
116 return basename($_SERVER['SHELL'] ?? '');
117 }
118
119 private function tailDebugLog(string $commandName, OutputInterface $output): void
120 {
121 $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log';
122 if (!file_exists($debugFile)) {
123 touch($debugFile);
124 }
125 $process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
126 $process->run(function (string $type, string $line) use ($output): void {
127 $output->write($line);
128 });
129 }
130
131 /**
132 * @return string[]
133 */
134 private function getSupportedShells(): array
135 {
136 if (isset($this->supportedShells)) {
137 return $this->supportedShells;
138 }
139
140 $shells = [];
141
142 foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
143 if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) {
144 $shells[] = $file->getExtension();
145 }
146 }
147 sort($shells);
148
149 return $this->supportedShells = $shells;
150 }
151}
diff --git a/vendor/symfony/console/Command/HelpCommand.php b/vendor/symfony/console/Command/HelpCommand.php
new file mode 100644
index 0000000..a2a72da
--- /dev/null
+++ b/vendor/symfony/console/Command/HelpCommand.php
@@ -0,0 +1,76 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Descriptor\ApplicationDescription;
15use Symfony\Component\Console\Helper\DescriptorHelper;
16use Symfony\Component\Console\Input\InputArgument;
17use Symfony\Component\Console\Input\InputInterface;
18use Symfony\Component\Console\Input\InputOption;
19use Symfony\Component\Console\Output\OutputInterface;
20
21/**
22 * HelpCommand displays the help for a given command.
23 *
24 * @author Fabien Potencier <fabien@symfony.com>
25 */
26class HelpCommand extends Command
27{
28 private Command $command;
29
30 protected function configure(): void
31 {
32 $this->ignoreValidationErrors();
33
34 $this
35 ->setName('help')
36 ->setDefinition([
37 new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())),
38 new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
39 new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
40 ])
41 ->setDescription('Display help for a command')
42 ->setHelp(<<<'EOF'
43The <info>%command.name%</info> command displays help for a given command:
44
45 <info>%command.full_name% list</info>
46
47You can also output the help in other formats by using the <comment>--format</comment> option:
48
49 <info>%command.full_name% --format=xml list</info>
50
51To display the list of available commands, please use the <info>list</info> command.
52EOF
53 )
54 ;
55 }
56
57 public function setCommand(Command $command): void
58 {
59 $this->command = $command;
60 }
61
62 protected function execute(InputInterface $input, OutputInterface $output): int
63 {
64 $this->command ??= $this->getApplication()->find($input->getArgument('command_name'));
65
66 $helper = new DescriptorHelper();
67 $helper->describe($output, $this->command, [
68 'format' => $input->getOption('format'),
69 'raw_text' => $input->getOption('raw'),
70 ]);
71
72 unset($this->command);
73
74 return 0;
75 }
76}
diff --git a/vendor/symfony/console/Command/LazyCommand.php b/vendor/symfony/console/Command/LazyCommand.php
new file mode 100644
index 0000000..fd2c300
--- /dev/null
+++ b/vendor/symfony/console/Command/LazyCommand.php
@@ -0,0 +1,206 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Application;
15use Symfony\Component\Console\Completion\CompletionInput;
16use Symfony\Component\Console\Completion\CompletionSuggestions;
17use Symfony\Component\Console\Completion\Suggestion;
18use Symfony\Component\Console\Helper\HelperInterface;
19use Symfony\Component\Console\Helper\HelperSet;
20use Symfony\Component\Console\Input\InputDefinition;
21use Symfony\Component\Console\Input\InputInterface;
22use Symfony\Component\Console\Output\OutputInterface;
23
24/**
25 * @author Nicolas Grekas <p@tchwork.com>
26 */
27final class LazyCommand extends Command
28{
29 private \Closure|Command $command;
30
31 public function __construct(
32 string $name,
33 array $aliases,
34 string $description,
35 bool $isHidden,
36 \Closure $commandFactory,
37 private ?bool $isEnabled = true,
38 ) {
39 $this->setName($name)
40 ->setAliases($aliases)
41 ->setHidden($isHidden)
42 ->setDescription($description);
43
44 $this->command = $commandFactory;
45 }
46
47 public function ignoreValidationErrors(): void
48 {
49 $this->getCommand()->ignoreValidationErrors();
50 }
51
52 public function setApplication(?Application $application): void
53 {
54 if ($this->command instanceof parent) {
55 $this->command->setApplication($application);
56 }
57
58 parent::setApplication($application);
59 }
60
61 public function setHelperSet(HelperSet $helperSet): void
62 {
63 if ($this->command instanceof parent) {
64 $this->command->setHelperSet($helperSet);
65 }
66
67 parent::setHelperSet($helperSet);
68 }
69
70 public function isEnabled(): bool
71 {
72 return $this->isEnabled ?? $this->getCommand()->isEnabled();
73 }
74
75 public function run(InputInterface $input, OutputInterface $output): int
76 {
77 return $this->getCommand()->run($input, $output);
78 }
79
80 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
81 {
82 $this->getCommand()->complete($input, $suggestions);
83 }
84
85 public function setCode(callable $code): static
86 {
87 $this->getCommand()->setCode($code);
88
89 return $this;
90 }
91
92 /**
93 * @internal
94 */
95 public function mergeApplicationDefinition(bool $mergeArgs = true): void
96 {
97 $this->getCommand()->mergeApplicationDefinition($mergeArgs);
98 }
99
100 public function setDefinition(array|InputDefinition $definition): static
101 {
102 $this->getCommand()->setDefinition($definition);
103
104 return $this;
105 }
106
107 public function getDefinition(): InputDefinition
108 {
109 return $this->getCommand()->getDefinition();
110 }
111
112 public function getNativeDefinition(): InputDefinition
113 {
114 return $this->getCommand()->getNativeDefinition();
115 }
116
117 /**
118 * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
119 */
120 public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
121 {
122 $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
123
124 return $this;
125 }
126
127 /**
128 * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
129 */
130 public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
131 {
132 $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
133
134 return $this;
135 }
136
137 public function setProcessTitle(string $title): static
138 {
139 $this->getCommand()->setProcessTitle($title);
140
141 return $this;
142 }
143
144 public function setHelp(string $help): static
145 {
146 $this->getCommand()->setHelp($help);
147
148 return $this;
149 }
150
151 public function getHelp(): string
152 {
153 return $this->getCommand()->getHelp();
154 }
155
156 public function getProcessedHelp(): string
157 {
158 return $this->getCommand()->getProcessedHelp();
159 }
160
161 public function getSynopsis(bool $short = false): string
162 {
163 return $this->getCommand()->getSynopsis($short);
164 }
165
166 public function addUsage(string $usage): static
167 {
168 $this->getCommand()->addUsage($usage);
169
170 return $this;
171 }
172
173 public function getUsages(): array
174 {
175 return $this->getCommand()->getUsages();
176 }
177
178 public function getHelper(string $name): HelperInterface
179 {
180 return $this->getCommand()->getHelper($name);
181 }
182
183 public function getCommand(): parent
184 {
185 if (!$this->command instanceof \Closure) {
186 return $this->command;
187 }
188
189 $command = $this->command = ($this->command)();
190 $command->setApplication($this->getApplication());
191
192 if (null !== $this->getHelperSet()) {
193 $command->setHelperSet($this->getHelperSet());
194 }
195
196 $command->setName($this->getName())
197 ->setAliases($this->getAliases())
198 ->setHidden($this->isHidden())
199 ->setDescription($this->getDescription());
200
201 // Will throw if the command is not correctly initialized.
202 $command->getDefinition();
203
204 return $command;
205 }
206}
diff --git a/vendor/symfony/console/Command/ListCommand.php b/vendor/symfony/console/Command/ListCommand.php
new file mode 100644
index 0000000..61b4b1b
--- /dev/null
+++ b/vendor/symfony/console/Command/ListCommand.php
@@ -0,0 +1,72 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Descriptor\ApplicationDescription;
15use Symfony\Component\Console\Helper\DescriptorHelper;
16use Symfony\Component\Console\Input\InputArgument;
17use Symfony\Component\Console\Input\InputInterface;
18use Symfony\Component\Console\Input\InputOption;
19use Symfony\Component\Console\Output\OutputInterface;
20
21/**
22 * ListCommand displays the list of all available commands for the application.
23 *
24 * @author Fabien Potencier <fabien@symfony.com>
25 */
26class ListCommand extends Command
27{
28 protected function configure(): void
29 {
30 $this
31 ->setName('list')
32 ->setDefinition([
33 new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
34 new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
35 new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
36 new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
37 ])
38 ->setDescription('List commands')
39 ->setHelp(<<<'EOF'
40The <info>%command.name%</info> command lists all commands:
41
42 <info>%command.full_name%</info>
43
44You can also display the commands for a specific namespace:
45
46 <info>%command.full_name% test</info>
47
48You can also output the information in other formats by using the <comment>--format</comment> option:
49
50 <info>%command.full_name% --format=xml</info>
51
52It's also possible to get raw list of commands (useful for embedding command runner):
53
54 <info>%command.full_name% --raw</info>
55EOF
56 )
57 ;
58 }
59
60 protected function execute(InputInterface $input, OutputInterface $output): int
61 {
62 $helper = new DescriptorHelper();
63 $helper->describe($output, $this->getApplication(), [
64 'format' => $input->getOption('format'),
65 'raw_text' => $input->getOption('raw'),
66 'namespace' => $input->getArgument('namespace'),
67 'short' => $input->getOption('short'),
68 ]);
69
70 return 0;
71 }
72}
diff --git a/vendor/symfony/console/Command/LockableTrait.php b/vendor/symfony/console/Command/LockableTrait.php
new file mode 100644
index 0000000..f0001cc
--- /dev/null
+++ b/vendor/symfony/console/Command/LockableTrait.php
@@ -0,0 +1,74 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Exception\LogicException;
15use Symfony\Component\Lock\LockFactory;
16use Symfony\Component\Lock\LockInterface;
17use Symfony\Component\Lock\Store\FlockStore;
18use Symfony\Component\Lock\Store\SemaphoreStore;
19
20/**
21 * Basic lock feature for commands.
22 *
23 * @author Geoffrey Brier <geoffrey.brier@gmail.com>
24 */
25trait LockableTrait
26{
27 private ?LockInterface $lock = null;
28
29 private ?LockFactory $lockFactory = null;
30
31 /**
32 * Locks a command.
33 */
34 private function lock(?string $name = null, bool $blocking = false): bool
35 {
36 if (!class_exists(SemaphoreStore::class)) {
37 throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".');
38 }
39
40 if (null !== $this->lock) {
41 throw new LogicException('A lock is already in place.');
42 }
43
44 if (null === $this->lockFactory) {
45 if (SemaphoreStore::isSupported()) {
46 $store = new SemaphoreStore();
47 } else {
48 $store = new FlockStore();
49 }
50
51 $this->lockFactory = (new LockFactory($store));
52 }
53
54 $this->lock = $this->lockFactory->createLock($name ?: $this->getName());
55 if (!$this->lock->acquire($blocking)) {
56 $this->lock = null;
57
58 return false;
59 }
60
61 return true;
62 }
63
64 /**
65 * Releases the command lock if there is one.
66 */
67 private function release(): void
68 {
69 if ($this->lock) {
70 $this->lock->release();
71 $this->lock = null;
72 }
73 }
74}
diff --git a/vendor/symfony/console/Command/SignalableCommandInterface.php b/vendor/symfony/console/Command/SignalableCommandInterface.php
new file mode 100644
index 0000000..40b301d
--- /dev/null
+++ b/vendor/symfony/console/Command/SignalableCommandInterface.php
@@ -0,0 +1,32 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14/**
15 * Interface for command reacting to signal.
16 *
17 * @author Grégoire Pineau <lyrixx@lyrix.info>
18 */
19interface SignalableCommandInterface
20{
21 /**
22 * Returns the list of signals to subscribe.
23 */
24 public function getSubscribedSignals(): array;
25
26 /**
27 * The method will be called when the application is signaled.
28 *
29 * @return int|false The exit code to return or false to continue the normal execution
30 */
31 public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false;
32}
diff --git a/vendor/symfony/console/Command/TraceableCommand.php b/vendor/symfony/console/Command/TraceableCommand.php
new file mode 100644
index 0000000..9ffb68d
--- /dev/null
+++ b/vendor/symfony/console/Command/TraceableCommand.php
@@ -0,0 +1,356 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Application;
15use Symfony\Component\Console\Completion\CompletionInput;
16use Symfony\Component\Console\Completion\CompletionSuggestions;
17use Symfony\Component\Console\Helper\HelperInterface;
18use Symfony\Component\Console\Helper\HelperSet;
19use Symfony\Component\Console\Input\InputDefinition;
20use Symfony\Component\Console\Input\InputInterface;
21use Symfony\Component\Console\Output\ConsoleOutputInterface;
22use Symfony\Component\Console\Output\OutputInterface;
23use Symfony\Component\Stopwatch\Stopwatch;
24
25/**
26 * @internal
27 *
28 * @author Jules Pietri <jules@heahprod.com>
29 */
30final class TraceableCommand extends Command implements SignalableCommandInterface
31{
32 public readonly Command $command;
33 public int $exitCode;
34 public ?int $interruptedBySignal = null;
35 public bool $ignoreValidation;
36 public bool $isInteractive = false;
37 public string $duration = 'n/a';
38 public string $maxMemoryUsage = 'n/a';
39 public InputInterface $input;
40 public OutputInterface $output;
41 /** @var array<string, mixed> */
42 public array $arguments;
43 /** @var array<string, mixed> */
44 public array $options;
45 /** @var array<string, mixed> */
46 public array $interactiveInputs = [];
47 public array $handledSignals = [];
48
49 public function __construct(
50 Command $command,
51 private readonly Stopwatch $stopwatch,
52 ) {
53 if ($command instanceof LazyCommand) {
54 $command = $command->getCommand();
55 }
56
57 $this->command = $command;
58
59 // prevent call to self::getDefaultDescription()
60 $this->setDescription($command->getDescription());
61
62 parent::__construct($command->getName());
63
64 // init below enables calling {@see parent::run()}
65 [$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () {
66 return [$this->code, $this->processTitle, $this->ignoreValidationErrors];
67 }, $command, Command::class)();
68
69 if (\is_callable($code)) {
70 $this->setCode($code);
71 }
72
73 if ($processTitle) {
74 parent::setProcessTitle($processTitle);
75 }
76
77 if ($ignoreValidationErrors) {
78 parent::ignoreValidationErrors();
79 }
80
81 $this->ignoreValidation = $ignoreValidationErrors;
82 }
83
84 public function __call(string $name, array $arguments): mixed
85 {
86 return $this->command->{$name}(...$arguments);
87 }
88
89 public function getSubscribedSignals(): array
90 {
91 return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
92 }
93
94 public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
95 {
96 if (!$this->command instanceof SignalableCommandInterface) {
97 return false;
98 }
99
100 $event = $this->stopwatch->start($this->getName().'.handle_signal');
101
102 $exit = $this->command->handleSignal($signal, $previousExitCode);
103
104 $event->stop();
105
106 if (!isset($this->handledSignals[$signal])) {
107 $this->handledSignals[$signal] = [
108 'handled' => 0,
109 'duration' => 0,
110 'memory' => 0,
111 ];
112 }
113
114 ++$this->handledSignals[$signal]['handled'];
115 $this->handledSignals[$signal]['duration'] += $event->getDuration();
116 $this->handledSignals[$signal]['memory'] = max(
117 $this->handledSignals[$signal]['memory'],
118 $event->getMemory() >> 20
119 );
120
121 return $exit;
122 }
123
124 /**
125 * {@inheritdoc}
126 *
127 * Calling parent method is required to be used in {@see parent::run()}.
128 */
129 public function ignoreValidationErrors(): void
130 {
131 $this->ignoreValidation = true;
132 $this->command->ignoreValidationErrors();
133
134 parent::ignoreValidationErrors();
135 }
136
137 public function setApplication(?Application $application = null): void
138 {
139 $this->command->setApplication($application);
140 }
141
142 public function getApplication(): ?Application
143 {
144 return $this->command->getApplication();
145 }
146
147 public function setHelperSet(HelperSet $helperSet): void
148 {
149 $this->command->setHelperSet($helperSet);
150 }
151
152 public function getHelperSet(): ?HelperSet
153 {
154 return $this->command->getHelperSet();
155 }
156
157 public function isEnabled(): bool
158 {
159 return $this->command->isEnabled();
160 }
161
162 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
163 {
164 $this->command->complete($input, $suggestions);
165 }
166
167 /**
168 * {@inheritdoc}
169 *
170 * Calling parent method is required to be used in {@see parent::run()}.
171 */
172 public function setCode(callable $code): static
173 {
174 $this->command->setCode($code);
175
176 return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
177 $event = $this->stopwatch->start($this->getName().'.code');
178
179 $this->exitCode = $code($input, $output);
180
181 $event->stop();
182
183 return $this->exitCode;
184 });
185 }
186
187 /**
188 * @internal
189 */
190 public function mergeApplicationDefinition(bool $mergeArgs = true): void
191 {
192 $this->command->mergeApplicationDefinition($mergeArgs);
193 }
194
195 public function setDefinition(array|InputDefinition $definition): static
196 {
197 $this->command->setDefinition($definition);
198
199 return $this;
200 }
201
202 public function getDefinition(): InputDefinition
203 {
204 return $this->command->getDefinition();
205 }
206
207 public function getNativeDefinition(): InputDefinition
208 {
209 return $this->command->getNativeDefinition();
210 }
211
212 public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
213 {
214 $this->command->addArgument($name, $mode, $description, $default, $suggestedValues);
215
216 return $this;
217 }
218
219 public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
220 {
221 $this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
222
223 return $this;
224 }
225
226 /**
227 * {@inheritdoc}
228 *
229 * Calling parent method is required to be used in {@see parent::run()}.
230 */
231 public function setProcessTitle(string $title): static
232 {
233 $this->command->setProcessTitle($title);
234
235 return parent::setProcessTitle($title);
236 }
237
238 public function setHelp(string $help): static
239 {
240 $this->command->setHelp($help);
241
242 return $this;
243 }
244
245 public function getHelp(): string
246 {
247 return $this->command->getHelp();
248 }
249
250 public function getProcessedHelp(): string
251 {
252 return $this->command->getProcessedHelp();
253 }
254
255 public function getSynopsis(bool $short = false): string
256 {
257 return $this->command->getSynopsis($short);
258 }
259
260 public function addUsage(string $usage): static
261 {
262 $this->command->addUsage($usage);
263
264 return $this;
265 }
266
267 public function getUsages(): array
268 {
269 return $this->command->getUsages();
270 }
271
272 public function getHelper(string $name): HelperInterface
273 {
274 return $this->command->getHelper($name);
275 }
276
277 public function run(InputInterface $input, OutputInterface $output): int
278 {
279 $this->input = $input;
280 $this->output = $output;
281 $this->arguments = $input->getArguments();
282 $this->options = $input->getOptions();
283 $event = $this->stopwatch->start($this->getName(), 'command');
284
285 try {
286 $this->exitCode = parent::run($input, $output);
287 } finally {
288 $event->stop();
289
290 if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
291 $output->getErrorOutput()->writeln((string) $event);
292 }
293
294 $this->duration = $event->getDuration().' ms';
295 $this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
296
297 if ($this->isInteractive) {
298 $this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
299 }
300 }
301
302 return $this->exitCode;
303 }
304
305 protected function initialize(InputInterface $input, OutputInterface $output): void
306 {
307 $event = $this->stopwatch->start($this->getName().'.init', 'command');
308
309 $this->command->initialize($input, $output);
310
311 $event->stop();
312 }
313
314 protected function interact(InputInterface $input, OutputInterface $output): void
315 {
316 if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) {
317 return;
318 }
319
320 $event = $this->stopwatch->start($this->getName().'.interact', 'command');
321
322 $this->command->interact($input, $output);
323
324 $event->stop();
325 }
326
327 protected function execute(InputInterface $input, OutputInterface $output): int
328 {
329 $event = $this->stopwatch->start($this->getName().'.execute', 'command');
330
331 $exitCode = $this->command->execute($input, $output);
332
333 $event->stop();
334
335 return $exitCode;
336 }
337
338 private function extractInteractiveInputs(array $arguments, array $options): void
339 {
340 foreach ($arguments as $argName => $argValue) {
341 if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
342 continue;
343 }
344
345 $this->interactiveInputs[$argName] = $argValue;
346 }
347
348 foreach ($options as $optName => $optValue) {
349 if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
350 continue;
351 }
352
353 $this->interactiveInputs['--'.$optName] = $optValue;
354 }
355 }
356}