Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/core/src/Database/Eloquent/Attributes/Boot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Attributes;

use Attribute;

/**
* Mark a static method as a boot method for a trait.
*
* This attribute allows trait boot methods to be named anything,
* instead of requiring the conventional `boot{TraitName}` naming.
*
* @example
* ```php
* trait HasCustomBehavior
* {
* #[Boot]
* public static function registerCustomBehavior(): void
* {
* // This method will be called during model boot
* }
* }
* ```
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Boot
{
}
31 changes: 31 additions & 0 deletions src/core/src/Database/Eloquent/Attributes/Initialize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Attributes;

use Attribute;

/**
* Mark a method as an initialize method for a trait.
*
* This attribute allows trait initialize methods to be named anything,
* instead of requiring the conventional `initialize{TraitName}` naming.
* Initialize methods are called on each new model instance.
*
* @example
* ```php
* trait HasCustomBehavior
* {
* #[Initialize]
* public function setupCustomBehavior(): void
* {
* // This method will be called when a new model instance is created
* }
* }
* ```
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Initialize
{
}
76 changes: 76 additions & 0 deletions src/core/src/Database/Eloquent/Concerns/HasBootableTraits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Concerns;

use Hyperf\Database\Model\TraitInitializers;
use Hypervel\Database\Eloquent\Attributes\Boot;
use Hypervel\Database\Eloquent\Attributes\Initialize;
use ReflectionClass;

use function Hyperf\Support\class_uses_recursive;

/**
* Provides support for Boot and Initialize attributes on trait methods.
*
* This trait overrides the default bootTraits() method to also check for
* #[Boot] and #[Initialize] attributes on methods, allowing trait methods
* to be named anything instead of requiring the conventional naming.
*/
trait HasBootableTraits
{
/**
* Boot all of the bootable traits on the model.
*
* This method extends the parent implementation to also support
* #[Boot] and #[Initialize] attributes on trait methods.
*/
protected function bootTraits(): void
{
$class = static::class;

$booted = [];
TraitInitializers::$container[$class] = [];

$uses = class_uses_recursive($class);

// Build conventional method names for traits
$conventionalBootMethods = array_map(
static fn (string $trait): string => '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]);
}
}
4 changes: 3 additions & 1 deletion src/core/src/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
192 changes: 192 additions & 0 deletions tests/Core/Database/Eloquent/Concerns/HasBootableTraitsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Core\Database\Eloquent\Concerns;

use Hyperf\Database\Model\Booted;
use Hyperf\Database\Model\TraitInitializers;
use Hypervel\Database\Eloquent\Attributes\Boot;
use Hypervel\Database\Eloquent\Attributes\Initialize;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Testbench\TestCase;

/**
* @internal
* @coversNothing
*/
class HasBootableTraitsTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

// Reset model booted state so each test starts fresh
Booted::$container = [];
TraitInitializers::$container = [];

// Reset static state before each test
BootableTraitsTestModel::$bootCalled = false;
BootableTraitsTestModel::$conventionalBootCalled = false;
BootableTraitsTestModel::$initializeCalled = false;
BootableTraitsTestModel::$conventionalInitializeCalled = false;
BootableTraitsTestModel::$bootCallCount = 0;
BootableTraitsTestModel::$initializeCallCount = 0;
}

protected function tearDown(): void
{
// Reset model booted state
Booted::$container = [];
TraitInitializers::$container = [];

// Reset static state after each test
BootableTraitsTestModel::$bootCalled = false;
BootableTraitsTestModel::$conventionalBootCalled = false;
BootableTraitsTestModel::$initializeCalled = false;
BootableTraitsTestModel::$conventionalInitializeCalled = false;
BootableTraitsTestModel::$bootCallCount = 0;
BootableTraitsTestModel::$initializeCallCount = 0;

parent::tearDown();
}

public function testBootAttributeCallsStaticMethodDuringBoot(): void
{
$this->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';
}