Skip to content

Commit a0f1bb5

Browse files
committed
update: container is only accepted only at load time.
- refactor: now accepts any implementation of PSR-11 Container Interface. - refactor: other codes and comments.
1 parent 777eab1 commit a0f1bb5

File tree

5 files changed

+77
-59
lines changed

5 files changed

+77
-59
lines changed

Src/CommandLoader.php

+53-35
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
namespace TheWebSolver\Codegarage\Cli;
55

66
use Countable;
7+
use LogicException;
8+
use Psr\Container\ContainerInterface;
79
use TheWebSolver\Codegarage\Cli\Console;
810
use TheWebSolver\Codegarage\Cli\Data\EventTask;
9-
use TheWebSolver\Codegarage\Container\Container;
1011
use TheWebSolver\Codegarage\Cli\Event\AfterLoadEvent;
1112
use Symfony\Component\EventDispatcher\EventDispatcher;
1213
use TheWebSolver\Codegarage\Cli\Event\CommandSubscriber;
@@ -19,23 +20,18 @@ class CommandLoader implements Countable {
1920
/** @var array{dirpath:string,namespace:string} */
2021
protected array $base;
2122
private bool $scanStarted;
23+
private ContainerInterface $container;
2224

2325
/**
24-
* @param Container $container
2526
* @param array<int,array<string,string>> $namespacedDirectory
2627
* @param array<string,class-string<Console>> $commands
2728
*/
2829
final private function __construct(
29-
private Container $container,
3030
private array $namespacedDirectory = array(),
3131
private array $commands = array(),
3232
private ?EventDispatcher $dispatcher = null
3333
) {}
3434

35-
public function getContainer(): Container {
36-
return $this->container;
37-
}
38-
3935
/** @return array<int,array<string,string>> List of directory path indexed by its namespace. */
4036
public function getNamespacedDirectories(): array {
4137
return $this->namespacedDirectory;
@@ -46,22 +42,22 @@ public function getCommands(): array {
4642
return $this->commands;
4743
}
4844

49-
public static function with( ?Container $container ): static {
50-
return self::getInstance( $container );
45+
public static function start(): static {
46+
return self::getInstance();
5147
}
5248

5349
/** @param callable(EventTask): void $listener */
54-
public static function withEvent( callable $listener, ?Container $container = null ): static {
55-
return self::getInstance( $container, event: true )->withListener( $listener );
50+
public static function withEvent( callable $listener ): static {
51+
return self::getInstance( event: true )->withListener( $listener );
5652
}
5753

5854
/** @param array<int,array<string,string>> $namespacedDirectories */
59-
public static function loadCommands( array $namespacedDirectories, ?Container $container = null ): static {
60-
$loader = self::getInstance( $container );
55+
public static function loadCommands( array $namespacedDirectories, ContainerInterface $container ): static {
56+
$loader = self::getInstance();
6157

6258
$loader->namespacedDirectory = $namespacedDirectories;
6359

64-
return $loader->load();
60+
return $loader->load( $container );
6561
}
6662

6763
public function inDirectory( string $path, string $namespace ): static {
@@ -70,18 +66,55 @@ public function inDirectory( string $path, string $namespace ): static {
7066
return $this;
7167
}
7268

73-
public function load(): static {
69+
final public function load( ContainerInterface $container ): static {
7470
if ( $this->scanStarted ?? false ) {
7571
return $this;
7672
}
7773

7874
$this->scanStarted = true;
75+
$this->container = $container;
7976

80-
$this->container->get( Cli::class )->eventDispatcher()->addSubscriber( new CommandSubscriber() );
77+
$this->getApp()->eventDispatcher()->addSubscriber( new CommandSubscriber() );
78+
79+
$this->initialize();
8180

8281
return $this->startScan();
8382
}
8483

84+
/**
85+
* Allows inheriting class to initialize its actions before scan has started.
86+
*/
87+
protected function initialize(): void {}
88+
89+
/**
90+
* Returns an instance of Console application (Cli by default).
91+
*
92+
* This may throw PSR-11 exceptions if no container binding identifier exists with $id as `Cli::class`.
93+
*
94+
* @throws LogicException When container resolves something other than `Cli` or its inheritance.
95+
*/
96+
final protected function getApp(): Cli {
97+
return ( $app = $this->container->get( Cli::class ) ) instanceof Cli
98+
? $app
99+
: throw new LogicException( 'Impossible to start Cli application using container.' );
100+
}
101+
102+
/**
103+
* Allows inheriting class to handle the found command.
104+
*
105+
* By default, it defers command instantiation until current command is ran. The provided container
106+
* must have `ContainerInterface::set()` method to defer command instantiation. If that is not the
107+
* case, then this method must be overridden to handle found command (preferably defer loading).
108+
*
109+
* @param class-string<Console> $classname
110+
* @param callable(ContainerInterface): void $command
111+
* @link https://symfony.com/doc/current/console/lazy_commands.html
112+
*/
113+
protected function useFoundCommand( string $classname, callable $command, string $commandName ): void {
114+
/** @disregard P1013 Undefined method 'set' */
115+
method_exists( $this->container, 'set' ) && $this->container->set( $classname, $command );
116+
}
117+
85118
protected function getRootPath(): string {
86119
return $this->base['dirpath'];
87120
}
@@ -99,29 +132,16 @@ protected function forCurrentFile(): void {
99132
$commandName = $commandClass::asCommandName();
100133
$this->commands[ $commandName ] = $commandClass;
101134

102-
$this->handleResolved( $commandClass, $lazyload, $commandName );
135+
$this->useFoundCommand( $commandClass, $lazyload, $commandName );
103136

104137
// Allow developers to listen for resolved command by Command Loader with the "EventTask".
105138
if ( $commandRunner = $this->getCommandRunnerFromEvent() ) {
106139
$commandRunner( new EventTask( $lazyload( ... ), $commandClass, $commandName, $this->container ) );
107140
}
108141
}
109142

110-
/**
111-
* Allows developers to handle the resolved command.
112-
*
113-
* By default, it defers command instantiation until current command is ran.
114-
*
115-
* @param class-string<Console> $classname
116-
* @param callable(Container): void $command
117-
* @link https://symfony.com/doc/current/console/lazy_commands.html
118-
*/
119-
protected function handleResolved( string $classname, callable $command, string $commandName ): void {
120-
$this->container->set( $classname, $command );
121-
}
122-
123-
private static function getInstance( ?Container $container, bool $event = false ): static {
124-
return new static( $container ??= Container::boot(), dispatcher: $event ? new EventDispatcher() : null );
143+
private static function getInstance( bool $event = false ): static {
144+
return new static( dispatcher: $event ? new EventDispatcher() : null );
125145
}
126146

127147
private function fromFilePathToPsr4SpecificationFullyQualifiedClassName(): ?string {
@@ -143,9 +163,7 @@ private function withListener( callable $listener ): static {
143163
private function startScan(): static {
144164
array_walk( $this->namespacedDirectory, $this->scanBaseDirectory( ... ) );
145165

146-
$this->container->get( Cli::class )->setCommandLoader(
147-
new ContainerCommandLoader( $this->container, $this->commands )
148-
);
166+
$this->getApp()->setCommandLoader( new ContainerCommandLoader( $this->container, $this->commands ) );
149167

150168
return $this;
151169
}

Src/Data/EventTask.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
namespace TheWebSolver\Codegarage\Cli\Data;
55

66
use Closure;
7+
use Psr\Container\ContainerInterface;
78
use TheWebSolver\Codegarage\Cli\Console;
8-
use TheWebSolver\Codegarage\Container\Container;
99

1010
/**
11-
* @param Closure(?Container): Console $command
11+
* @param Closure(ContainerInterface): Console $command
1212
* @param class-string<Console> $className
1313
*/
1414
readonly class EventTask {
1515
public function __construct(
1616
public Closure $command,
1717
public string $className,
1818
public string $commandName,
19-
public Container $container
19+
public ContainerInterface $container
2020
) {}
2121
}

Tests/CommandLoaderTest.php

+12-18
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected function setUp(): void {
3131

3232
#[Test]
3333
public function itScansAndLazyloadCommandFromGivenLocation(): void {
34-
$loader = CommandLoader::loadCommands( array( self::NAMESPACED_DIR ) );
34+
$loader = CommandLoader::loadCommands( array( self::NAMESPACED_DIR ), new Container() );
3535

3636
$this->assertEmpty( array_diff_key( self::EXPECTED_COMMANDS, $loader->getCommands() ) );
3737
$this->assertEmpty( array_diff( self::EXPECTED_FILENAMES, $loader->getScannedItems() ) );
@@ -41,7 +41,7 @@ public function itScansAndLazyloadCommandFromGivenLocation(): void {
4141
public function itListensForEventsForEachResolvedCommandFile(): void {
4242
$loader = CommandLoader::withEvent( $this->assertLoadedCommandIsListened( ... ) )
4343
->inDirectory( ...self::LOCATION )
44-
->load();
44+
->load( new Container() );
4545

4646
$this->assertCount( 2, $fileNames = $loader->getScannedItems() );
4747
$this->assertEmpty( array_diff( self::EXPECTED_FILENAMES, $fileNames ) );
@@ -59,13 +59,13 @@ public function assertLoadedCommandIsListened( EventTask $task ): void {
5959

6060
#[Test]
6161
public function itEnsuresCommandsAreLazyLoadedToContainer(): void {
62-
$loader = CommandLoader::loadCommands( array( self::NAMESPACED_DIR ), new Container() );
62+
CommandLoader::loadCommands( array( self::NAMESPACED_DIR ), $container = new Container() );
6363

6464
foreach ( self::EXPECTED_COMMANDS as $class ) {
6565
// The command is registered to container as a closure by CommandLoader.
66-
$this->assertEquals( $class::start( ... ), $loader->getContainer()->getBinding( $class )->material );
66+
$this->assertEquals( $class::start( ... ), $container->getBinding( $class )->material );
6767
// But once the command is resolved by container, it becomes a singleton.
68-
$this->assertSame( $loader->getContainer()->get( $class ), $loader->getContainer()->get( $class ) );
68+
$this->assertSame( $container->get( $class ), $container->get( $class ) );
6969
}
7070
}
7171

@@ -75,8 +75,9 @@ public function itProvidesLazyLoadedCommandsToCli(): void {
7575

7676
$container->setShared( Cli::class );
7777

78-
$loader = CommandLoader::loadCommands( array( self::NAMESPACED_DIR ), $container );
79-
$cli = $loader->getContainer()->get( Cli::class );
78+
CommandLoader::loadCommands( array( self::NAMESPACED_DIR ), $container );
79+
80+
$cli = $container->get( Cli::class );
8081

8182
$this->assertCount( 1, $cli->all( 'app' ) );
8283
$this->assertCount( 1, $cli->all( 'scanned' ) );
@@ -86,28 +87,21 @@ public function itProvidesLazyLoadedCommandsToCli(): void {
8687
}
8788
}
8889

89-
#[Test]
90-
public function itEnsuresCommandLoaderIsInstantiatedWithContainer(): void {
91-
$loader = CommandLoader::loadCommands( array( self::LOCATION ), $c = new Container() );
92-
93-
$this->assertSame( $c, $loader->getContainer() );
94-
}
95-
9690
#[Test]
9791
public function itRegistersCommandsFromSubDirectories(): void {
98-
$subDirLoader = SubDirectoryAwareLoader::with( container: null )
92+
$subDirLoader = SubDirectoryAwareLoader::start()
9993
->usingSubDirectory( 'SubStub', 2 )
10094
->inDirectory( ...self::LOCATION )
101-
->load();
95+
->load( new Container() );
10296

10397
$this->assertContains( FirstDepthCommand::class, $subDirLoader->getCommands() );
10498
$this->assertCount( 4, $subDirLoader->getScannedItems() );
10599

106-
$subDirLoader = SubDirectoryAwareLoader::with( container: null )
100+
$subDirLoader = SubDirectoryAwareLoader::start()
107101
->inDirectory( DirectoryScannerTest::SCAN_PATH, __NAMESPACE__ . '\\Scan' )
108102
->usingSubDirectory( 'SubStub', 2 )
109103
->inDirectory( ...self::LOCATION )
110-
->load();
104+
->load( new Container() );
111105

112106
$this->assertContains( Valid::class, $subDirLoader->getCommands() );
113107
$this->assertCount( 6, $subDirLoader->getScannedItems() );

bootstrap.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use Closure;
2323
use LogicException;
2424
use TheWebSolver\Codegarage\Cli\CommandLoader;
25-
use TheWebSolver\Codegarage\Container\Container;
2625

2726
abstract class Bootstrap {
2827
final private function __construct( private string $packageRootPath = '' ) {}
@@ -128,7 +127,7 @@ private function configure( string $slash = DIRECTORY_SEPARATOR ): array {
128127

129128
/** @var class-string<CommandLoader> */
130129
$commandLoaderClass = $config['commandLoader'] ?? CommandLoader::class;
131-
$commandLoader = $commandLoaderClass::with( Container::boot() );
130+
$commandLoader = $commandLoaderClass::start();
132131
$packageRootPath = $hasPackageConfig ? $this->packageRootPath : $this->cliPackagePath();
133132

134133
foreach ( $config['directory'] ?? array() as ['path' => $dirname, 'namespace' => $namespace] ) {

phpstan.neon

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ parameters:
1414
-
1515
path: Src/CommandLoader.php
1616
identifier: argument.type
17-
count: 3 # 1 for ConsoleChildClass::start() as container binding & others related to "ScannedItemAware" methods.
17+
message: '|CommandLoader\:\:useFoundCommand\(\) expects callable|'
18+
count: 1 # 1 for ConsoleChildClass::start() as container binding.
1819
-
1920
path: Src/Traits/DirectoryScanner.php
2021
identifier: method.notFound
2122
message: "|::registerCurrentItemDepth|"
23+
-
24+
path: Src/Traits/DirectoryScanner.php
25+
identifier: argument.type
26+
messages:
27+
- '|\$value of function count expects array\|Countable\, mixed given|'
28+
- '|\:\:maybeRegisterCurrentDepth\(\) expects array\<string\>\, mixed given|'
2229
-
2330
paths:
2431
- Src/CommandLoader.php

0 commit comments

Comments
 (0)