Claude-skill-registry laravel-models
Eloquent model patterns and database layer. Use when working with models, database entities, Eloquent ORM, or when user mentions models, eloquent, relationships, casts, observers, database entities.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/laravel-models" ~/.claude/skills/majiayu000-claude-skill-registry-laravel-models && rm -rf "$T"
manifest:
skills/data/laravel-models/SKILL.mdsource content
Laravel Models
Models represent database tables and domain entities.
Related guides:
- Query Builders - Custom query builders (not scopes)
- Actions - Actions contain business logic
- DTOs - Casting model JSON columns to DTOs
Philosophy
Models should:
- Use custom query builders (not local scopes) - see Query Builders
- Define relationships
- Define casts
- Contain simple accessors/mutators
- NOT contain business logic (that belongs in Actions)
Basic Model Structure
<?php declare(strict_types=1); namespace App\Models; use App\Builders\OrderBuilder; use App\Enums\OrderStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Order extends Model { use HasFactory; protected function casts(): array { return [ 'status' => OrderStatus::class, 'total' => 'integer', ]; } // Custom Query Builder public function newEloquentBuilder($query): OrderBuilder { return new OrderBuilder($query); } // Relationships public function user(): BelongsTo { return $this->belongsTo(User::class); } public function items(): HasMany { return $this->hasMany(OrderItem::class); } }
Casts
Define casts for type safety:
protected function casts(): array { return [ 'status' => OrderStatus::class, // Enum 'total' => 'integer', // Integer 'is_paid' => 'boolean', // Boolean 'metadata' => OrderMetadataData::class, // DTO 'completed_at' => 'datetime', // Carbon 'tags' => 'array', // JSON array ]; }
Available casts:
,'integer'
,'real'
,'float''double'
,'string''boolean'
,'array'
,'json'
,'object''collection'
,'date'
,'datetime'
,'immutable_date''immutable_datetime''timestamp'
,'encrypted'
,'encrypted:array'
,'encrypted:collection'
,'encrypted:json''encrypted:object'- Custom cast classes
- Enum classes
- DTO classes
Relationships
BelongsTo
public function user(): BelongsTo { return $this->belongsTo(User::class); } public function customer(): BelongsTo { return $this->belongsTo(Customer::class, 'customer_id', 'id'); }
HasMany
public function orders(): HasMany { return $this->hasMany(Order::class); } public function items(): HasMany { return $this->hasMany(OrderItem::class); }
HasOne
public function profile(): HasOne { return $this->hasOne(UserProfile::class); }
BelongsToMany
public function roles(): BelongsToMany { return $this->belongsToMany(Role::class) ->withTimestamps() ->withPivot('assigned_at'); }
HasManyThrough
public function deployments(): HasManyThrough { return $this->hasManyThrough(Deployment::class, Environment::class); }
MorphTo / MorphMany
// MorphTo public function commentable(): MorphTo { return $this->morphTo(); } // MorphMany public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); }
Accessors & Mutators
Accessors (Get)
use Illuminate\Database\Eloquent\Casts\Attribute; protected function fullName(): Attribute { return Attribute::make( get: fn () => "{$this->first_name} {$this->last_name}", ); } // Usage $user->full_name; // "John Doe"
Mutators (Set)
protected function password(): Attribute { return Attribute::make( set: fn (string $value) => bcrypt($value), ); } // Usage $user->password = 'secret'; // Automatically hashed
Both Get and Set
protected function email(): Attribute { return Attribute::make( get: fn (string $value) => strtolower($value), set: fn (string $value) => strtolower(trim($value)), ); }
Model Methods
Simple helper methods are acceptable:
class Order extends Model { public function isPending(): bool { return $this->status === OrderStatus::Pending; } public function isCompleted(): bool { return $this->status === OrderStatus::Completed; } public function canBeCancelled(): bool { return $this->isPending() || $this->status === OrderStatus::Processing; } }
But NOT business logic:
// ❌ Bad - business logic in model class Order extends Model { public function cancel(): void { DB::transaction(function () { $this->update(['status' => OrderStatus::Cancelled]); $this->refundPayment(); $this->notifyCustomer(); }); } } // ✅ Good - business logic in action class CancelOrderAction { public function __invoke(Order $order): Order { return DB::transaction(function () use ($order) { $order->update(['status' => OrderStatus::Cancelled]); resolve(RefundPaymentAction::class)($order); resolve(NotifyCustomerAction::class)($order); return $order; }); } }
Model Observers
For model lifecycle hooks:
<?php declare(strict_types=1); namespace App\Observers; use App\Models\Order; use Illuminate\Support\Str; class OrderObserver { public function creating(Order $order): void { if (! $order->uuid) { $order->uuid = Str::uuid(); } } public function created(Order $order): void { // Dispatch event, queue job, etc. } public function updating(Order $order): void { // Before update } public function updated(Order $order): void { // After update } public function deleted(Order $order): void { // After delete } }
Register in AppServiceProvider:
use App\Models\Order; use App\Observers\OrderObserver; public function boot(): void { Order::observe(OrderObserver::class); }
Model Concerns (Traits)
Extract reusable behavior:
Use in models:
class Order extends Model { use HasUuid; }
Route Model Binding
Implicit Binding
// Route Route::get('/orders/{order}', [OrderController::class, 'show']); // Controller - automatically receives Order model public function show(Order $order) { }
Custom Key
Route::get('/orders/{order:uuid}', [OrderController::class, 'show']);
Custom Resolution
public function resolveRouteBinding($value, $field = null) { return $this->where($field ?? 'id', $value) ->where('is_active', true) ->firstOrFail(); }
Mass Assignment Protection
All models should be unguarded by default.
AppServiceProvider Setup
In your
AppServiceProvider::boot() method, call Model::unguard():
<?php declare(strict_types=1); namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::unguard(); } }
Model Configuration
Do NOT use
or $fillable
properties on your models:$guarded
// ✅ Good - no fillable/guarded class Order extends Model { protected function casts(): array { return [ 'status' => OrderStatus::class, ]; } } // ❌ Bad - don't use fillable class Order extends Model { protected $fillable = ['name', 'email']; } // ❌ Bad - don't use guarded class Order extends Model { protected $guarded = []; }
Why Unguard?
- Simplicity: No need to maintain fillable/guarded arrays
- Flexibility: All attributes can be mass-assigned
- Trust: With proper validation in Form Requests and Actions, mass assignment protection is redundant
- Cleaner Models: Less boilerplate code
Important: Always validate input in Form Requests before passing to Actions/Models.
Timestamps
// Disable timestamps public $timestamps = false; // Custom timestamp columns const CREATED_AT = 'creation_date'; const UPDATED_AT = 'updated_date';
Soft Deletes
use Illuminate\Database\Eloquent\SoftDeletes; class Order extends Model { use SoftDeletes; }
Usage:
$order->delete(); // Soft delete $order->forceDelete(); // Permanent delete $order->restore(); // Restore Order::withTrashed()->find($id); Order::onlyTrashed()->get();
Collections
Query results return Collections:
$orders = Order::all(); // Illuminate\Database\Eloquent\Collection $orders->filter(fn($order) => $order->isPending()); $orders->map(fn($order) => $order->total); $orders->sum('total');
Model Organization
app/Models/ ├── Order.php ├── User.php ├── Concerns/ │ ├── HasUuid.php │ ├── BelongsToTenant.php │ └── Searchable.php └── Contracts/ └── Searchable.php
Testing Models
it('can mass assign attributes', function () { $order = Order::create([ 'user_id' => 1, 'status' => 'pending', 'total' => 1000, 'notes' => 'Test order', ]); expect($order->user_id)->toBe(1) ->and($order->total)->toBe(1000); }); it('casts status to enum', function () { $order = Order::factory()->create(['status' => 'pending']); expect($order->status)->toBeInstanceOf(OrderStatus::class); }); it('has user relationship', function () { $order = Order::factory()->create(); expect($order->user)->toBeInstanceOf(User::class); });
Summary
Models should:
- Be unguarded globally via
in AppServiceProviderModel::unguard() - Define structure (casts, relationships)
- Use custom query builders (not scopes)
- Have simple helper methods
- Use observers for lifecycle hooks
Models should NOT:
- Use
or$fillable
properties$guarded - Contain business logic (use Actions)
- Have complex methods (use Actions)
- Use local scopes (use custom builders)