Skip to content

Commit 4772245

Browse files
authored
Merge pull request #1190 from spiral/issue/1064
feature: PHP Attributes for Bootloader Methods
2 parents f0e4aac + 82eb70a commit 4772245

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1776
-159
lines changed

src/Boot/src/AbstractKernel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Spiral\Boot\Bootloader\BootloaderRegistry;
1010
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
1111
use Spiral\Boot\Bootloader\CoreBootloader;
12+
use Spiral\Boot\BootloadManager\AttributeResolver;
13+
use Spiral\Boot\BootloadManager\AttributeResolverRegistryInterface;
1214
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
1315
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
1416
use Spiral\Boot\BootloadManager\Initializer;
@@ -119,9 +121,12 @@ final public static function create(
119121
$exceptionHandler->register();
120122
}
121123

124+
$container->bind(AttributeResolverRegistryInterface::class, AttributeResolver::class);
125+
122126
if (!$container->has(InitializerInterface::class)) {
123127
$container->bind(InitializerInterface::class, Initializer::class);
124128
}
129+
125130
if (!$container->has(InvokerStrategyInterface::class)) {
126131
$container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
127132
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Abstract base class for method attributes used in bootloaders.
9+
*
10+
* This abstract class serves as a base for all method-level attributes in the bootloader system,
11+
* providing common functionality for managing binding aliases.
12+
*
13+
* @internal This class is for internal use within the framework and should not be used directly by application code.
14+
*/
15+
abstract class AbstractMethod
16+
{
17+
/**
18+
* @param non-empty-string|null $alias Alias for the method. If not provided, the return type of the method will be used as the alias.
19+
* @param bool $aliasesFromReturnType Add aliases from the return type of the method even if the method has an alias.
20+
*/
21+
public function __construct(
22+
/**
23+
* Alias for the method.
24+
* If not provided, the return type of the method will be used as the alias.
25+
*/
26+
public readonly ?string $alias = null,
27+
/**
28+
* Add aliases from the return type of the method even if the method has an alias.
29+
*/
30+
public readonly bool $aliasesFromReturnType = false,
31+
) {}
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Attribute to define additional aliases for a method.
9+
*
10+
* This attribute allows defining multiple aliases for a method in a bootloader class.
11+
* It can be used to bind a method's return value to multiple interface or class names.
12+
*
13+
* This attribute can be applied multiple times to the same method.
14+
*
15+
* Example usage:
16+
* ```php
17+
* class MyBootloader extends Bootloader
18+
* {
19+
* #[BindAlias(LoggerInterface::class, PsrLoggerInterface::class)]
20+
* #[BindAlias(MonologLoggerInterface::class)]
21+
* public function createLogger(): Logger
22+
* {
23+
* return new Logger();
24+
* }
25+
* }
26+
* ```
27+
*
28+
* The above example binds the returned Logger instance to LoggerInterface,
29+
* PsrLoggerInterface, and MonologLoggerInterface.
30+
*/
31+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
32+
final class BindAlias
33+
{
34+
/**
35+
* @param non-empty-string[] $aliases List of class or interface names to bind the returned value to.
36+
*/
37+
public readonly array $aliases;
38+
39+
/**
40+
* @param non-empty-string ...$aliases List of class or interface names to bind the returned value to.
41+
*/
42+
public function __construct(string ...$aliases)
43+
{
44+
$this->aliases = $aliases;
45+
}
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Attribute for marking methods that provide container bindings.
9+
*
10+
* Methods marked with this attribute will be invoked each time the container
11+
* needs to resolve the dependency, creating a new instance each time.
12+
* The return value of the method will be bound to the specified alias
13+
* or to all interfaces/classes in the return type.
14+
*
15+
* Example usage:
16+
* ```php
17+
* class MyBootloader extends Bootloader
18+
* {
19+
* // Method will be called each time the container resolves HttpClientInterface
20+
* #[BindMethod]
21+
* public function createHttpClient(): HttpClientInterface
22+
* {
23+
* return new HttpClient();
24+
* }
25+
*
26+
* // Method will be called each time the container resolves DbFactory
27+
* // instead of its return type (DatabaseFactory)
28+
* #[BindMethod(alias: DbFactory::class)]
29+
* public function createDatabaseFactory(): DatabaseFactory
30+
* {
31+
* return new DatabaseFactory();
32+
* }
33+
*
34+
* // Method will be called each time the container resolves either
35+
* // LogManagerInterface or its return type (LogManager)
36+
* #[BindMethod(alias: LogManagerInterface::class, aliasesFromReturnType: true)]
37+
* public function createLogManager(): LogManager
38+
* {
39+
* return new LogManager();
40+
* }
41+
* }
42+
* ```
43+
*
44+
* This attribute is similar to defining bindings via the `defineBindings()` method,
45+
* but with a more expressive and type-safe approach.
46+
*
47+
* @see SingletonMethod For binding singleton instances
48+
* @see InjectorMethod For binding injector methods
49+
*/
50+
#[\Attribute(\Attribute::TARGET_METHOD)]
51+
final class BindMethod extends AbstractMethod {}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Attribute to bind a method's result to a specific container scope.
9+
*
10+
* This attribute allows defining a specific scope for the binding created
11+
* by method attributes such as BindMethod or SingletonMethod.
12+
*
13+
* Example usage:
14+
* ```php
15+
* class MyBootloader extends Bootloader
16+
* {
17+
* // Bind to the 'http' scope
18+
* #[BindMethod]
19+
* #[BindScope('http')]
20+
* public function createHttpClient(): HttpClientInterface
21+
* {
22+
* return new HttpClient();
23+
* }
24+
*
25+
* // Bind to the 'console' scope using an enum
26+
* #[SingletonMethod]
27+
* #[BindScope(ScopeEnum::Console)]
28+
* public function createConsoleOutput(): OutputInterface
29+
* {
30+
* return new ConsoleOutput();
31+
* }
32+
* }
33+
* ```
34+
*
35+
* When using a scope, the binding will only be available when the container
36+
* is running within that scope.
37+
*/
38+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
39+
final class BindScope
40+
{
41+
/**
42+
* The scope name to bind to.
43+
*/
44+
public readonly string $scope;
45+
46+
/**
47+
* @param string|\BackedEnum $scope The scope name or enum value to bind to.
48+
* If an enum is provided, its value will be used as the scope name.
49+
*/
50+
public function __construct(string|\BackedEnum $scope)
51+
{
52+
$this->scope = \is_object($scope) ? (string) $scope->value : $scope;
53+
}
54+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Attribute for marking methods that should be called during the boot phase.
9+
*
10+
* Methods marked with this attribute will be called during the bootloader's
11+
* boot phase, after all initialization methods have been called.
12+
* The boot phase is where you typically configure services, register event listeners,
13+
* or perform other setup tasks.
14+
*
15+
* The priority parameter determines the order in which boot methods are called.
16+
* Higher priority values are executed first.
17+
*
18+
* Example usage:
19+
* ```php
20+
* class MyBootloader extends Bootloader
21+
* {
22+
* // Called during boot phase with default priority (0)
23+
* #[BootMethod]
24+
* public function configureRoutes(RouterInterface $router): void
25+
* {
26+
* $router->addRoute('home', '/');
27+
* }
28+
*
29+
* // Called during boot phase with high priority (10)
30+
* #[BootMethod(priority: 10)]
31+
* public function configureDatabase(DatabaseInterface $db): void
32+
* {
33+
* $db->setDefaultConnection('default');
34+
* }
35+
*
36+
* // Called during boot phase with low priority (-10)
37+
* #[BootMethod(priority: -10)]
38+
* public function registerEventListeners(EventDispatcherInterface $dispatcher): void
39+
* {
40+
* $dispatcher->addListener(ApplicationStarted::class, fn() => $this->onStart());
41+
* }
42+
* }
43+
* ```
44+
*
45+
* Boot methods are executed after all bootloaders' init methods have been called.
46+
*
47+
* @see InitMethod For methods to be called during initialization phase
48+
*/
49+
#[\Attribute(\Attribute::TARGET_METHOD)]
50+
final class BootMethod
51+
{
52+
/**
53+
* @param int $priority The priority of this boot method. Higher values are executed first.
54+
*/
55+
public function __construct(
56+
public readonly int $priority = 0,
57+
) {}
58+
}

src/Boot/src/Attribute/BootloadConfig.php

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,62 @@
44

55
namespace Spiral\Boot\Attribute;
66

7-
use Spiral\Attributes\NamedArgumentConstructor;
8-
9-
#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor]
7+
/**
8+
* Attribute to configure bootloader behavior.
9+
*
10+
* This attribute allows defining configuration for bootloaders, including
11+
* constructor arguments, enabling/disabling based on environment variables,
12+
* and controlling how configuration overrides work.
13+
*
14+
* Example usage:
15+
* ```php
16+
* // Basic configuration with constructor arguments
17+
* #[BootloadConfig(args: ['defaultConnection' => 'sqlite'])]
18+
* class DatabaseBootloader extends Bootloader
19+
* {
20+
* public function __construct(
21+
* private readonly string $defaultConnection
22+
* ) {}
23+
*
24+
* // ...
25+
* }
26+
*
27+
* // Conditionally enable based on environment
28+
* #[BootloadConfig(
29+
* allowEnv: ['APP_ENV' => ['local', 'development']],
30+
* denyEnv: ['TESTING' => [true, 1, 'true']]
31+
* )]
32+
* class DevToolsBootloader extends Bootloader
33+
* {
34+
* // Only loaded in local or development environments
35+
* // And not when TESTING is true
36+
* }
37+
*
38+
* // Prevent runtime configuration from overriding attribute configuration
39+
* #[BootloadConfig(args: ['debug' => true], override: false)]
40+
* class DebugBootloader extends Bootloader
41+
* {
42+
* // The 'debug' argument will always be true, even if different
43+
* // configuration is provided at runtime
44+
* }
45+
* ```
46+
*
47+
* When a bootloader has both runtime configuration and a BootloadConfig attribute,
48+
* the override parameter controls which configuration takes precedence.
49+
*/
50+
#[\Attribute(\Attribute::TARGET_CLASS)]
1051
class BootloadConfig
1152
{
53+
/**
54+
* @param array $args Arguments to pass to the bootloader's constructor.
55+
* @param bool $enabled Whether this bootloader is enabled.
56+
* @param array $allowEnv Environment conditions that must be satisfied for the bootloader to be enabled.
57+
* Format: ['ENV_VAR' => ['allowed_value1', 'allowed_value2']]
58+
* @param array $denyEnv Environment conditions that must not be satisfied for the bootloader to be enabled.
59+
* Format: ['ENV_VAR' => ['denied_value1', 'denied_value2']]
60+
* @param bool $override Whether runtime configuration should override the attribute configuration.
61+
* If false, attribute configuration takes precedence.
62+
*/
1263
public function __construct(
1364
public array $args = [],
1465
public bool $enabled = true,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Boot\Attribute;
6+
7+
/**
8+
* Attribute for marking methods that should be called during the initialization phase.
9+
*
10+
* Methods marked with this attribute will be called during the bootloader's
11+
* initialization phase, before the boot phase begins. This is where you typically
12+
* set up container bindings, register services, or perform other initialization tasks.
13+
*
14+
* The priority parameter determines the order in which init methods are called.
15+
* Higher priority values are executed first.
16+
*
17+
* Example usage:
18+
* ```php
19+
* class MyBootloader extends Bootloader
20+
* {
21+
* // Called during initialization phase with default priority (0)
22+
* #[InitMethod]
23+
* public function registerServices(Container $container): void
24+
* {
25+
* $container->bindSingleton(MyService::class, MyServiceImplementation::class);
26+
* }
27+
*
28+
* // Called during initialization phase with high priority (10)
29+
* #[InitMethod(priority: 10)]
30+
* public function setupCore(): void
31+
* {
32+
* // Setup core components first
33+
* }
34+
*
35+
* // Called during initialization phase with low priority (-10)
36+
* #[InitMethod(priority: -10)]
37+
* public function setupExtensions(): void
38+
* {
39+
* // Setup extensions after core components
40+
* }
41+
* }
42+
* ```
43+
*
44+
* Init methods are executed before any bootloader's boot methods are called.
45+
*
46+
* @see BootMethod For methods to be called during boot phase
47+
*/
48+
#[\Attribute(\Attribute::TARGET_METHOD)]
49+
final class InitMethod
50+
{
51+
/**
52+
* @param int $priority The priority of this init method. Higher values are executed first.
53+
*/
54+
public function __construct(
55+
public readonly int $priority = 0,
56+
) {}
57+
}

0 commit comments

Comments
 (0)