--- name: laravel description: Laravel and PHP development patterns and best practices license: MIT compatibility: opencode --- # Laravel Skill Comprehensive patterns and best practices for Laravel and PHP development. ## What I Know ### Project Structure ``` app/ ├── Http/ │ ├── Controllers/ # Controllers │ ├── Middleware/ # Middleware │ ├── Requests/ # Form Requests (validation) │ └── Resources/ # API Resources ├── Models/ # Eloquent models ├── Services/ # Business logic ├── Exceptions/ # Custom exceptions └── Helpers/ # Helper functions ``` ### Routing **API Routes** ```php // routes/api.php Route::middleware('auth:sanctum')->group(function () { Route::apiResource('users', UserController::class); Route::prefix('admin')->group(function () { Route::apiResource('posts', PostController::class); }); }); ``` **Web Routes** ```php // routes/web.php Route::get('/', [HomeController::class, 'index'])->name('home'); Route::resource('posts', PostController::class); ``` ### Controllers **Best Practice Controller** ```php latest() ->paginate(20); return PostResource::collection($posts); } public function store(StorePostRequest $request): JsonResponse { $post = $this->postService->create($request->validated()); return response()->json([ 'data' => new PostResource($post), ], 201); } public function update(StorePostRequest $request, Post $post): JsonResponse { $post = $this->postService->update($post, $request->validated()); return response()->json([ 'data' => new PostResource($post), ]); } public function destroy(Post $post): JsonResponse { $this->authorize('delete', $post); $post->delete(); return response()->json(null, 204); } } ``` ### Services (Business Logic) **Service Class** ```php tags()->sync($data['tags']); } return $post->load(['author', 'tags']); } public function update(Post $post, array $data): Post { $post->update($data); if (isset($data['tags'])) { $post->tags()->sync($data['tags']); } return $post->load(['author', 'tags']); } } ``` ### Models **Eloquent Model** ```php 'datetime', ]; protected $hidden = [ 'deleted_at', ]; public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class) ->withTimestamps(); } public function scopePublished($query) { return $query->whereNotNull('published_at') ->where('published_at', '<=', now()); } public function scopeWithTag($query, string $tag) { return $query->whereHas('tags', function ($q) use ($tag) { $q->where('name', $tag); }); } } ``` ### Validation **Form Request** ```php user()->can('create', Post::class); } public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], 'tags' => ['array'], 'tags.*' => ['exists:tags,id'], 'published_at' => ['nullable', 'date'], ]; } public function messages(): array { return [ 'title.required' => 'A title is required', 'content.required' => 'Content cannot be empty', ]; } } ``` ### API Resources **Resource** ```php $this->id, 'title' => $this->title, 'excerpt' => Str::limit($this->content, 150), 'author' => new UserResource($this->whenLoaded('author')), 'tags' => TagResource::collection($this->whenLoaded('tags')), 'created_at' => $this->created_at->toIso8601String(), 'updated_at' => $this->updated_at->toIso8601String(), ]; } } ``` ### Migrations **Migration** ```php id(); $table->foreignId('author_id')->constrained('users')->cascadeOnDelete(); $table->string('title'); $table->text('content'); $table->timestamp('published_at')->nullable(); $table->softDeletes(); $table->timestamps(); $table->index(['author_id', 'published_at']); }); } public function down(): void { Schema::dropIfExists('posts'); } }; ``` ### Jobs & Queues **Job Class** ```php post->update(['published_at' => now()]); $notification->notifyPostPublished($this->post); } public function failed(\Throwable $exception): void { \Log::error('Failed to publish post: ' . $exception->getMessage()); } } ``` ### Events & Listeners **Event** ```php subscription->notifyAbout($event->post); } } ``` ### Middleware **Custom Middleware** ```php user()?->hasRole($role)) { return response()->json(['error' => 'Forbidden'], 403); } return $next($request); } } ``` ### Config **Service Provider** ```php app->singleton(PostService::class); } public function boot(): void { // Blade directives, macros, etc. } } ``` ### Testing **Feature Test** ```php create(); $this->actingAs($user); $response = $this->postJson('/api/posts', [ 'title' => 'Test Post', 'content' => 'Test content', ]); $response->assertStatus(201) ->assertJsonPath('data.title', 'Test Post'); $this->assertDatabaseHas('posts', [ 'title' => 'Test Post', 'author_id' => $user->id, ]); } } ``` ### Common Patterns **Repository Pattern (Optional)** ```php latest() ->paginate(20); } // ... other methods } ``` ### Best Practices 1. **Use Form Requests** for validation (not in controllers) 2. **Use Services** for business logic (not in controllers) 3. **Use API Resources** for consistent responses 4. **Use Route Model Binding** for type-hinted models 5. **Use Eloquent relationships** instead of manual joins 6. **Use migrations** for all schema changes 7. **Use queues** for long-running tasks 8. **Use config** for environment-specific settings 9. **Implement rate limiting** for API endpoints 10. **Use database transactions** for multi-step operations 11. **Cache frequently accessed data** with Redis 12. **Sanitize user input** to prevent XSS attacks ### Common Pitfalls 1. **N+1 queries** → Use eager loading (`with()`) 2. **Fat controllers** → Move logic to services 3. **Direct env() calls** → Use config instead 4. **Mass assignment vulnerabilities** → Use `$fillable` or guarded 5. **Missing foreign key constraints** → Use database constraints 6. **Not using transactions** → Wrap multi-step operations in transactions 7. **Forgetting to cache** → Use Redis caching for expensive queries 8. **SQL injection** → Use parameterized queries (Eloquent handles this) ### API Design Patterns **RESTful Resource Controller** ```php // routes/api.php Route::middleware('auth:sanctum')->group(function () { Route::apiResource('users', UserController::class); Route::apiResource('posts', PostController::class); }); // With additional routes Route::prefix('posts')->group(function () { Route::apiResource('posts', PostController::class); Route::get('posts/{post}/comments', [PostCommentController::class, 'index']); Route::post('posts/{post}/comments', [PostCommentController::class, 'store']); }); ``` **API Versioning** ```php // routes/api.php Route::prefix('v1')->group(function () { Route::apiResource('users', UserV1Controller::class); }); Route::prefix('v2')->group(function () { Route::apiResource('users', UserV2Controller::class); }); ``` **Standardized Response** ```php // app/Http/Responses/ApiResponse.php json([ 'success' => true, 'message' => $message, 'data' => $data, ], $statusCode); } public static function error( string $message = 'Error', int $statusCode = 400, $errors = null ) { return response()->json([ 'success' => false, 'message' => $message, 'errors' => $errors, ], $statusCode); } public static function paginated( $data, int $statusCode = 200 ) { return response()->json([ 'success' => true, 'data' => $data->items(), 'meta' => [ 'current_page' => $data->currentPage(), 'last_page' => $data->lastPage(), 'per_page' => $data->perPage(), 'total' => $data->total(), ], ], $statusCode); } } ``` **Rate Limiting** ```php // routes/api.php use Illuminate\Support\Facades\RateLimiter; // Global API rate limit RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Apply to routes Route::middleware(['throttle:api'])->group(function () { Route::apiResource('posts', PostController::class); }); // Stricter for auth endpoints RateLimiter::for('auth', function (Request $request) { return Limit::perMinute(5)->by($request->ip()); }); Route::middleware(['throttle:auth'])->group(function () { Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']); }); ``` ### Database Patterns **Query Scopes** ```php // app/Models/Post.php class Post extends Model { public function scopePublished($query) { return $query->whereNotNull('published_at') ->where('published_at', '<=', now()); } public function scopeWithTag($query, string $tag) { return $query->whereHas('tags', function ($q) use ($tag) { $q->where('name', $tag); }); } public function scopeSearch($query, string $search) { return $query->where('title', 'like', "%{$search}%") ->orWhere('content', 'like', "%{$search}%"); } } // Usage $posts = Post::published()->withTag('laravel')->search('tutorial')->get(); ``` **Eager Loading Strategies** ```php // Prevent N+1 queries $posts = Post::with(['author', 'tags', 'comments'])->paginate(20); // Nested eager loading $users = User::with(['posts.tags', 'posts.comments'])->get(); // Conditional eager loading $posts = Post::when(request()->include('author'), function ($q) { return $q->with('author'); })->get(); // Lazy eager loading $posts = Post::all(); $posts->load(['author', 'tags']); ``` **Database Transactions** ```php // app/Services/OrderService.php class OrderService { public function createOrder(array $data): Order { return DB::transaction(function () use ($data) { // Create order $order = Order::create([ 'user_id' => $data['user_id'], 'total' => $data['total'], ]); // Create order items foreach ($data['items'] as $item) { OrderItem::create([ 'order_id' => $order->id, 'product_id' => $item['product_id'], 'quantity' => $item['quantity'], 'price' => $item['price'], ]); // Update inventory Product::where('id', $item['product_id']) ->decrement('stock', $item['quantity']); } // Process payment $this->paymentService->charge($order); return $order; }); } } ``` **Query Optimization** ```php // Select only needed columns $users = User::select(['id', 'name', 'email'])->get(); // Use chunk for large datasets User::chunk(1000, function ($users) { foreach ($users as $user) { // Process user } }); // Use lazy collection for memory efficiency User::lazy()->each(function ($user) { // Process user }); // Use indexes in migration $table->index(['user_id', 'created_at']); $table->unique('email'); // Use count instead of getCollection->count() $count = User::where('active', true)->count(); ``` **Database Indexes** ```php // database/migrations/xxxx_create_posts_table.php Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('author_id')->constrained()->cascadeOnDelete(); $table->string('slug')->unique(); $table->string('title'); $table->text('content'); $table->timestamp('published_at')->nullable(); $table->timestamps(); // Indexes for common queries $table->index(['author_id', 'published_at']); $table->index('published_at'); }); // Composite index for sorting $table->index(['status', 'created_at']); ``` ### Security Patterns **Authentication with Laravel Sanctum** ```php // config/sanctum.php 'expiration' => 60, // Token expires in 60 minutes // Controller use Illuminate\Http\Request; use Laravel\Sanctum\PersonalAccessToken; class AuthController extends Controller { public function login(Request $request) { $request->validate([ 'email' => 'required|email', 'password' => 'required', ]); if (!Auth::attempt($request->only('email', 'password'))) { return response()->json(['error' => 'Invalid credentials'], 401); } $token = $request->user()->createToken('auth-token')->plainTextToken; return response()->json(['token' => $token]); } public function logout(Request $request) { $request->user()->currentAccessToken()->delete(); return response()->json(['message' => 'Logged out']); } } ``` **Authorization with Policies** ```php // app/Policies/PostPolicy.php class PostPolicy { public function view(User $user, Post $post) { return $post->published_at !== null || $user->id === $post->author_id; } public function update(User $user, Post $post) { return $user->id === $post->author_id; } public function delete(User $user, Post $post) { return $user->id === $post->author_id || $user->isAdmin(); } } // In Controller public function update(Post $post) { $this->authorize('update', $post); $post->update(request()->validated()); return new PostResource($post); } ``` **CSRF Protection** ```php // Handle API routes that don't need CSRF // app/Http/Middleware/VerifyCsrfToken.php class VerifyCsrfToken extends Middleware { protected $except = [ 'api/*', // Exclude all API routes 'stripe/*', // Exclude webhook endpoints ]; } ``` **Input Validation and Sanitization** ```php // app/Http/Requests/StorePostRequest.php class StorePostRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], 'tags' => ['array', 'max:5'], 'tags.*' => ['exists:tags,id'], 'excerpt' => ['nullable', 'string', 'max:500'], ]; } public function prepareForValidation() { $this->merge([ 'title' => strip_tags($this->title), 'excerpt' => strip_tags($this->excerpt), ]); } } ``` **SQL Injection Prevention** ```php // Laravel's query builder and Eloquent automatically prevent SQL injection // Good - Using parameterized queries DB::table('users')->where('email', $email)->first(); User::where('email', $email)->first(); // Bad - Never do this DB::select("SELECT * FROM users WHERE email = '$email'"); // For raw queries, use bindings DB::select('SELECT * FROM users WHERE email = :email', ['email' => $email]); ``` **Mass Assignment Protection** ```php // app/Models/User.php class User extends Model { protected $fillable = ['name', 'email', 'password']; // OR use guarded protected $guarded = ['id', 'is_admin']; } ``` ### Performance Patterns **Caching with Redis** ```php // Cache query results $users = Cache::remember('users.all', 3600, function () { return User::with(['posts', 'roles'])->get(); }); // Cache with tags Cache::tags(['posts', 'user:' . $userId])->remember("user.{$userId}.posts", 3600, function () use ($userId) { return Post::where('user_id', $userId)->get(); }); // Clear cache on update class PostObserver { public function updated(Post $post) { Cache::forget("post.{$post->id}"); Cache::tags(['posts'])->flush(); } } ``` **Query Optimization** ```php // Use eager loading $posts = Post::with(['author', 'tags', 'comments'])->get(); // Select specific columns $users = User::select(['id', 'name', 'email'])->get(); // Use pagination $posts = Post::with(['author'])->paginate(20); // Chunk large results DB::table('users')->orderBy('id')->chunk(1000, function ($users) { foreach ($users as $user) { // Process user } }); // Use lazy collections User::lazy()->each(function ($user) { // Process user without loading all into memory }); ``` **Queue for Long-Running Tasks** ```php // jobs/SendWelcomeEmail.php class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 3; public $timeout = 120; public function __construct(public User $user) {} public function handle(MailService $mail): void { $mail->sendWelcomeEmail($this->user); } } // Dispatch job SendWelcomeEmail::dispatch($user); ``` **Route Caching** ```bash # Cache routes for production php artisan route:cache # Clear cache when routes change php artisan route:clear ``` **Config Caching** ```bash # Cache config for production php artisan config:cache # Clear cache php artisan config:clear ``` **Artisan Commands for Maintenance** ```bash # Clear all caches php artisan optimize:clear # Optimize for production php artisan optimize # Compile views php artisan view:cache ``` ## Version Notes ### Supported Versions - **Recommended:** Laravel 12.x (latest stable) - **Minimum:** Laravel 10+ (PHP 8.1+) ### Version Summary | Version | Status | PHP Required | Key Features | |---------|--------|--------------|--------------| | 12.x | Latest (Feb 2025) | 8.2+ | Queue batches 2.0, typed properties, health checks, streamlined structure | | 11.x | Stable (Mar 2024) | 8.2+ | Slimmed application structure, per-second rate limiting, improved health | | 10.x | Stable (Feb 2023) | 8.1+ | Native types, process helpers, Horizon & Scout improvements | ### Recent Breaking Changes #### Laravel 11 → 12 ```php // v12: New batch API use Illuminate\Bus\Batch; use Illuminate\Support\Facades\Bus; $batch = Bus::batch([ new Job1(), new Job2(), ])->then(function (Batch $batch) { // All jobs complete })->catch(function (Batch $batch, Throwable $e) { // First failure })->finally(function (Batch $batch) { // All jobs complete })->name('Process Order')->dispatch(); ``` ```php // v12+: Models have typed properties by default class User extends Model { public string $name; public string $email; protected $fillable = ['name', 'email']; } ``` ```php // routes/web.php - v12 health endpoint use Illuminate\Support\Facades\Health; Health::check('database', fn () => DB::connection()->getPdo()); Health::check('redis', fn () => Redis::connection()->ping()); Health::check('storage', fn () => is_writable(storage_path())); // Automatic /health endpoint registered ``` ```bash # v12: Even slimmer structure # - No bootstrap/app.php (config handled automatically) # - routes/ consolidated # - Simplified configuration ``` #### Laravel 10 → 11 ```bash # v11: Reduced default files # - No HTTP/Kernel.php, Console/Kernel.php # - Merged exceptions handling # - Fewer config files ``` ```php // v11+: Per-second rate limiting use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('api', function (Request $request) { return Limit::perSecond(10)->by($request->user()?->id ?: $request->ip()); }); ``` ```php // v11+: Once helper for expensive operations use function Illuminate\Support\once; $user = once(fn () => User::firstWhere('email', 'user@example.com')); ``` ```php // v11+: Health checks built-in php artisan make:command HealthCheckCommand // Built-in health endpoint available GET /up ``` #### Laravel 9 → 10 ```php // v10+: All framework code has native type hints // Application code should also use types public function store(StorePostRequest $request): JsonResponse { return response()->json(['data' => $post], 201); } ``` ```php // v10+: New process helpers use Illuminate\Support\Facades\Process; $result = Process::run('ls -la'); if ($result->successful()) { echo $result->output(); } ``` ### Version Callouts by Feature **Routing** ```php // Stable across v10, v11, v12 Route::apiResource('posts', PostController::class); Route::middleware('auth:sanctum')->group(function () { // Routes }); ``` **Eloquent Models** ```php // v12+: Use typed properties class Post extends Model { public string $title; public string $content; protected $fillable = ['title', 'content']; public function author(): BelongsTo { return $this->belongsTo(User::class); } } ``` **Migrations** ```php // Stable across versions Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('author_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->timestamps(); }); ``` **Queues** ```php // v12+: Better batch API $batch = Bus::batch([ new ProcessOrder($order), new SendNotification($order), ])->allowFailures()->dispatch(); ``` **Validation** ```php // Stable across versions class StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]; } } ``` **Sanctum Authentication** ```php // Stable across versions Route::middleware('auth:sanctum')->group(function () { Route::apiResource('users', UserController::class); }); ``` **Application Structure** ```bash # v10: Traditional structure app/ ├── Http/ │ ├── Controllers/ │ ├── Middleware/ │ └── Kernel.php # v11+: No Kernel.php, merged middleware # v12+: Even simpler bootstrap, fewer config files ``` **Testing** ```php // Stable across versions class PostTest extends TestCase { use RefreshDatabase; public function test_user_can_create_post(): void { $response = $this->postJson('/api/posts', [ 'title' => 'Test', ]); $response->assertStatus(201); } } ``` ### Upgrade Recommendations **From 10 to 11:** 1. Run `composer require laravel/framework:^11.0` 2. Review application structure changes 3. Remove unused config files 4. Update custom middleware to remove constructor dependencies 5. Test per-second rate limiting implementation **From 11 to 12:** 1. Run `composer require laravel/framework:^12.0` 2. Add typed properties to models 3. Review batch API updates 4. Implement new health checks 5. Test queue batches thoroughly ### Minimum Requirements by Version | Version | PHP | Release Date | Support Until | |---------|-----|--------------|---------------| | 12.x | 8.2+ | Feb 2025 | Aug 2026 | | 11.x | 8.2+ | Mar 2024 | Aug 2025 | | 10.x | 8.1+ | Feb 2023 | Feb 2025 | ### Annual Release Cycle Laravel follows an annual release cycle with major releases in February: - **Laravel 12**: February 2025 - **Laravel 11**: February 2024 - **Laravel 10**: February 2023 Each version receives bug fixes for 18 months and security fixes for 2 years. --- *Part of SuperAI GitHub - Centralized OpenCode Configuration*