From b9ebf98d14a1252896ba8e23b9ff5bf4caeb6cf2 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Wed, 31 Dec 2025 04:09:25 +0000 Subject: [PATCH] Add Boot and Initialize attributes Ports Laravel's #[Boot] and #[Initialize] attributes to Hypervel, allowing trait methods to be marked as boot/initialize methods without requiring the conventional `boot{TraitName}` or `initialize{TraitName}` naming. - Add Boot attribute for static boot methods - Add Initialize attribute for instance initialize methods - Add HasBootableTraits trait with enhanced bootTraits() method - Modify Model to use HasBootableTraits trait - Add tests (7 tests, 18 assertions) --- .../src/Database/Eloquent/Attributes/Boot.php | 30 +++ .../Eloquent/Attributes/Initialize.php | 31 +++ .../Eloquent/Concerns/HasBootableTraits.php | 76 +++++++ src/core/src/Database/Eloquent/Model.php | 4 +- .../Concerns/HasBootableTraitsTest.php | 192 ++++++++++++++++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/core/src/Database/Eloquent/Attributes/Boot.php create mode 100644 src/core/src/Database/Eloquent/Attributes/Initialize.php create mode 100644 src/core/src/Database/Eloquent/Concerns/HasBootableTraits.php create mode 100644 tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php diff --git a/src/core/src/Database/Eloquent/Attributes/Boot.php b/src/core/src/Database/Eloquent/Attributes/Boot.php new file mode 100644 index 000000000..3133986b9 --- /dev/null +++ b/src/core/src/Database/Eloquent/Attributes/Boot.php @@ -0,0 +1,30 @@ + 'boot' . class_basename($trait), + $uses + ); + $conventionalInitMethods = array_map( + static fn (string $trait): string => 'initialize' . class_basename($trait), + $uses + ); + + // Iterate through all methods looking for boot/initialize methods + foreach ((new ReflectionClass($class))->getMethods() as $method) { + $methodName = $method->getName(); + + // Handle boot methods (conventional naming OR #[Boot] attribute) + if ( + ! in_array($methodName, $booted, true) + && $method->isStatic() + && ( + in_array($methodName, $conventionalBootMethods, true) + || $method->getAttributes(Boot::class) !== [] + ) + ) { + $method->invoke(null); + $booted[] = $methodName; + } + + // Handle initialize methods (conventional naming OR #[Initialize] attribute) + if ( + in_array($methodName, $conventionalInitMethods, true) + || $method->getAttributes(Initialize::class) !== [] + ) { + TraitInitializers::$container[$class][] = $methodName; + } + } + + TraitInitializers::$container[$class] = array_unique(TraitInitializers::$container[$class]); + } +} diff --git a/src/core/src/Database/Eloquent/Model.php b/src/core/src/Database/Eloquent/Model.php index 2fb1e5a64..805f59df1 100644 --- a/src/core/src/Database/Eloquent/Model.php +++ b/src/core/src/Database/Eloquent/Model.php @@ -9,6 +9,7 @@ use Hypervel\Broadcasting\Contracts\HasBroadcastChannel; use Hypervel\Context\Context; use Hypervel\Database\Eloquent\Concerns\HasAttributes; +use Hypervel\Database\Eloquent\Concerns\HasBootableTraits; use Hypervel\Database\Eloquent\Concerns\HasCallbacks; use Hypervel\Database\Eloquent\Concerns\HasObservers; use Hypervel\Database\Eloquent\Concerns\HasRelations; @@ -67,10 +68,11 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChannel { use HasAttributes; + use HasBootableTraits; use HasCallbacks; + use HasObservers; use HasRelations; use HasRelationships; - use HasObservers; protected ?string $connection = null; diff --git a/tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php b/tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php new file mode 100644 index 000000000..db228a345 --- /dev/null +++ b/tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php @@ -0,0 +1,192 @@ +assertFalse(BootableTraitsTestModel::$bootCalled); + + // Creating a model triggers boot + new BootableTraitsTestModel(); + + $this->assertTrue(BootableTraitsTestModel::$bootCalled); + } + + public function testConventionalBootMethodStillWorks(): void + { + $this->assertFalse(BootableTraitsTestModel::$conventionalBootCalled); + + new BootableTraitsTestModel(); + + $this->assertTrue(BootableTraitsTestModel::$conventionalBootCalled); + } + + public function testInitializeAttributeAddsMethodToInitializers(): void + { + $this->assertFalse(BootableTraitsTestModel::$initializeCalled); + + // Creating a model triggers initialize + new BootableTraitsTestModel(); + + $this->assertTrue(BootableTraitsTestModel::$initializeCalled); + } + + public function testConventionalInitializeMethodStillWorks(): void + { + $this->assertFalse(BootableTraitsTestModel::$conventionalInitializeCalled); + + new BootableTraitsTestModel(); + + $this->assertTrue(BootableTraitsTestModel::$conventionalInitializeCalled); + } + + public function testBothAttributeAndConventionalMethodsWorkTogether(): void + { + $this->assertFalse(BootableTraitsTestModel::$bootCalled); + $this->assertFalse(BootableTraitsTestModel::$conventionalBootCalled); + $this->assertFalse(BootableTraitsTestModel::$initializeCalled); + $this->assertFalse(BootableTraitsTestModel::$conventionalInitializeCalled); + + new BootableTraitsTestModel(); + + $this->assertTrue(BootableTraitsTestModel::$bootCalled); + $this->assertTrue(BootableTraitsTestModel::$conventionalBootCalled); + $this->assertTrue(BootableTraitsTestModel::$initializeCalled); + $this->assertTrue(BootableTraitsTestModel::$conventionalInitializeCalled); + } + + public function testBootMethodIsOnlyCalledOnce(): void + { + BootableTraitsTestModel::$bootCallCount = 0; + + new BootableTraitsTestModel(); + new BootableTraitsTestModel(); + new BootableTraitsTestModel(); + + // Boot should only be called once regardless of how many instances + $this->assertSame(1, BootableTraitsTestModel::$bootCallCount); + } + + public function testInitializeMethodIsCalledForEachInstance(): void + { + BootableTraitsTestModel::$initializeCallCount = 0; + + new BootableTraitsTestModel(); + new BootableTraitsTestModel(); + new BootableTraitsTestModel(); + + // Initialize should be called for each instance + $this->assertSame(3, BootableTraitsTestModel::$initializeCallCount); + } +} + +// Test trait with #[Boot] attribute method +trait HasCustomBootMethod +{ + #[Boot] + public static function customBootMethod(): void + { + static::$bootCalled = true; + ++static::$bootCallCount; + } +} + +// Test trait with conventional boot method +trait HasConventionalBootMethod +{ + public static function bootHasConventionalBootMethod(): void + { + static::$conventionalBootCalled = true; + } +} + +// Test trait with #[Initialize] attribute method +trait HasCustomInitializeMethod +{ + #[Initialize] + public function customInitializeMethod(): void + { + static::$initializeCalled = true; + ++static::$initializeCallCount; + } +} + +// Test trait with conventional initialize method +trait HasConventionalInitializeMethod +{ + public function initializeHasConventionalInitializeMethod(): void + { + static::$conventionalInitializeCalled = true; + } +} + +class BootableTraitsTestModel extends Model +{ + use HasCustomBootMethod; + use HasConventionalBootMethod; + use HasCustomInitializeMethod; + use HasConventionalInitializeMethod; + + public static bool $bootCalled = false; + + public static bool $conventionalBootCalled = false; + + public static bool $initializeCalled = false; + + public static bool $conventionalInitializeCalled = false; + + public static int $bootCallCount = 0; + + public static int $initializeCallCount = 0; + + protected ?string $table = 'test_models'; +}