Skill
Community

laravel-conventions

Install
1
Install the plugin
$
npx claudepluginhub the-rabak/compound-engineering-plugin --plugin compound-engineering

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

Modern Laravel 11+ / PHP 8.3+ coding standards reference. Use when writing or reviewing PHP/Laravel code to ensure convention compliance.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

Laravel Coding Conventions (Laravel 11+ / PHP 8.3+)

Reference skill for modern Laravel coding standards and PHP 8.3+ best practices.

Architecture Pattern

Controller -> FormRequest -> Action/Service -> Model
  • Controllers: Thin, single-responsibility. Invoke actions or services, return API resources
  • Actions: Single-purpose classes for discrete operations (e.g., CreateUser, ProcessPayment)
  • Services: Coordinate multiple actions or complex business logic
  • Models: Eloquent models with relationships, scopes, casts, and accessors/mutators
  • DTOs: Readonly data transfer objects for passing structured data between layers

PHP 8.3+ Features to Use

declare(strict_types=1);

// Readonly classes for DTOs
readonly class CreateUserData {
    public function __construct(
        public string $name,
        public string $email,
        public ?string $phone = null,
    ) {}
}

// Enums instead of constants
enum UserStatus: string {
    case Active = 'active';
    case Suspended = 'suspended';
    case Pending = 'pending';
}

// First-class callable syntax
$users->map($this->transformUser(...));

// Named arguments for clarity
Cache::put(key: $cacheKey, value: $data, ttl: 3600);

// Match expressions over switch
$label = match($status) {
    UserStatus::Active => 'Active User',
    UserStatus::Suspended => 'Account Suspended',
    default => 'Unknown',
};

Naming Conventions

TypeConventionExample
ClassesPascalCaseUserService
MethodscamelCasegetUserById
Properties/VariablescamelCase$userData
ConstantsUPPER_SNAKE_CASEMAX_RETRIES
Database tablesplural, snake_caseuser_accounts
Database columnssnake_casecreated_at
Routeskebab-case/user-profiles
Config keyssnake_casecache.default_ttl
EnumsPascalCaseUserStatus::Active

Code Style

  • PSR-12 compliance, enforced via Laravel Pint
  • declare(strict_types=1) at the top of every PHP file
  • Single quotes for strings (unless interpolation needed)
  • Short array syntax [] with trailing commas
  • Type declarations on all parameters, return types, and properties
  • No mixed type unless absolutely necessary
  • Constructor property promotion where appropriate

Directory Structure (Laravel 11+)

app/
├── Actions/              # Single-purpose action classes
├── Console/Commands/     # Artisan commands
├── DTOs/                 # Readonly data transfer objects
├── Enums/                # PHP 8.1+ backed enums
├── Events/               # Event classes
├── Exceptions/           # Custom exceptions
├── Http/
│   ├── Controllers/      # API/Web controllers
│   ├── Middleware/        # Request middleware
│   ├── Requests/         # FormRequest validation
│   └── Resources/        # API Resources (JSON transformations)
├── Jobs/                 # Queued jobs
├── Listeners/            # Event listeners
├── Mail/                 # Mailable classes
├── Models/               # Eloquent models
│   └── Concerns/         # Model traits
├── Notifications/        # Notification classes
├── Observers/            # Model observers
├── Policies/             # Authorization policies
├── Providers/            # Service providers
├── Rules/                # Custom validation rules
└── Services/             # Business logic services
database/
├── factories/            # Model factories
├── migrations/           # Database migrations
└── seeders/              # Database seeders
routes/
├── api.php               # API routes
├── web.php               # Web routes
└── console.php           # Console routes (Laravel 11+)
tests/
├── Feature/              # Feature/integration tests
├── Unit/                 # Unit tests
└── Pest.php              # Pest configuration

Eloquent Best Practices

// Use $casts property (not method unless dynamic)
protected $casts = [
    'email_verified_at' => 'datetime',
    'status' => UserStatus::class,
    'settings' => 'array',
    'is_admin' => 'boolean',
];

// Scope queries for reuse
public function scopeActive(Builder $query): Builder
{
    return $query->where('status', UserStatus::Active);
}

// Prevent lazy loading in development
// In AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

// Use strict mode
Model::shouldBeStrict(! app()->isProduction());

// Eager load relationships
User::with(['posts', 'profile'])->get();

// Use whenLoaded in API Resources
'posts' => PostResource::collection($this->whenLoaded('posts')),

Database Migrations

Naming Convention

YYYY_MM_DD_HHMMSS_description.php

Best Practices

  • One concern per migration file
  • Always include down() method for rollbacks
  • Never modify migrations that have been run in production
  • Use foreignId() and constrained() for foreign keys
  • Use after() to control column order
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->index(['status', 'published_at']);
});

API Resources

// Always use API Resources for JSON responses
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

// In controller
return UserResource::make($user);
return UserResource::collection($users);

Queues and Jobs

// Implement ShouldQueue for async processing
class ProcessPayment implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 120;

    public function __construct(
        private readonly Order $order,
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        $gateway->charge($this->order);
    }

    public function failed(\Throwable $exception): void
    {
        // Notify team of failure
    }
}

Events and Listeners

// Use typed events
class OrderPlaced
{
    public function __construct(
        public readonly Order $order,
    ) {}
}

// Auto-discovered listeners (Laravel 11+)
// Just type-hint the event in handle()
class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void
    {
        $event->order->user->notify(new OrderConfirmationNotification($event->order));
    }
}

Routes

// Use controller class references
Route::apiResource('users', UserController::class);

// Group related routes
Route::prefix('admin')->middleware('auth:sanctum', 'admin')->group(function () {
    Route::apiResource('users', Admin\UserController::class);
});

// Single-action controllers
Route::post('/webhooks/stripe', StripeWebhookController::class);

Security

  • Use Laravel Sanctum or Passport for API authentication
  • Always validate with FormRequest classes
  • Use Policies for authorization (not Gates in controllers)
  • Never expose internal IDs if security-sensitive -- use UUIDs or hashids
  • Use $request->validated() to only access validated data
  • Set rate limiting on API routes
  • Use encrypt()/decrypt() for sensitive data at rest

Error Handling

// Custom exceptions with render method
class InsufficientFundsException extends Exception
{
    public function render(Request $request): JsonResponse
    {
        return response()->json([
            'message' => 'Insufficient funds for this transaction.',
        ], 422);
    }
}

// Use abort helpers
abort_if(! $user->canAccess($resource), 403);
abort_unless($order->isPending(), 422, 'Order is no longer pending.');

Testing with Pest

// Feature test
it('creates a user', function () {
    $response = postJson('/api/users', [
        'name' => 'Jane Doe',
        'email' => 'jane@example.com',
        'password' => 'secure-password',
    ]);

    $response->assertCreated()
        ->assertJsonPath('data.name', 'Jane Doe');

    $this->assertDatabaseHas('users', ['email' => 'jane@example.com']);
});

// Unit test with mocking
it('calculates order total with tax', function () {
    $order = Order::factory()->create(['subtotal' => 10000]);

    expect($order->totalWithTax())->toBe(10800);
});

// Use datasets for parameterized tests
it('validates required fields', function (string $field) {
    $data = User::factory()->make()->toArray();
    unset($data[$field]);

    postJson('/api/users', $data)->assertUnprocessable()
        ->assertJsonValidationErrors($field);
})->with(['name', 'email', 'password']);

Running Tests

# All tests
php artisan test

# With Pest directly
./vendor/bin/pest

# Specific test file
./vendor/bin/pest tests/Feature/UserTest.php

# Filter by name
./vendor/bin/pest --filter "creates a user"

# Parallel execution
php artisan test --parallel

Running Linter

./vendor/bin/pint

# Check only (no changes)
./vendor/bin/pint --test

# Static analysis
./vendor/bin/phpstan analyse
Stats
Stars0
Forks0
Last CommitMar 3, 2026

Similar Skills