agentsclimarketplace

Laravel models

Skill noppu-labs/ai-toolkit/laravel/skills/laravel-models

Toolkit for agentic development of Laravel & React apps

Install
npx -y skills add noppu-labs/ai-toolkit --skill laravel-models

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Eloquent model patterns and database layer. Use when creating or modifying models, relationships, casts, or observers.

SKILL.md

7.6 KB, as published. Nobody here has run it

Laravel Models

Models represent database tables and domain entities.

Related guides:

  • Query Builders - Custom query builders (not scopes)
  • 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)
  • Prefer PHP attributes over properties/methods where available (Laravel 12+ for #[UseEloquentBuilder], Laravel 13+ for #[Table], #[ObservedBy], #[UsePolicy], #[UseFactory], etc.)

Basic Model Structure

<?php

declare(strict_types=1);

namespace App\Models;

use App\Builders\OrderBuilder;
use App\Enums\OrderStatus;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

#[UseEloquentBuilder(OrderBuilder::class)]
class Order extends Model
{
    use HasFactory;

    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class,
            'total' => 'integer',
        ];
    }

    // 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
    ];
}

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:

View full implementation →

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

Every model defines an explicit $fillable allowlist. Never call Model::unguard() and never use $guarded = [] — both allow attackers to set fields like is_admin, role, or user_id by adding extra request parameters. See sec-injection-prevention.

Model Configuration

Prefer the #[Fillable] attribute; the $fillable property works the same way:

// ✅ Good - explicit allowlist via attribute
#[Fillable([
    'status',
    'total',
    'notes',
])]
class Order extends Model
{
    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class,
        ];
    }
}

// ✅ Also good - explicit allowlist via property
class Order extends Model
{
    protected $fillable = [
        'status',
        'total',
        'notes',
    ];
}

// ❌ Bad - everything becomes mass assignable
class Order extends Model
{
    protected $guarded = [];
}

// ❌ Bad - Model::unguard() in a service provider disables protection globally

What Belongs in $fillable

Only fields the user may submit. Ownership and system-controlled fields (user_id, is_admin, published_at) stay out of $fillable and are set explicitly:

Order::create([
    ...$request->validated(),
    'user_id' => auth()->id(), // set explicitly, never mass-assigned
]);

Why Explicit $fillable?

  • Security: The allowlist is the last line of defense when validation misses a field
  • Auditability: The model documents exactly which fields accept user input
  • Defense in depth: Pair with $request->validated() — never pass $request->all() to create()/fill()/update()

Factories and seeders are unaffected — Laravel factories bypass mass assignment protection internally.

Model Organization

app/Models/
├── Order.php
├── User.php
├── Concerns/
│   ├── HasUuid.php
│   ├── BelongsToTenant.php
│   └── Searchable.php
└── Contracts/
    └── Searchable.php

Testing Models

it('only mass assigns fillable attributes', function () {
    $order = Order::factory()->create();

    $order->fill([
        'total' => 1000,
        'user_id' => 999, // not in $fillable — must be ignored
    ]);

    expect($order->total)->toBe(1000)
        ->and($order->user_id)->not->toBe(999);
});

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);
});

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.