Claude-skill-registry laravel
Laravel and PHP development patterns and best practices
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/laravel" ~/.claude/skills/majiayu000-claude-skill-registry-laravel && rm -rf "$T"
skills/data/laravel/SKILL.mdLaravel 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
// 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
// routes/web.php Route::get('/', [HomeController::class, 'index'])->name('home'); Route::resource('posts', PostController::class);
Controllers
Best Practice Controller
<?php namespace App\Http\Controllers; use App\Http\Requests\StorePostRequest; use App\Http\Resources\PostResource; use App\Models\Post; use Illuminate\Http\JsonResponse; class PostController extends Controller { public function index() { $posts = Post::with(['author', 'tags']) ->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 namespace App\Services; use App\Models\Post; use Illuminate\Database\Eloquent\Model; class PostService { public function create(array $data): Post { $post = Post::create($data); // Handle relationships if (isset($data['tags'])) { $post->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 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; class Post extends Model { use HasFactory; protected $fillable = [ 'title', 'content', 'author_id', 'published_at', ]; protected $casts = [ 'published_at' => '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 namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\ValidationRule; 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'], '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 namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $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 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('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 namespace App\Jobs; use App\Models\Post; use App\Services\NotificationService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class PublishPostJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 3; public $timeout = 120; public function __construct( public Post $post ) {} public function handle(NotificationService $notification): void { $this->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 namespace App\Events; use App\Models\Post; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class PostPublished { use Dispatchable, SerializesModels; public function __construct( public Post $post ) {} }
Listener
<?php namespace App\Listeners; use App\Events\PostPublished; use App\Services\SubscriptionService; class NotifySubscribers { public function __construct( private SubscriptionService $subscription ) {} public function handle(PostPublished $event): void { $this->subscription->notifyAbout($event->post); } }
Middleware
Custom Middleware
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class EnsureUserHasRole { public function handle(Request $request, Closure $next, string $role): Response { if (!$request->user()?->hasRole($role)) { return response()->json(['error' => 'Forbidden'], 403); } return $next($request); } }
Config
Service Provider
<?php namespace App\Providers; use App\Services\PostService; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(PostService::class); } public function boot(): void { // Blade directives, macros, etc. } }
Testing
Feature Test
<?php namespace Tests\Feature; use App\Models\Post; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostManagementTest extends TestCase { use RefreshDatabase; public function test_user_can_create_post(): void { $user = User::factory()->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 namespace App\Repositories; use App\Models\Post; use Illuminate\Pagination\LengthAwarePaginator; interface PostRepositoryInterface { public function all(): LengthAwarePaginator; public function find(int $id): ?Post; public function create(array $data): Post; public function update(Post $post, array $data): Post; public function delete(Post $post): bool; } class EloquentPostRepository implements PostRepositoryInterface { public function all(): LengthAwarePaginator { return Post::with(['author', 'tags']) ->latest() ->paginate(20); } // ... other methods }
Best Practices
- Use Form Requests for validation (not in controllers)
- Use Services for business logic (not in controllers)
- Use API Resources for consistent responses
- Use Route Model Binding for type-hinted models
- Use Eloquent relationships instead of manual joins
- Use migrations for all schema changes
- Use queues for long-running tasks
- Use config for environment-specific settings
- Implement rate limiting for API endpoints
- Use database transactions for multi-step operations
- Cache frequently accessed data with Redis
- Sanitize user input to prevent XSS attacks
Common Pitfalls
- N+1 queries → Use eager loading (
)with() - Fat controllers → Move logic to services
- Direct env() calls → Use config instead
- Mass assignment vulnerabilities → Use
or guarded$fillable - Missing foreign key constraints → Use database constraints
- Not using transactions → Wrap multi-step operations in transactions
- Forgetting to cache → Use Redis caching for expensive queries
- SQL injection → Use parameterized queries (Eloquent handles this)
API Design Patterns
RESTful Resource Controller
// 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
// 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
// app/Http/Responses/ApiResponse.php <?php namespace App\Http\Responses; class ApiResponse { public static function success( $data = null, string $message = 'Success', int $statusCode = 200 ) { return response()->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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
# Cache routes for production php artisan route:cache # Clear cache when routes change php artisan route:clear
Config Caching
# Cache config for production php artisan config:cache # Clear cache php artisan config:clear
Artisan Commands for Maintenance
# 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
<!-- 🆕 v12: Queue batches 2.0 with improved API --><!-- 🆕 v12: Typed model properties default -->// 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();
<!-- 🆕 v12: Improved health checks -->// v12+: Models have typed properties by default class User extends Model { public string $name; public string $email; protected $fillable = ['name', 'email']; }
<!-- 🔄 v12: Application structure changes -->// 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
# v12: Even slimmer structure # - No bootstrap/app.php (config handled automatically) # - routes/ consolidated # - Simplified configuration
Laravel 10 → 11
<!-- 🔄 v11: Slimmed application structure --><!-- 🆕 v11: Per-second rate limiting --># v11: Reduced default files # - No HTTP/Kernel.php, Console/Kernel.php # - Merged exceptions handling # - Fewer config files
<!-- 🆕 v11: Once helper for lazy evaluation -->// 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()); });
<!-- 🆕 v11: Improved health checks -->// v11+: Once helper for expensive operations use function Illuminate\Support\once; $user = once(fn () => User::firstWhere('email', 'user@example.com'));
// v11+: Health checks built-in php artisan make:command HealthCheckCommand // Built-in health endpoint available GET /up
Laravel 9 → 10
<!-- 🆕 v10: Native type declarations --><!-- 🔄 v10: Process helpers -->// 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); }
<!-- 🚫 v10: PHP 8.0 minimum required --> <!-- 🚫 v10: Several deprecated features removed -->// 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
<!-- ✅ v10+: All routing patterns stable -->// Stable across v10, v11, v12 Route::apiResource('posts', PostController::class); Route::middleware('auth:sanctum')->group(function () { // Routes });
Eloquent Models
<!-- ✅ v10+: All model patterns stable --> <!-- 🆕 v12: Typed properties encouraged -->// 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
<!-- ✅ v10+: All migration patterns stable -->// Stable across versions Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('author_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->timestamps(); });
Queues
<!-- ✅ v10+: Job patterns stable --> <!-- 🆕 v12: Improved batch API -->// v12+: Better batch API $batch = Bus::batch([ new ProcessOrder($order), new SendNotification($order), ])->allowFailures()->dispatch();
Validation
<!-- ✅ v10+: Form Request patterns stable -->// Stable across versions class StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]; } }
Sanctum Authentication
<!-- ✅ v10+: Sanctum API stable -->// Stable across versions Route::middleware('auth:sanctum')->group(function () { Route::apiResource('users', UserController::class); });
Application Structure
<!-- 🔄 v11: Slimmed structure --> <!-- 🔄 v12: Further simplification --># v10: Traditional structure app/ ├── Http/ │ ├── Controllers/ │ ├── Middleware/ │ └── Kernel.php # v11+: No Kernel.php, merged middleware # v12+: Even simpler bootstrap, fewer config files
Testing
<!-- ✅ v10+: All testing patterns stable -->// 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:
- Run
composer require laravel/framework:^11.0 - Review application structure changes
- Remove unused config files
- Update custom middleware to remove constructor dependencies
- Test per-second rate limiting implementation
From 11 to 12:
- Run
composer require laravel/framework:^12.0 - Add typed properties to models
- Review batch API updates
- Implement new health checks
- 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