From aa119b15aabf383f08f17d8743062876ddeab807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sun, 30 Jul 2023 21:43:59 +0200 Subject: [PATCH] feat: Introduce a command loader factory and improve laziness --- src/Application/Application.php | 3 +- src/Application/ApplicationRunner.php | 13 +++-- src/Bridge/Application/SymfonyApplication.php | 37 ++----------- .../Command/BasicSymfonyCommandFactory.php | 16 ++++++ src/Bridge/Command/SymfonyCommandFactory.php | 10 ++++ .../CommandLoader/CommandLoaderFactory.php | 26 +++++++++ .../SymfonyFactoryCommandLoaderFactory.php | 55 +++++++++++++++++++ src/Command/LazyCommandEnvelope.php | 43 +++++++++++++++ src/Test/AppTester.php | 7 ++- tests/Application/ApplicationRunnerTest.php | 4 +- .../Feature/BaseApplicationTest.php | 9 ++- ...hp => FakeSymfonyCommandLoaderFactory.php} | 9 ++- .../FakeCommandLoaderFactory.php | 26 +++++++++ ...SymfonyFactoryCommandLoaderFactoryTest.php | 26 +++++++++ .../StandaloneSymfonyApplication.php | 12 +++- tests/Integration/services.yaml | 4 +- 16 files changed, 247 insertions(+), 53 deletions(-) create mode 100644 src/Bridge/CommandLoader/CommandLoaderFactory.php create mode 100644 src/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactory.php create mode 100644 src/Command/LazyCommandEnvelope.php rename tests/Bridge/Command/{FakeSymfonyCommandFactory.php => FakeSymfonyCommandLoaderFactory.php} (55%) create mode 100644 tests/Bridge/CommandLoader/FakeCommandLoaderFactory.php create mode 100644 tests/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactoryTest.php diff --git a/src/Application/Application.php b/src/Application/Application.php index 2ac54e0e..daded4f3 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -14,6 +14,7 @@ namespace Fidry\Console\Application; use Fidry\Console\Command\Command; +use Fidry\Console\Command\LazyCommandEnvelope; interface Application { @@ -43,7 +44,7 @@ public function getHelp(): string; * Exhaustive list of the custom commands. A few more commands such as * the HelpCommand or ListCommand are also included besides those. * - * @return Command[] + * @return array */ public function getCommands(): array; diff --git a/src/Application/ApplicationRunner.php b/src/Application/ApplicationRunner.php index 45616a55..25ffa174 100644 --- a/src/Application/ApplicationRunner.php +++ b/src/Application/ApplicationRunner.php @@ -15,7 +15,8 @@ use Fidry\Console\Bridge\Application\SymfonyApplication; use Fidry\Console\Bridge\Command\BasicSymfonyCommandFactory; -use Fidry\Console\Bridge\Command\SymfonyCommandFactory; +use Fidry\Console\Bridge\CommandLoader\CommandLoaderFactory; +use Fidry\Console\Bridge\CommandLoader\SymfonyFactoryCommandLoaderFactory; use Fidry\Console\IO; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; @@ -28,11 +29,13 @@ final class ApplicationRunner public function __construct( Application $application, - ?SymfonyCommandFactory $commandFactory = null, + ?CommandLoaderFactory $commandLoaderFactory = null, ) { $this->application = new SymfonyApplication( $application, - $commandFactory ?? new BasicSymfonyCommandFactory(), + $commandLoaderFactory ?? new SymfonyFactoryCommandLoaderFactory( + new BasicSymfonyCommandFactory(), + ), ); } @@ -49,11 +52,11 @@ public static function runApplication( Application $application, ?InputInterface $input = null, ?OutputInterface $output = null, - ?SymfonyCommandFactory $commandFactory = null, + ?CommandLoaderFactory $commandLoaderFactory = null, ): int { $runner = new self( $application, - $commandFactory, + $commandLoaderFactory, ); return $runner->run( diff --git a/src/Bridge/Application/SymfonyApplication.php b/src/Bridge/Application/SymfonyApplication.php index 9a7d8642..03041e5d 100644 --- a/src/Bridge/Application/SymfonyApplication.php +++ b/src/Bridge/Application/SymfonyApplication.php @@ -15,19 +15,15 @@ use Fidry\Console\Application\Application; use Fidry\Console\Application\ConfigurableIO; -use Fidry\Console\Bridge\Command\SymfonyCommandFactory; +use Fidry\Console\Bridge\CommandLoader\CommandLoaderFactory; use Fidry\Console\IO; use LogicException; use Symfony\Component\Console\Application as BaseSymfonyApplication; -use Symfony\Component\Console\Command\Command as BaseSymfonyCommand; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Contracts\Service\ResetInterface; -use function array_map; -use function array_values; /** * Bridge to create a traditional Symfony application from the new Application @@ -37,7 +33,7 @@ final class SymfonyApplication extends BaseSymfonyApplication { public function __construct( private readonly Application $application, - private readonly SymfonyCommandFactory $commandFactory, + CommandLoaderFactory $commandLoaderFactory, ) { parent::__construct( $application->getName(), @@ -47,6 +43,9 @@ public function __construct( $this->setDefaultCommand($application->getDefaultCommand()); $this->setAutoExit($application->isAutoExitEnabled()); $this->setCatchExceptions($application->areExceptionsCaught()); + $this->setCommandLoader( + $commandLoaderFactory->createCommandLoader($application->getCommands()), + ); } public function reset(): void @@ -76,11 +75,6 @@ public function getLongVersion(): string return $this->application->getLongVersion(); } - public function setCommandLoader(CommandLoaderInterface $commandLoader): void - { - throw new LogicException('Not supported'); - } - public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void { throw new LogicException('Not supported'); @@ -106,25 +100,4 @@ protected function configureIO(InputInterface $input, OutputInterface $output): ); } } - - protected function getDefaultCommands(): array - { - return [ - ...parent::getDefaultCommands(), - ...$this->getSymfonyCommands(), - ]; - } - - /** - * @return list - */ - private function getSymfonyCommands(): array - { - return array_values( - array_map( - $this->commandFactory->crateSymfonyCommand(...), - $this->application->getCommands(), - ), - ); - } } diff --git a/src/Bridge/Command/BasicSymfonyCommandFactory.php b/src/Bridge/Command/BasicSymfonyCommandFactory.php index efd66ba3..4ad69c4a 100644 --- a/src/Bridge/Command/BasicSymfonyCommandFactory.php +++ b/src/Bridge/Command/BasicSymfonyCommandFactory.php @@ -13,6 +13,7 @@ namespace Fidry\Console\Bridge\Command; +use Closure; use Fidry\Console\Command\Command as FidryCommand; use Fidry\Console\Command\LazyCommand as FidryLazyCommand; use Symfony\Component\Console\Command\Command as BaseSymfonyCommand; @@ -33,4 +34,19 @@ public function crateSymfonyCommand(FidryCommand $command): BaseSymfonyCommand ) : new SymfonyCommand($command); } + + public function crateSymfonyLazyCommand( + string $name, + string $description, + Closure $factory, + ): BaseSymfonyCommand { + return new SymfonyLazyCommand( + $name, + [], + $description, + false, + static fn () => new SymfonyCommand($factory()), + true, + ); + } } diff --git a/src/Bridge/Command/SymfonyCommandFactory.php b/src/Bridge/Command/SymfonyCommandFactory.php index c85baffe..85292d05 100644 --- a/src/Bridge/Command/SymfonyCommandFactory.php +++ b/src/Bridge/Command/SymfonyCommandFactory.php @@ -13,10 +13,20 @@ namespace Fidry\Console\Bridge\Command; +use Closure; use Fidry\Console\Command\Command as FidryCommand; use Symfony\Component\Console\Command\Command as BaseSymfonyCommand; interface SymfonyCommandFactory { public function crateSymfonyCommand(FidryCommand $command): BaseSymfonyCommand; + + /** + * @param Closure(): FidryCommand $factory + */ + public function crateSymfonyLazyCommand( + string $name, + string $description, + Closure $factory, + ): BaseSymfonyCommand; } diff --git a/src/Bridge/CommandLoader/CommandLoaderFactory.php b/src/Bridge/CommandLoader/CommandLoaderFactory.php new file mode 100644 index 00000000..9daab9bf --- /dev/null +++ b/src/Bridge/CommandLoader/CommandLoaderFactory.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Fidry\Console\Bridge\CommandLoader; + +use Fidry\Console\Command\Command as FidryCommand; +use Fidry\Console\Command\LazyCommandEnvelope; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; + +interface CommandLoaderFactory +{ + /** + * @param array $commands + */ + public function createCommandLoader(array $commands): CommandLoaderInterface; +} diff --git a/src/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactory.php b/src/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactory.php new file mode 100644 index 00000000..3d72f96e --- /dev/null +++ b/src/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactory.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Fidry\Console\Bridge\CommandLoader; + +use Fidry\Console\Bridge\Command\SymfonyCommandFactory; +use Fidry\Console\Command\Command as FidryCommand; +use Fidry\Console\Command\LazyCommandEnvelope; +use Symfony\Component\Console\Command\Command as SymfonyNativeCommand; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; + +final class SymfonyFactoryCommandLoaderFactory implements CommandLoaderFactory +{ + public function __construct( + private readonly SymfonyCommandFactory $commandFactory, + ) { + } + + public function createCommandLoader(array $commands): CommandLoaderInterface + { + $factories = []; + + foreach ($commands as $commandOrEnvelope) { + $command = $this->createCommand($commandOrEnvelope); + /** @var string $name */ + $name = $command->getName(); + + $factories[$name] = static fn (): SymfonyNativeCommand => $command; + } + + return new FactoryCommandLoader($factories); + } + + private function createCommand(LazyCommandEnvelope|FidryCommand $commandOrCommandFactory): SymfonyNativeCommand + { + return $commandOrCommandFactory instanceof FidryCommand + ? $this->commandFactory->crateSymfonyCommand($commandOrCommandFactory) + : $this->commandFactory->crateSymfonyLazyCommand( + $commandOrCommandFactory->name, + $commandOrCommandFactory->description, + $commandOrCommandFactory->factory, + ); + } +} diff --git a/src/Command/LazyCommandEnvelope.php b/src/Command/LazyCommandEnvelope.php new file mode 100644 index 00000000..2ec8923b --- /dev/null +++ b/src/Command/LazyCommandEnvelope.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Fidry\Console\Command; + +// TODO: description & doc +use Closure; + +final class LazyCommandEnvelope +{ + /** + * @param Closure(): Command $factory + */ + public function __construct( + public readonly string $name, + public readonly string $description, + public readonly Closure $factory, + ) { + } + + /** + * @param class-string $commandClassName + * @param Closure():LazyCommand $factory + */ + public static function wrap(string $commandClassName, Closure $factory): self + { + return new self( + $commandClassName::getName(), + $commandClassName::getDescription(), + $factory, + ); + } +} diff --git a/src/Test/AppTester.php b/src/Test/AppTester.php index afc8bd51..46e267be 100644 --- a/src/Test/AppTester.php +++ b/src/Test/AppTester.php @@ -16,7 +16,8 @@ use Fidry\Console\Application\Application as ConsoleApplication; use Fidry\Console\Bridge\Application\SymfonyApplication; use Fidry\Console\Bridge\Command\BasicSymfonyCommandFactory; -use Fidry\Console\Bridge\Command\SymfonyCommandFactory; +use Fidry\Console\Bridge\CommandLoader\CommandLoaderFactory; +use Fidry\Console\Bridge\CommandLoader\SymfonyFactoryCommandLoaderFactory; use Fidry\Console\DisplayNormalizer; use Symfony\Component\Console\Tester\ApplicationTester; @@ -27,7 +28,9 @@ final class AppTester extends ApplicationTester { public static function fromConsoleApp( ConsoleApplication $application, - SymfonyCommandFactory $commandFactory = new BasicSymfonyCommandFactory(), + CommandLoaderFactory $commandFactory = new SymfonyFactoryCommandLoaderFactory( + new BasicSymfonyCommandFactory(), + ), ): self { return new self( new SymfonyApplication( diff --git a/tests/Application/ApplicationRunnerTest.php b/tests/Application/ApplicationRunnerTest.php index 718212bd..e339f32c 100644 --- a/tests/Application/ApplicationRunnerTest.php +++ b/tests/Application/ApplicationRunnerTest.php @@ -15,7 +15,7 @@ use DomainException; use Fidry\Console\Application\ApplicationRunner; -use Fidry\Console\Tests\Bridge\Command\FakeSymfonyCommandFactory; +use Fidry\Console\Tests\Bridge\Command\FakeSymfonyCommandLoaderFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -26,7 +26,7 @@ public function test_it_uses_the_command_factory_given(): void { $runner = new ApplicationRunner( new DummyApplication(), - new FakeSymfonyCommandFactory(), + new FakeSymfonyCommandLoaderFactory(), ); $this->expectException(DomainException::class); diff --git a/tests/Application/Feature/BaseApplicationTest.php b/tests/Application/Feature/BaseApplicationTest.php index a624e45b..b4dd4691 100644 --- a/tests/Application/Feature/BaseApplicationTest.php +++ b/tests/Application/Feature/BaseApplicationTest.php @@ -16,6 +16,7 @@ use Fidry\Console\Application\ApplicationRunner; use Fidry\Console\Bridge\Application\SymfonyApplication; use Fidry\Console\Bridge\Command\BasicSymfonyCommandFactory; +use Fidry\Console\Bridge\CommandLoader\SymfonyFactoryCommandLoaderFactory; use Fidry\Console\IO; use Fidry\Console\Tests\Application\Fixture\SimpleApplicationUsingBaseApplication; use Fidry\Console\Tests\Application\OutputAssertions; @@ -77,7 +78,9 @@ public function test_it_can_be_run_without_the_static_helper(): void $runner = new ApplicationRunner( new SimpleApplicationUsingBaseApplication(), - new BasicSymfonyCommandFactory(), + new SymfonyFactoryCommandLoaderFactory( + new BasicSymfonyCommandFactory(), + ), ); $runner->run( @@ -97,7 +100,9 @@ public function test_it_can_display_the_version_used(): void $runner = new ApplicationRunner( new SimpleApplicationUsingBaseApplication(), - new BasicSymfonyCommandFactory(), + new SymfonyFactoryCommandLoaderFactory( + new BasicSymfonyCommandFactory(), + ), ); $runner->run( diff --git a/tests/Bridge/Command/FakeSymfonyCommandFactory.php b/tests/Bridge/Command/FakeSymfonyCommandLoaderFactory.php similarity index 55% rename from tests/Bridge/Command/FakeSymfonyCommandFactory.php rename to tests/Bridge/Command/FakeSymfonyCommandLoaderFactory.php index 3fcbb5e4..fd7f7338 100644 --- a/tests/Bridge/Command/FakeSymfonyCommandFactory.php +++ b/tests/Bridge/Command/FakeSymfonyCommandLoaderFactory.php @@ -14,13 +14,12 @@ namespace Fidry\Console\Tests\Bridge\Command; use DomainException; -use Fidry\Console\Bridge\Command\SymfonyCommandFactory; -use Fidry\Console\Command\Command as FidryCommand; -use Symfony\Component\Console\Command\Command as BaseSymfonyCommand; +use Fidry\Console\Bridge\CommandLoader\CommandLoaderFactory; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; -final class FakeSymfonyCommandFactory implements SymfonyCommandFactory +final class FakeSymfonyCommandLoaderFactory implements CommandLoaderFactory { - public function crateSymfonyCommand(FidryCommand $command): BaseSymfonyCommand + public function createCommandLoader(array $commands): CommandLoaderInterface { throw new DomainException('Should not be called.'); } diff --git a/tests/Bridge/CommandLoader/FakeCommandLoaderFactory.php b/tests/Bridge/CommandLoader/FakeCommandLoaderFactory.php new file mode 100644 index 00000000..8f959481 --- /dev/null +++ b/tests/Bridge/CommandLoader/FakeCommandLoaderFactory.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Fidry\Console\Tests\Bridge\CommandLoader; + +use DomainException; +use Fidry\Console\Bridge\CommandLoader\CommandLoaderFactory; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; + +final class FakeCommandLoaderFactory implements CommandLoaderFactory +{ + public function createCommandLoader(array $commands): CommandLoaderInterface + { + throw new DomainException('Should not be called.'); + } +} diff --git a/tests/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactoryTest.php b/tests/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactoryTest.php new file mode 100644 index 00000000..e29570ae --- /dev/null +++ b/tests/Bridge/CommandLoader/SymfonyFactoryCommandLoaderFactoryTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Fidry\Console\Tests\Bridge\CommandLoader; + +use PHPUnit\Framework\TestCase; + +/** + * @covers \Fidry\Console\Bridge\CommandLoader\SymfonyFactoryCommandLoaderFactory + */ +final class SymfonyFactoryCommandLoaderFactoryTest extends TestCase +{ + public function ctor(): void + { + } +} diff --git a/tests/Integration/StandaloneSymfonyApplication.php b/tests/Integration/StandaloneSymfonyApplication.php index 1237d70a..0290881e 100644 --- a/tests/Integration/StandaloneSymfonyApplication.php +++ b/tests/Integration/StandaloneSymfonyApplication.php @@ -15,6 +15,7 @@ use DomainException; use Fidry\Console\Application\BaseApplication; +use Fidry\Console\Command\LazyCommandEnvelope; use Fidry\Console\Helper\QuestionHelper; use Fidry\Console\Tests\Application\Fixture\ManualLazyCommand; use Fidry\Console\Tests\Command\Fixture\CommandAwareCommand; @@ -65,8 +66,15 @@ public function getCommands(): array new ManualLazyCommand( static fn () => new FakeCommand(), ), - // TODO: add easier support for lazy commands in order to allow this. - // new SimpleLazyCommand(static fn () => throw new DomainException()), + new LazyCommandEnvelope( + 'app:wrapped-lazy', + 'wrapped lazy command description', + static fn () => new SimpleCommand(), + ), + LazyCommandEnvelope::wrap( + SimpleLazyCommand::class, + static fn () => new SimpleLazyCommand(static fn () => throw new DomainException()), + ), ]; } } diff --git a/tests/Integration/services.yaml b/tests/Integration/services.yaml index 14e637f1..cb718606 100644 --- a/tests/Integration/services.yaml +++ b/tests/Integration/services.yaml @@ -19,7 +19,7 @@ services: autoconfigure: false app.simple_lazy_1: - class: Fidry\Console\Command\SymfonyCommand + class: Fidry\Console\Bridge\Command\SymfonyCommand arguments: - '@app.simple_command' tags: @@ -28,7 +28,7 @@ services: description: 'First instance of a "traditional" lazy Symfony command' app.simple_lazy_2: - class: Fidry\Console\Command\SymfonyCommand + class: Fidry\Console\Bridge\Command\SymfonyCommand arguments: - '@app.simple_command' tags: