diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index f89190927..3ae0b6f0f 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -173,7 +173,8 @@ public function run(?EnvironmentInterface $environment = null): ?self function (Container $container): void { $registry = $container->get(BootloaderRegistryInterface::class); - $this->bootloader->bootload($registry->getSystemBootloaders()); + /** @psalm-suppress TooManyArguments */ + $this->bootloader->bootload($registry->getSystemBootloaders(), [], [], false); $this->fireCallbacks($this->runningCallbacks); $this->bootload($registry->getBootloaders()); diff --git a/src/Boot/src/Attribute/BootloadConfig.php b/src/Boot/src/Attribute/BootloadConfig.php new file mode 100644 index 000000000..2ab9ae3b7 --- /dev/null +++ b/src/Boot/src/Attribute/BootloadConfig.php @@ -0,0 +1,20 @@ +initializer->getRegistry()->getClasses(); } - public function bootload(array $classes, array $bootingCallbacks = [], array $bootedCallbacks = []): void - { + public function bootload( + array $classes, + array $bootingCallbacks = [], + array $bootedCallbacks = [], + bool $useConfig = true + ): void { $this->scope->runScope( [self::class => $this], - function () use ($classes, $bootingCallbacks, $bootedCallbacks): void { - $this->boot($classes, $bootingCallbacks, $bootedCallbacks); + function () use ($classes, $bootingCallbacks, $bootedCallbacks, $useConfig): void { + /** @psalm-suppress TooManyArguments */ + $this->boot($classes, $bootingCallbacks, $bootedCallbacks, $useConfig); } ); } diff --git a/src/Boot/src/BootloadManager/Checker/BootloaderChecker.php b/src/Boot/src/BootloadManager/Checker/BootloaderChecker.php new file mode 100644 index 000000000..234eefca0 --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/BootloaderChecker.php @@ -0,0 +1,27 @@ +registry->getCheckers() as $checker) { + if (!$checker->canInitialize($bootloader, $config)) { + return false; + } + } + + return true; + } +} diff --git a/src/Boot/src/BootloadManager/Checker/BootloaderCheckerInterface.php b/src/Boot/src/BootloadManager/Checker/BootloaderCheckerInterface.php new file mode 100644 index 000000000..f69da67cf --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/BootloaderCheckerInterface.php @@ -0,0 +1,16 @@ +|BootloaderInterface $bootloader + */ + public function canInitialize(string|BootloaderInterface $bootloader, ?BootloadConfig $config = null): bool; +} diff --git a/src/Boot/src/BootloadManager/Checker/CanBootedChecker.php b/src/Boot/src/BootloadManager/Checker/CanBootedChecker.php new file mode 100644 index 000000000..1e4f3358c --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/CanBootedChecker.php @@ -0,0 +1,27 @@ +bootloaders->isBooted($ref->getName()) + && !$ref->isAbstract() + && !$ref->isInterface() + && $ref->implementsInterface(BootloaderInterface::class); + } +} diff --git a/src/Boot/src/BootloadManager/Checker/CheckerRegistry.php b/src/Boot/src/BootloadManager/Checker/CheckerRegistry.php new file mode 100644 index 000000000..3a9dd879f --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/CheckerRegistry.php @@ -0,0 +1,26 @@ + + */ + private array $checkers = []; + + public function register(BootloaderCheckerInterface $checker): void + { + $this->checkers[] = $checker; + } + + /** + * @return array + */ + public function getCheckers(): array + { + return $this->checkers; + } +} diff --git a/src/Boot/src/BootloadManager/Checker/CheckerRegistryInterface.php b/src/Boot/src/BootloadManager/Checker/CheckerRegistryInterface.php new file mode 100644 index 000000000..12203a79c --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/CheckerRegistryInterface.php @@ -0,0 +1,15 @@ + + */ + public function getCheckers(): array; +} diff --git a/src/Boot/src/BootloadManager/Checker/ClassExistsChecker.php b/src/Boot/src/BootloadManager/Checker/ClassExistsChecker.php new file mode 100644 index 000000000..0d0f0b894 --- /dev/null +++ b/src/Boot/src/BootloadManager/Checker/ClassExistsChecker.php @@ -0,0 +1,28 @@ +enabled) { + return false; + } + + foreach ($config->denyEnv as $env => $denyValues) { + $value = $this->environment->get($env); + if ($value !== null && \in_array($value, (array) $denyValues, true)) { + return false; + } + } + + foreach ($config->allowEnv as $env => $allowValues) { + $value = $this->environment->get($env); + if ($value === null || !\in_array($value, (array) $allowValues, true)) { + return false; + } + } + + return true; + } +} diff --git a/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php b/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php index ec95c60ff..2dc939b97 100644 --- a/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php +++ b/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php @@ -17,9 +17,14 @@ public function __construct( ) { } - public function invokeBootloaders(array $classes, array $bootingCallbacks, array $bootedCallbacks): void - { - $bootloaders = \iterator_to_array($this->initializer->init($classes)); + public function invokeBootloaders( + array $classes, + array $bootingCallbacks, + array $bootedCallbacks, + bool $useConfig = true + ): void { + /** @psalm-suppress TooManyArguments */ + $bootloaders = \iterator_to_array($this->initializer->init($classes, $useConfig)); foreach ($bootloaders as $data) { $this->invokeBootloader($data['bootloader'], Methods::INIT, $data['options']); diff --git a/src/Boot/src/BootloadManager/Initializer.php b/src/Boot/src/BootloadManager/Initializer.php index b1d905662..6bdd9dc84 100644 --- a/src/Boot/src/BootloadManager/Initializer.php +++ b/src/Boot/src/BootloadManager/Initializer.php @@ -5,12 +5,19 @@ namespace Spiral\Boot\BootloadManager; use Psr\Container\ContainerInterface; +use Spiral\Boot\Attribute\BootloadConfig; use Spiral\Boot\Bootloader\BootloaderInterface; use Spiral\Boot\Bootloader\DependedInterface; +use Spiral\Boot\BootloadManager\Checker\BootloaderChecker; +use Spiral\Boot\BootloadManager\Checker\BootloaderCheckerInterface; +use Spiral\Boot\BootloadManager\Checker\CanBootedChecker; +use Spiral\Boot\BootloadManager\Checker\CheckerRegistry; +use Spiral\Boot\BootloadManager\Checker\ClassExistsChecker; +use Spiral\Boot\BootloadManager\Checker\ConfigChecker; use Spiral\Boot\BootloadManagerInterface; -use Spiral\Boot\Exception\ClassNotFoundException; use Spiral\Core\BinderInterface; use Spiral\Core\Container; +use Spiral\Core\ResolverInterface; /** * @internal @@ -19,10 +26,13 @@ */ class Initializer implements InitializerInterface, Container\SingletonInterface { + protected ?BootloaderCheckerInterface $checker = null; + public function __construct( protected readonly ContainerInterface $container, protected readonly BinderInterface $binder, - protected readonly ClassesRegistry $bootloaders = new ClassesRegistry() + protected readonly ClassesRegistry $bootloaders = new ClassesRegistry(), + ?BootloaderCheckerInterface $checker = null, ) { } @@ -31,42 +41,34 @@ public function __construct( * * @param TClass[]|array> $classes */ - public function init(array $classes): \Generator + public function init(array $classes, bool $useConfig = true): \Generator { + $this->checker ??= $this->initDefaultChecker(); + foreach ($classes as $bootloader => $options) { // default bootload syntax as simple array if (\is_string($options) || $options instanceof BootloaderInterface) { $bootloader = $options; $options = []; } + $options = $useConfig ? $this->getBootloadConfig($bootloader, $options) : []; - // Replace class aliases with source classes - try { - $ref = (new \ReflectionClass($bootloader)); - $className = $ref->getName(); - } catch (\ReflectionException) { - throw new ClassNotFoundException( - \sprintf('Bootloader class `%s` is not exist.', $bootloader) - ); - } - - if ($this->bootloaders->isBooted($className) || $ref->isAbstract()) { + if (!$this->checker->canInitialize($bootloader, $useConfig ? $options : null)) { continue; } - $this->bootloaders->register($className); + $this->bootloaders->register($bootloader instanceof BootloaderInterface ? $bootloader::class : $bootloader); if (!$bootloader instanceof BootloaderInterface) { $bootloader = $this->container->get($bootloader); } - if (!$this->isBootloader($bootloader)) { - continue; - } - /** @var BootloaderInterface $bootloader */ yield from $this->initBootloader($bootloader); - yield $className => \compact('bootloader', 'options'); + yield $bootloader::class => [ + 'bootloader' => $bootloader, + 'options' => $options instanceof BootloadConfig ? $options->args : $options, + ]; } } @@ -156,4 +158,69 @@ protected function isBootloader(string|object $class): bool { return \is_subclass_of($class, BootloaderInterface::class); } + + protected function initDefaultChecker(): BootloaderCheckerInterface + { + $registry = new CheckerRegistry(); + $registry->register($this->container->get(ConfigChecker::class)); + $registry->register(new ClassExistsChecker()); + $registry->register(new CanBootedChecker($this->bootloaders)); + + return new BootloaderChecker($registry); + } + + /** + * Returns merged config. Attribute config have lower priority. + * + * @param class-string|BootloaderInterface $bootloader + */ + private function getBootloadConfig( + string|BootloaderInterface $bootloader, + array|callable|BootloadConfig $config + ): BootloadConfig { + if ($config instanceof \Closure) { + $config = $this->container instanceof ResolverInterface + ? $config(...$this->container->resolveArguments(new \ReflectionFunction($config))) + : $config(); + } + $attr = $this->getBootloadConfigAttribute($bootloader); + + $getArgument = static function (string $key, bool $override, mixed $default = []) use ($config, $attr): mixed { + return match (true) { + $config instanceof BootloadConfig && $override => $config->{$key}, + $config instanceof BootloadConfig && !$override && \is_array($default) => + $config->{$key} + ($attr->{$key} ?? []), + $config instanceof BootloadConfig && !$override && \is_bool($default) => $config->{$key}, + \is_array($config) && $config !== [] && $key === 'args' => $config, + default => $attr->{$key} ?? $default, + }; + }; + + $override = $config instanceof BootloadConfig ? $config->override : true; + + return new BootloadConfig( + args: $getArgument('args', $override), + enabled: $getArgument('enabled', $override, true), + allowEnv: $getArgument('allowEnv', $override), + denyEnv: $getArgument('denyEnv', $override), + ); + } + + /** + * @param class-string|BootloaderInterface $bootloader + */ + private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig + { + $attribute = null; + if ($bootloader instanceof BootloaderInterface || \class_exists($bootloader)) { + $ref = new \ReflectionClass($bootloader); + $attribute = $ref->getAttributes(BootloadConfig::class)[0] ?? null; + } + + if ($attribute === null) { + return null; + } + + return $attribute->newInstance(); + } } diff --git a/src/Boot/src/BootloadManager/StrategyBasedBootloadManager.php b/src/Boot/src/BootloadManager/StrategyBasedBootloadManager.php index 6d36783ec..308719147 100644 --- a/src/Boot/src/BootloadManager/StrategyBasedBootloadManager.php +++ b/src/Boot/src/BootloadManager/StrategyBasedBootloadManager.php @@ -23,8 +23,13 @@ public function __construct( * * @throws \Throwable */ - protected function boot(array $classes, array $bootingCallbacks, array $bootedCallbacks): void - { - $this->invoker->invokeBootloaders($classes, $bootingCallbacks, $bootedCallbacks); + protected function boot( + array $classes, + array $bootingCallbacks, + array $bootedCallbacks, + bool $useConfig = true + ): void { + /** @psalm-suppress TooManyArguments */ + $this->invoker->invokeBootloaders($classes, $bootingCallbacks, $bootedCallbacks, $useConfig); } } diff --git a/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php b/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php new file mode 100644 index 000000000..cd37245e4 --- /dev/null +++ b/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php @@ -0,0 +1,150 @@ +initializer->init([BootloaderE::class, BootloaderD::class])); + + $this->assertEquals([ + BootloaderE::class => ['bootloader' => new BootloaderE(), 'options' => []], + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testDisabledBootloader(): void + { + $result = \iterator_to_array($this->initializer->init([BootloaderF::class, BootloaderD::class])); + + $this->assertEquals([ + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testArguments(): void + { + $result = \iterator_to_array($this->initializer->init([BootloaderG::class])); + + $this->assertEquals([ + BootloaderG::class => ['bootloader' => new BootloaderG(), 'options' => ['a' => 'b', 'c' => 'd']], + ], $result); + } + + public function testDisabledConfig(): void + { + $result = \iterator_to_array($this->initializer->init([BootloaderF::class, BootloaderD::class], false)); + + $this->assertEquals([ + BootloaderF::class => ['bootloader' => new BootloaderF(), 'options' => []], + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + #[DataProvider('allowEnvDataProvider')] + public function testAllowEnv(array $env, array $expected): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment($env), true); + + $result = \iterator_to_array($this->initializer->init([BootloaderH::class])); + + $this->assertEquals($expected, $result); + } + + #[DataProvider('denyEnvDataProvider')] + public function testDenyEnv(array $env, array $expected): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment($env), true); + + $result = \iterator_to_array($this->initializer->init([BootloaderI::class])); + + $this->assertEquals($expected, $result); + } + + public function testDenyEnvShouldHaveHigherPriority(): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment(['APP_DEBUG' => true]), true); + + $result = \iterator_to_array($this->initializer->init([BootloaderJ::class])); + + $this->assertEquals([], $result); + } + + public function testExtendedAttribute(): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment(['RR_MODE' => 'http']), true); + $result = \iterator_to_array($this->initializer->init([BootloaderK::class])); + $this->assertEquals([BootloaderK::class => ['bootloader' => new BootloaderK(), 'options' => []]], $result); + + $this->container->bindSingleton(EnvironmentInterface::class, new Environment(['RR_MODE' => 'jobs']), true); + $result = \iterator_to_array($this->initializer->init([BootloaderK::class])); + $this->assertEquals([], $result); + } + + public static function allowEnvDataProvider(): \Traversable + { + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], + [BootloaderH::class => ['bootloader' => new BootloaderH(), 'options' => []]] + ]; + yield [ + ['APP_ENV' => 'dev', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], + [] + ]; + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => true, 'RR_MODE' => 'http'], + [] + ]; + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'jobs'], + [] + ]; + } + + public static function denyEnvDataProvider(): \Traversable + { + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'prod', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'jobs', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'dev', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'dev', 'DB_HOST' => 'localhost'], + [] + ]; + yield [ + ['RR_MODE' => 'jobs', 'APP_ENV' => 'dev', 'DB_HOST' => 'localhost'], + [BootloaderI::class => ['bootloader' => new BootloaderI(), 'options' => []]] + ]; + } +} diff --git a/src/Boot/tests/BootloadManager/BootloadConfigTest.php b/src/Boot/tests/BootloadManager/BootloadConfigTest.php new file mode 100644 index 000000000..ce4549a76 --- /dev/null +++ b/src/Boot/tests/BootloadManager/BootloadConfigTest.php @@ -0,0 +1,186 @@ +initializer->init([ + BootloaderA::class => new BootloadConfig(), + BootloaderD::class + ])); + + $this->assertEquals([ + BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []], + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testDisabledBootloader(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(enabled: false), + BootloaderD::class + ])); + + $this->assertEquals([ + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testArguments(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(args: ['a' => 'b']) + ])); + + $this->assertEquals([ + BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => ['a' => 'b']], + ], $result); + } + + public function testDisabledConfig(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(enabled: false), + BootloaderD::class + ], false)); + + $this->assertEquals([ + BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []], + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testCallableConfig(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => static fn () => new BootloadConfig(args: ['a' => 'b']), + ])); + + $this->assertEquals([ + BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => ['a' => 'b']], + ], $result); + } + + public function testCallableConfigWithArguments(): void + { + $this->container->bind(AppEnvironment::class, AppEnvironment::Production); + + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => static fn (AppEnvironment $env) => new BootloadConfig(enabled: $env->isLocal()), + ])); + $this->assertEquals([], $result); + + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => static fn (AppEnvironment $env) => new BootloadConfig(enabled: $env->isProduction()), + ])); + $this->assertEquals([BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []]], $result); + } + + #[DataProvider('allowEnvDataProvider')] + public function testAllowEnv(array $env, array $expected): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment($env), true); + + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(allowEnv: [ + 'APP_ENV' => 'prod', + 'APP_DEBUG' => false, + 'RR_MODE' => ['http'] + ]), + ])); + + $this->assertEquals($expected, $result); + } + + #[DataProvider('denyEnvDataProvider')] + public function testDenyEnv(array $env, array $expected): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment($env), true); + + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(denyEnv: [ + 'RR_MODE' => 'http', + 'APP_ENV' => ['production', 'prod'], + 'DB_HOST' => 'db.example.com', + ]), + ])); + + $this->assertEquals($expected, $result); + } + + public function testDenyEnvShouldHaveHigherPriority(): void + { + $this->container->bindSingleton(EnvironmentInterface::class, new Environment(['APP_DEBUG' => true]), true); + + $result = \iterator_to_array($this->initializer->init([ + BootloaderA::class => new BootloadConfig(allowEnv: ['APP_DEBUG' => true], denyEnv: ['APP_DEBUG' => true]), + ])); + + $this->assertEquals([], $result); + } + + public static function allowEnvDataProvider(): \Traversable + { + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], + [BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []]] + ]; + yield [ + ['APP_ENV' => 'dev', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], + [] + ]; + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => true, 'RR_MODE' => 'http'], + [] + ]; + yield [ + ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'jobs'], + [] + ]; + } + + public static function denyEnvDataProvider(): \Traversable + { + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'prod', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'jobs', 'APP_ENV' => 'production', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'dev', 'DB_HOST' => 'db.example.com'], + [] + ]; + yield [ + ['RR_MODE' => 'http', 'APP_ENV' => 'dev', 'DB_HOST' => 'localhost'], + [] + ]; + yield [ + ['RR_MODE' => 'jobs', 'APP_ENV' => 'dev', 'DB_HOST' => 'localhost'], + [BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []]] + ]; + } +} diff --git a/src/Boot/tests/BootloadManager/BootloadManagerTest.php b/src/Boot/tests/BootloadManager/BootloadManagerTest.php index 4788bc3f5..5ee23d254 100644 --- a/src/Boot/tests/BootloadManager/BootloadManagerTest.php +++ b/src/Boot/tests/BootloadManager/BootloadManagerTest.php @@ -19,14 +19,13 @@ final class BootloadManagerTest extends TestCase { public function testWithoutInvokerStrategy(): void { - $container = new Container(); - $container->bind(InitializerInterface::class, new Initializer($container, $container)); + $this->container->bind(InitializerInterface::class, new Initializer($this->container, $this->container)); $bootloader = new BootloadManager( - $container, - $container, - $container, - $container->get(InitializerInterface::class) + $this->container, + $this->container, + $this->container, + $this->container->get(InitializerInterface::class) ); $bootloader->bootload($classes = [ @@ -43,15 +42,16 @@ static function(Container $container, SampleBoot $boot) { } ]); - $this->assertTrue($container->has('abc')); - $this->assertTrue($container->hasInstance('cde')); - $this->assertTrue($container->hasInstance('def')); - $this->assertTrue($container->hasInstance('efg')); - $this->assertTrue($container->has('single')); - $this->assertTrue($container->has('ghi')); - $this->assertNotInstanceOf(SampleBoot::class, $container->get('efg')); - $this->assertInstanceOf(SampleBoot::class, $container->get('ghi')); + $this->assertTrue($this->container->has('abc')); + $this->assertTrue($this->container->hasInstance('cde')); + $this->assertTrue($this->container->hasInstance('def')); + $this->assertTrue($this->container->hasInstance('efg')); + $this->assertTrue($this->container->has('single')); + $this->assertTrue($this->container->has('ghi')); + $this->assertNotInstanceOf(SampleBoot::class, $this->container->get('efg')); + $this->assertInstanceOf(SampleBoot::class, $this->container->get('ghi')); + $classes = \array_filter($classes, static fn(string $class): bool => $class !== SampleClass::class); $this->assertSame(\array_merge($classes, [ BootloaderA::class, BootloaderB::class, diff --git a/src/Boot/tests/BootloadManager/BootloadersTest.php b/src/Boot/tests/BootloadManager/BootloadersTest.php index f0d9e4ba9..ecf472590 100644 --- a/src/Boot/tests/BootloadManager/BootloadersTest.php +++ b/src/Boot/tests/BootloadManager/BootloadersTest.php @@ -15,13 +15,11 @@ use Spiral\Tests\Boot\Fixtures\SampleClass; use Spiral\Tests\Boot\TestCase; -class BootloadersTest extends TestCase +final class BootloadersTest extends TestCase { public function testSchemaLoading(): void { - $container = new Container(); - - $bootloader = $this->getBootloadManager($container); + $bootloader = $this->getBootloadManager(); $bootloader->bootload($classes = [ SampleClass::class, @@ -37,15 +35,16 @@ static function(Container $container, SampleBoot $boot) { } ]); - $this->assertTrue($container->has('abc')); - $this->assertTrue($container->hasInstance('cde')); - $this->assertTrue($container->hasInstance('def')); - $this->assertTrue($container->hasInstance('efg')); - $this->assertTrue($container->has('single')); - $this->assertTrue($container->has('ghi')); - $this->assertNotInstanceOf(SampleBoot::class, $container->get('efg')); - $this->assertInstanceOf(SampleBoot::class, $container->get('ghi')); + $this->assertTrue($this->container->has('abc')); + $this->assertTrue($this->container->hasInstance('cde')); + $this->assertTrue($this->container->hasInstance('def')); + $this->assertTrue($this->container->hasInstance('efg')); + $this->assertTrue($this->container->has('single')); + $this->assertTrue($this->container->has('ghi')); + $this->assertNotInstanceOf(SampleBoot::class, $this->container->get('efg')); + $this->assertInstanceOf(SampleBoot::class, $this->container->get('ghi')); + $classes = \array_filter($classes, static fn(string $class): bool => $class !== SampleClass::class); $this->assertSame(\array_merge($classes, [ BootloaderA::class, BootloaderB::class, @@ -54,9 +53,7 @@ static function(Container $container, SampleBoot $boot) { public function testBootloadFromInstance(): void { - $container = new Container(); - - $bootloader = $this->getBootloadManager($container); + $bootloader = $this->getBootloadManager(); $bootloader->bootload([ SampleClass::class, @@ -64,15 +61,14 @@ public function testBootloadFromInstance(): void new SampleBoot(), ]); - $this->assertTrue($container->has('abc')); - $this->assertTrue($container->has('single')); - $this->assertTrue($container->hasInstance('def')); - $this->assertTrue($container->hasInstance('efg')); - $this->assertTrue($container->hasInstance('cde')); - $this->assertTrue($container->has('ghi')); + $this->assertTrue($this->container->has('abc')); + $this->assertTrue($this->container->has('single')); + $this->assertTrue($this->container->hasInstance('def')); + $this->assertTrue($this->container->hasInstance('efg')); + $this->assertTrue($this->container->hasInstance('cde')); + $this->assertTrue($this->container->has('ghi')); $this->assertSame([ - SampleClass::class, SampleBootWithMethodBoot::class, SampleBoot::class, BootloaderA::class, @@ -82,9 +78,7 @@ public function testBootloadFromInstance(): void public function testBootloadFromAnonymousClass(): void { - $container = new Container(); - - $bootloader = $this->getBootloadManager($container); + $bootloader = $this->getBootloadManager(); $bootloader->bootload([ new class () extends Bootloader { @@ -104,11 +98,11 @@ public function boot(BinderInterface $binder): void }, ]); - $this->assertTrue($container->has('abc')); - $this->assertTrue($container->has('single')); - $this->assertTrue($container->hasInstance('def')); - $this->assertTrue($container->hasInstance('efg')); - $this->assertTrue($container->has('ghi')); + $this->assertTrue($this->container->has('abc')); + $this->assertTrue($this->container->has('single')); + $this->assertTrue($this->container->hasInstance('def')); + $this->assertTrue($this->container->hasInstance('efg')); + $this->assertTrue($this->container->has('ghi')); $this->assertCount(1, $bootloader->getClasses()); } diff --git a/src/Boot/tests/BootloadManager/Checker/BootloaderCheckerTest.php b/src/Boot/tests/BootloadManager/Checker/BootloaderCheckerTest.php new file mode 100644 index 000000000..301b1d59f --- /dev/null +++ b/src/Boot/tests/BootloadManager/Checker/BootloaderCheckerTest.php @@ -0,0 +1,45 @@ +createMock(BootloaderCheckerInterface::class); + $checker1->method('canInitialize')->willReturn(true); + $checker2 = $this->createMock(BootloaderCheckerInterface::class); + $checker2->method('canInitialize')->willReturn(false); + + $registry = new CheckerRegistry(); + $registry->register($checker1); + $registry->register($checker2); + + $checker = new BootloaderChecker($registry); + + $this->assertFalse($checker->canInitialize('foo')); + } + + public function testCanInitializeSuccess(): void + { + $checker1 = $this->createMock(BootloaderCheckerInterface::class); + $checker1->method('canInitialize')->willReturn(true); + $checker2 = $this->createMock(BootloaderCheckerInterface::class); + $checker2->method('canInitialize')->willReturn(true); + + $registry = new CheckerRegistry(); + $registry->register($checker1); + $registry->register($checker2); + + $checker = new BootloaderChecker($registry); + + $this->assertTrue($checker->canInitialize('foo')); + } +} diff --git a/src/Boot/tests/BootloadManager/Checker/CanBootedCheckerTest.php b/src/Boot/tests/BootloadManager/Checker/CanBootedCheckerTest.php new file mode 100644 index 000000000..e4e7307d7 --- /dev/null +++ b/src/Boot/tests/BootloadManager/Checker/CanBootedCheckerTest.php @@ -0,0 +1,48 @@ +assertTrue($checker->canInitialize(BootloaderA::class)); + $this->assertTrue($checker->canInitialize(new BootloaderA())); + } + + public function testCanInitializeBootloaderAlreadyBooted(): void + { + $registry = new ClassesRegistry(); + $registry->register(BootloaderA::class); + + $checker = new CanBootedChecker($registry); + + $this->assertFalse($checker->canInitialize(BootloaderA::class)); + $this->assertFalse($checker->canInitialize(new BootloaderA())); + } + + public function testCanInitializeAbstractBootloader(): void + { + $checker = new CanBootedChecker(new ClassesRegistry()); + + $this->assertFalse($checker->canInitialize(AbstractBootloader::class)); + } + + public function testCanInitializeNotImplementInterface(): void + { + $checker = new CanBootedChecker(new ClassesRegistry()); + + $this->assertFalse($checker->canInitialize(SampleClass::class)); + } +} diff --git a/src/Boot/tests/BootloadManager/Checker/ClassExistsCheckerTest.php b/src/Boot/tests/BootloadManager/Checker/ClassExistsCheckerTest.php new file mode 100644 index 000000000..fdbcaed87 --- /dev/null +++ b/src/Boot/tests/BootloadManager/Checker/ClassExistsCheckerTest.php @@ -0,0 +1,29 @@ +assertTrue($checker->canInitialize(BootloaderA::class)); + $this->assertTrue($checker->canInitialize(new BootloaderA())); + } + + public function testCanInitializeException(): void + { + $checker = new ClassExistsChecker(); + + $this->expectException(ClassNotFoundException::class); + $checker->canInitialize('foo'); + } +} diff --git a/src/Boot/tests/BootloadManager/Checker/ConfigCheckerTest.php b/src/Boot/tests/BootloadManager/Checker/ConfigCheckerTest.php new file mode 100644 index 000000000..e040a1770 --- /dev/null +++ b/src/Boot/tests/BootloadManager/Checker/ConfigCheckerTest.php @@ -0,0 +1,46 @@ + 'dev' + ])); + + $this->assertSame($expected, $checker->canInitialize(BootloaderA::class, $config)); + } + + public static function canInitializeDataProvider(): \Traversable + { + yield [true, null]; + yield [true, new BootloadConfig()]; + yield [false, new BootloadConfig(enabled: false)]; + + yield [true, new BootloadConfig(allowEnv: ['APP_ENV' => 'dev'])]; + yield [true, new BootloadConfig(allowEnv: ['APP_ENV' => ['dev']])]; + yield [false, new BootloadConfig(allowEnv: ['APP_ENV' => 'dev', 'DEBUG' => true])]; + yield [false, new BootloadConfig(allowEnv: ['APP_ENV' => 'prod'])]; + yield [false, new BootloadConfig(allowEnv: ['APP_ENV' => ['prod']])]; + yield [false, new BootloadConfig(allowEnv: ['APP_ENV' => ['prod'], 'DEBUG' => true])]; + + yield [false, new BootloadConfig(denyEnv: ['APP_ENV' => 'dev'])]; + yield [false, new BootloadConfig(denyEnv: ['APP_ENV' => ['dev']])]; + yield [false, new BootloadConfig(denyEnv: ['APP_ENV' => 'dev', 'DEBUG' => true])]; + yield [true, new BootloadConfig(denyEnv: ['APP_ENV' => 'prod'])]; + yield [true, new BootloadConfig(denyEnv: ['APP_ENV' => ['prod']])]; + yield [true, new BootloadConfig(denyEnv: ['APP_ENV' => ['prod'], 'DEBUG' => true])]; + } +} diff --git a/src/Boot/tests/BootloadManager/DependenciesTest.php b/src/Boot/tests/BootloadManager/DependenciesTest.php index 80fa15a00..61d6fa095 100644 --- a/src/Boot/tests/BootloadManager/DependenciesTest.php +++ b/src/Boot/tests/BootloadManager/DependenciesTest.php @@ -5,31 +5,28 @@ namespace Spiral\Tests\Boot\BootloadManager; use Spiral\Tests\Boot\TestCase; -use Spiral\Core\Container; use Spiral\Tests\Boot\Fixtures\BootloaderA; use Spiral\Tests\Boot\Fixtures\BootloaderB; -class DependenciesTest extends TestCase +final class DependenciesTest extends TestCase { public function testDep(): void { - $c = new Container(); - $b = $this->getBootloadManager($c); + $b = $this->getBootloadManager(); $b->bootload([BootloaderA::class]); - $this->assertTrue($c->has('a')); - $this->assertFalse($c->has('b')); + $this->assertTrue($this->container->has('a')); + $this->assertFalse($this->container->has('b')); } public function testDep2(): void { - $c = new Container(); - $b = $this->getBootloadManager($c); + $b = $this->getBootloadManager(); $b->bootload([BootloaderB::class]); - $this->assertTrue($c->has('a')); - $this->assertTrue($c->has('b')); + $this->assertTrue($this->container->has('a')); + $this->assertTrue($this->container->has('b')); } } diff --git a/src/Boot/tests/BootloadManager/InitializerTest.php b/src/Boot/tests/BootloadManager/InitializerTest.php index 2a9062c25..ebf99dc31 100644 --- a/src/Boot/tests/BootloadManager/InitializerTest.php +++ b/src/Boot/tests/BootloadManager/InitializerTest.php @@ -4,21 +4,14 @@ namespace Spiral\Tests\Boot\BootloadManager; -use PHPUnit\Framework\TestCase; -use Spiral\Boot\BootloadManager\Initializer; -use Spiral\Core\Container; use Spiral\Tests\Boot\Fixtures\AbstractBootloader; use Spiral\Tests\Boot\Fixtures\BootloaderD; -final class InitializerTest extends TestCase +final class InitializerTest extends InitializerTestCase { public function testDontBootloadAbstractBootloader(): void { - $container = new Container(); - - $initializer = new Initializer($container, $container); - - $result = \iterator_to_array($initializer->init([AbstractBootloader::class, BootloaderD::class])); + $result = \iterator_to_array($this->initializer->init([AbstractBootloader::class, BootloaderD::class])); $this->assertCount(1, $result); $this->assertIsArray($result[BootloaderD::class]); diff --git a/src/Boot/tests/BootloadManager/InitializerTestCase.php b/src/Boot/tests/BootloadManager/InitializerTestCase.php new file mode 100644 index 000000000..817d9347f --- /dev/null +++ b/src/Boot/tests/BootloadManager/InitializerTestCase.php @@ -0,0 +1,20 @@ +initializer = new Initializer($this->container, $this->container); + } +} diff --git a/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php b/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php new file mode 100644 index 000000000..8e1b28f72 --- /dev/null +++ b/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php @@ -0,0 +1,112 @@ +initializer->init([ + BootloaderF::class => new BootloadConfig(enabled: true), + BootloaderD::class + ])); + + $this->assertEquals([ + BootloaderF::class => ['bootloader' => new BootloaderF(), 'options' => []], + BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + ], $result); + } + + public function testOverrideArgs(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderG::class => new BootloadConfig(args: ['foo' => 'bar']), + ])); + + $this->assertEquals([ + BootloaderG::class => ['bootloader' => new BootloaderG(), 'options' => ['foo' => 'bar']] + ], $result); + } + + public function testMergeArgs(): void + { + $result = \iterator_to_array($this->initializer->init([ + BootloaderG::class => new BootloadConfig(args: ['foo' => 'bar', 'a' => 'baz'], override: false), + ])); + + $this->assertEquals([ + BootloaderG::class => ['bootloader' => new BootloaderG(), 'options' => [ + 'a' => 'baz', + 'foo' => 'bar', + 'c' => 'd' + ]] + ], $result); + } + + public function testOverrideAllowEnv(): void + { + $ref = new \ReflectionMethod($this->initializer, 'getBootloadConfig'); + $config = $ref->invoke( + $this->initializer, + BootloaderH::class, + new BootloadConfig(allowEnv: ['foo' => 'bar']) + ); + + $this->assertEquals(['foo' => 'bar'], $config->allowEnv); + } + + public function testMergeAllowEnv(): void + { + $ref = new \ReflectionMethod($this->initializer, 'getBootloadConfig'); + $config = $ref->invoke( + $this->initializer, + BootloaderH::class, + new BootloadConfig(allowEnv: ['APP_ENV' => 'dev', 'foo' => 'bar'], override: false) + ); + + $this->assertEquals([ + 'foo' => 'bar', + 'APP_ENV' => 'dev', + 'APP_DEBUG' => false, + 'RR_MODE' => ['http'] + ], $config->allowEnv); + } + + public function testOverrideDenyEnv(): void + { + $ref = new \ReflectionMethod($this->initializer, 'getBootloadConfig'); + $config = $ref->invoke( + $this->initializer, + BootloaderI::class, + new BootloadConfig(denyEnv: ['foo' => 'bar']) + ); + + $this->assertEquals(['foo' => 'bar'], $config->denyEnv); + } + + public function testMergeDenyEnv(): void + { + $ref = new \ReflectionMethod($this->initializer, 'getBootloadConfig'); + $config = $ref->invoke( + $this->initializer, + BootloaderI::class, + new BootloadConfig(denyEnv: ['DB_HOST' => 'localhost', 'foo' => 'bar'], override: false) + ); + + $this->assertEquals([ + 'foo' => 'bar', + 'RR_MODE' => 'http', + 'APP_ENV' => ['production', 'prod'], + 'DB_HOST' => 'localhost', + ], $config->denyEnv); + } +} diff --git a/src/Boot/tests/Fixtures/Attribute/TargetWorker.php b/src/Boot/tests/Fixtures/Attribute/TargetWorker.php new file mode 100644 index 000000000..b9b05ea9a --- /dev/null +++ b/src/Boot/tests/Fixtures/Attribute/TargetWorker.php @@ -0,0 +1,17 @@ + $workers]); + } +} diff --git a/src/Boot/tests/Fixtures/BootloaderE.php b/src/Boot/tests/Fixtures/BootloaderE.php new file mode 100644 index 000000000..cbcc0cc30 --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderE.php @@ -0,0 +1,12 @@ + 'b', 'c' => 'd'])] +class BootloaderG extends AbstractBootloader +{ +} diff --git a/src/Boot/tests/Fixtures/BootloaderH.php b/src/Boot/tests/Fixtures/BootloaderH.php new file mode 100644 index 000000000..f2b92d879 --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderH.php @@ -0,0 +1,16 @@ + 'prod', + 'APP_DEBUG' => false, + 'RR_MODE' => ['http'] +])] +class BootloaderH extends AbstractBootloader +{ +} diff --git a/src/Boot/tests/Fixtures/BootloaderI.php b/src/Boot/tests/Fixtures/BootloaderI.php new file mode 100644 index 000000000..bb331ec20 --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderI.php @@ -0,0 +1,16 @@ + 'http', + 'APP_ENV' => ['production', 'prod'], + 'DB_HOST' => 'db.example.com', +])] +class BootloaderI extends AbstractBootloader +{ +} diff --git a/src/Boot/tests/Fixtures/BootloaderJ.php b/src/Boot/tests/Fixtures/BootloaderJ.php new file mode 100644 index 000000000..78b1b4c16 --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderJ.php @@ -0,0 +1,12 @@ + true], denyEnv: ['APP_DEBUG' => true])] +class BootloaderJ extends AbstractBootloader +{ +} diff --git a/src/Boot/tests/Fixtures/BootloaderK.php b/src/Boot/tests/Fixtures/BootloaderK.php new file mode 100644 index 000000000..ecc7d5c2e --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderK.php @@ -0,0 +1,12 @@ +container = new Container(); + $this->container->bindSingleton(EnvironmentInterface::class, Environment::class, true); + } + + public function getBootloadManager(): StrategyBasedBootloadManager { - $initializer = new Initializer($container, $container); + $initializer = new Initializer($this->container, $this->container); return new StrategyBasedBootloadManager( - new DefaultInvokerStrategy($initializer, $container, $container), - $container, + new DefaultInvokerStrategy($initializer, $this->container, $this->container), + $this->container, $initializer ); }