Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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
 
12
namespace Symfony\Component\Console\Command;
13
 
14
use Symfony\Component\Console\Application;
15
use Symfony\Component\Console\Attribute\AsCommand;
16
use Symfony\Component\Console\Completion\CompletionInput;
17
use Symfony\Component\Console\Completion\CompletionSuggestions;
18
use Symfony\Component\Console\Completion\Suggestion;
19
use Symfony\Component\Console\Exception\ExceptionInterface;
20
use Symfony\Component\Console\Exception\InvalidArgumentException;
21
use Symfony\Component\Console\Exception\LogicException;
22
use Symfony\Component\Console\Helper\HelperInterface;
23
use Symfony\Component\Console\Helper\HelperSet;
24
use Symfony\Component\Console\Input\InputArgument;
25
use Symfony\Component\Console\Input\InputDefinition;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
 
30
/**
31
 * Base class for all commands.
32
 *
33
 * @author Fabien Potencier <fabien@symfony.com>
34
 */
35
class 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
    /**
43
     * @var string|null The default command name
44
     *
45
     * @deprecated since Symfony 6.1, use the AsCommand attribute instead
46
     */
47
    protected static $defaultName;
48
 
49
    /**
50
     * @var string|null The default command description
51
     *
52
     * @deprecated since Symfony 6.1, use the AsCommand attribute instead
53
     */
54
    protected static $defaultDescription;
55
 
56
    private ?Application $application = null;
57
    private ?string $name = null;
58
    private ?string $processTitle = null;
59
    private array $aliases = [];
60
    private InputDefinition $definition;
61
    private bool $hidden = false;
62
    private string $help = '';
63
    private string $description = '';
64
    private ?InputDefinition $fullDefinition = null;
65
    private bool $ignoreValidationErrors = false;
66
    private ?\Closure $code = null;
67
    private array $synopsis = [];
68
    private array $usages = [];
69
    private ?HelperSet $helperSet = null;
70
 
71
    public static function getDefaultName(): ?string
72
    {
73
        $class = static::class;
74
 
75
        if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
76
            return $attribute[0]->newInstance()->name;
77
        }
78
 
79
        $r = new \ReflectionProperty($class, 'defaultName');
80
 
81
        if ($class !== $r->class || null === static::$defaultName) {
82
            return null;
83
        }
84
 
85
        trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);
86
 
87
        return static::$defaultName;
88
    }
89
 
90
    public static function getDefaultDescription(): ?string
91
    {
92
        $class = static::class;
93
 
94
        if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
95
            return $attribute[0]->newInstance()->description;
96
        }
97
 
98
        $r = new \ReflectionProperty($class, 'defaultDescription');
99
 
100
        if ($class !== $r->class || null === static::$defaultDescription) {
101
            return null;
102
        }
103
 
104
        trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);
105
 
106
        return static::$defaultDescription;
107
    }
108
 
109
    /**
110
     * @param string|null $name The name of the command; passing null means it must be set in configure()
111
     *
112
     * @throws LogicException When the command name is empty
113
     */
114
    public function __construct(string $name = null)
115
    {
116
        $this->definition = new InputDefinition();
117
 
118
        if (null === $name && null !== $name = static::getDefaultName()) {
119
            $aliases = explode('|', $name);
120
 
121
            if ('' === $name = array_shift($aliases)) {
122
                $this->setHidden(true);
123
                $name = array_shift($aliases);
124
            }
125
 
126
            $this->setAliases($aliases);
127
        }
128
 
129
        if (null !== $name) {
130
            $this->setName($name);
131
        }
132
 
133
        if ('' === $this->description) {
134
            $this->setDescription(static::getDefaultDescription() ?? '');
135
        }
136
 
137
        $this->configure();
138
    }
139
 
140
    /**
141
     * Ignores validation errors.
142
     *
143
     * This is mainly useful for the help command.
144
     */
145
    public function ignoreValidationErrors()
146
    {
147
        $this->ignoreValidationErrors = true;
148
    }
149
 
150
    public function setApplication(Application $application = null)
151
    {
152
        if (1 > \func_num_args()) {
153
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
154
        }
155
        $this->application = $application;
156
        if ($application) {
157
            $this->setHelperSet($application->getHelperSet());
158
        } else {
159
            $this->helperSet = null;
160
        }
161
 
162
        $this->fullDefinition = null;
163
    }
164
 
165
    public function setHelperSet(HelperSet $helperSet)
166
    {
167
        $this->helperSet = $helperSet;
168
    }
169
 
170
    /**
171
     * Gets the helper set.
172
     */
173
    public function getHelperSet(): ?HelperSet
174
    {
175
        return $this->helperSet;
176
    }
177
 
178
    /**
179
     * Gets the application instance for this command.
180
     */
181
    public function getApplication(): ?Application
182
    {
183
        return $this->application;
184
    }
185
 
186
    /**
187
     * Checks whether the command is enabled or not in the current environment.
188
     *
189
     * Override this to check for x or y and return false if the command cannot
190
     * run properly under the current conditions.
191
     *
192
     * @return bool
193
     */
194
    public function isEnabled()
195
    {
196
        return true;
197
    }
198
 
199
    /**
200
     * Configures the current command.
201
     */
202
    protected function configure()
203
    {
204
    }
205
 
206
    /**
207
     * Executes the current command.
208
     *
209
     * This method is not abstract because you can use this class
210
     * as a concrete class. In this case, instead of defining the
211
     * execute() method, you set the code to execute by passing
212
     * a Closure to the setCode() method.
213
     *
214
     * @return int 0 if everything went fine, or an exit code
215
     *
216
     * @throws LogicException When this abstract method is not implemented
217
     *
218
     * @see setCode()
219
     */
220
    protected function execute(InputInterface $input, OutputInterface $output)
221
    {
222
        throw new LogicException('You must override the execute() method in the concrete command class.');
223
    }
224
 
225
    /**
226
     * Interacts with the user.
227
     *
228
     * This method is executed before the InputDefinition is validated.
229
     * This means that this is the only place where the command can
230
     * interactively ask for values of missing required arguments.
231
     */
232
    protected function interact(InputInterface $input, OutputInterface $output)
233
    {
234
    }
235
 
236
    /**
237
     * Initializes the command after the input has been bound and before the input
238
     * is validated.
239
     *
240
     * This is mainly useful when a lot of commands extends one main command
241
     * where some things need to be initialized based on the input arguments and options.
242
     *
243
     * @see InputInterface::bind()
244
     * @see InputInterface::validate()
245
     */
246
    protected function initialize(InputInterface $input, OutputInterface $output)
247
    {
248
    }
249
 
250
    /**
251
     * Runs the command.
252
     *
253
     * The code to execute is either defined directly with the
254
     * setCode() method or by overriding the execute() method
255
     * in a sub-class.
256
     *
257
     * @return int The command exit code
258
     *
259
     * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
260
     *
261
     * @see setCode()
262
     * @see execute()
263
     */
264
    public function run(InputInterface $input, OutputInterface $output): int
265
    {
266
        // add the application arguments and options
267
        $this->mergeApplicationDefinition();
268
 
269
        // bind the input against the command specific arguments/options
270
        try {
271
            $input->bind($this->getDefinition());
272
        } catch (ExceptionInterface $e) {
273
            if (!$this->ignoreValidationErrors) {
274
                throw $e;
275
            }
276
        }
277
 
278
        $this->initialize($input, $output);
279
 
280
        if (null !== $this->processTitle) {
281
            if (\function_exists('cli_set_process_title')) {
282
                if (!@cli_set_process_title($this->processTitle)) {
283
                    if ('Darwin' === \PHP_OS) {
284
                        $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
285
                    } else {
286
                        cli_set_process_title($this->processTitle);
287
                    }
288
                }
289
            } elseif (\function_exists('setproctitle')) {
290
                setproctitle($this->processTitle);
291
            } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
292
                $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
293
            }
294
        }
295
 
296
        if ($input->isInteractive()) {
297
            $this->interact($input, $output);
298
        }
299
 
300
        // The command name argument is often omitted when a command is executed directly with its run() method.
301
        // It would fail the validation if we didn't make sure the command argument is present,
302
        // since it's required by the application.
303
        if ($input->hasArgument('command') && null === $input->getArgument('command')) {
304
            $input->setArgument('command', $this->getName());
305
        }
306
 
307
        $input->validate();
308
 
309
        if ($this->code) {
310
            $statusCode = ($this->code)($input, $output);
311
        } else {
312
            $statusCode = $this->execute($input, $output);
313
 
314
            if (!\is_int($statusCode)) {
315
                throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
316
            }
317
        }
318
 
319
        return is_numeric($statusCode) ? (int) $statusCode : 0;
320
    }
321
 
322
    /**
323
     * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
324
     */
325
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
326
    {
327
        $definition = $this->getDefinition();
328
        if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
329
            $definition->getOption($input->getCompletionName())->complete($input, $suggestions);
330
        } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
331
            $definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
332
        }
333
    }
334
 
335
    /**
336
     * Sets the code to execute when running this command.
337
     *
338
     * If this method is used, it overrides the code defined
339
     * in the execute() method.
340
     *
341
     * @param callable $code A callable(InputInterface $input, OutputInterface $output)
342
     *
343
     * @return $this
344
     *
345
     * @throws InvalidArgumentException
346
     *
347
     * @see execute()
348
     */
349
    public function setCode(callable $code): static
350
    {
351
        if ($code instanceof \Closure) {
352
            $r = new \ReflectionFunction($code);
353
            if (null === $r->getClosureThis()) {
354
                set_error_handler(static function () {});
355
                try {
356
                    if ($c = \Closure::bind($code, $this)) {
357
                        $code = $c;
358
                    }
359
                } finally {
360
                    restore_error_handler();
361
                }
362
            }
363
        } else {
364
            $code = $code(...);
365
        }
366
 
367
        $this->code = $code;
368
 
369
        return $this;
370
    }
371
 
372
    /**
373
     * Merges the application definition with the command definition.
374
     *
375
     * This method is not part of public API and should not be used directly.
376
     *
377
     * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
378
     *
379
     * @internal
380
     */
381
    public function mergeApplicationDefinition(bool $mergeArgs = true)
382
    {
383
        if (null === $this->application) {
384
            return;
385
        }
386
 
387
        $this->fullDefinition = new InputDefinition();
388
        $this->fullDefinition->setOptions($this->definition->getOptions());
389
        $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
390
 
391
        if ($mergeArgs) {
392
            $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
393
            $this->fullDefinition->addArguments($this->definition->getArguments());
394
        } else {
395
            $this->fullDefinition->setArguments($this->definition->getArguments());
396
        }
397
    }
398
 
399
    /**
400
     * Sets an array of argument and option instances.
401
     *
402
     * @return $this
403
     */
404
    public function setDefinition(array|InputDefinition $definition): static
405
    {
406
        if ($definition instanceof InputDefinition) {
407
            $this->definition = $definition;
408
        } else {
409
            $this->definition->setDefinition($definition);
410
        }
411
 
412
        $this->fullDefinition = null;
413
 
414
        return $this;
415
    }
416
 
417
    /**
418
     * Gets the InputDefinition attached to this Command.
419
     */
420
    public function getDefinition(): InputDefinition
421
    {
422
        return $this->fullDefinition ?? $this->getNativeDefinition();
423
    }
424
 
425
    /**
426
     * Gets the InputDefinition to be used to create representations of this Command.
427
     *
428
     * Can be overridden to provide the original command representation when it would otherwise
429
     * be changed by merging with the application InputDefinition.
430
     *
431
     * This method is not part of public API and should not be used directly.
432
     */
433
    public function getNativeDefinition(): InputDefinition
434
    {
435
        return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
436
    }
437
 
438
    /**
439
     * Adds an argument.
440
     *
441
     * @param $mode    The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
442
     * @param $default The default value (for InputArgument::OPTIONAL mode only)
443
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
444
     *
445
     * @return $this
446
     *
447
     * @throws InvalidArgumentException When argument mode is not valid
448
     */
449
    public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static
450
    {
451
        $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
452
        if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
453
            throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
454
        }
455
        $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
456
        $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
457
 
458
        return $this;
459
    }
460
 
461
    /**
462
     * Adds an option.
463
     *
464
     * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
465
     * @param $mode     The option mode: One of the InputOption::VALUE_* constants
466
     * @param $default  The default value (must be null for InputOption::VALUE_NONE)
467
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
468
     *
469
     * @return $this
470
     *
471
     * @throws InvalidArgumentException If option mode is invalid or incompatible
472
     */
473
    public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
474
    {
475
        $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
476
        if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
477
            throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
478
        }
479
        $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
480
        $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
481
 
482
        return $this;
483
    }
484
 
485
    /**
486
     * Sets the name of the command.
487
     *
488
     * This method can set both the namespace and the name if
489
     * you separate them by a colon (:)
490
     *
491
     *     $command->setName('foo:bar');
492
     *
493
     * @return $this
494
     *
495
     * @throws InvalidArgumentException When the name is invalid
496
     */
497
    public function setName(string $name): static
498
    {
499
        $this->validateName($name);
500
 
501
        $this->name = $name;
502
 
503
        return $this;
504
    }
505
 
506
    /**
507
     * Sets the process title of the command.
508
     *
509
     * This feature should be used only when creating a long process command,
510
     * like a daemon.
511
     *
512
     * @return $this
513
     */
514
    public function setProcessTitle(string $title): static
515
    {
516
        $this->processTitle = $title;
517
 
518
        return $this;
519
    }
520
 
521
    /**
522
     * Returns the command name.
523
     */
524
    public function getName(): ?string
525
    {
526
        return $this->name;
527
    }
528
 
529
    /**
530
     * @param bool $hidden Whether or not the command should be hidden from the list of commands
531
     *
532
     * @return $this
533
     */
534
    public function setHidden(bool $hidden = true): static
535
    {
536
        $this->hidden = $hidden;
537
 
538
        return $this;
539
    }
540
 
541
    /**
542
     * @return bool whether the command should be publicly shown or not
543
     */
544
    public function isHidden(): bool
545
    {
546
        return $this->hidden;
547
    }
548
 
549
    /**
550
     * Sets the description for the command.
551
     *
552
     * @return $this
553
     */
554
    public function setDescription(string $description): static
555
    {
556
        $this->description = $description;
557
 
558
        return $this;
559
    }
560
 
561
    /**
562
     * Returns the description for the command.
563
     */
564
    public function getDescription(): string
565
    {
566
        return $this->description;
567
    }
568
 
569
    /**
570
     * Sets the help for the command.
571
     *
572
     * @return $this
573
     */
574
    public function setHelp(string $help): static
575
    {
576
        $this->help = $help;
577
 
578
        return $this;
579
    }
580
 
581
    /**
582
     * Returns the help for the command.
583
     */
584
    public function getHelp(): string
585
    {
586
        return $this->help;
587
    }
588
 
589
    /**
590
     * Returns the processed help for the command replacing the %command.name% and
591
     * %command.full_name% patterns with the real values dynamically.
592
     */
593
    public function getProcessedHelp(): string
594
    {
595
        $name = $this->name;
596
        $isSingleCommand = $this->application?->isSingleCommand();
597
 
598
        $placeholders = [
599
            '%command.name%',
600
            '%command.full_name%',
601
        ];
602
        $replacements = [
603
            $name,
604
            $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
605
        ];
606
 
607
        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
608
    }
609
 
610
    /**
611
     * Sets the aliases for the command.
612
     *
613
     * @param string[] $aliases An array of aliases for the command
614
     *
615
     * @return $this
616
     *
617
     * @throws InvalidArgumentException When an alias is invalid
618
     */
619
    public function setAliases(iterable $aliases): static
620
    {
621
        $list = [];
622
 
623
        foreach ($aliases as $alias) {
624
            $this->validateName($alias);
625
            $list[] = $alias;
626
        }
627
 
628
        $this->aliases = \is_array($aliases) ? $aliases : $list;
629
 
630
        return $this;
631
    }
632
 
633
    /**
634
     * Returns the aliases for the command.
635
     */
636
    public function getAliases(): array
637
    {
638
        return $this->aliases;
639
    }
640
 
641
    /**
642
     * Returns the synopsis for the command.
643
     *
644
     * @param bool $short Whether to show the short version of the synopsis (with options folded) or not
645
     */
646
    public function getSynopsis(bool $short = false): string
647
    {
648
        $key = $short ? 'short' : 'long';
649
 
650
        if (!isset($this->synopsis[$key])) {
651
            $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
652
        }
653
 
654
        return $this->synopsis[$key];
655
    }
656
 
657
    /**
658
     * Add a command usage example, it'll be prefixed with the command name.
659
     *
660
     * @return $this
661
     */
662
    public function addUsage(string $usage): static
663
    {
664
        if (!str_starts_with($usage, $this->name)) {
665
            $usage = sprintf('%s %s', $this->name, $usage);
666
        }
667
 
668
        $this->usages[] = $usage;
669
 
670
        return $this;
671
    }
672
 
673
    /**
674
     * Returns alternative usages of the command.
675
     */
676
    public function getUsages(): array
677
    {
678
        return $this->usages;
679
    }
680
 
681
    /**
682
     * Gets a helper instance by name.
683
     *
684
     * @return HelperInterface
685
     *
686
     * @throws LogicException           if no HelperSet is defined
687
     * @throws InvalidArgumentException if the helper is not defined
688
     */
689
    public function getHelper(string $name): mixed
690
    {
691
        if (null === $this->helperSet) {
692
            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));
693
        }
694
 
695
        return $this->helperSet->get($name);
696
    }
697
 
698
    /**
699
     * Validates a command name.
700
     *
701
     * It must be non-empty and parts can optionally be separated by ":".
702
     *
703
     * @throws InvalidArgumentException When the name is invalid
704
     */
705
    private function validateName(string $name)
706
    {
707
        if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
708
            throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
709
        }
710
    }
711
}