Skip to content

Commit bada76f

Browse files
committed
feat(composer): add command runtime hybrid foundation
1 parent 686eab0 commit bada76f

13 files changed

Lines changed: 412 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add a hybrid command runtime bootstrap and capability bridge that keeps command discovery split between migrated Symfony commands (`DevTools`) and legacy Composer `BaseCommand` commands (`DevToolsComposer`) while exposing proxy commands during Composer execution for the first migration step (#199)
13+
1014
## [1.22.3] - 2026-04-25
1115

1216
### Fixed

bin/dev-tools.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,19 @@
2020
namespace FastForward\DevTools;
2121

2222
use FastForward\DevTools\Console\DevTools;
23+
use FastForward\DevTools\Console\DevToolsComposer;
2324
use Symfony\Component\Console\Input\ArgvInput;
2425

2526
$projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php';
2627
$pluginVendorAutoload = \dirname(__DIR__) . '/vendor/autoload.php';
2728

2829
require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload;
2930

30-
DevTools::create()->run(new ArgvInput([...$argv, '--no-plugins']));
31+
$input = new ArgvInput([...$argv, '--no-plugins']);
32+
33+
$command = $input->getFirstArgument();
34+
$application = (null !== $command && DevTools::create()->has($command))
35+
? DevTools::create()
36+
: DevToolsComposer::create();
37+
38+
$application->run($input);

src/Composer/Capability/DevToolsCommandProvider.php

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
use Composer\Command\BaseCommand;
2323
use Composer\Plugin\Capability\CommandProvider;
24+
use FastForward\DevTools\Console\Command\ProxyCommand;
2425
use FastForward\DevTools\Console\DevTools;
26+
use FastForward\DevTools\Console\DevToolsComposer;
27+
use Symfony\Component\Console\Command\Command;
2528

2629
/**
2730
* Provides a registry of custom dev-tools commands mapped for Composer integration.
@@ -34,9 +37,76 @@ final class DevToolsCommandProvider implements CommandProvider
3437
*/
3538
public function getCommands()
3639
{
40+
$legacyCommands = DevToolsComposer::create()->all();
41+
$reservedCommandNames = $this->collectCommandNames($legacyCommands);
42+
$migratedCommands = DevTools::create()->all();
43+
44+
$commands = $legacyCommands;
45+
46+
foreach ($migratedCommands as $command) {
47+
if (! $command instanceof Command || $command instanceof BaseCommand) {
48+
continue;
49+
}
50+
51+
if ($this->hasReservedName($command, $reservedCommandNames)) {
52+
continue;
53+
}
54+
55+
$commands[] = new ProxyCommand($command);
56+
}
57+
3758
return array_values(array_filter(
38-
DevTools::create()->all(),
59+
$commands,
3960
static fn(object $command): bool => $command instanceof BaseCommand,
4061
));
4162
}
63+
64+
/**
65+
* Collects command names and aliases that must remain mapped to legacy commands.
66+
*
67+
* @param array<int, BaseCommand> $commands
68+
*
69+
* @return array<string, true>
70+
*/
71+
private function collectCommandNames(array $commands): array
72+
{
73+
$commandNames = [];
74+
75+
foreach ($commands as $command) {
76+
if (! $command instanceof BaseCommand) {
77+
continue;
78+
}
79+
80+
if (null !== $command->getName()) {
81+
$commandNames[$command->getName()] = true;
82+
}
83+
84+
foreach ($command->getAliases() as $alias) {
85+
$commandNames[$alias] = true;
86+
}
87+
}
88+
89+
return $commandNames;
90+
}
91+
92+
/**
93+
* Verifies whether the command name or any aliases collide with legacy command names.
94+
*
95+
* @param Command $command
96+
* @param array<string, true> $reservedCommandNames
97+
*/
98+
private function hasReservedName(Command $command, array $reservedCommandNames): bool
99+
{
100+
if (null !== $command->getName() && isset($reservedCommandNames[$command->getName()])) {
101+
return true;
102+
}
103+
104+
foreach ($command->getAliases() as $alias) {
105+
if (isset($reservedCommandNames[$alias])) {
106+
return true;
107+
}
108+
}
109+
110+
return false;
111+
}
42112
}

src/Console/Command/AgentsCommand.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@
2020
namespace FastForward\DevTools\Console\Command;
2121

2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
23-
use Composer\Command\BaseCommand;
2423
use FastForward\DevTools\Console\Input\HasJsonOption;
2524
use FastForward\DevTools\Filesystem\FilesystemInterface;
2625
use FastForward\DevTools\Path\DevToolsPathResolver;
2726
use FastForward\DevTools\Sync\PackagedDirectorySynchronizer;
2827
use Psr\Log\LoggerInterface;
2928
use Symfony\Component\Console\Attribute\AsCommand;
29+
use Symfony\Component\Console\Command\Command;
3030
use Symfony\Component\Console\Input\InputInterface;
3131
use Symfony\Component\Console\Output\OutputInterface;
3232

3333
/**
3434
* Synchronizes packaged Fast Forward project agents into the consumer repository.
3535
*/
3636
#[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')]
37-
final class AgentsCommand extends BaseCommand implements LoggerAwareCommandInterface
37+
final class AgentsCommand extends Command implements LoggerAwareCommandInterface
3838
{
3939
use HasJsonOption;
4040
use LogsCommandResults;
@@ -51,7 +51,7 @@ public function __construct(
5151
private readonly FilesystemInterface $filesystem,
5252
private readonly LoggerInterface $logger,
5353
) {
54-
parent::__construct();
54+
parent::__construct('agents');
5555
}
5656

5757
/**
@@ -98,7 +98,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9898
$this->logger->info('Created .agents/agents directory.');
9999
}
100100

101-
$this->synchronizer->setLogger($this->getIO());
101+
$this->synchronizer->setLogger($this->logger);
102102

103103
$result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY);
104104

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(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+
20+
namespace FastForward\DevTools\Console\Command;
21+
22+
use Composer\Command\BaseCommand;
23+
use Symfony\Component\Console\Command\Command;
24+
use Symfony\Component\Console\Input\InputInterface;
25+
use Symfony\Component\Console\Output\OutputInterface;
26+
27+
/**
28+
* Adapts migrated Symfony commands to Composer's BaseCommand contract.
29+
*/
30+
final class ProxyCommand extends BaseCommand
31+
{
32+
public function __construct(
33+
private readonly Command $command,
34+
) {
35+
parent::__construct($command->getName());
36+
37+
$this->setAliases($command->getAliases());
38+
$this->setDescription($command->getDescription());
39+
$this->setHelp($command->getHelp());
40+
$this->setDefinition(clone $command->getDefinition());
41+
$this->setHidden($command->isHidden());
42+
$this->setIgnoreValidationErrors($command->ignoreValidationErrors());
43+
}
44+
45+
/**
46+
* @param InputInterface $input
47+
* @param OutputInterface $output
48+
*
49+
* @return int
50+
*/
51+
protected function execute(InputInterface $input, OutputInterface $output): int
52+
{
53+
$this->command->setApplication($this->getApplication());
54+
$this->command->setHelperSet($this->getHelperSet());
55+
56+
return $this->command->run($input, $output);
57+
}
58+
}

src/Console/CommandLoader/DevToolsCommandLoader.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
namespace FastForward\DevTools\Console\CommandLoader;
2121

22+
use Composer\Command\BaseCommand;
2223
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
2324
use Psr\Container\ContainerInterface;
2425
use ReflectionClass;
@@ -35,6 +36,7 @@
3536
* console commands and SHALL only register classes that:
3637
* - Are instantiable
3738
* - Extend the Symfony\Component\Console\Command\Command base class
39+
* - Are not Composer\Command\BaseCommand-based command implementations
3840
* - Declare the Symfony\Component\Console\Attribute\AsCommand attribute
3941
*
4042
* The command name MUST be extracted from the AsCommand attribute metadata and
@@ -53,9 +55,9 @@ final class DevToolsCommandLoader extends ContainerCommandLoader
5355
* @param FinderFactoryInterface $finderFactory
5456
* @param ContainerInterface $container
5557
*/
56-
public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container)
58+
public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container, bool $skipLegacyBaseCommands = false)
5759
{
58-
parent::__construct($container, $this->getCommandMap($finderFactory));
60+
parent::__construct($container, $this->getCommandMap($finderFactory, $skipLegacyBaseCommands));
5961
}
6062

6163
/**
@@ -65,7 +67,7 @@ public function __construct(FinderFactoryInterface $finderFactory, ContainerInte
6567
*
6668
* @return array
6769
*/
68-
private function getCommandMap(FinderFactoryInterface $finderFactory): array
70+
private function getCommandMap(FinderFactoryInterface $finderFactory, bool $skipLegacyBaseCommands): array
6971
{
7072
$commandMap = [];
7173

@@ -89,6 +91,10 @@ private function getCommandMap(FinderFactoryInterface $finderFactory): array
8991
continue;
9092
}
9193

94+
if ($skipLegacyBaseCommands && $reflection->isSubclassOf(BaseCommand::class)) {
95+
continue;
96+
}
97+
9298
$attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null;
9399

94100
if (null === $attribute) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(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+
20+
namespace FastForward\DevTools\Console\CommandLoader;
21+
22+
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
23+
use Psr\Container\ContainerInterface;
24+
25+
/**
26+
* Loads only migrated Symfony commands for the standalone DevTools application runtime.
27+
*/
28+
final class SymfonyDevToolsCommandLoader extends DevToolsCommandLoader
29+
{
30+
/**
31+
* @param FinderFactoryInterface $finderFactory
32+
* @param ContainerInterface $container
33+
*/
34+
public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container)
35+
{
36+
parent::__construct($finderFactory, $container, true);
37+
}
38+
}

src/Console/DevTools.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider;
2323
use Override;
24-
use Composer\Console\Application as ComposerApplication;
2524
use DI\Container;
2625
use Psr\Container\ContainerInterface;
2726
use ReflectionMethod;
@@ -32,7 +31,7 @@
3231
* Wraps the fast-forward console tooling suite conceptually as an isolated application instance.
3332
* Extending the base application, it MUST provide default command injections safely.
3433
*/
35-
final class DevTools extends ComposerApplication
34+
final class DevTools extends Application
3635
{
3736
/**
3837
* @var ContainerInterface holds the static container instance for global access within the DevTools context

0 commit comments

Comments
 (0)