Loading...
Loading...
Laravel 12 conventions and best practices. Use when creating controllers, models, migrations, validation, services, or structuring Laravel applications. Triggers on tasks involving Laravel architecture, Eloquent, database, API development, or PHP patterns.
npx skill4agent add asyrafhussin/agent-skills laravel-best-practices| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Architecture & Structure | CRITICAL | |
| 2 | Eloquent & Database | CRITICAL | |
| 3 | Controllers & Routing | HIGH | |
| 4 | Validation & Requests | HIGH | |
| 5 | Security | HIGH | |
| 6 | Performance | MEDIUM | |
| 7 | API Design | MEDIUM | |
arch-service-classesarch-action-classesarch-repository-patternarch-dto-patternarch-value-objectsarch-event-drivenarch-feature-folderseloquent-eager-loadingeloquent-chunkingeloquent-query-scopeseloquent-model-eventseloquent-relationshipseloquent-castseloquent-accessors-mutatorseloquent-soft-deleteseloquent-pruningctrl-resource-controllerscontroller-single-actioncontroller-resource-methodscontroller-form-requestscontroller-api-resourcescontroller-middlewarecontroller-dependency-injectionvalidation-form-requestsvalidation-custom-rulesvalidation-conditional-rulesvalidation-array-validationvalidation-after-hookssec-mass-assignment<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
class PostController extends Controller
{
public function store(StorePostRequest $request): RedirectResponse
{
// Validation happens automatically
$validated = $request->validated();
$post = Post::create($validated);
return redirect()
->route('posts.show', $post)
->with('success', 'Post created successfully.');
}
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
$post->update($request->validated());
return redirect()
->route('posts.show', $post)
->with('success', 'Post updated successfully.');
}
}<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'min:100'],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['nullable', 'array'],
'tags.*' => ['exists:tags,id'],
'published_at' => ['nullable', 'date', 'after:now'],
];
}
public function messages(): array
{
return [
'body.min' => 'The post body must be at least 100 characters.',
];
}
}<?php
namespace App\Services;
use App\Models\User;
use App\Models\Post;
use App\Events\PostPublished;
use Illuminate\Support\Facades\DB;
class PostService
{
public function __construct(
private readonly NotificationService $notifications,
) {}
public function publish(Post $post): Post
{
return DB::transaction(function () use ($post) {
$post->update([
'published_at' => now(),
'status' => 'published',
]);
event(new PostPublished($post));
$this->notifications->notifyFollowers($post->author, $post);
return $post->fresh();
});
}
}<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'slug',
'body',
'category_id',
'published_at',
];
protected $casts = [
'published_at' => 'datetime',
];
// Relationships
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
// Scopes
public function scopePublished(Builder $query): Builder
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function scopeByCategory(Builder $query, int $categoryId): Builder
{
return $query->where('category_id', $categoryId);
}
// Accessors & Mutators
protected function title(): Attribute
{
return Attribute::make(
set: fn (string $value) => ucfirst($value),
);
}
}<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamp('published_at')->nullable();
$table->timestamps();
// Indexes for common queries
$table->index(['user_id', 'published_at']);
$table->index('category_id');
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};// ❌ N+1 Problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Query per post
}
// ✅ Eager loading
$posts = Post::with(['author', 'category', 'tags'])->get();
foreach ($posts as $post) {
echo $post->author->name; // No additional queries
}
// ✅ Nested eager loading
$posts = Post::with([
'author.profile',
'comments.user',
'tags',
])->get();
// ✅ Constrained eager loading
$posts = Post::with([
'comments' => fn ($query) => $query->latest()->limit(5),
])->get();rules/arch-service-classes.md
rules/eloquent-eager-loading.md
rules/validation-form-requests.md
rules/_sections.mdAGENTS.md