From 66912da5ecf6292e0da45138691a896dfbef17d5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Sep 2025 11:11:04 +0200 Subject: [PATCH 1/3] avoid loading commands when not necessary --- src/Application/Cli.php | 44 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 100781a..6dd050b 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -34,14 +34,14 @@ final class Cli implements Implementation * @psalm-mutation-free * * @param \Closure(OperatingSystem, Environment): Builder $container - * @param Sequence $commands + * @param (\Closure(Container): Command)|Sequence|null $commands * @param \Closure(Command, Container): Command $mapCommand */ private function __construct( private OperatingSystem $os, private Environment $env, private \Closure $container, - private Sequence $commands, + private \Closure|Sequence|null $commands, private \Closure $mapCommand, ) { } @@ -55,7 +55,7 @@ public static function of(OperatingSystem $os, Environment $env): self $os, $env, static fn() => Builder::new(), - Sequence::of(), + null, static fn(Command $command) => $command, ); } @@ -118,11 +118,21 @@ public function service(Service $name, callable $definition): self #[\Override] public function command(callable $command): self { + $commands = $this->commands; + + if (\is_null($commands)) { + $commands = \Closure::fromCallable($command); + } else if ($commands instanceof Sequence) { + $commands = ($commands)($command); + } else { + $commands = Sequence::of($commands, $command); + } + return new self( $this->os, $this->env, $this->container, - ($this->commands)($command), + $commands, $this->mapCommand, ); } @@ -197,15 +207,23 @@ public function run($input) $command, $container, ); - $commands = $this->commands->map(static fn($command) => new Defer( - \Closure::fromCallable($command), - $container, - $mapCommand, - )); - return $commands->match( - static fn($first, $rest) => Commands::of($first, ...$rest->toList())($input), - static fn() => $input->output(Str::of("Hello world\n")), - ); + if (\is_null($this->commands)) { + return $input->output(Str::of("Hello world\n")); + } + + if ($this->commands instanceof Sequence) { + $commands = $this->commands->map(static fn($command) => new Defer( + \Closure::fromCallable($command), + $container, + $mapCommand, + )); + + return Commands::for($commands)($input); + } + + return Commands::of($mapCommand( + ($this->commands)($container), + ))($input); } } From a5b8109a81cfe29ca7316788d012b2ebf386f6ef Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Sep 2025 11:39:15 +0200 Subject: [PATCH 2/3] add mechanism to loazy load commands --- CHANGELOG.md | 1 + src/Cli/Command.php | 71 +++++++++++++++++++++++++++++++++++ tests/ApplicationTest.php | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/Cli/Command.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b02d7..6078223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `Innmind\Framework\Application::mapRoute()` - `Innmind\Framework\Application::routes(class-string)` - `Innmind\Framework\Application::recoverRouteError()` +- `Innmind\Framework\Cli\Command` ### Changed diff --git a/src/Cli/Command.php b/src/Cli/Command.php new file mode 100644 index 0000000..cd9ddee --- /dev/null +++ b/src/Cli/Command.php @@ -0,0 +1,71 @@ + $class + * @param list $dependencies + */ + private function __construct( + private Container $get, + private string $class, + private array $dependencies, + ) { + } + + #[\Override] + public function __invoke(Console $console): Attempt + { + return $this->load()($console); + } + + /** + * @psalm-pure + * @no-named-arguments + * + * @param class-string $class + */ + public static function of( + string $class, + Service ...$dependencies, + ): callable { + return static fn(Container $get) => new self($get, $class, $dependencies); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function usage(): Usage + { + return Usage::for($this->class)->load( + fn() => $this->load()->usage(), + ); + } + + private function load(): CommandInterface + { + return $this->command ??= new ($this->class)( + ...\array_map( + $this->get, + $this->dependencies, + ), + ); + } +} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index f787f82..091da91 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -10,6 +10,7 @@ Middleware\Optional, Middleware\LoadDotEnv, Http\Route, + Cli\Command as CommandReference, }; use Innmind\OperatingSystem\Factory; use Innmind\CLI\{ @@ -63,6 +64,25 @@ public function route(): callable } } +#[Command\Name('lazy')] +final class Lazy implements Command +{ + public function __construct( + private Str $output, + ) { + } + + public function __invoke(Console $console): Attempt + { + return $console->output($this->output); + } + + public function usage(): Usage + { + return Usage::for(self::class)->flag('foo'); + } +} + class ApplicationTest extends TestCase { use BlackBox; @@ -477,6 +497,65 @@ public function usage(): Usage }); } + #[\PHPUnit\Framework\Attributes\Group('wip')] + public function testLazyCommandAreNotLoaded(): BlackBox\Proof + { + return $this + ->forAll( + Set::sequence(Set::strings())->between(0, 10), + Set::of(true, false), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + Set::strings(), + ) + ->prove(function($inputs, $interactive, $variables, $output) { + $loaded = false; + $app = Application::cli(Factory::build(), Environment::test($variables)) + ->command(CommandReference::of(Lazy::class, Services::service)) + ->command(CommandReference::of(Lazy::class, Services::service)) + ->service(Services::service, static function() use ($output, &$loaded) { + $loaded = true; + + return Str::of($output); + }); + + $env = $app->run(InMemory::of( + $inputs, + $interactive, + ['script-name', 'help'], + $variables, + '/', + ))->unwrap(); + + $this->assertSame([" lazy \n", " lazy \n"], $env->outputs()); + $this->assertNull($env->exitCode()->match( + static fn($exit) => $exit, + static fn() => null, + )); + $this->assertFalse($loaded); + + $env = $app->run(InMemory::of( + $inputs, + $interactive, + ['script-name', 'lazy'], + $variables, + '/', + ))->unwrap(); + + $this->assertSame([$output], $env->outputs()); + $this->assertNull($env->exitCode()->match( + static fn($exit) => $exit, + static fn() => null, + )); + $this->assertTrue($loaded); + }); + } + public function testDecoratingCommands(): BlackBox\Proof { return $this From 8dbe14b8836eee9535e7d7b69cf4566eb514039e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Sep 2025 11:40:45 +0200 Subject: [PATCH 3/3] remove wip tag --- tests/ApplicationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 091da91..b07c450 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -497,7 +497,6 @@ public function usage(): Usage }); } - #[\PHPUnit\Framework\Attributes\Group('wip')] public function testLazyCommandAreNotLoaded(): BlackBox\Proof { return $this