laravel
Complete Laravel development guide covering Eloquent, Blade, testing with Pest/PHPUnit, queues, caching, API resources, migrations, and Laravel best practices. Use when building Laravel applications, writing Laravel code, implementing features in Laravel, debugging Laravel issues, or when user mentions Laravel, Eloquent, Blade, Artisan, or PHP frameworks.
$ Instalar
git clone https://github.com/vapvarun/claude-backup /tmp/claude-backup && cp -r /tmp/claude-backup/skills/laravel ~/.claude/skills/claude-backup// tip: Run this command in your terminal to install the skill
SKILL.md
name: laravel description: Complete Laravel development guide covering Eloquent, Blade, testing with Pest/PHPUnit, queues, caching, API resources, migrations, and Laravel best practices. Use when building Laravel applications, writing Laravel code, implementing features in Laravel, debugging Laravel issues, or when user mentions Laravel, Eloquent, Blade, Artisan, or PHP frameworks.
Laravel Development
Modern Laravel development patterns, best practices, and workflows.
Runner Selection
# With Laravel Sail (Docker)
sail artisan <command>
sail composer <command>
sail npm <command>
# Without Sail (local PHP)
php artisan <command>
composer <command>
npm <command>
Eloquent Relationships & Loading
Eager Loading (Prevent N+1)
// BAD: N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Query per post
}
// GOOD: Eager loading
$posts = Post::with(['author', 'tags'])->get();
// Constrained eager loading
User::with(['posts' => fn($q) => $q->latest()->where('published', true)])->find($id);
// With counts and aggregates
Post::withCount('comments')->withSum('orders', 'total')->get();
Relationships
// Define clear relationships
class Post extends Model
{
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
// Pivot operations
$post->tags()->sync([1, 2, 3]); // Replace all
$post->tags()->syncWithoutDetaching([4]); // Add without removing
$post->tags()->attach($tagId); // Add one
$post->tags()->detach($tagId); // Remove one
Migrations & Factories
Migrations
// Create migration
// sail artisan make:migration create_posts_table
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'published_at']);
});
Factories
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(),
'slug' => fake()->unique()->slug(),
'content' => fake()->paragraphs(3, true),
'status' => 'draft',
];
}
public function published(): static
{
return $this->state(fn() => [
'status' => 'published',
'published_at' => now(),
]);
}
}
// Usage
Post::factory()->count(10)->published()->create();
Post::factory()->for(User::factory()->admin())->create();
Form Requests & Validation
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'],
'slug' => ['required', 'string', 'max:255', 'unique:posts'],
'content' => ['required', 'string'],
'status' => ['required', Rule::in(['draft', 'published'])],
'tags' => ['array'],
'tags.*' => ['exists:tags,id'],
];
}
public function messages(): array
{
return [
'title.required' => 'Post title is required.',
'slug.unique' => 'This slug is already taken.',
];
}
}
// Controller usage
public function store(StorePostRequest $request): JsonResponse
{
$post = Post::create($request->validated());
return response()->json($post, 201);
}
API Resources
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => Str::limit($this->content, 150),
'author' => new UserResource($this->whenLoaded('author')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments_count' => $this->whenCounted('comments'),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
// Paginated response
return PostResource::collection(
Post::with(['author', 'tags'])
->withCount('comments')
->latest()
->paginate(20)
);
TDD with Pest
RED-GREEN-REFACTOR Cycle
// 1. RED: Write failing test first
it('creates a post with valid data', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/posts', [
'title' => 'My Post',
'slug' => 'my-post',
'content' => 'Post content here',
'status' => 'draft',
]);
$response->assertCreated()
->assertJsonPath('data.title', 'My Post');
$this->assertDatabaseHas('posts', [
'title' => 'My Post',
'user_id' => $user->id,
]);
});
it('rejects empty title', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/posts', [
'title' => '',
'slug' => 'test',
'content' => 'Content',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors('title');
});
// 2. GREEN: Write minimal code to pass
// 3. REFACTOR: Clean up while keeping tests green
Run Tests
# All tests (parallel)
sail artisan test --parallel
# Specific test file
sail artisan test tests/Feature/PostTest.php
# With coverage
sail artisan test --coverage --min=80
Queues & Horizon
Job Definition
class ProcessUpload implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
public int $timeout = 300;
public function __construct(
public Upload $upload
) {}
public function handle(): void
{
// Process the upload
$this->upload->process();
}
public function failed(Throwable $exception): void
{
Log::error('Upload processing failed', [
'upload_id' => $this->upload->id,
'error' => $exception->getMessage(),
]);
}
}
// Dispatch
ProcessUpload::dispatch($upload);
ProcessUpload::dispatch($upload)->onQueue('uploads');
ProcessUpload::dispatch($upload)->delay(now()->addMinutes(5));
Horizon Configuration
// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],
Caching
// Simple caching
$posts = Cache::remember('posts.featured', 3600, function () {
return Post::featured()->with('author')->get();
});
// Cache tags (Redis required)
Cache::tags(['posts', 'users'])->put('user.1.posts', $posts, 3600);
Cache::tags('posts')->flush();
// Model caching pattern
class Post extends Model
{
protected static function booted(): void
{
static::saved(fn() => Cache::tags('posts')->flush());
static::deleted(fn() => Cache::tags('posts')->flush());
}
}
Routes Best Practices
// api.php
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
Route::post('posts/{post}/publish', [PostController::class, 'publish']);
Route::prefix('admin')->middleware('can:admin')->group(function () {
Route::apiResource('users', Admin\UserController::class);
});
});
// Rate limiting
Route::middleware(['throttle:api'])->group(function () {
Route::get('/search', SearchController::class);
});
Policies & Authorization
class PostPolicy
{
public function view(?User $user, Post $post): bool
{
return $post->status === 'published' || $user?->id === $post->user_id;
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
}
// Controller usage
public function update(UpdatePostRequest $request, Post $post)
{
$this->authorize('update', $post);
// ...
}
Exception Handling
// app/Exceptions/Handler.php
public function register(): void
{
$this->renderable(function (ModelNotFoundException $e, Request $request) {
if ($request->wantsJson()) {
return response()->json(['message' => 'Resource not found'], 404);
}
});
$this->renderable(function (AuthorizationException $e, Request $request) {
if ($request->wantsJson()) {
return response()->json(['message' => 'Forbidden'], 403);
}
});
}
Quality Checks
# Laravel Pint (code style)
./vendor/bin/pint
# PHPStan (static analysis)
./vendor/bin/phpstan analyse
# PHP Insights (code quality)
./vendor/bin/phpinsights
# All checks
./vendor/bin/pint && ./vendor/bin/phpstan analyse && sail artisan test
Blade Components
// Component class
class Alert extends Component
{
public function __construct(
public string $type = 'info',
public ?string $message = null
) {}
public function render(): View
{
return view('components.alert');
}
}
// Blade template
<x-alert type="success" :message="$message" />
// Anonymous component (resources/views/components/button.blade.php)
@props(['type' => 'button', 'variant' => 'primary'])
<button type="{{ $type }}" {{ $attributes->merge(['class' => "btn btn-{$variant}"]) }}>
{{ $slot }}
</button>
Performance Tips
- Use eager loading - Always
with()relationships you'll access - Select specific columns -
->select(['id', 'name'])when possible - Use chunking for large datasets -
->chunk(1000, fn($batch) => ...) - Cache expensive queries - Use
Cache::remember() - Index database columns - Add indexes for frequently queried columns
- Use queues - Offload heavy processing to background jobs
- Enable OPcache - In production for PHP performance
