wendelladriel/laravel-lift
Experimental Laravel package that supercharges Eloquent models with typed public properties matching your schema, powered by PHP 8 attributes. Add validation rules and other metadata directly on models and access them via handy methods, using Eloquent events for easy drop-in use.
Installation:
composer require wendelladriel/laravel-lift
Add the Lift trait to your Eloquent model:
use WendellAdriel\Lift\Lift;
class Product extends Model
{
use Lift;
// ...
}
First Use Case:
Define a typed public property with attributes (e.g., Fillable, Cast, Rules):
#[Fillable]
#[Rules(['required', 'string'])]
public string $name;
This automatically configures the model to:
$fillable (via Fillable attribute).Rules attribute).string.First Interaction:
Use castAndCreate to create a model with type-safe casting:
$product = Product::castAndCreate(['name' => 'Laptop']);
Replace traditional $fillable, $casts, and $rules with attributes for cleaner, self-documenting code:
final class Product extends Model
{
use Lift;
#[Fillable]
#[Rules(['required', 'string', 'max:255'])]
public string $name;
#[Fillable]
#[Cast('float')]
#[Rules(['nullable', 'min:0'])]
public ?float $price = null;
}
Benefits:
$fillable, $casts, etc.).#[Cast('...')]).Use Lift’s casting methods to ensure type safety during model operations:
// Create
$product = Product::castAndCreate(['name' => 'Phone', 'price' => '99.99']);
// Update
$product->castAndUpdate(['price' => '89.99']);
// Fill (e.g., in a form request)
$product->castAndFill($request->validated());
Why?
string to float).Protect sensitive fields (e.g., created_by) from modification:
#[Immutable]
#[Fillable]
public string $created_by;
Use Case:
ImmutablePropertyException if modified after creation.Trigger custom events when specific properties change:
#[Watch(PriceUpdatedEvent::class)]
#[Fillable]
#[Cast('float')]
public float $price;
Example Event:
class PriceUpdatedEvent
{
use Dispatchable;
public function __construct(public Product $product) {}
}
When to Use:
Define relationships declaratively:
use WendellAdriel\Lift\Attributes\Relations\BelongsTo;
#[BelongsTo(User::class, 'user_id')]
public ?User $user = null;
Advantages:
belongsTo() method definitions.$product->user).Override default table/connection settings:
#[DB(connection: 'pgsql', table: 'products_v2')]
final class Product extends Model
{
use Lift;
// ...
}
Use Case:
Set defaults or computed values via Column attribute:
#[Column(default: 'generateDefaultName')]
public string $slug;
public function generateDefaultName(): string
{
return Str::slug($this->name);
}
When to Use:
slug from name).created_at fallback).castAndFill in handle() to validate and cast input:
public function handle()
{
$product = new Product();
$product->castAndFill($this->validated());
$product->save();
}
public function toArray($request)
{
return [
'name' => $this->name,
'price' => $this->price, // Always returns float
];
}
Attribute Order Matters:
#[Fillable] before #[Rules] or #[Cast] to avoid conflicts.#[Fillable] // ✅ Correct
#[Rules(['required'])]
public string $name;
// ❌ Wrong (may not apply rules)
#[Rules(['required'])]
#[Fillable]
public string $name;
Circular Dependencies in Relationships:
#[BelongsTo] + #[HasMany]) that reference each other without proper foreign keys.#[BelongsTo(User::class, 'user_id')]
public ?User $user = null;
Immutable Properties and Mass Assignment:
#[Fillable].castAndSet() for immutable fields:
$product->castAndSet('created_by', auth()->id());
Custom Cast Types:
'int', 'array', etc.), but custom casts (e.g., Json or Encrypted) may not work out of the box.#[Cast('json')] with manual handling.Database Column Mismatches:
#[Column('custom_name')] doesn’t match the DB column, queries will fail.dd($model->getConnection()->getSchemaBuilder()->getColumnListing($model->getTable())) to verify column names.Event Dispatching Timing:
#[Watch] events fire after the model is saved, not during validation.Inspect Model Configuration:
Use dd($model->getLiftConfiguration()) to debug applied attributes.
Validate Attribute Parsing: Check if attributes are parsed correctly:
$reflection = new ReflectionClass($model);
$property = $reflection->getProperty('name');
$attributes = $property->getAttributes();
Override Casting Logic:
Extend the Lift trait to customize casting behavior:
trait CustomLift extends Lift
{
protected function customizeCast($key, $value)
{
if ($key === 'price' && is_string($value)) {
return (float) str_replace('$', '', $value);
}
return parent::customizeCast($key, $value);
}
}
Handle Immutable Exceptions:
Catch ImmutablePropertyException in controllers:
try {
$product->name = 'New Name';
$product->save();
} catch (ImmutablePropertyException $e) {
abort(403, 'Cannot modify immutable field.');
}
Attribute Reflection Overhead:
Event Dispatching:
#[Watch] events are dispatched after saving, which may impact performance for bulk operations.Product::withoutEvents(function () {
Product::castAndUpdate([...]);
});
Type Casting:
CarbonImmutable) adds CPU overhead.#[Encrypt]):
namespace Wendell
How can I help you explore Laravel packages today?