Command::setHelperSet()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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\Exception\ExceptionInterface;
17
use Symfony\Component\Console\Exception\InvalidArgumentException;
18
use Symfony\Component\Console\Exception\LogicException;
19
use Symfony\Component\Console\Helper\HelperSet;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputDefinition;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Input\InputOption;
24
use Symfony\Component\Console\Output\OutputInterface;
25
26
/**
27
 * Base class for all commands.
28
 *
29
 * @author Fabien Potencier <[email protected]>
30
 */
31
class Command
32
{
33
    // see https://tldp.org/LDP/abs/html/exitcodes.html
34
    public const SUCCESS = 0;
35
    public const FAILURE = 1;
36
    public const INVALID = 2;
37
38
    /**
39
     * @var string|null The default command name
40
     */
41
    protected static $defaultName;
42
43
    /**
44
     * @var string|null The default command description
45
     */
46
    protected static $defaultDescription;
47
48
    private $application;
49
    private $name;
50
    private $processTitle;
51
    private $aliases = [];
52
    private $definition;
53
    private $hidden = false;
54
    private $help = '';
55
    private $description = '';
56
    private $fullDefinition;
57
    private $ignoreValidationErrors = false;
58
    private $code;
59
    private $synopsis = [];
60
    private $usages = [];
61
    private $helperSet;
62
63
    /**
64
     * @return string|null The default command name or null when no default name is set
65
     */
66
    public static function getDefaultName()
67
    {
68
        $class = static::class;
69
70
        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
71
            return $attribute[0]->newInstance()->name;
72
        }
73
74
        $r = new \ReflectionProperty($class, 'defaultName');
75
76
        return $class === $r->class ? static::$defaultName : null;
77
    }
78
79
    /**
80
     * @return string|null The default command description or null when no default description is set
81
     */
82
    public static function getDefaultDescription(): ?string
83
    {
84
        $class = static::class;
85
86
        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
87
            return $attribute[0]->newInstance()->description;
88
        }
89
90
        $r = new \ReflectionProperty($class, 'defaultDescription');
91
92
        return $class === $r->class ? static::$defaultDescription : null;
93
    }
94
95
    /**
96
     * @param string|null $name The name of the command; passing null means it must be set in configure()
97
     *
98
     * @throws LogicException When the command name is empty
99
     */
100
    public function __construct(string $name = null)
101
    {
102
        $this->definition = new InputDefinition();
103
104
        if (null !== $name || null !== $name = static::getDefaultName()) {
105
            $this->setName($name);
106
        }
107
108
        if ('' === $this->description) {
0 ignored issues
show
introduced by
The condition '' === $this->description is always false.
Loading history...
109
            $this->setDescription(static::getDefaultDescription() ?? '');
110
        }
111
112
        $this->configure();
113
    }
114
115
    /**
116
     * Ignores validation errors.
117
     *
118
     * This is mainly useful for the help command.
119
     */
120
    public function ignoreValidationErrors()
121
    {
122
        $this->ignoreValidationErrors = true;
123
    }
124
125
    public function setApplication(Application $application = null)
126
    {
127
        $this->application = $application;
128
        if ($application) {
129
            $this->setHelperSet($application->getHelperSet());
130
        } else {
131
            $this->helperSet = null;
132
        }
133
134
        $this->fullDefinition = null;
135
    }
136
137
    public function setHelperSet(HelperSet $helperSet)
138
    {
139
        $this->helperSet = $helperSet;
140
    }
141
142
    /**
143
     * Gets the helper set.
144
     *
145
     * @return HelperSet|null A HelperSet instance
146
     */
147
    public function getHelperSet()
148
    {
149
        return $this->helperSet;
150
    }
151
152
    /**
153
     * Gets the application instance for this command.
154
     *
155
     * @return Application|null An Application instance
156
     */
157
    public function getApplication()
158
    {
159
        return $this->application;
160
    }
161
162
    /**
163
     * Checks whether the command is enabled or not in the current environment.
164
     *
165
     * Override this to check for x or y and return false if the command can not
166
     * run properly under the current conditions.
167
     *
168
     * @return bool
169
     */
170
    public function isEnabled()
171
    {
172
        return true;
173
    }
174
175
    /**
176
     * Configures the current command.
177
     */
178
    protected function configure()
179
    {
180
    }
181
182
    /**
183
     * Executes the current command.
184
     *
185
     * This method is not abstract because you can use this class
186
     * as a concrete class. In this case, instead of defining the
187
     * execute() method, you set the code to execute by passing
188
     * a Closure to the setCode() method.
189
     *
190
     * @return int 0 if everything went fine, or an exit code
191
     *
192
     * @throws LogicException When this abstract method is not implemented
193
     *
194
     * @see setCode()
195
     */
196
    protected function execute(InputInterface $input, OutputInterface $output)
197
    {
198
        throw new LogicException('You must override the execute() method in the concrete command class.');
199
    }
200
201
    /**
202
     * Interacts with the user.
203
     *
204
     * This method is executed before the InputDefinition is validated.
205
     * This means that this is the only place where the command can
206
     * interactively ask for values of missing required arguments.
207
     */
208
    protected function interact(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Unused Code introduced by
The parameter $output is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

208
    protected function interact(InputInterface $input, /** @scrutinizer ignore-unused */ OutputInterface $output)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $input is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

208
    protected function interact(/** @scrutinizer ignore-unused */ InputInterface $input, OutputInterface $output)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
209
    {
210
    }
211
212
    /**
213
     * Initializes the command after the input has been bound and before the input
214
     * is validated.
215
     *
216
     * This is mainly useful when a lot of commands extends one main command
217
     * where some things need to be initialized based on the input arguments and options.
218
     *
219
     * @see InputInterface::bind()
220
     * @see InputInterface::validate()
221
     */
222
    protected function initialize(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Unused Code introduced by
The parameter $input is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

222
    protected function initialize(/** @scrutinizer ignore-unused */ InputInterface $input, OutputInterface $output)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $output is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

222
    protected function initialize(InputInterface $input, /** @scrutinizer ignore-unused */ OutputInterface $output)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
223
    {
224
    }
225
226
    /**
227
     * Runs the command.
228
     *
229
     * The code to execute is either defined directly with the
230
     * setCode() method or by overriding the execute() method
231
     * in a sub-class.
232
     *
233
     * @return int The command exit code
234
     *
235
     * @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}.
236
     *
237
     * @see setCode()
238
     * @see execute()
239
     */
240
    public function run(InputInterface $input, OutputInterface $output)
241
    {
242
        // add the application arguments and options
243
        $this->mergeApplicationDefinition();
244
245
        // bind the input against the command specific arguments/options
246
        try {
247
            $input->bind($this->getDefinition());
248
        } catch (ExceptionInterface $e) {
249
            if (!$this->ignoreValidationErrors) {
250
                throw $e;
251
            }
252
        }
253
254
        $this->initialize($input, $output);
255
256
        if (null !== $this->processTitle) {
257
            if (\function_exists('cli_set_process_title')) {
258
                if (!@cli_set_process_title($this->processTitle)) {
259
                    if ('Darwin' === \PHP_OS) {
260
                        $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
261
                    } else {
262
                        cli_set_process_title($this->processTitle);
263
                    }
264
                }
265
            } elseif (\function_exists('setproctitle')) {
266
                setproctitle($this->processTitle);
267
            } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
268
                $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
269
            }
270
        }
271
272
        if ($input->isInteractive()) {
273
            $this->interact($input, $output);
274
        }
275
276
        // The command name argument is often omitted when a command is executed directly with its run() method.
277
        // It would fail the validation if we didn't make sure the command argument is present,
278
        // since it's required by the application.
279
        if ($input->hasArgument('command') && null === $input->getArgument('command')) {
280
            $input->setArgument('command', $this->getName());
281
        }
282
283
        $input->validate();
284
285
        if ($this->code) {
286
            $statusCode = ($this->code)($input, $output);
287
        } else {
288
            $statusCode = $this->execute($input, $output);
289
290
            if (!\is_int($statusCode)) {
291
                throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
292
            }
293
        }
294
295
        return is_numeric($statusCode) ? (int) $statusCode : 0;
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)
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
        }
327
328
        $this->code = $code;
329
330
        return $this;
331
    }
332
333
    /**
334
     * Merges the application definition with the command definition.
335
     *
336
     * This method is not part of public API and should not be used directly.
337
     *
338
     * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
339
     *
340
     * @internal
341
     */
342
    public function mergeApplicationDefinition(bool $mergeArgs = true)
343
    {
344
        if (null === $this->application) {
345
            return;
346
        }
347
348
        $this->fullDefinition = new InputDefinition();
349
        $this->fullDefinition->setOptions($this->definition->getOptions());
350
        $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
351
352
        if ($mergeArgs) {
353
            $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
354
            $this->fullDefinition->addArguments($this->definition->getArguments());
355
        } else {
356
            $this->fullDefinition->setArguments($this->definition->getArguments());
357
        }
358
    }
359
360
    /**
361
     * Sets an array of argument and option instances.
362
     *
363
     * @param array|InputDefinition $definition An array of argument and option instances or a definition instance
364
     *
365
     * @return $this
366
     */
367
    public function setDefinition($definition)
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
     * @return InputDefinition An InputDefinition instance
384
     */
385
    public function getDefinition()
386
    {
387
        return $this->fullDefinition ?? $this->getNativeDefinition();
388
    }
389
390
    /**
391
     * Gets the InputDefinition to be used to create representations of this Command.
392
     *
393
     * Can be overridden to provide the original command representation when it would otherwise
394
     * be changed by merging with the application InputDefinition.
395
     *
396
     * This method is not part of public API and should not be used directly.
397
     *
398
     * @return InputDefinition An InputDefinition instance
399
     */
400
    public function getNativeDefinition()
401
    {
402
        if (null === $this->definition) {
403
            throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
404
        }
405
406
        return $this->definition;
407
    }
408
409
    /**
410
     * Adds an argument.
411
     *
412
     * @param int|null             $mode    The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
413
     * @param string|string[]|null $default The default value (for InputArgument::OPTIONAL mode only)
414
     *
415
     * @throws InvalidArgumentException When argument mode is not valid
416
     *
417
     * @return $this
418
     */
419
    public function addArgument(string $name, int $mode = null, string $description = '', $default = null)
420
    {
421
        $this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
422
        if (null !== $this->fullDefinition) {
423
            $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
424
        }
425
426
        return $this;
427
    }
428
429
    /**
430
     * Adds an option.
431
     *
432
     * @param string|array|null         $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
433
     * @param int|null                  $mode     The option mode: One of the InputOption::VALUE_* constants
434
     * @param string|string[]|bool|null $default  The default value (must be null for InputOption::VALUE_NONE)
435
     *
436
     * @throws InvalidArgumentException If option mode is invalid or incompatible
437
     *
438
     * @return $this
439
     */
440
    public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
441
    {
442
        $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
443
        if (null !== $this->fullDefinition) {
444
            $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
445
        }
446
447
        return $this;
448
    }
449
450
    /**
451
     * Sets the name of the command.
452
     *
453
     * This method can set both the namespace and the name if
454
     * you separate them by a colon (:)
455
     *
456
     *     $command->setName('foo:bar');
457
     *
458
     * @return $this
459
     *
460
     * @throws InvalidArgumentException When the name is invalid
461
     */
462
    public function setName(string $name)
463
    {
464
        $this->validateName($name);
465
466
        $this->name = $name;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Sets the process title of the command.
473
     *
474
     * This feature should be used only when creating a long process command,
475
     * like a daemon.
476
     *
477
     * @return $this
478
     */
479
    public function setProcessTitle(string $title)
480
    {
481
        $this->processTitle = $title;
482
483
        return $this;
484
    }
485
486
    /**
487
     * Returns the command name.
488
     *
489
     * @return string|null
490
     */
491
    public function getName()
492
    {
493
        return $this->name;
494
    }
495
496
    /**
497
     * @param bool $hidden Whether or not the command should be hidden from the list of commands
498
     *                     The default value will be true in Symfony 6.0
499
     *
500
     * @return Command The current instance
501
     *
502
     * @final since Symfony 5.1
503
     */
504
    public function setHidden(bool $hidden /*= true*/)
505
    {
506
        $this->hidden = $hidden;
507
508
        return $this;
509
    }
510
511
    /**
512
     * @return bool whether the command should be publicly shown or not
513
     */
514
    public function isHidden()
515
    {
516
        return $this->hidden;
517
    }
518
519
    /**
520
     * Sets the description for the command.
521
     *
522
     * @return $this
523
     */
524
    public function setDescription(string $description)
525
    {
526
        $this->description = $description;
527
528
        return $this;
529
    }
530
531
    /**
532
     * Returns the description for the command.
533
     *
534
     * @return string The description for the command
535
     */
536
    public function getDescription()
537
    {
538
        return $this->description;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->description returns the type mixed which is incompatible with the documented return type string.
Loading history...
539
    }
540
541
    /**
542
     * Sets the help for the command.
543
     *
544
     * @return $this
545
     */
546
    public function setHelp(string $help)
547
    {
548
        $this->help = $help;
549
550
        return $this;
551
    }
552
553
    /**
554
     * Returns the help for the command.
555
     *
556
     * @return string The help for the command
557
     */
558
    public function getHelp()
559
    {
560
        return $this->help;
561
    }
562
563
    /**
564
     * Returns the processed help for the command replacing the %command.name% and
565
     * %command.full_name% patterns with the real values dynamically.
566
     *
567
     * @return string The processed help for the command
568
     */
569
    public function getProcessedHelp()
570
    {
571
        $name = $this->name;
572
        $isSingleCommand = $this->application && $this->application->isSingleCommand();
573
574
        $placeholders = [
575
            '%command.name%',
576
            '%command.full_name%',
577
        ];
578
        $replacements = [
579
            $name,
580
            $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
581
        ];
582
583
        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
584
    }
585
586
    /**
587
     * Sets the aliases for the command.
588
     *
589
     * @param string[] $aliases An array of aliases for the command
590
     *
591
     * @return $this
592
     *
593
     * @throws InvalidArgumentException When an alias is invalid
594
     */
595
    public function setAliases(iterable $aliases)
596
    {
597
        $list = [];
598
599
        foreach ($aliases as $alias) {
600
            $this->validateName($alias);
601
            $list[] = $alias;
602
        }
603
604
        $this->aliases = \is_array($aliases) ? $aliases : $list;
605
606
        return $this;
607
    }
608
609
    /**
610
     * Returns the aliases for the command.
611
     *
612
     * @return array An array of aliases for the command
613
     */
614
    public function getAliases()
615
    {
616
        return $this->aliases;
617
    }
618
619
    /**
620
     * Returns the synopsis for the command.
621
     *
622
     * @param bool $short Whether to show the short version of the synopsis (with options folded) or not
623
     *
624
     * @return string The synopsis
625
     */
626
    public function getSynopsis(bool $short = false)
627
    {
628
        $key = $short ? 'short' : 'long';
629
630
        if (!isset($this->synopsis[$key])) {
631
            $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
632
        }
633
634
        return $this->synopsis[$key];
635
    }
636
637
    /**
638
     * Add a command usage example, it'll be prefixed with the command name.
639
     *
640
     * @return $this
641
     */
642
    public function addUsage(string $usage)
643
    {
644
        if (0 !== strpos($usage, $this->name)) {
645
            $usage = sprintf('%s %s', $this->name, $usage);
646
        }
647
648
        $this->usages[] = $usage;
649
650
        return $this;
651
    }
652
653
    /**
654
     * Returns alternative usages of the command.
655
     *
656
     * @return array
657
     */
658
    public function getUsages()
659
    {
660
        return $this->usages;
661
    }
662
663
    /**
664
     * Gets a helper instance by name.
665
     *
666
     * @return mixed The helper value
667
     *
668
     * @throws LogicException           if no HelperSet is defined
669
     * @throws InvalidArgumentException if the helper is not defined
670
     */
671
    public function getHelper(string $name)
672
    {
673
        if (null === $this->helperSet) {
674
            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));
675
        }
676
677
        return $this->helperSet->get($name);
678
    }
679
680
    /**
681
     * Validates a command name.
682
     *
683
     * It must be non-empty and parts can optionally be separated by ":".
684
     *
685
     * @throws InvalidArgumentException When the name is invalid
686
     */
687
    private function validateName(string $name)
688
    {
689
        if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
690
            throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
691
        }
692
    }
693
}
694