Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
DevToolsCommandLoader
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
2 / 2
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandMap
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
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            $commandMap[$arguments['name']] = $class;
100        }
101
102        return $commandMap;
103    }
104}