laravel

Modern Laravel development patterns, best practices, and workflows.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "laravel" with this command: npx skills add vapvarun/claude-backup/vapvarun-claude-backup-laravel

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

wp-theme-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review

No summary provided by upstream source.

Repository SourceNeeds Review