Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.30% covered (success)
96.30%
26 / 27
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Plugin
96.30% covered (success)
96.30%
26 / 27
88.89% covered (warning)
88.89%
8 / 9
13
0.00% covered (danger)
0.00%
0 / 1
 getCapabilities
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSubscribedEvents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 runSyncCommand
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
2
 getReservedCommandNames
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 activate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deactivate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uninstall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRootScriptCommandNames
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
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;
21
22use Composer\Composer;
23use Composer\EventDispatcher\EventSubscriberInterface;
24use Composer\IO\IOInterface;
25use Composer\Plugin\Capability\CommandProvider;
26use Composer\Plugin\Capable;
27use Composer\Script\Event;
28use Composer\Script\ScriptEvents;
29use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider;
30
31/**
32 * Implements the lifecycle of the Composer dev-tools extension framework.
33 * This plugin class MUST initialize and coordinate custom script registrations securely.
34 */
35 class Plugin implements Capable, DevToolsPluginInterface, EventSubscriberInterface
36{
37    private ?Composer $composer = null;
38
39    /**
40     * Resolves the implemented Composer capabilities structure.
41     *
42     * This method MUST map the primary capability handlers to custom implementations.
43     * It SHALL describe how tools seamlessly integrate into the execution layer.
44     *
45     * @return array<string, string> the capability mapping configurations
46     */
47    public function getCapabilities(): array
48    {
49        return [
50            CommandProvider::class => DevToolsCommandProvider::class,
51        ];
52    }
53
54    /**
55     * Retrieves the comprehensive map of events this listener SHALL handle.
56     *
57     * This method MUST define the lifecycle triggers for script installation and
58     * synchronization during Composer package operations.
59     *
60     * @return array<string, string> the event mapping registry
61     */
62    public static function getSubscribedEvents(): array
63    {
64        return [
65            ScriptEvents::POST_INSTALL_CMD => 'runSyncCommand',
66            ScriptEvents::POST_UPDATE_CMD => 'runSyncCommand',
67        ];
68    }
69
70    /**
71     * Handles the automated script installation.
72     *
73     * This method MUST execute the `dev-tools:sync` command after relevant Composer operations to ensure
74     * the development tools are correctly synchronized with the current project state.
75     *
76     * @param Event $event the Composer script event context
77     *
78     * @return void
79     */
80    public function runSyncCommand(Event $event): void
81    {
82        $event->getComposer()
83            ->getLoop()
84            ->getProcessExecutor()
85            ->execute('vendor/bin/dev-tools dev-tools:sync');
86    }
87
88    /**
89     * Detects whether a command name or alias is already registered in Composer's command surface.
90     *
91     * @param string|null $name the command name or alias being evaluated
92     */
93    public function isRegisteredCommand(?string $name): bool
94    {
95        return null !== $name && \in_array($name, $this->getReservedCommandNames(), true);
96    }
97
98    /**
99     * Returns command names and aliases that DevTools plugin commands MUST NOT override.
100     *
101     * @return list<string>
102     */
103    private function getReservedCommandNames(): array
104    {
105        return array_values(array_unique([
106            ...self::COMPOSER_COMMAND_NAMES,
107            ...$this->getRootScriptCommandNames(),
108        ]));
109    }
110
111    /**
112     * Handles activation lifecycle events for the Composer session.
113     *
114     * This method MUST adhere to the standard Composer plugin activation protocol, even if no specific logic is required.
115     *
116     * @param Composer $composer the primary package configuration instance over Composer
117     * @param IOInterface $io interactive communication channels
118     *
119     * @return void
120     */
121    public function activate(Composer $composer, IOInterface $io): void
122    {
123        $this->composer = $composer;
124    }
125
126    /**
127     * Cleans up operations during Composer plugin deactivation events.
128     *
129     * This method MUST implement the standard Composer lifecycle correctly, even if vacant.
130     *
131     * @param Composer $composer the primary metadata controller object
132     * @param IOInterface $io defined interactions proxy
133     *
134     * @return void
135     */
136    public function deactivate(Composer $composer, IOInterface $io): void
137    {
138        $this->composer = null;
139    }
140
141    /**
142     * Handles final uninstallation processes logically.
143     *
144     * This method MUST manage cleanup duties per Composer constraints, even if empty.
145     *
146     * @param Composer $composer system package registry utility
147     * @param IOInterface $io execution runtime outputs and inputs proxy interface
148     *
149     * @return void
150     */
151    public function uninstall(Composer $composer, IOInterface $io): void
152    {
153        $this->composer = null;
154    }
155
156    /**
157     * Returns custom Composer script command names from the active root package.
158     *
159     * @return list<string>
160     */
161    private function getRootScriptCommandNames(): array
162    {
163        if (! $this->composer instanceof Composer) {
164            return [];
165        }
166
167        $names = [];
168
169        foreach (array_keys($this->composer->getPackage()->getScripts()) as $script) {
170            if (\defined(ScriptEvents::class . '::' . str_replace('-', '_', strtoupper($script)))) {
171                continue;
172            }
173
174            $names[] = $script;
175        }
176
177        return $names;
178    }
179}