Skip to content

Commit e964070

Browse files
authored
Merge pull request #333 from binaryfire/feature/boot-initialize-attributes
feat: Add `#[Boot]` and `#[Initialize]` attributes
2 parents 890ba8d + 8dffbf6 commit e964070

File tree

5 files changed

+331
-0
lines changed

5 files changed

+331
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Attributes;
6+
7+
use Attribute;
8+
9+
/**
10+
* Mark a static method as a boot method for a trait.
11+
*
12+
* This attribute allows trait boot methods to be named anything,
13+
* instead of requiring the conventional `boot{TraitName}` naming.
14+
*
15+
* @example
16+
* ```php
17+
* trait HasCustomBehavior
18+
* {
19+
* #[Boot]
20+
* public static function registerCustomBehavior(): void
21+
* {
22+
* // This method will be called during model boot
23+
* }
24+
* }
25+
* ```
26+
*/
27+
#[Attribute(Attribute::TARGET_METHOD)]
28+
class Boot
29+
{
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Attributes;
6+
7+
use Attribute;
8+
9+
/**
10+
* Mark a method as an initialize method for a trait.
11+
*
12+
* This attribute allows trait initialize methods to be named anything,
13+
* instead of requiring the conventional `initialize{TraitName}` naming.
14+
* Initialize methods are called on each new model instance.
15+
*
16+
* @example
17+
* ```php
18+
* trait HasCustomBehavior
19+
* {
20+
* #[Initialize]
21+
* public function setupCustomBehavior(): void
22+
* {
23+
* // This method will be called when a new model instance is created
24+
* }
25+
* }
26+
* ```
27+
*/
28+
#[Attribute(Attribute::TARGET_METHOD)]
29+
class Initialize
30+
{
31+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Concerns;
6+
7+
use Hyperf\Database\Model\TraitInitializers;
8+
use Hypervel\Database\Eloquent\Attributes\Boot;
9+
use Hypervel\Database\Eloquent\Attributes\Initialize;
10+
use ReflectionClass;
11+
12+
use function Hyperf\Support\class_uses_recursive;
13+
14+
/**
15+
* Provides support for Boot and Initialize attributes on trait methods.
16+
*
17+
* This trait overrides the default bootTraits() method to also check for
18+
* #[Boot] and #[Initialize] attributes on methods, allowing trait methods
19+
* to be named anything instead of requiring the conventional naming.
20+
*/
21+
trait HasBootableTraits
22+
{
23+
/**
24+
* Boot all of the bootable traits on the model.
25+
*
26+
* This method extends the parent implementation to also support
27+
* #[Boot] and #[Initialize] attributes on trait methods.
28+
*/
29+
protected function bootTraits(): void
30+
{
31+
$class = static::class;
32+
33+
$booted = [];
34+
TraitInitializers::$container[$class] = [];
35+
36+
$uses = class_uses_recursive($class);
37+
38+
// Build conventional method names for traits
39+
$conventionalBootMethods = array_map(
40+
static fn (string $trait): string => 'boot' . class_basename($trait),
41+
$uses
42+
);
43+
$conventionalInitMethods = array_map(
44+
static fn (string $trait): string => 'initialize' . class_basename($trait),
45+
$uses
46+
);
47+
48+
// Iterate through all methods looking for boot/initialize methods
49+
foreach ((new ReflectionClass($class))->getMethods() as $method) {
50+
$methodName = $method->getName();
51+
52+
// Handle boot methods (conventional naming OR #[Boot] attribute)
53+
if (
54+
! in_array($methodName, $booted, true)
55+
&& $method->isStatic()
56+
&& (
57+
in_array($methodName, $conventionalBootMethods, true)
58+
|| $method->getAttributes(Boot::class) !== []
59+
)
60+
) {
61+
$method->invoke(null);
62+
$booted[] = $methodName;
63+
}
64+
65+
// Handle initialize methods (conventional naming OR #[Initialize] attribute)
66+
if (
67+
in_array($methodName, $conventionalInitMethods, true)
68+
|| $method->getAttributes(Initialize::class) !== []
69+
) {
70+
TraitInitializers::$container[$class][] = $methodName;
71+
}
72+
}
73+
74+
TraitInitializers::$container[$class] = array_unique(TraitInitializers::$container[$class]);
75+
}
76+
}

src/core/src/Database/Eloquent/Model.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Hypervel\Broadcasting\Contracts\HasBroadcastChannel;
1010
use Hypervel\Context\Context;
1111
use Hypervel\Database\Eloquent\Concerns\HasAttributes;
12+
use Hypervel\Database\Eloquent\Concerns\HasBootableTraits;
1213
use Hypervel\Database\Eloquent\Concerns\HasCallbacks;
1314
use Hypervel\Database\Eloquent\Concerns\HasGlobalScopes;
1415
use Hypervel\Database\Eloquent\Concerns\HasLocalScopes;
@@ -70,6 +71,7 @@
7071
abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChannel
7172
{
7273
use HasAttributes;
74+
use HasBootableTraits;
7375
use HasCallbacks;
7476
use HasGlobalScopes;
7577
use HasLocalScopes;
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Core\Database\Eloquent\Concerns;
6+
7+
use Hyperf\Database\Model\Booted;
8+
use Hyperf\Database\Model\TraitInitializers;
9+
use Hypervel\Database\Eloquent\Attributes\Boot;
10+
use Hypervel\Database\Eloquent\Attributes\Initialize;
11+
use Hypervel\Database\Eloquent\Model;
12+
use Hypervel\Testbench\TestCase;
13+
14+
/**
15+
* @internal
16+
* @coversNothing
17+
*/
18+
class HasBootableTraitsTest extends TestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
// Reset model booted state so each test starts fresh
25+
Booted::$container = [];
26+
TraitInitializers::$container = [];
27+
28+
// Reset static state before each test
29+
BootableTraitsTestModel::$bootCalled = false;
30+
BootableTraitsTestModel::$conventionalBootCalled = false;
31+
BootableTraitsTestModel::$initializeCalled = false;
32+
BootableTraitsTestModel::$conventionalInitializeCalled = false;
33+
BootableTraitsTestModel::$bootCallCount = 0;
34+
BootableTraitsTestModel::$initializeCallCount = 0;
35+
}
36+
37+
protected function tearDown(): void
38+
{
39+
// Reset model booted state
40+
Booted::$container = [];
41+
TraitInitializers::$container = [];
42+
43+
// Reset static state after each test
44+
BootableTraitsTestModel::$bootCalled = false;
45+
BootableTraitsTestModel::$conventionalBootCalled = false;
46+
BootableTraitsTestModel::$initializeCalled = false;
47+
BootableTraitsTestModel::$conventionalInitializeCalled = false;
48+
BootableTraitsTestModel::$bootCallCount = 0;
49+
BootableTraitsTestModel::$initializeCallCount = 0;
50+
51+
parent::tearDown();
52+
}
53+
54+
public function testBootAttributeCallsStaticMethodDuringBoot(): void
55+
{
56+
$this->assertFalse(BootableTraitsTestModel::$bootCalled);
57+
58+
// Creating a model triggers boot
59+
new BootableTraitsTestModel();
60+
61+
$this->assertTrue(BootableTraitsTestModel::$bootCalled);
62+
}
63+
64+
public function testConventionalBootMethodStillWorks(): void
65+
{
66+
$this->assertFalse(BootableTraitsTestModel::$conventionalBootCalled);
67+
68+
new BootableTraitsTestModel();
69+
70+
$this->assertTrue(BootableTraitsTestModel::$conventionalBootCalled);
71+
}
72+
73+
public function testInitializeAttributeAddsMethodToInitializers(): void
74+
{
75+
$this->assertFalse(BootableTraitsTestModel::$initializeCalled);
76+
77+
// Creating a model triggers initialize
78+
new BootableTraitsTestModel();
79+
80+
$this->assertTrue(BootableTraitsTestModel::$initializeCalled);
81+
}
82+
83+
public function testConventionalInitializeMethodStillWorks(): void
84+
{
85+
$this->assertFalse(BootableTraitsTestModel::$conventionalInitializeCalled);
86+
87+
new BootableTraitsTestModel();
88+
89+
$this->assertTrue(BootableTraitsTestModel::$conventionalInitializeCalled);
90+
}
91+
92+
public function testBothAttributeAndConventionalMethodsWorkTogether(): void
93+
{
94+
$this->assertFalse(BootableTraitsTestModel::$bootCalled);
95+
$this->assertFalse(BootableTraitsTestModel::$conventionalBootCalled);
96+
$this->assertFalse(BootableTraitsTestModel::$initializeCalled);
97+
$this->assertFalse(BootableTraitsTestModel::$conventionalInitializeCalled);
98+
99+
new BootableTraitsTestModel();
100+
101+
$this->assertTrue(BootableTraitsTestModel::$bootCalled);
102+
$this->assertTrue(BootableTraitsTestModel::$conventionalBootCalled);
103+
$this->assertTrue(BootableTraitsTestModel::$initializeCalled);
104+
$this->assertTrue(BootableTraitsTestModel::$conventionalInitializeCalled);
105+
}
106+
107+
public function testBootMethodIsOnlyCalledOnce(): void
108+
{
109+
BootableTraitsTestModel::$bootCallCount = 0;
110+
111+
new BootableTraitsTestModel();
112+
new BootableTraitsTestModel();
113+
new BootableTraitsTestModel();
114+
115+
// Boot should only be called once regardless of how many instances
116+
$this->assertSame(1, BootableTraitsTestModel::$bootCallCount);
117+
}
118+
119+
public function testInitializeMethodIsCalledForEachInstance(): void
120+
{
121+
BootableTraitsTestModel::$initializeCallCount = 0;
122+
123+
new BootableTraitsTestModel();
124+
new BootableTraitsTestModel();
125+
new BootableTraitsTestModel();
126+
127+
// Initialize should be called for each instance
128+
$this->assertSame(3, BootableTraitsTestModel::$initializeCallCount);
129+
}
130+
}
131+
132+
// Test trait with #[Boot] attribute method
133+
trait HasCustomBootMethod
134+
{
135+
#[Boot]
136+
public static function customBootMethod(): void
137+
{
138+
static::$bootCalled = true;
139+
++static::$bootCallCount;
140+
}
141+
}
142+
143+
// Test trait with conventional boot method
144+
trait HasConventionalBootMethod
145+
{
146+
public static function bootHasConventionalBootMethod(): void
147+
{
148+
static::$conventionalBootCalled = true;
149+
}
150+
}
151+
152+
// Test trait with #[Initialize] attribute method
153+
trait HasCustomInitializeMethod
154+
{
155+
#[Initialize]
156+
public function customInitializeMethod(): void
157+
{
158+
static::$initializeCalled = true;
159+
++static::$initializeCallCount;
160+
}
161+
}
162+
163+
// Test trait with conventional initialize method
164+
trait HasConventionalInitializeMethod
165+
{
166+
public function initializeHasConventionalInitializeMethod(): void
167+
{
168+
static::$conventionalInitializeCalled = true;
169+
}
170+
}
171+
172+
class BootableTraitsTestModel extends Model
173+
{
174+
use HasCustomBootMethod;
175+
use HasConventionalBootMethod;
176+
use HasCustomInitializeMethod;
177+
use HasConventionalInitializeMethod;
178+
179+
public static bool $bootCalled = false;
180+
181+
public static bool $conventionalBootCalled = false;
182+
183+
public static bool $initializeCalled = false;
184+
185+
public static bool $conventionalInitializeCalled = false;
186+
187+
public static int $bootCallCount = 0;
188+
189+
public static int $initializeCallCount = 0;
190+
191+
protected ?string $table = 'test_models';
192+
}

0 commit comments

Comments
 (0)