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.
composer require wendelladriel/laravel-lift
Lift trait to a model and define typed public properties with attributes:
use WendellAdriel\Lift\Lift;
use WendellAdriel\Lift\Attributes\Fillable;
use WendellAdriel\Lift\Attributes\Rules;
final class Product extends Model
{
use Lift;
#[Fillable]
#[Rules(['required', 'string'])]
public string $name;
}
create() with castAndCreate() for automatic casting:
$product = Product::castAndCreate(['name' => 'Test Product']);
Cast, Fillable, and Rules for initial adoption.fillable, casts, and rules arrays with attributes.Model Definition:
#[Cast('float')]) instead of $casts array.#[Fillable]
#[Cast('float')]
#[Rules(['min:0'])]
public float $price;
$product->price is float).Data Handling:
create() → castAndCreate()fill() → castAndFill()update() → castAndUpdate()$product->castAndFill(['price' => '19.99']); // Automatically casts to float
Relationships:
#[BelongsTo(User::class)]).#[HasManyThrough] for complex relationships without methods.Immutable Properties:
#[Immutable]
#[Fillable]
public string $sku;
Event-Driven Logic:
#[Watch(PriceUpdatedEvent::class)]
public float $price;
booted() or use Observers for side effects.$guarded in array format).castAndFill() in unit tests to ensure type safety:
$product = new Product();
$product->castAndFill(['price' => 'invalid']); // Fails validation early
#[Column(default: 0.0)]).public function toArray(): array
{
return [
'name' => $this->name,
'price' => $this->price, // Always returns float
];
}
---
## Gotchas and Tips
### Pitfalls
1. **Attribute Order Matters**:
- Place `#[Fillable]` **before** `#[Rules]` or `#[Cast]` to avoid conflicts.
- **Example**:
```php
#[Fillable] // ✅ Correct
#[Rules(['required'])]
public string $name;
```
2. **Immutable Properties**:
- **Exception**: `ImmutablePropertyException` is thrown **after** `save()`, not during `fill()`.
- **Workaround**: Validate immutability in `booted()`:
```php
protected static function booted(): void
{
static::updating(function (self $model) {
if ($model->isDirty('sku')) {
throw new \RuntimeException('SKU cannot be modified.');
}
});
}
```
3. **Casting Quirks**:
- **JSON Columns**: Use `#[Cast('array')]` for JSON fields, but ensure database column is `json` or `text`.
- **Carbon Casting**: `immutable_datetime` requires `CarbonImmutable` type hint:
```php
#[Cast('immutable_datetime')]
public CarbonImmutable $created_at;
```
4. **Relationship Pitfalls**:
- **Attribute vs. Method**: Attributes **override** method-defined relationships.
- **Debugging**: Use `dd($model->relations)` to inspect dynamically generated relationships.
5. **Performance**:
- **Attribute Parsing**: Lift processes attributes on model instantiation. Avoid overusing on large models.
- **Event Overhead**: `#[Watch]` events fire per-property; batch updates may trigger multiple events.
### Debugging Tips
1. **Attribute Validation**:
- Check for typos in attribute names (e.g., `#[Cast]` vs. `#[CastAttribute]`).
- Use `php artisan lift:debug` (if available) or dump the model’s `$attributes` array.
2. **Casting Issues**:
- **Symptom**: Property remains `null` or wrong type after `castAndFill()`.
- **Fix**: Verify the cast type matches the property type hint (e.g., `float` vs. `double`).
3. **Immutable Errors**:
- **Symptom**: `ImmutablePropertyException` in unexpected places.
- **Debug**: Temporarily remove `#[Immutable]` to isolate the issue.
4. **Relationship Errors**:
- **Symptom**: `RelationNotFoundException` for attribute-defined relationships.
- **Fix**: Ensure the target model’s table/foreign keys match the attribute config.
### Extension Points
1. **Custom Attributes**:
- Extend Lift by creating new attributes (e.g., `#[Encrypt]` for sensitive fields).
- **Example**:
```php
#[Attribute]
public function Encrypt(): void { /* Logic */ }
```
2. **Global Configuration**:
- Override default behavior in `config/lift.php` (if supported in future versions).
- **Current Workaround**: Use model boot events:
```php
Product::booting(function () {
// Modify Lift behavior globally
});
```
3. **Testing Helpers**:
- Create a trait for test models to skip Lift processing:
```php
trait SkipLift
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->skipLiftProcessing = true;
}
}
```
4. **Database Schema**:
- Use `Schema::table()` to align columns with attribute defaults (e.g., `default: 0.0`).
- **Example**:
```php
Schema::table('products', function (Blueprint $table) {
$table->float('price')->default(0.0);
});
```
### Pro Tips
- **IDE Support**: Use PHPStorm’s "Generate PHPDoc" to auto-document typed properties.
- **Migration Strategy**: Start with non-critical models (e.g., `Product`) before applying to core models.
- **Validation**: Combine `#[Rules]` with Laravel’s validation for complex logic:
```php
#[Rules(['required', 'custom:unique_name'])]
public string $name;
#[Column] to map public properties to existing database columns without renaming.#[HasMany(Comment::class)]
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->withTimestamps();
}
How can I help you explore Laravel packages today?