Eloquent Model Event Handling Attributes e.g. #[Created]

This commit is contained in:
Marvin Osswald
2023-09-10 20:02:57 +02:00
parent dd3a00f643
commit 32194fdac5
23 changed files with 595 additions and 1 deletions

View File

@@ -7,3 +7,4 @@ parameters:
excludePaths:
- src/Support/PropertyInfo.php
- src/Support/MethodInfo.php

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Created
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Creating
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Deleted
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Deleting
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class ForceDeleted
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class ForceDeleting
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Replicating
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Restored
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Restoring
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Retrieved
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Saved
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Saving
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Updated
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class Updating
{
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Concerns;
use Illuminate\Database\Eloquent\Model;
use WendellAdriel\Lift\Attributes\Events\Created;
use WendellAdriel\Lift\Attributes\Events\Creating;
use WendellAdriel\Lift\Attributes\Events\Deleted;
use WendellAdriel\Lift\Attributes\Events\Deleting;
use WendellAdriel\Lift\Attributes\Events\ForceDeleted;
use WendellAdriel\Lift\Attributes\Events\ForceDeleting;
use WendellAdriel\Lift\Attributes\Events\Replicating;
use WendellAdriel\Lift\Attributes\Events\Restored;
use WendellAdriel\Lift\Attributes\Events\Restoring;
use WendellAdriel\Lift\Attributes\Events\Retrieved;
use WendellAdriel\Lift\Attributes\Events\Saved;
use WendellAdriel\Lift\Attributes\Events\Saving;
use WendellAdriel\Lift\Attributes\Events\Updated;
use WendellAdriel\Lift\Attributes\Events\Updating;
trait EventsHandler
{
private static ?array $modelEventMethods = null;
private static function eventHandlerMethods(): array
{
if (is_null(self::$modelEventMethods)) {
self::buildEventHandlers(new static());
}
return self::$modelEventMethods;
}
private static function buildEventHandlers(Model $model): void
{
self::$modelEventMethods = [];
$methods = self::getMethodsWithAttributes($model);
self::$modelEventMethods['retrieved'] = self::getMethodForAttribute($methods, Retrieved::class);
self::$modelEventMethods['creating'] = self::getMethodForAttribute($methods, Creating::class);
self::$modelEventMethods['created'] = self::getMethodForAttribute($methods, Created::class);
self::$modelEventMethods['updating'] = self::getMethodForAttribute($methods, Updating::class);
self::$modelEventMethods['updated'] = self::getMethodForAttribute($methods, Updated::class);
self::$modelEventMethods['saving'] = self::getMethodForAttribute($methods, Saving::class);
self::$modelEventMethods['saved'] = self::getMethodForAttribute($methods, Saved::class);
self::$modelEventMethods['deleting'] = self::getMethodForAttribute($methods, Deleting::class);
self::$modelEventMethods['deleted'] = self::getMethodForAttribute($methods, Deleted::class);
self::$modelEventMethods['forceDeleting'] = self::getMethodForAttribute($methods, ForceDeleting::class);
self::$modelEventMethods['forceDeleted'] = self::getMethodForAttribute($methods, ForceDeleted::class);
self::$modelEventMethods['restoring'] = self::getMethodForAttribute($methods, Restoring::class);
self::$modelEventMethods['restored'] = self::getMethodForAttribute($methods, Restored::class);
self::$modelEventMethods['replicating'] = self::getMethodForAttribute($methods, Replicating::class);
}
private static function handleEvent(?Model $model, string $event): void
{
self::eventHandlerMethods()[$event]?->method->invoke($model, $model);
}
}

View File

@@ -5,19 +5,23 @@ declare(strict_types=1);
namespace WendellAdriel\Lift;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
use WendellAdriel\Lift\Concerns\AttributesGuard;
use WendellAdriel\Lift\Concerns\CastValues;
use WendellAdriel\Lift\Concerns\CustomPrimary;
use WendellAdriel\Lift\Concerns\DatabaseConfigurations;
use WendellAdriel\Lift\Concerns\EventsHandler;
use WendellAdriel\Lift\Concerns\ManageRelations;
use WendellAdriel\Lift\Concerns\RulesValidation;
use WendellAdriel\Lift\Concerns\WatchProperties;
use WendellAdriel\Lift\Exceptions\ImmutablePropertyException;
use WendellAdriel\Lift\Support\MethodInfo;
use WendellAdriel\Lift\Support\PropertyInfo;
trait Lift
@@ -26,6 +30,7 @@ trait Lift
CastValues,
CustomPrimary,
DatabaseConfigurations,
EventsHandler,
ManageRelations,
RulesValidation,
WatchProperties;
@@ -77,6 +82,7 @@ trait Lift
}
self::handleRelationsKeys($model);
self::handleEvent($model, 'saving');
});
static::saved(function (Model $model) {
@@ -88,9 +94,29 @@ trait Lift
}
$model->dispatchEvents = [];
self::handleEvent($model, 'saved');
});
static::retrieved(fn (Model $model) => self::fillProperties($model));
static::retrieved(function (Model $model) {
self::fillProperties($model);
self::handleEvent($model, 'retrieved');
});
static::creating(fn (Model $model) => self::handleEvent($model, 'creating'));
static::created(fn (Model $model) => self::handleEvent($model, 'created'));
static::updating(fn (Model $model) => self::handleEvent($model, 'updating'));
static::updated(fn (Model $model) => self::handleEvent($model, 'updated'));
static::deleting(fn (Model $model) => self::handleEvent($model, 'deleting'));
static::deleted(fn (Model $model) => self::handleEvent($model, 'deleted'));
$traitsUsed = class_uses_recursive(new static());
if (in_array(SoftDeletes::class, $traitsUsed)) {
static::forceDeleting(fn (Model $model) => self::handleEvent($model, 'forceDeleting'));
static::forceDeleted(fn (Model $model) => self::handleEvent($model, 'forceDeleted'));
static::restoring(fn (Model $model) => self::handleEvent($model, 'restoring'));
static::restored(fn (Model $model) => self::handleEvent($model, 'restored'));
static::replicating(fn (Model $model) => self::handleEvent($model, 'replicating'));
}
}
public function syncOriginal(): void
@@ -165,6 +191,32 @@ trait Lift
return collect($result);
}
private static function getMethodsWithAttributes(Model $model): Collection
{
$publicMethods = self::getModelPublicMethods($model);
$result = [];
foreach ($publicMethods as $method) {
try {
$reflectionMethod = new ReflectionMethod($model, $method);
$attributes = $reflectionMethod->getAttributes();
if (count($attributes) > 0) {
$result[] = new MethodInfo(
name: $method,
method: $reflectionMethod,
attributes: collect($attributes),
);
}
} catch (ReflectionException) {
continue;
}
}
return collect($result);
}
private static function getModelPublicProperties(Model $model): array
{
$reflectionClass = new ReflectionClass($model);
@@ -180,6 +232,18 @@ trait Lift
return $properties;
}
private static function getModelPublicMethods(Model $model): array
{
$reflectionClass = new ReflectionClass($model);
$methods = [];
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methods[] = $method->getName();
}
return $methods;
}
/**
* @param Collection<PropertyInfo> $properties
* @param array<string> $attributes
@@ -194,6 +258,29 @@ trait Lift
);
}
/**
* @param Collection<MethodInfo> $methods
* @param array<string> $attributes
* @return Collection<MethodInfo>
*/
private static function getMethodsForAttributes(Collection $methods, array $attributes): Collection
{
return $methods->filter(
fn ($method) => $method->attributes->contains(
fn ($attribute) => in_array($attribute->getName(), $attributes)
)
);
}
private static function getMethodForAttribute(Collection $methods, string $attributeClass): ?MethodInfo
{
return $methods->first(
fn ($method) => $method->attributes->contains(
fn ($attribute) => $attribute->getName() === $attributeClass
)
);
}
/**
* @param Collection<PropertyInfo> $properties
* @param class-string $attributeClass

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace WendellAdriel\Lift\Support;
use Illuminate\Support\Collection;
use ReflectionAttribute;
use ReflectionMethod;
final class MethodInfo
{
public function __construct(
public readonly string $name,
public readonly ReflectionMethod $method,
/**
* @var Collection<ReflectionAttribute>
*/
public readonly Collection $attributes
) {
}
}

48
tests/Datasets/Car.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Tests\Datasets;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Log;
use WendellAdriel\Lift\Attributes\Events\ForceDeleted;
use WendellAdriel\Lift\Attributes\Events\ForceDeleting;
use WendellAdriel\Lift\Attributes\Events\Restored;
use WendellAdriel\Lift\Attributes\Events\Restoring;
use WendellAdriel\Lift\Lift;
class Car extends Model
{
use Lift;
use SoftDeletes;
public int $id;
public string $name;
#[ForceDeleting]
public function onForceDeleting(Car $car): void
{
Log::info('onForceDeleting has been called');
}
#[ForceDeleted]
public function onForceDeleted(Car $car): void
{
Log::info('onForceDeleted has been called');
}
#[Restoring]
public function onRestoring(Car $car): void
{
Log::info('onRestoring has been called');
}
#[Restored]
public function onRestored(Car $car): void
{
Log::info('onRestored has been called');
}
}

View File

@@ -6,7 +6,18 @@ namespace Tests\Datasets;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use WendellAdriel\Lift\Attributes\Cast;
use WendellAdriel\Lift\Attributes\Events\Created;
use WendellAdriel\Lift\Attributes\Events\Creating;
use WendellAdriel\Lift\Attributes\Events\Deleted;
use WendellAdriel\Lift\Attributes\Events\Deleting;
use WendellAdriel\Lift\Attributes\Events\Replicating;
use WendellAdriel\Lift\Attributes\Events\Retrieved;
use WendellAdriel\Lift\Attributes\Events\Saved;
use WendellAdriel\Lift\Attributes\Events\Saving;
use WendellAdriel\Lift\Attributes\Events\Updated;
use WendellAdriel\Lift\Attributes\Events\Updating;
use WendellAdriel\Lift\Lift;
class Product extends Model
@@ -34,4 +45,64 @@ class Product extends Model
'expires_at',
'json_column',
];
#[Retrieved]
public function onRetrieved(Product $product): void
{
Log::info('onRetrieved has been called');
}
#[Creating]
public function onCreating(Product $product): void
{
Log::info('onCreating has been called');
}
#[Created]
public function onCreated(Product $product): void
{
Log::info('onCreated has been called');
}
#[Updating]
public function onUpdating(Product $product): void
{
Log::info('onUpdating has been called');
}
#[Updated]
public function onUpdated(Product $product): void
{
Log::info('onUpdated has been called');
}
#[Saving]
public function onSaving(Product $product): void
{
Log::info('onSaving has been called');
}
#[Saved]
public function onSaved(Product $product): void
{
Log::info('onSaved has been called');
}
#[Deleting]
public function onDeleting(Product $product): void
{
Log::info('onDeleting has been called');
}
#[Deleted]
public function onDeleted(Product $product): void
{
Log::info('onDeleted has been called');
}
#[Replicating]
public function onReplicating(Product $product): void
{
Log::info('onReplicating has been called');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Log;
use Tests\Datasets\Car;
it('force deletion event handlers get called', function () {
$car = Car::castAndCreate(['name' => 'yellow card']);
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onForceDeleting has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onForceDeleted has been called'));
$car->forceDelete();
$this->assertTrue(true);
});
it('restore event handlers get called', function () {
$car = Car::castAndCreate(['name' => 'yellow card']);
$car->delete();
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onRestoring has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onRestored has been called'));
$car->restore();
$this->assertTrue(true);
});

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Log;
use Tests\Datasets\Product;
use Tests\Datasets\ProductConfig;
it('onRetrieved method gets called', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onRetrieved has been called'));
$product->refresh();
$this->assertTrue(true);
});
it('cause events on a model without event listeners', function () {
Log::shouldReceive('info')->never()->withArgs(fn ($message) => str_contains($message, 'onCreating has been called'));
Log::shouldReceive('info')->never()->withArgs(fn ($message) => str_contains($message, 'onCreated has been called'));
Log::shouldReceive('info')->never()->withArgs(fn ($message) => str_contains($message, 'onSaving has been called'));
Log::shouldReceive('info')->never()->withArgs(fn ($message) => str_contains($message, 'onSaved has been called'));
$product = ProductConfig::create([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
]);
});
it('creation event handlers get called', function () {
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onCreating has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onCreated has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onSaving has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onSaved has been called'));
Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);
$this->assertTrue(true);
});
it('update event handlers get called', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onUpdating has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onSaving has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onSaved has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onUpdated has been called'));
$product->update(['name' => 'Product11']);
$this->assertTrue(true);
});
it('deletion event handlers get called', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onDeleting has been called'));
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onDeleted has been called'));
$product->delete();
$this->assertTrue(true);
});
it('onReplicating method gets called', function () {
$product = Product::castAndCreate([
'name' => 'Product 1',
'price' => '10.99',
'random_number' => '123',
'expires_at' => '2023-12-31 23:59:59',
'json_column' => ['foo' => 'bar'],
]);
Log::shouldReceive('info')->withArgs(fn ($message) => str_contains($message, 'onReplicating has been called'));
$product->replicate();
$this->assertTrue(true);
});

View File

@@ -178,6 +178,13 @@ abstract class TestCase extends BaseTestCase
$table->string('password');
$table->timestamps();
});
Schema::create('cars', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->softDeletes();
$table->timestamps();
});
}
protected function getPackageProviders($app)