diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index 7b9a7f832..5f9a0528d 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -10,6 +10,8 @@ use Spiral\Boot\Bootloader\BootloaderRegistry; use Spiral\Boot\Bootloader\BootloaderRegistryInterface; use Spiral\Boot\Bootloader\CoreBootloader; +use Spiral\Boot\BootloadManager\AttributeResolver; +use Spiral\Boot\BootloadManager\AttributeResolverRegistryInterface; use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager; use Spiral\Boot\BootloadManager\DefaultInvokerStrategy; use Spiral\Boot\BootloadManager\Initializer; @@ -128,9 +130,12 @@ final public static function create( $exceptionHandler->register(); } + $container->bind(AttributeResolverRegistryInterface::class, AttributeResolver::class); + if (!$container->has(InitializerInterface::class)) { $container->bind(InitializerInterface::class, Initializer::class); } + if (!$container->has(InvokerStrategyInterface::class)) { $container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class); } diff --git a/src/Boot/src/Attribute/AbstractMethod.php b/src/Boot/src/Attribute/AbstractMethod.php new file mode 100644 index 000000000..23d8a26d1 --- /dev/null +++ b/src/Boot/src/Attribute/AbstractMethod.php @@ -0,0 +1,23 @@ +aliases = $aliases; + } +} diff --git a/src/Boot/src/Attribute/BindMethod.php b/src/Boot/src/Attribute/BindMethod.php new file mode 100644 index 000000000..41617c628 --- /dev/null +++ b/src/Boot/src/Attribute/BindMethod.php @@ -0,0 +1,8 @@ +scope = \is_object($scope) ? (string) $scope->value : $scope; + } +} diff --git a/src/Boot/src/Attribute/BootMethod.php b/src/Boot/src/Attribute/BootMethod.php new file mode 100644 index 000000000..1ec1a56ff --- /dev/null +++ b/src/Boot/src/Attribute/BootMethod.php @@ -0,0 +1,13 @@ + + * @implements AttributeResolverRegistryInterface + */ +#[Singleton] +final class AttributeResolver implements AttributeResolverInterface, AttributeResolverRegistryInterface +{ + /** + * @var array, AttributeResolverInterface> + */ + private array $resolvers = []; + + public function __construct(Container $container) + { + /** @psalm-suppress InvalidArgument */ + $this->register(SingletonMethod::class, $container->get(AttributeResolver\SingletonMethodResolver::class)); + /** @psalm-suppress InvalidArgument */ + $this->register(BindMethod::class, $container->get(AttributeResolver\BindMethodResolver::class)); + /** @psalm-suppress InvalidArgument */ + $this->register(InjectorMethod::class, $container->get(AttributeResolver\InjectorMethodResolver::class)); + } + + public function register(string $attribute, AttributeResolverInterface $resolver): void + { + $this->resolvers[$attribute] = $resolver; + } + + /** + * @return class-string[] + */ + public function getResolvers(): array + { + return \array_keys($this->resolvers); + } + + public function resolve(object $attribute, object $service, \ReflectionMethod $method): void + { + $attributeClass = $attribute::class; + if (!isset($this->resolvers[$attributeClass])) { + throw new \RuntimeException("No resolver for attribute {$attributeClass}"); + } + + $this->resolvers[$attributeClass]->resolve($attribute, $service, $method); + } +} diff --git a/src/Boot/src/BootloadManager/AttributeResolver/AbstractResolver.php b/src/Boot/src/BootloadManager/AttributeResolver/AbstractResolver.php new file mode 100644 index 000000000..dff0ec4b7 --- /dev/null +++ b/src/Boot/src/BootloadManager/AttributeResolver/AbstractResolver.php @@ -0,0 +1,95 @@ + + */ +abstract class AbstractResolver implements AttributeResolverInterface +{ + public function __construct( + protected readonly BinderInterface $binder, + ) {} + + /** + * @psalm-param T $attribute + * @return list + */ + protected function getAliases(object $attribute, \ReflectionMethod $method): array + { + $alias = $attribute->alias ?? null; + + $aliases = []; + if ($alias !== null) { + $aliases[] = $alias; + } + + $attrs = $method->getAttributes(name: BindAlias::class); + foreach ($attrs as $attr) { + $aliases = [...$aliases, ...$attr->newInstance()->aliases]; + } + + // If no aliases are provided, we will use the return type as the alias. + if (\count($aliases) > 0 && !$attribute->aliasesFromReturnType) { + return \array_unique(\array_filter($aliases)); + } + + $type = $method->getReturnType(); + + if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $type) { + if ($type->isBuiltin()) { + continue; + } + + $aliases[] = $type->getName(); + } + } elseif ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + $aliases[] = $type->getName(); + } + + if ($aliases === []) { + throw new \LogicException( + "No alias provided for binding {$method->getDeclaringClass()->getName()}::{$method->getName()}", + ); + } + + return \array_unique(\array_filter($aliases)); + } + + protected function getScope(\ReflectionMethod $method): ?string + { + $attrs = $method->getAttributes(name: BindScope::class); + + if ($attrs === []) { + return null; + } + + return $attrs[0]->newInstance()->scope; + } + + protected function bind(array $aliases, Binding $binding, ?string $scope = null): void + { + $binder = $this->binder->getBinder($scope); + + $alias = \array_shift($aliases); + foreach ($aliases as $a) { + $binder->bind($alias, $a); + $alias = \array_shift($aliases); + } + + $binder->bind($alias, $binding); + } +} diff --git a/src/Boot/src/BootloadManager/AttributeResolver/BindMethodResolver.php b/src/Boot/src/BootloadManager/AttributeResolver/BindMethodResolver.php new file mode 100644 index 000000000..621f82533 --- /dev/null +++ b/src/Boot/src/BootloadManager/AttributeResolver/BindMethodResolver.php @@ -0,0 +1,27 @@ + + */ +final class BindMethodResolver extends AbstractResolver +{ + public function resolve(object $attribute, object $service, \ReflectionMethod $method): void + { + $aliases = $this->getAliases($attribute, $method); + $closure = new Factory( + callable: $method->getClosure($service), + singleton: false, + ); + + $this->bind($aliases, $closure, $this->getScope($method)); + } +} diff --git a/src/Boot/src/BootloadManager/AttributeResolver/InjectorMethodResolver.php b/src/Boot/src/BootloadManager/AttributeResolver/InjectorMethodResolver.php new file mode 100644 index 000000000..7d4007fe1 --- /dev/null +++ b/src/Boot/src/BootloadManager/AttributeResolver/InjectorMethodResolver.php @@ -0,0 +1,32 @@ + + */ +final class InjectorMethodResolver implements AttributeResolverInterface +{ + public function __construct( + private readonly BinderInterface $binder, + private readonly InvokerInterface $invoker, + ) {} + + public function resolve(object $attribute, object $service, \ReflectionMethod $method): void + { + $this->binder->bind( + $attribute->alias, + new Injectable($this->invoker->invoke($method->getClosure($service))), + ); + } +} diff --git a/src/Boot/src/BootloadManager/AttributeResolver/SingletonMethodResolver.php b/src/Boot/src/BootloadManager/AttributeResolver/SingletonMethodResolver.php new file mode 100644 index 000000000..fbf934c4f --- /dev/null +++ b/src/Boot/src/BootloadManager/AttributeResolver/SingletonMethodResolver.php @@ -0,0 +1,27 @@ + + */ +final class SingletonMethodResolver extends AbstractResolver +{ + public function resolve(object $attribute, object $service, \ReflectionMethod $method): void + { + $aliases = $this->getAliases($attribute, $method); + $closure = new Factory( + callable: $method->getClosure($service), + singleton: true, + ); + + $this->bind($aliases, $closure, $this->getScope($method)); + } +} diff --git a/src/Boot/src/BootloadManager/AttributeResolverInterface.php b/src/Boot/src/BootloadManager/AttributeResolverInterface.php new file mode 100644 index 000000000..8c6a20a4c --- /dev/null +++ b/src/Boot/src/BootloadManager/AttributeResolverInterface.php @@ -0,0 +1,20 @@ + $attribute + */ + public function register(string $attribute, AttributeResolverInterface $resolver): void; +} diff --git a/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php b/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php index 2dc939b97..a9f4a9792 100644 --- a/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php +++ b/src/Boot/src/BootloadManager/DefaultInvokerStrategy.php @@ -27,26 +27,30 @@ public function invokeBootloaders( $bootloaders = \iterator_to_array($this->initializer->init($classes, $useConfig)); foreach ($bootloaders as $data) { - $this->invokeBootloader($data['bootloader'], Methods::INIT, $data['options']); + foreach ($data['init_methods'] as $methodName) { + $this->invokeBootloader($data['bootloader'], $methodName, $data['options']); + } } $this->fireCallbacks($bootingCallbacks); foreach ($bootloaders as $data) { - $this->invokeBootloader($data['bootloader'], Methods::BOOT, $data['options']); + foreach ($data['boot_methods'] as $methodName) { + $this->invokeBootloader($data['bootloader'], $methodName, $data['options']); + } } $this->fireCallbacks($bootedCallbacks); } - private function invokeBootloader(BootloaderInterface $bootloader, Methods $method, array $options): void + private function invokeBootloader(BootloaderInterface $bootloader, string $method, array $options): void { $refl = new \ReflectionClass($bootloader); - if (!$refl->hasMethod($method->value)) { + if (!$refl->hasMethod($method)) { return; } - $method = $refl->getMethod($method->value); + $method = $refl->getMethod($method); $args = $this->resolver->resolveArguments($method); if (!isset($args['boot'])) { diff --git a/src/Boot/src/BootloadManager/Initializer.php b/src/Boot/src/BootloadManager/Initializer.php index 03b2cc158..88aa89802 100644 --- a/src/Boot/src/BootloadManager/Initializer.php +++ b/src/Boot/src/BootloadManager/Initializer.php @@ -4,8 +4,12 @@ namespace Spiral\Boot\BootloadManager; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Spiral\Boot\Attribute\BootloadConfig; +use Spiral\Boot\Attribute\BootMethod; +use Spiral\Boot\Attribute\InitMethod; use Spiral\Boot\Bootloader\BootloaderInterface; use Spiral\Boot\Bootloader\DependedInterface; use Spiral\Boot\BootloadManager\Checker\BootloaderChecker; @@ -34,13 +38,15 @@ public function __construct( protected readonly BinderInterface $binder, protected readonly ClassesRegistry $bootloaders = new ClassesRegistry(), ?BootloaderCheckerInterface $checker = null, - ) { - } + ) {} /** * Instantiate bootloader objects and resolve dependencies * * @param TClass[]|array, array> $classes + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \ReflectionException */ public function init(array $classes, bool $useConfig = true): \Generator { @@ -64,11 +70,26 @@ public function init(array $classes, bool $useConfig = true): \Generator $bootloader = $this->container->get($bootloader); } - /** @var BootloaderInterface $bootloader */ - yield from $this->initBootloader($bootloader); + $initMethods = $this->findMethodsWithPriority( + $bootloader, + [0 => [Methods::INIT->value]], + InitMethod::class, + ); + + $bootMethods = $this->findMethodsWithPriority( + $bootloader, + [0 => [Methods::BOOT->value]], + BootMethod::class, + ); + + yield from $this->resolveDependencies($bootloader, \array_unique([...$initMethods, ...$bootMethods])); + + $this->initBootloader($bootloader); yield $bootloader::class => [ 'bootloader' => $bootloader, 'options' => $options instanceof BootloadConfig ? $options->args : $options, + 'init_methods' => $initMethods, + 'boot_methods' => $bootMethods, ]; } } @@ -81,65 +102,17 @@ public function getRegistry(): ClassesRegistry /** * Resolve all bootloader dependencies and init bindings */ - protected function initBootloader(BootloaderInterface $bootloader): iterable - { - if ($bootloader instanceof DependedInterface) { - yield from $this->init($this->getDependencies($bootloader)); - } - - $this->initBindings( - $bootloader->defineBindings(), - $bootloader->defineSingletons() - ); - } - - /** - * Bind declared bindings. - * - * @param TFullBinding $bindings - * @param TFullBinding $singletons - */ - protected function initBindings(array $bindings, array $singletons): void - { - foreach ($bindings as $aliases => $resolver) { - $this->binder->bind($aliases, $resolver); - } - - foreach ($singletons as $aliases => $resolver) { - $this->binder->bindSingleton($aliases, $resolver); - } - } - - protected function getDependencies(DependedInterface $bootloader): array + private function initBootloader(BootloaderInterface $bootloader): void { - $deps = $bootloader->defineDependencies(); - - $reflectionClass = new \ReflectionClass($bootloader); - - $methodsDeps = []; - - foreach (Methods::cases() as $method) { - if ($reflectionClass->hasMethod($method->value)) { - $methodsDeps[] = $this->findBootloaderClassesInMethod( - $reflectionClass->getMethod($method->value) - ); - } + foreach ($bootloader->defineBindings() as $alias => $resolver) { + $this->binder->bind($alias, $resolver); } - return \array_values(\array_unique(\array_merge($deps, ...$methodsDeps))); - } - - protected function findBootloaderClassesInMethod(\ReflectionMethod $method): array - { - $args = []; - foreach ($method->getParameters() as $parameter) { - $type = $parameter->getType(); - if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) { - $args[] = $type->getName(); - } + foreach ($bootloader->defineSingletons() as $alias => $resolver) { + $this->binder->bindSingleton($alias, $resolver); } - return $args; + $this->resolveAttributeBindings($bootloader); } protected function shouldBeBooted(\ReflectionNamedType $type): bool @@ -171,22 +144,24 @@ protected function initDefaultChecker(): BootloaderCheckerInterface } /** - * Returns merged config. Attribute config have lower priority. + * Returns merged config. Attribute config has lower priority. * * @param class-string|BootloaderInterface $bootloader + * @throws \ReflectionException */ private function getBootloadConfig( string|BootloaderInterface $bootloader, - array|callable|BootloadConfig $config + 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 fn (string $key, bool $override, mixed $default = []): mixed => match (true) { + $getArgument = static fn(string $key, bool $override, mixed $default = []): mixed => match (true) { $config instanceof BootloadConfig && $override => $config->{$key}, $config instanceof BootloadConfig && !$override && \is_array($default) => $config->{$key} + ($attr->{$key} ?? []), @@ -206,7 +181,10 @@ private function getBootloadConfig( } /** + * This method is used to find and instantiate BootloadConfig attribute. + * * @param class-string|BootloaderInterface $bootloader + * @throws \ReflectionException */ private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig { @@ -222,4 +200,123 @@ private function getBootloadConfigAttribute(string|BootloaderInterface $bootload return $attribute->newInstance(); } + + /** + * This method is used to find methods with InitMethod or BootMethod attributes. + * + * @param class-string $attribute + * @param list $initialMethods + * @return list + */ + private function findMethodsWithPriority( + BootloaderInterface $bootloader, + array $initialMethods, + string $attribute, + ): array { + $methods = $initialMethods; + + $refl = new \ReflectionClass($bootloader); + foreach ($refl->getMethods() as $method) { + if ($method->isStatic()) { + continue; + } + + $attrs = $method->getAttributes($attribute); + if (\count($attrs) === 0) { + continue; + } + /** @var InitMethod|BootMethod $attr */ + $attr = $attrs[0]->newInstance(); + $methods[$attr->priority][] = $method->getName(); + } + + \ksort($methods); + + return \array_merge(...$methods); + } + + /** + * This method is used to resolve bindings from attributes. + * + * @throws \ReflectionException + */ + private function resolveAttributeBindings(BootloaderInterface $bootloader): void + { + if (!$this->container->has(AttributeResolver::class)) { + return; + } + + /** @var AttributeResolver $attributeResolver */ + $attributeResolver = $this->container->get(AttributeResolver::class); + + $availableAttributes = $attributeResolver->getResolvers(); + + $refl = new \ReflectionClass($bootloader); + foreach ($refl->getMethods() as $method) { + if ($method->isStatic()) { + continue; + } + + foreach ($availableAttributes as $attributeClass) { + $attrs = $method->getAttributes($attributeClass); + foreach ($attrs as $attr) { + $instance = $attr->newInstance(); + $attributeResolver->resolve($instance, $bootloader, $method); + } + } + } + } + + /** + * @param non-empty-string[] $bootloaderMethods + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \ReflectionException + */ + private function resolveDependencies(BootloaderInterface $bootloader, array $bootloaderMethods): iterable + { + $deps = $this->findDependenciesInMethods($bootloader, $bootloaderMethods); + if ($bootloader instanceof DependedInterface) { + $deps = [...$deps, ...$bootloader->defineDependencies()]; + } + + yield from $this->init(\array_values(\array_unique($deps))); + } + + /** + * @param non-empty-string[] $methods + * @return class-string[] + */ + private function findDependenciesInMethods(BootloaderInterface $bootloader, array $methods): array + { + $reflectionClass = new \ReflectionClass($bootloader); + + $methodsDeps = []; + + foreach ($methods as $method) { + if ($reflectionClass->hasMethod($method)) { + $methodsDeps[] = $this->findBootloaderClassesInMethod( + $reflectionClass->getMethod($method), + ); + } + } + + return \array_merge(...$methodsDeps); + } + + /** + * @return class-string[] + */ + private function findBootloaderClassesInMethod(\ReflectionMethod $method): array + { + $args = []; + foreach ($method->getParameters() as $parameter) { + $type = $parameter->getType(); + if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) { + $args[] = $type->getName(); + } + } + + return $args; + } } diff --git a/src/Boot/src/BootloadManager/Methods.php b/src/Boot/src/BootloadManager/Methods.php index 0bd537577..b9bfd7761 100644 --- a/src/Boot/src/BootloadManager/Methods.php +++ b/src/Boot/src/BootloadManager/Methods.php @@ -4,6 +4,9 @@ namespace Spiral\Boot\BootloadManager; +/** + * @deprecated Will be removed in v4.0 + */ enum Methods: string { case INIT = 'init'; diff --git a/src/Boot/src/Bootloader/BootloaderInterface.php b/src/Boot/src/Bootloader/BootloaderInterface.php index 05675c88d..ef2e4402e 100644 --- a/src/Boot/src/Bootloader/BootloaderInterface.php +++ b/src/Boot/src/Bootloader/BootloaderInterface.php @@ -4,6 +4,8 @@ namespace Spiral\Boot\Bootloader; +use Spiral\Core\Config\Binding; + /** * Similar to laravel service provider, however allowed only to define bindings in a simple form so * they can be cached. @@ -11,7 +13,7 @@ * To make class bootable (using method boot() with method injections) declare constant BOOT = true; * * @psalm-type TStaticBindingValue = class-string|non-empty-string|array{class-string, non-empty-string} - * @psalm-type TContainerBindingValue = TStaticBindingValue|object|callable + * @psalm-type TContainerBindingValue = TStaticBindingValue|object|callable|Binding * @psalm-type TConstantBinding = array * @psalm-type TFullBinding = array */ diff --git a/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php b/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php index cd37245e4..8990fbf8c 100644 --- a/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php +++ b/src/Boot/tests/BootloadManager/AttributeBootloadConfigTest.php @@ -23,8 +23,18 @@ public function testDefaultBootloadConfig(): void $result = \iterator_to_array($this->initializer->init([BootloaderE::class, BootloaderD::class])); $this->assertEquals([ - BootloaderE::class => ['bootloader' => new BootloaderE(), 'options' => []], - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderE::class => [ + 'bootloader' => new BootloaderE(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -33,7 +43,12 @@ public function testDisabledBootloader(): void $result = \iterator_to_array($this->initializer->init([BootloaderF::class, BootloaderD::class])); $this->assertEquals([ - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -42,7 +57,12 @@ 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']], + BootloaderG::class => [ + 'bootloader' => new BootloaderG(), + 'options' => ['a' => 'b', 'c' => 'd'], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -51,8 +71,18 @@ 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' => []] + BootloaderF::class => [ + 'bootloader' => new BootloaderF(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -89,7 +119,14 @@ 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->assertEquals([ + BootloaderK::class => [ + 'bootloader' => new BootloaderK(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], $result); $this->container->bindSingleton(EnvironmentInterface::class, new Environment(['RR_MODE' => 'jobs']), true); $result = \iterator_to_array($this->initializer->init([BootloaderK::class])); @@ -100,19 +137,26 @@ public static function allowEnvDataProvider(): \Traversable { yield [ ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], - [BootloaderH::class => ['bootloader' => new BootloaderH(), 'options' => []]] + [ + BootloaderH::class => [ + 'bootloader' => new BootloaderH(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], ]; 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'], - [] + [], ]; } @@ -120,31 +164,38 @@ 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' => []]] + [ + BootloaderI::class => [ + 'bootloader' => new BootloaderI(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], ]; } } diff --git a/src/Boot/tests/BootloadManager/AttributeResolverTest.php b/src/Boot/tests/BootloadManager/AttributeResolverTest.php new file mode 100644 index 000000000..b53bd3348 --- /dev/null +++ b/src/Boot/tests/BootloadManager/AttributeResolverTest.php @@ -0,0 +1,65 @@ +container); + + $resolver->register( + SampleMethod::class, + new SampleMethodResolver($this->container), + ); + + $bootloader = new SampleBootWithMethodBoot(); + $refl = new \ReflectionClass($bootloader); + + $resolver->resolve( + new SampleMethod('foo'), + $bootloader, + $refl->getMethod('sampleMethod'), + ); + + $this->assertInstanceOf(SampleClass::class, $this->container->get('foo')); + } + + public function testRegisterResolverThroughBootloader() + { + $this->container->bind(AttributeResolverRegistryInterface::class, AttributeResolver::class); + $this->container->bind(InitializerInterface::class, new Initializer($this->container, $this->container)); + + $bootloader = new BootloadManager( + $this->container, + $this->container, + $this->container, + $this->container->get(InitializerInterface::class), + ); + + $bootloader->bootload([ + BootloaderWithResolver::class, + SampleBootWithMethodBoot::class, + ]); + + $this->assertInstanceOf(SampleClass::class, $this->container->get('sampleMethod')); + } +} diff --git a/src/Boot/tests/BootloadManager/BootloadConfigTest.php b/src/Boot/tests/BootloadManager/BootloadConfigTest.php index 3e3fde5e2..07121d798 100644 --- a/src/Boot/tests/BootloadManager/BootloadConfigTest.php +++ b/src/Boot/tests/BootloadManager/BootloadConfigTest.php @@ -18,12 +18,22 @@ public function testDefaultBootloadConfig(): void { $result = \iterator_to_array($this->initializer->init([ BootloaderA::class => new BootloadConfig(), - BootloaderD::class + BootloaderD::class, ])); $this->assertEquals([ - BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []], - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -31,22 +41,32 @@ public function testDisabledBootloader(): void { $result = \iterator_to_array($this->initializer->init([ BootloaderA::class => new BootloadConfig(enabled: false), - BootloaderD::class + BootloaderD::class, ])); $this->assertEquals([ - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } public function testArguments(): void { $result = \iterator_to_array($this->initializer->init([ - BootloaderA::class => new BootloadConfig(args: ['a' => 'b']) + BootloaderA::class => new BootloadConfig(args: ['a' => 'b']), ])); $this->assertEquals([ - BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => ['a' => 'b']], + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => ['a' => 'b'], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -54,23 +74,38 @@ public function testDisabledConfig(): void { $result = \iterator_to_array($this->initializer->init([ BootloaderA::class => new BootloadConfig(enabled: false), - BootloaderD::class + BootloaderD::class, ], false)); $this->assertEquals([ - BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []], - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } public function testCallableConfig(): void { $result = \iterator_to_array($this->initializer->init([ - BootloaderA::class => static fn (): BootloadConfig => new BootloadConfig(args: ['a' => 'b']), + BootloaderA::class => static fn() => new BootloadConfig(args: ['a' => 'b']), ])); $this->assertEquals([ - BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => ['a' => 'b']], + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => ['a' => 'b'], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -79,14 +114,21 @@ public function testCallableConfigWithArguments(): void $this->container->bind(AppEnvironment::class, AppEnvironment::Production); $result = \iterator_to_array($this->initializer->init([ - BootloaderA::class => static fn (AppEnvironment $env): BootloadConfig => new BootloadConfig(enabled: $env->isLocal()), + 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): BootloadConfig => new BootloadConfig(enabled: $env->isProduction()), + BootloaderA::class => static fn(AppEnvironment $env) => new BootloadConfig(enabled: $env->isProduction()), ])); - $this->assertEquals([BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []]], $result); + $this->assertEquals([ + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], $result); } #[DataProvider('allowEnvDataProvider')] @@ -98,7 +140,7 @@ public function testAllowEnv(array $env, array $expected): void BootloaderA::class => new BootloadConfig(allowEnv: [ 'APP_ENV' => 'prod', 'APP_DEBUG' => false, - 'RR_MODE' => ['http'] + 'RR_MODE' => ['http'], ]), ])); @@ -136,19 +178,26 @@ public static function allowEnvDataProvider(): \Traversable { yield [ ['APP_ENV' => 'prod', 'APP_DEBUG' => false, 'RR_MODE' => 'http'], - [BootloaderA::class => ['bootloader' => new BootloaderA(), 'options' => []]] + [ + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], ]; 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'], - [] + [], ]; } @@ -156,31 +205,38 @@ 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' => []]] + [ + BootloaderA::class => [ + 'bootloader' => new BootloaderA(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + ], ]; } } diff --git a/src/Boot/tests/BootloadManager/BootloadManagerTest.php b/src/Boot/tests/BootloadManager/BootloadManagerTest.php index 43431298f..13cfa732c 100644 --- a/src/Boot/tests/BootloadManager/BootloadManagerTest.php +++ b/src/Boot/tests/BootloadManager/BootloadManagerTest.php @@ -4,57 +4,149 @@ namespace Spiral\Tests\Boot\BootloadManager; +use PHPUnit\Framework\Attributes\DataProvider; +use Spiral\Boot\BootloadManager\AttributeResolver; use Spiral\Boot\BootloadManager\BootloadManager; use Spiral\Boot\BootloadManager\Initializer; use Spiral\Boot\BootloadManager\InitializerInterface; +use Spiral\Boot\BootloadManagerInterface; use Spiral\Core\Container; use Spiral\Tests\Boot\Fixtures\BootloaderA; use Spiral\Tests\Boot\Fixtures\BootloaderB; +use Spiral\Tests\Boot\Fixtures\BootloaderL; +use Spiral\Tests\Boot\Fixtures\BootloaderM; +use Spiral\Tests\Boot\Fixtures\BootloaderO; +use Spiral\Tests\Boot\Fixtures\BootloaderP; +use Spiral\Tests\Boot\Fixtures\BootloaderQ; +use Spiral\Tests\Boot\Fixtures\BootloaderR; +use Spiral\Tests\Boot\Fixtures\BootloaderS; use Spiral\Tests\Boot\Fixtures\SampleBoot; use Spiral\Tests\Boot\Fixtures\SampleBootWithMethodBoot; use Spiral\Tests\Boot\Fixtures\SampleClass; +use Spiral\Tests\Boot\Fixtures\SampleClass2; +use Spiral\Tests\Boot\Fixtures\SampleClass3; +use Spiral\Tests\Boot\Fixtures\SampleClassInterface; +use Spiral\Tests\Boot\Fixtures\SampleInjectableClass; use Spiral\Tests\Boot\TestCase; final class BootloadManagerTest extends TestCase { public function testWithoutInvokerStrategy(): void { - $this->container->bind(InitializerInterface::class, new Initializer($this->container, $this->container)); - - $bootloader = new BootloadManager( - $this->container, - $this->container, - $this->container, - $this->container->get(InitializerInterface::class) + $bootloader = $this->getBootloadManager(); + $bootloader->bootload( + $classes = [ + SampleClass::class, + SampleBootWithMethodBoot::class, + SampleBoot::class, + ], + [ + static function (Container $container, SampleBoot $boot): void { + $container->bind('efg', $boot); + }, + ], + [ + static function (Container $container, SampleBoot $boot): void { + $container->bind('ghi', $boot); + }, + ], ); - $bootloader->bootload($classes = [ - SampleClass::class, - SampleBootWithMethodBoot::class, - SampleBoot::class, - ], [ - static function(Container $container, SampleBoot $boot): void { - $container->bind('efg', $boot); - } - ], [ - static function(Container $container, SampleBoot $boot): void { - $container->bind('ghi', $boot); - } - ]); - $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->assertTrue($this->container->has(SampleClassInterface::class)); + $this->assertTrue($this->container->has(SampleClass::class)); + $this->assertTrue($this->container->has(SampleClass2::class)); + $this->assertTrue($this->container->has(SampleClass3::class)); + + $this->assertTrue($this->container->hasInjector(SampleInjectableClass::class)); + $this->assertInstanceOf( + SampleInjectableClass::class, + $injectable = $this->container->get(SampleInjectableClass::class), + ); + $this->assertSame('foo', $injectable->name); + $this->assertNotInstanceOf(SampleBoot::class, $this->container->get('efg')); $this->assertInstanceOf(SampleBoot::class, $this->container->get('ghi')); + $this->assertNotSame($this->container->get(SampleClass2::class), $this->container->get(SampleClass2::class)); + $this->assertSame($this->container->get(SampleClass3::class), $this->container->get(SampleClass3::class)); + $classes = \array_filter($classes, static fn(string $class): bool => $class !== SampleClass::class); $this->assertSame(\array_merge($classes, [ BootloaderA::class, BootloaderB::class, ]), $bootloader->getClasses()); } + + public function testSingletonAliases(): void + { + $bootloader = $this->getBootloadManager(); + + $bootloader->bootload([BootloaderQ::class]); + + $this->assertTrue($this->container->has(SampleClass::class)); + $this->assertTrue($this->container->has(SampleClassInterface::class)); + + $this->assertSame( + $this->container->get(SampleClass::class), + $this->container->get(SampleClassInterface::class), + ); + } + + public function testBindingAliases(): void + { + $bootloader = $this->getBootloadManager(); + + $bootloader->bootload([BootloaderR::class]); + + $this->assertTrue($this->container->has(SampleClass::class)); + $this->assertTrue($this->container->has(SampleClassInterface::class)); + + $this->assertNotSame( + $this->container->get(SampleClass::class), + $this->container->get(SampleClassInterface::class), + ); + } + + public function testBindingAliases2(): void + { + $bootloader = $this->getBootloadManager(); + + $bootloader->bootload([BootloaderS::class]); + + $this->assertTrue($this->container->has('sample1')); + $this->assertTrue($this->container->has('sample2')); + $this->assertTrue($this->container->has('sample3')); + $this->assertFalse($this->container->has(SampleClass::class)); + $this->assertFalse($this->container->has(SampleClassInterface::class)); + + $this->assertTrue($this->container->has('sample4')); + $this->assertTrue($this->container->has('sample5')); + $this->assertTrue($this->container->has('sample6')); + $this->assertTrue($this->container->has('sample7')); + $this->assertTrue($this->container->has(SampleClass2::class)); + } + + #[DataProvider('provideErrorBootloader')] + public function testErrorAttributes(string $bootloaderClass): void + { + $bootloader = $this->getBootloadManager(); + + $this->expectException(\LogicException::class); + + $bootloader->bootload([$bootloaderClass]); + } + + public static function provideErrorBootloader(): iterable + { + yield [BootloaderL::class]; + yield [BootloaderM::class]; + yield [BootloaderO::class]; + yield [BootloaderP::class]; + } } diff --git a/src/Boot/tests/BootloadManager/BootloadersTest.php b/src/Boot/tests/BootloadManager/BootloadersTest.php index 777daa6c4..7cf398cfe 100644 --- a/src/Boot/tests/BootloadManager/BootloadersTest.php +++ b/src/Boot/tests/BootloadManager/BootloadersTest.php @@ -10,6 +10,8 @@ use Spiral\Tests\Boot\Fixtures\BootloaderA; use Spiral\Tests\Boot\Fixtures\BootloaderB; use Spiral\Tests\Boot\Fixtures\BootloaderC; +use Spiral\Tests\Boot\Fixtures\BootloaderD; +use Spiral\Tests\Boot\Fixtures\BootloaderWithAttributes; use Spiral\Tests\Boot\Fixtures\SampleBoot; use Spiral\Tests\Boot\Fixtures\SampleBootWithMethodBoot; use Spiral\Tests\Boot\Fixtures\SampleClass; @@ -21,28 +23,42 @@ public function testSchemaLoading(): void { $bootloader = $this->getBootloadManager(); - $bootloader->bootload($classes = [ - SampleClass::class, - SampleBootWithMethodBoot::class, - SampleBoot::class, - ], [ - static function(Container $container, SampleBoot $boot): void { - $container->bind('efg', $boot); - } - ], [ - static function(Container $container, SampleBoot $boot): void { - $container->bind('ghi', $boot); - } - ]); + $bootloader->bootload( + $classes = [ + SampleClass::class, + SampleBootWithMethodBoot::class, + SampleBoot::class, + ], + [ + static function (Container $container, SampleBoot $boot) { + $container->bind('efg', $boot); + }, + ], + [ + static function (Container $container, SampleBoot $boot) { + $container->bind('ghi', $boot); + }, + ], + ); $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->hasInstance('efg')); + $this->assertTrue($this->container->hasInstance('ijk')); $this->assertTrue($this->container->has('single')); + $this->assertTrue($this->container->has('singleAbc')); $this->assertTrue($this->container->has('ghi')); + $this->assertTrue($this->container->has('hij')); + $this->assertNotInstanceOf(SampleBoot::class, $this->container->get('efg')); $this->assertInstanceOf(SampleBoot::class, $this->container->get('ghi')); + $this->assertInstanceOf(SampleClass::class, $this->container->get('hij')); + $this->assertInstanceOf(SampleClass::class, $this->container->get('singleAbc')); + + $this->assertSame($this->container->get('singleAbc'), $this->container->get('singleAbc')); + $this->assertNotSame($this->container->get('hij'), $this->container->get('hij')); $classes = \array_filter($classes, static fn(string $class): bool => $class !== SampleClass::class); $this->assertSame(\array_merge($classes, [ @@ -107,6 +123,31 @@ public function boot(BinderInterface $binder): void $this->assertCount(1, $bootloader->getClasses()); } + public function testBootloaderWithAttributes(): void + { + $bootloader = $this->getBootloadManager(); + + $bootloader->bootload([ + BootloaderWithAttributes::class, + ]); + + $this->assertTrue($this->container->has('init')); + $this->assertTrue($this->container->has('initMethodF')); + $this->assertTrue($this->container->has('initMethodE')); + $this->assertTrue($this->container->has('initMethodB')); + $this->assertTrue($this->container->has('initMethodC')); + $this->assertTrue($this->container->has('initMethodD')); + $this->assertFalse($this->container->has('initMethodA')); + + $this->assertTrue($this->container->has('boot')); + $this->assertTrue($this->container->has('bootMethodF')); + $this->assertTrue($this->container->has('bootMethodE')); + $this->assertTrue($this->container->has('bootMethodB')); + $this->assertTrue($this->container->has('bootMethodC')); + $this->assertTrue($this->container->has('bootMethodD')); + $this->assertFalse($this->container->has('bootMethodA')); + } + public function testException(): void { $this->expectException(\Spiral\Boot\Exception\ClassNotFoundException::class); @@ -119,9 +160,11 @@ public function testException(): void public function testDependenciesFromConstant(): void { $bootloader = $this->getBootloadManager(); - $bootloader->bootload($classes = [ - SampleBoot::class, - ]); + $bootloader->bootload( + $classes = [ + SampleBoot::class, + ], + ); $this->assertSame(\array_merge($classes, [ BootloaderA::class, @@ -132,9 +175,11 @@ public function testDependenciesFromConstant(): void public function testDependenciesFromInterfaceMethod(): void { $bootloader = $this->getBootloadManager(); - $bootloader->bootload($classes = [ - BootloaderB::class, - ]); + $bootloader->bootload( + $classes = [ + BootloaderB::class, + ], + ); $this->assertSame(\array_merge($classes, [ BootloaderA::class, @@ -144,13 +189,16 @@ public function testDependenciesFromInterfaceMethod(): void public function testDependenciesFromInitAndBootMethods(): void { $bootloader = $this->getBootloadManager(); - $bootloader->bootload($classes = [ - BootloaderC::class, - ]); + $bootloader->bootload( + $classes = [ + BootloaderC::class, + ], + ); $this->assertSame(\array_merge($classes, [ BootloaderA::class, - BootloaderB::class + BootloaderD::class, + BootloaderB::class, ]), $bootloader->getClasses()); } } diff --git a/src/Boot/tests/BootloadManager/InitializerAttributesTest.php b/src/Boot/tests/BootloadManager/InitializerAttributesTest.php new file mode 100644 index 000000000..5ae6ae490 --- /dev/null +++ b/src/Boot/tests/BootloadManager/InitializerAttributesTest.php @@ -0,0 +1,38 @@ +initializer->init([BootloaderWithAttributes::class])); + + $this->assertSame([ + 'initMethodF', + 'init', + 'initMethodB', + 'initMethodE', + 'initMethodD', + 'initMethodC', + ], $result[BootloaderWithAttributes::class]['init_methods']); + } + + public function testFindBootMethods(): void + { + $result = \iterator_to_array($this->initializer->init([BootloaderWithAttributes::class])); + + $this->assertSame([ + 'bootMethodF', + 'boot', + 'bootMethodB', + 'bootMethodE', + 'bootMethodD', + 'bootMethodC', + ], $result[BootloaderWithAttributes::class]['boot_methods']); + } +} diff --git a/src/Boot/tests/BootloadManager/InitializerTestCase.php b/src/Boot/tests/BootloadManager/InitializerTestCase.php index 817d9347f..4b54c8480 100644 --- a/src/Boot/tests/BootloadManager/InitializerTestCase.php +++ b/src/Boot/tests/BootloadManager/InitializerTestCase.php @@ -4,6 +4,7 @@ namespace Spiral\Tests\Boot\BootloadManager; +use Spiral\Boot\BootloadManager\AttributeResolver; use Spiral\Boot\BootloadManager\Initializer; use Spiral\Tests\Boot\TestCase; @@ -15,6 +16,9 @@ protected function setUp(): void { parent::setUp(); - $this->initializer = new Initializer($this->container, $this->container); + $this->initializer = new Initializer( + $this->container, + $this->container, + ); } } diff --git a/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php b/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php index 8e1b28f72..802fb32dc 100644 --- a/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php +++ b/src/Boot/tests/BootloadManager/MergeBootloadConfigTest.php @@ -17,12 +17,22 @@ public function testOverrideEnabled(): void { $result = \iterator_to_array($this->initializer->init([ BootloaderF::class => new BootloadConfig(enabled: true), - BootloaderD::class + BootloaderD::class, ])); $this->assertEquals([ - BootloaderF::class => ['bootloader' => new BootloaderF(), 'options' => []], - BootloaderD::class => ['bootloader' => new BootloaderD(), 'options' => []] + BootloaderF::class => [ + 'bootloader' => new BootloaderF(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], + BootloaderD::class => [ + 'bootloader' => new BootloaderD(), + 'options' => [], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -33,7 +43,12 @@ public function testOverrideArgs(): void ])); $this->assertEquals([ - BootloaderG::class => ['bootloader' => new BootloaderG(), 'options' => ['foo' => 'bar']] + BootloaderG::class => [ + 'bootloader' => new BootloaderG(), + 'options' => ['foo' => 'bar'], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -44,11 +59,16 @@ public function testMergeArgs(): void ])); $this->assertEquals([ - BootloaderG::class => ['bootloader' => new BootloaderG(), 'options' => [ - 'a' => 'baz', - 'foo' => 'bar', - 'c' => 'd' - ]] + BootloaderG::class => [ + 'bootloader' => new BootloaderG(), + 'options' => [ + 'a' => 'baz', + 'foo' => 'bar', + 'c' => 'd', + ], + 'init_methods' => ['init'], + 'boot_methods' => ['boot'], + ], ], $result); } @@ -58,7 +78,7 @@ public function testOverrideAllowEnv(): void $config = $ref->invoke( $this->initializer, BootloaderH::class, - new BootloadConfig(allowEnv: ['foo' => 'bar']) + new BootloadConfig(allowEnv: ['foo' => 'bar']), ); $this->assertEquals(['foo' => 'bar'], $config->allowEnv); @@ -70,14 +90,14 @@ public function testMergeAllowEnv(): void $config = $ref->invoke( $this->initializer, BootloaderH::class, - new BootloadConfig(allowEnv: ['APP_ENV' => 'dev', 'foo' => 'bar'], override: false) + new BootloadConfig(allowEnv: ['APP_ENV' => 'dev', 'foo' => 'bar'], override: false), ); $this->assertEquals([ 'foo' => 'bar', 'APP_ENV' => 'dev', 'APP_DEBUG' => false, - 'RR_MODE' => ['http'] + 'RR_MODE' => ['http'], ], $config->allowEnv); } @@ -87,7 +107,7 @@ public function testOverrideDenyEnv(): void $config = $ref->invoke( $this->initializer, BootloaderI::class, - new BootloadConfig(denyEnv: ['foo' => 'bar']) + new BootloadConfig(denyEnv: ['foo' => 'bar']), ); $this->assertEquals(['foo' => 'bar'], $config->denyEnv); @@ -99,7 +119,7 @@ public function testMergeDenyEnv(): void $config = $ref->invoke( $this->initializer, BootloaderI::class, - new BootloadConfig(denyEnv: ['DB_HOST' => 'localhost', 'foo' => 'bar'], override: false) + new BootloadConfig(denyEnv: ['DB_HOST' => 'localhost', 'foo' => 'bar'], override: false), ); $this->assertEquals([ diff --git a/src/Boot/tests/Fixtures/Attribute/SampleMethod.php b/src/Boot/tests/Fixtures/Attribute/SampleMethod.php new file mode 100644 index 000000000..530aebec9 --- /dev/null +++ b/src/Boot/tests/Fixtures/Attribute/SampleMethod.php @@ -0,0 +1,9 @@ +bind(__FUNCTION__, SampleClass::class); + } + + #[InitMethod(priority: -1)] + public function initMethodF(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[InitMethod(priority: 50)] + private function initMethodE(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + public function initMethodA(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[InitMethod] + protected function initMethodB(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[InitMethod(priority: 100)] + public function initMethodC(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[InitMethod(priority: 50)] + public function initMethodD(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + // Boot + public function boot(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[BootMethod(priority: -1)] + public function bootMethodF(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[BootMethod(priority: 50)] + private function bootMethodE(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + public function bootMethodA(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[BootMethod] + protected function bootMethodB(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[BootMethod(priority: 100)] + public function bootMethodC(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } + + #[BootMethod(priority: 50)] + public function bootMethodD(BinderInterface $binder): void + { + $binder->bind(__FUNCTION__, SampleClass::class); + } +} diff --git a/src/Boot/tests/Fixtures/BootloaderWithResolver.php b/src/Boot/tests/Fixtures/BootloaderWithResolver.php new file mode 100644 index 000000000..90419d999 --- /dev/null +++ b/src/Boot/tests/Fixtures/BootloaderWithResolver.php @@ -0,0 +1,18 @@ +register(SampleMethod::class, new SampleMethodResolver($container)); + } +} diff --git a/src/Boot/tests/Fixtures/SampleBootWithMethodBoot.php b/src/Boot/tests/Fixtures/SampleBootWithMethodBoot.php index 357a3450c..bd333b58e 100644 --- a/src/Boot/tests/Fixtures/SampleBootWithMethodBoot.php +++ b/src/Boot/tests/Fixtures/SampleBootWithMethodBoot.php @@ -4,14 +4,19 @@ namespace Spiral\Tests\Boot\Fixtures; +use Spiral\Boot\Attribute\BindMethod; +use Spiral\Boot\Attribute\InjectorMethod; +use Spiral\Boot\Attribute\SingletonMethod; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Core\BinderInterface; +use Spiral\Core\Container\InjectorInterface; +use Spiral\Tests\Boot\Fixtures\Attribute\SampleMethod; class SampleBootWithMethodBoot extends Bootloader { public const BOOT = true; - public const BINDINGS = ['abc' => self::class]; + public const BINDINGS = ['abc' => self::class]; public const SINGLETONS = ['single' => self::class]; public function init(BinderInterface $binder): void @@ -24,4 +29,57 @@ public function boot(BinderInterface $binder): void $binder->bind('efg', new SampleClass()); $binder->bind('ghi', 'foo'); } + + #[BindMethod(alias: 'hij')] + private function bindMethodA(): SampleClass + { + return new SampleClass(); + } + + #[BindMethod(alias: 'ijk')] + protected function bindMethodB(): string + { + return 'foo'; + } + + #[BindMethod] + private function bindMethodC(): SampleClass2|string|int + { + return new SampleClass2(); + } + + #[BindMethod] + private function bindMethodD(): SampleClass|SampleClassInterface + { + return new SampleClass(); + } + + #[SingletonMethod(alias: 'singleAbc')] + private function singletonMethod(): SampleClass + { + return new SampleClass(); + } + + #[SingletonMethod] + private function singletonMethodA(): SampleClass3 + { + return new SampleClass3(); + } + + #[SampleMethod('sampleMethod')] + private function sampleMethod(): SampleClass|string|int + { + return new SampleClass(); + } + + #[InjectorMethod(alias: SampleInjectableClass::class)] + protected function sampleInjector(): InjectorInterface + { + return new class implements InjectorInterface { + public function createInjection(\ReflectionClass $class, ?string $context = null): object + { + return new SampleInjectableClass('foo'); + } + }; + } } diff --git a/src/Boot/tests/Fixtures/SampleClass.php b/src/Boot/tests/Fixtures/SampleClass.php index 27c1557b9..944315565 100644 --- a/src/Boot/tests/Fixtures/SampleClass.php +++ b/src/Boot/tests/Fixtures/SampleClass.php @@ -4,6 +4,6 @@ namespace Spiral\Tests\Boot\Fixtures; -class SampleClass +class SampleClass implements SampleClassInterface { } diff --git a/src/Boot/tests/Fixtures/SampleClass2.php b/src/Boot/tests/Fixtures/SampleClass2.php new file mode 100644 index 000000000..0de15190e --- /dev/null +++ b/src/Boot/tests/Fixtures/SampleClass2.php @@ -0,0 +1,7 @@ + + */ +final class SampleMethodResolver extends AbstractResolver +{ + public function resolve( + object $attribute, + object $service, + \ReflectionMethod $method, + ): void { + $this->binder->bind($attribute->alias, $method->getClosure($service)); + } +} diff --git a/src/Boot/tests/TestCase.php b/src/Boot/tests/TestCase.php index bd9e8fa56..15346a2b6 100644 --- a/src/Boot/tests/TestCase.php +++ b/src/Boot/tests/TestCase.php @@ -4,6 +4,9 @@ namespace Spiral\Tests\Boot; +use Spiral\Boot\BootloadManager\AttributeResolver; +use Spiral\Boot\BootloadManager\InitializerInterface; +use Spiral\Boot\BootloadManager\InvokerStrategyInterface; use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager; use Spiral\Boot\BootloadManager\DefaultInvokerStrategy; use Spiral\Boot\BootloadManager\Initializer; @@ -23,12 +26,17 @@ protected function setUp(): void public function getBootloadManager(): StrategyBasedBootloadManager { - $initializer = new Initializer($this->container, $this->container); + $this->container->bind(AttributeResolver::class, AttributeResolver::class); + $this->container->bind( + InitializerInterface::class, + $initializer = new Initializer($this->container, $this->container), + ); - return new StrategyBasedBootloadManager( - new DefaultInvokerStrategy($initializer, $this->container, $this->container), - $this->container, - $initializer + $this->container->bind( + InvokerStrategyInterface::class, + $invoker = new DefaultInvokerStrategy($initializer, $this->container, $this->container), ); + + return new StrategyBasedBootloadManager($invoker, $this->container, $initializer); } }