Install
$ agentstack add skill-noppu-labs-ai-toolkit-laravel-models ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Laravel Models
Models represent database tables and domain entities.
Related guides:
- [Query Builders](../laravel-query-builders/SKILL.md) - Custom query builders (not scopes)
- [DTOs](../laravel-dtos/SKILL.md) - Casting model JSON columns to DTOs
Philosophy
Models should:
- Use custom query builders (not local scopes) - see [Query Builders](../laravel-query-builders/SKILL.md)
- 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
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:
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 →](references/HasUuid.php)
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](../laravel-owasp-security/rules/sec-injection-prevention.md).
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()tocreate()/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);
});
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: noppu-labs
- Source: noppu-labs/ai-toolkit
- License: MIT
- Homepage: https://noppu-labs.github.io/ai-toolkit/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.