portable/compound-engineering/skills/laravel-conventions/SKILL.md
Modern Laravel 11+ / PHP 8.3+ coding standards reference. Use when writing or reviewing PHP/Laravel code to ensure convention compliance.
npx skillsauth add the-rabak/compound-engineering-plugin laravel-conventionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference skill for modern Laravel coding standards and PHP 8.3+ best practices.
Controller -> FormRequest -> Action/Service -> Model
CreateUser, ProcessPayment)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',
};
| Type | Convention | Example |
|------|------------|---------|
| Classes | PascalCase | UserService |
| Methods | camelCase | getUserById |
| Properties/Variables | camelCase | $userData |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES |
| Database tables | plural, snake_case | user_accounts |
| Database columns | snake_case | created_at |
| Routes | kebab-case | /user-profiles |
| Config keys | snake_case | cache.default_ttl |
| Enums | PascalCase | UserStatus::Active |
declare(strict_types=1) at the top of every PHP file[] with trailing commasmixed type unless absolutely necessaryapp/
├── 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
// 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')),
YYYY_MM_DD_HHMMSS_description.php
down() method for rollbacksforeignId() and constrained() for foreign keysafter() to control column orderSchema::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']);
});
// 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);
// 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
}
}
// 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));
}
}
// 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);
$request->validated() to only access validated dataencrypt()/decrypt() for sensitive data at rest// 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.');
// Feature test
it('creates a user', function () {
$response = postJson('/api/users', [
'name' => 'Jane Doe',
'email' => '[email protected]',
'password' => 'secure-password',
]);
$response->assertCreated()
->assertJsonPath('data.name', 'Jane Doe');
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// 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']);
# 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
./vendor/bin/pint
# Check only (no changes)
./vendor/bin/pint --test
# Static analysis
./vendor/bin/phpstan analyse
tools
Package one plan execution packet into a compact ticket-local execution packet with parent refs, scope fences, feature-home ownership, and evidence commands. Use when converting plans into local tickets or when execution needs one ticket-sized context pack without the full plan.
tools
Package one plan execution packet into a compact ticket-local execution packet with parent refs, scope fences, feature-home ownership, and evidence commands. Use when converting plans into local tickets or when execution needs one ticket-sized context pack without the full plan.
testing
Run a deep adversarial review of plans and architecture before implementation. Use when validating strategy docs, contracts, roadmaps, and competitive positioning with scored findings and prioritized recommendations.
testing
Run a deep adversarial review of plans and architecture before implementation. Use when validating strategy docs, contracts, roadmaps, and competitive positioning with scored findings and prioritized recommendations.