Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
25 / 26
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
DevToolsCommandLoader
96.15% covered (success)
96.15%
25 / 26
50.00% covered (danger)
50.00%
1 / 2
8
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandMap
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
7
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\CommandLoader;
21
22use FastForward\DevTools\Filesystem\FinderFactoryInterface;
23use Psr\Container\ContainerInterface;
24use ReflectionClass;
25use Symfony\Component\Console\Attribute\AsCommand;
26use Symfony\Component\Console\Command\Command;
27use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
28
29/**
30 * Responsible for dynamically discovering and loading Symfony Console commands
31 * within the DevTools context. This class extends the ContainerCommandLoader
32 * and integrates with a PSR-11 compatible container to lazily instantiate commands.
33 *
34 * The implementation MUST scan a predefined directory for PHP classes representing
35 * console commands and SHALL only register classes that:
36 * - Are instantiable
37 * - Extend the Symfony\Component\Console\Command\Command base class
38 * - Declare the Symfony\Component\Console\Attribute\AsCommand attribute
39 *
40 * The command name MUST be extracted from the AsCommand attribute metadata and
41 * used as the key in the command map. Classes that do not meet these criteria
42 * MUST NOT be included in the command map.
43 */
44 class DevToolsCommandLoader extends ContainerCommandLoader
45{
46    /**
47     * Constructs the DevToolsCommandLoader.
48     *
49     * This constructor initializes the command loader by scanning the Command directory for classes that are
50     * instantiable and have the AsCommand attribute.
51     * It builds a command map associating command names with their respective classes.
52     *
53     * @param FinderFactoryInterface $finderFactory
54     * @param ContainerInterface $container
55     */
56    public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container)
57    {
58        parent::__construct($container, $this->getCommandMap($finderFactory));
59    }
60
61    /**
62     * Builds a command map by scanning the Command directory for classes that are instantiable and have the AsCommand attribute.
63     *
64     * @param FinderFactoryInterface $finderFactory
65     *
66     * @return array
67     */
68    private function getCommandMap(FinderFactoryInterface $finderFactory): array
69    {
70        $commandMap = [];
71
72        $commandsDirectory = $finderFactory
73            ->create()
74            ->files()
75            ->in(__DIR__ . '/../Command')
76            ->notPath('Traits')
77            ->name('*.php');
78
79        $namespace = substr(__NAMESPACE__, 0, strrpos(__NAMESPACE__, '\\')) . '\\Command\\';
80
81        foreach ($commandsDirectory as $file) {
82            $class = $namespace . $file->getBasename('.php');
83            $reflection = new ReflectionClass($class);
84            if (! $reflection->isInstantiable()) {
85                continue;
86            }
87
88            if (! $reflection->isSubclassOf(Command::class)) {
89                continue;
90            }
91
92            $attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null;
93
94            if (null === $attribute) {
95                continue;
96            }
97
98            $arguments = $attribute->getArguments();
99            $commandNames = [$arguments['name'], ...($arguments['aliases'] ?? [])];
100
101            foreach ($commandNames as $commandName) {
102                if ('' === $commandName) {
103                    continue;
104                }
105
106                $commandMap[$commandName] = $class;
107            }
108        }
109
110        return $commandMap;
111    }
112}