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 00000000..3133986b --- /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 2fa4b38d..85e70c3c 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\HasGlobalScopes; use Hypervel\Database\Eloquent\Concerns\HasLocalScopes; @@ -69,6 +70,7 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChannel { use HasAttributes; + use HasBootableTraits; use HasCallbacks; use HasGlobalScopes; use HasLocalScopes; diff --git a/tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php b/tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php new file mode 100644 index 00000000..db228a34 --- /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'; +}