laravel-best-practices

laravel-best-practices

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.

2звезд
0форков
Обновлено 2/1/2026
SKILL.md
readonlyread-only
name
laravel-best-practices
description

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.

version
"1.0.0"

Laravel 12 Best Practices

Comprehensive best practices guide for Laravel 12 applications. Contains 45+ rules across 8 categories for building scalable, maintainable Laravel applications.

When to Apply

Reference these guidelines when:

  • Creating controllers, models, and services
  • Writing migrations and database queries
  • Implementing validation and form requests
  • Building APIs with Laravel
  • Structuring Laravel applications

Rule Categories by Priority

Priority Category Impact Prefix
1 Architecture & Structure CRITICAL arch-
2 Eloquent & Database CRITICAL eloquent-
3 Controllers & Routing HIGH controller-, ctrl-
4 Validation & Requests HIGH validation-, valid-
5 Security HIGH sec-
6 Performance MEDIUM perf-
7 API Design MEDIUM api-

Quick Reference

1. Architecture & Structure (CRITICAL)

  • arch-service-classes - Extract business logic to services
  • arch-action-classes - Single-purpose action classes
  • arch-repository-pattern - When to use repositories
  • arch-dto-pattern - Data transfer objects
  • arch-value-objects - Encapsulate domain concepts
  • arch-event-driven - Decouple with events and listeners
  • arch-feature-folders - Organize by domain/feature

2. Eloquent & Database (CRITICAL)

  • eloquent-eager-loading - Prevent N+1 queries
  • eloquent-chunking - Process large datasets
  • eloquent-query-scopes - Reusable query logic
  • eloquent-model-events - Use observers for side effects
  • eloquent-relationships - Define relationships properly
  • eloquent-casts - Automatic attribute casting
  • eloquent-accessors-mutators - Transform attributes
  • eloquent-soft-deletes - Safe deletion with recovery
  • eloquent-pruning - Automatic cleanup of old records

3. Controllers & Routing (HIGH)

  • ctrl-resource-controllers - Use resource controllers
  • controller-single-action - Single action invokable controllers
  • controller-resource-methods - RESTful resource methods
  • controller-form-requests - Use form requests
  • controller-api-resources - Transform API responses
  • controller-middleware - Apply middleware properly
  • controller-dependency-injection - Inject dependencies

4. Validation & Requests (HIGH)

  • validation-form-requests - Use form request classes
  • validation-custom-rules - Create custom rules
  • validation-conditional-rules - Conditional validation
  • validation-array-validation - Validate nested arrays
  • validation-after-hooks - Complex validation logic

5. Security (HIGH)

  • sec-mass-assignment - Protect against mass assignment
  • Additional security rules can be added as needed

6. Performance (MEDIUM)

  • Performance rules can be added for caching, queues, and optimization

7. API Design (MEDIUM)

  • API design rules can be added for versioning and response formatting

Essential Patterns

Controller with Form Request

<?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.');
    }
}

Form Request Class

<?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.',
        ];
    }
}

Service Class Pattern

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

Eloquent Model

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

Migration Best Practices

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

Eager Loading

// ❌ 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();

How to Use

Read individual rule files for detailed explanations and code examples:

rules/arch-service-classes.md
rules/eloquent-eager-loading.md
rules/validation-form-requests.md
rules/_sections.md

Each rule file contains:

  • YAML frontmatter with metadata (title, impact, tags)
  • Brief explanation of why it matters
  • Incorrect code example with explanation
  • Correct code example with explanation
  • Laravel 12 and PHP 8.5 specific context and references

Full Compiled Document

For the complete guide with all rules expanded: AGENTS.md

You Might Also Like

Related Skills

verify

verify

243K

Use when you want to validate changes before committing, or when you need to check all React contribution requirements.

test

test

243K

Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.

Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.

Use when adding new error messages to React, or seeing "unknown error code" warnings.

flow

flow

243K

Use when you need to run Flow type checking, or when seeing Flow type errors in React code.

flags

flags

243K

Use when you need to check feature flag states, compare channels, or debug why a feature behaves differently across release channels.