PluginBench
Skill
Pass
Audit score 90

laravel-best-practices

asyrafhussin/agent-skills

Laravel 13 conventions and best practices for scalable, maintainable applications.

What is laravel-best-practices?

Comprehensive guide with 31 rules across 7 categories for Laravel 13 development. Use when creating controllers, models, migrations, validation, services, or structuring Laravel applications to follow proven patterns for architecture, Eloquent, database design, API development, and security.

  • Provides 31 best-practice rules organized by priority (Architecture, Eloquent & Database, Controllers & Routing, Validation & Requests, Security, Performance, API Design)
  • Includes code examples for controllers with form requests, service classes, Eloquent models, and migrations
  • Covers critical patterns: eager loading, N+1 prevention, query scopes, model events, and soft deletes
  • Guides feature-folder organization, dependency injection, and event-driven architecture
  • Addresses Laravel 13+ features like queue routing and vector search with pgvector
  • Enforces security practices including mass-assignment protection and form request validation

How to install laravel-best-practices

npx skills add https://github.com/asyrafhussin/agent-skills --skill laravel-best-practices
Prerequisites
  • Laravel 13.x framework installed
  • PHP 8.3 or higher
Claude Code
Cursor
Windsurf
Cline

How to use laravel-best-practices

  1. 1.Reference the rule categories by priority when starting a new feature or component
  2. 2.Use the provided code examples (controllers, models, services, migrations) as templates for your implementation
  3. 3.Apply form requests for validation instead of inline validation in controllers
  4. 4.Extract business logic into service classes rather than keeping it in controllers
  5. 5.Use Eloquent scopes and eager loading to optimize database queries
  6. 6.Organize code by feature/domain folders rather than by type (controllers, models, etc.)

Use cases

Good for
  • Building RESTful APIs with proper resource controllers and API response transformation
  • Structuring large Laravel applications using feature folders and service/action classes
  • Preventing database performance issues through eager loading and query optimization
  • Implementing complex validation logic with form requests and custom validation rules
  • Designing scalable architectures with events, listeners, and decoupled business logic
Who it's for
  • Laravel developers building new applications or refactoring existing codebases
  • Backend engineers working with Laravel 13.x and PHP 8.3+
  • Teams establishing coding standards and architectural patterns
  • Developers transitioning to service-oriented or domain-driven design in Laravel

laravel-best-practices FAQ

When should I use service classes vs. action classes?

Use service classes for reusable business logic that multiple controllers or jobs might need. Use action classes for single-purpose, one-time operations that encapsulate a specific domain action.

How do I prevent N+1 query problems?

Use eager loading with `with()` when fetching models with relationships, implement query scopes for common filters, and use `chunking()` for processing large datasets to avoid loading everything into memory.

What's the difference between form requests and inline validation?

Form requests separate validation logic from controllers, provide authorization checks, and allow custom error messages. They make validation reusable and testable, while inline validation clutters controller code.

Should I always use repositories?

Repositories are useful for complex query logic or when you need to swap implementations, but for simple CRUD operations, Eloquent models with scopes are often sufficient and less boilerplate.

How do I handle complex business logic safely?

Use database transactions in service classes, leverage model events for side effects, and use event-driven architecture to decouple components and maintain data consistency.

Full instructions (SKILL.md)

Source of truth, from asyrafhussin/agent-skills.


name: laravel-best-practices description: Laravel 13 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. license: MIT metadata: author: Laravel Community version: "2.1.0" laravelVersion: "13.x" phpVersion: "8.3+"

Laravel 13 Best Practices

Comprehensive best practices guide for Laravel 13 applications. Contains 31 rules across 7 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

PriorityCategoryImpactPrefix
1Architecture & StructureCRITICALarch-
2Eloquent & DatabaseCRITICALeloquent-
3Controllers & RoutingHIGHcontroller-, ctrl-
4Validation & RequestsHIGHvalidation-, valid-
5SecurityHIGHsec-
6PerformanceMEDIUMperf-
7API DesignMEDIUMapi-

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
  • arch-queue-routing - Centralized job queue routing (Laravel 13+)

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
  • eloquent-vector-search - Semantic search with pgvector (Laravel 13+)

3. Controllers & Routing (HIGH)

  • controller-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

6. Performance (MEDIUM)

No rule files exist yet for this category.

7. API Design (MEDIUM)

No rule files exist yet for this category.

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 — only 3 queries total
$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
  • Bad Example with explanation
  • Good Example with explanation
  • Laravel 13 and PHP 8.3 specific context and references

Full Compiled Document

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