Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
16 / 17
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DevToolsCommandProvider
94.12% covered (success)
94.12%
16 / 17
75.00% covered (warning)
75.00%
3 / 4
9.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getCommands
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 getComposerAliases
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isRegisteredCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\Composer\Capability;
21
22use Composer\Plugin\Capability\CommandProvider;
23use FastForward\DevTools\Composer\Command\ProxyCommand;
24use FastForward\DevTools\Composer\DevToolsPluginInterface;
25use FastForward\DevTools\Console\DevTools;
26use FastForward\DevTools\Container\ContainerFactory;
27use Symfony\Component\Console\Command\Command;
28
29/**
30 * Provides a registry of custom dev-tools commands mapped for Composer integration.
31 * This capability struct MUST implement the defined `CommandProvider`.
32 */
33  class DevToolsCommandProvider implements CommandProvider
34{
35    /**
36     * @var string the namespace prefix for dev-tools console commands to be registered as Composer commands
37     */
38    private const string COMMAND_NAMESPACE = 'FastForward\DevTools\Console\Command';
39
40    private ?DevToolsPluginInterface $plugin;
41
42    /**
43     * @param array<string, mixed> $constructorArguments the Composer capability constructor arguments
44     */
45    public function __construct(array $constructorArguments = [])
46    {
47        $plugin = $constructorArguments['plugin'] ?? null;
48        $this->plugin = $plugin instanceof DevToolsPluginInterface ? $plugin : null;
49    }
50
51    /**
52     * {@inheritDoc}
53     */
54    public function getCommands()
55    {
56        $commands = [];
57
58        foreach (ContainerFactory::get(DevTools::class)->all() as $registeredName => $command) {
59            /**
60             * Composer plugin registrations must be canonicalized to one command per Symfony command.
61             * The application exposes alias keys in `all()`, but Composer interprets each entry as
62             * an independent command and emits override warnings.
63             */
64            if ($registeredName !== $command->getName()) {
65                continue;
66            }
67
68            if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) {
69                continue;
70            }
71
72            if ($this->isRegisteredCommand($command->getName())) {
73                continue;
74            }
75
76            $commands[] = new ProxyCommand($command, $this->getComposerAliases($command));
77        }
78
79        return $commands;
80    }
81
82    /**
83     * Returns command aliases that may be safely exposed to Composer.
84     *
85     * @param Command $command the Symfony command being proxied
86     *
87     * @return list<string>
88     */
89    private function getComposerAliases(Command $command): array
90    {
91        return array_values(array_filter(
92            $command->getAliases(),
93            fn(string $alias): bool => ! $this->isRegisteredCommand($alias),
94        ));
95    }
96
97    /**
98     * Detects names already owned by Composer's active command surface.
99     *
100     * @param string|null $name the command name or alias being evaluated
101     */
102    private function isRegisteredCommand(?string $name): bool
103    {
104        return $this->plugin?->isRegisteredCommand($name) ?? false;
105    }
106}