developing-with-laravel
Laravel Skill - Quick Reference
Laravel framework patterns for modern PHP applications. For PHP language fundamentals, see the PHP skill. For advanced patterns, see REFERENCE.md.
Table of Contents
- Project Structure
- Routing & Controllers
- Eloquent ORM
- Validation
- Middleware
- Authentication
- Artisan CLI
- Queues & Jobs
- Events & Listeners
- Testing
- Service Providers
- Task Scheduling
Project Structure
app/
├── Console/Commands/ # Artisan commands
├── Http/
│ ├── Controllers/ # Request handlers
│ ├── Middleware/ # Request/response filters
│ └── Requests/ # Form validation
├── Jobs/ # Queueable jobs
├── Models/ # Eloquent models
├── Providers/ # Service providers
└── Services/ # Business logic
config/ # Configuration files
database/
├── factories/ # Model factories
├── migrations/ # Database migrations
└── seeders/ # Database seeders
routes/
├── api.php # API routes
└── web.php # Web routes
tests/
├── Feature/ # Integration tests
└── Unit/ # Unit tests
Routing & Controllers
Route Definitions
// Basic routes
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
// Resource routes (all CRUD)
Route::resource('posts', PostController::class);
Route::apiResource('comments', CommentController::class);
// Route groups with middleware
Route::prefix('api/v1')->middleware(['auth:sanctum'])->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
});
Controllers
class UserController extends Controller
{
public function index()
{
return UserResource::collection(
User::with(['profile', 'roles'])->paginate(20)
);
}
public function store(StoreUserRequest $request)
{
return new UserResource(User::create($request->validated()));
}
public function show(User $user) // Route model binding
{
return new UserResource($user->load('profile'));
}
}
Eloquent ORM
Model Definition
class Post extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'slug', 'content', 'status', 'author_id'];
protected $casts = ['status' => PostStatus::class, 'published_at' => 'datetime'];
protected $with = ['author']; // Always eager load
// Relationships
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
// Scopes
public function scopePublished(Builder $query): void
{
$query->where('status', PostStatus::Published)
->where('published_at', '<=', now());
}
// Accessors (Laravel 9+)
protected function excerpt(): Attribute
{
return Attribute::make(
get: fn () => Str::limit(strip_tags($this->content), 150),
);
}
}
Relationships Quick Reference
| Method | Relationship | Example |
|---|---|---|
hasOne |
1:1 | User has one Profile |
belongsTo |
1:1 inverse | Profile belongs to User |
hasMany |
1:n | User has many Posts |
belongsToMany |
n:n | Post has many Tags |
morphMany |
1:n polymorphic | Post has many Comments |
Advanced: For hasOneThrough, hasManyThrough, polymorphic relationships, see REFERENCE.md
Query Builder
// Filtering
$posts = Post::where('status', 'published')
->whereHas('tags', fn($q) => $q->where('name', 'laravel'))
->with(['author', 'comments'])
->latest('published_at')
->paginate(10);
// Aggregates
$count = Post::where('status', 'published')->count();
$avg = Order::avg('total');
// Chunking for large datasets
Post::chunk(100, fn($posts) => $posts->each->process());
Transactions
// Closure-based (auto commit/rollback)
$order = DB::transaction(function () use ($data) {
$order = Order::create($data['order']);
foreach ($data['items'] as $item) {
$order->items()->create($item);
}
return $order;
});
Validation
Form Requests
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', Rule::unique('posts')->ignore($this->post)],
'content' => ['required', 'string', 'min:100'],
'status' => ['required', Rule::enum(PostStatus::class)],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['array'],
'tags.*' => ['exists:tags,id'],
];
}
public function messages(): array
{
return ['title.required' => 'A post title is required.'];
}
}
Common Validation Rules
| Rule | Description |
|---|---|
required |
Must be present and not empty |
nullable |
Can be null |
string, integer, boolean |
Type validation |
email |
Valid email format |
unique:table,column |
Unique in database |
exists:table,column |
Must exist in database |
in:a,b,c |
Must be one of values |
min:n, max:n |
Size constraints |
Advanced: For custom validation rules and complex conditional validation, see REFERENCE.md
Middleware
class EnsureUserIsActive
{
public function handle(Request $request, Closure $next): Response
{
if (!$request->user()?->isActive()) {
return response()->json(['message' => 'Account inactive.'], 403);
}
return $next($request);
}
}
// With parameters
class CheckRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!$request->user()?->hasAnyRole($roles)) {
abort(403);
}
return $next($request);
}
}
// Usage: Route::middleware('role:admin,moderator')
Advanced: For tenant-scoping middleware and session management, see REFERENCE.md
Authentication
Sanctum (API Tokens)
// Login and issue token
public function login(LoginRequest $request)
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Invalid credentials.'],
]);
}
$user->tokens()->delete(); // Revoke existing
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
return response()->json(['user' => new UserResource($user), 'token' => $token]);
}
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn(Request $request) => $request->user());
});
Advanced: For Passport OAuth, spatie/permission RBAC, see REFERENCE.md
Artisan CLI
Custom Command
class ProcessContacts extends Command
{
protected $signature = 'contacts:process
{--status=active : Filter by status}
{--limit=100 : Maximum to process}
{--dry-run : Simulate without changes}';
protected $description = 'Process contacts based on criteria';
public function handle(): int
{
$query = Contact::where('status', $this->option('status'))
->limit((int) $this->option('limit'));
if (!$this->confirm("Process {$query->count()} contacts?")) {
return Command::SUCCESS;
}
$this->withProgressBar($query->cursor(), fn($c) => $this->process($c));
$this->newLine();
$this->info('Done.');
return Command::SUCCESS;
}
}
Common Artisan Commands
| Command | Description |
|---|---|
php artisan make:model -mfc |
Model + migration + factory + controller |
php artisan make:controller --api |
API resource controller |
php artisan make:request |
Form request validation |
php artisan make:job |
Queueable job |
php artisan queue:work |
Process queue jobs |
php artisan schedule:run |
Run scheduled tasks |
Advanced: For long-running consumers and graceful shutdown, see REFERENCE.md
Queues & Jobs
Job Definition
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public array $backoff = [30, 60, 120];
public function __construct(public User $user) {}
public function handle(Mailer $mailer): void
{
$mailer->to($this->user->email)->send(new WelcomeMail($this->user));
}
public function failed(\Throwable $e): void
{
Log::error('Welcome email failed', ['user' => $this->user->id, 'error' => $e->getMessage()]);
}
}
Dispatching Jobs
// Basic dispatch
SendWelcomeEmail::dispatch($user);
// With options
SendWelcomeEmail::dispatch($user)
->onQueue('emails')
->delay(now()->addMinutes(10));
// Conditional
SendWelcomeEmail::dispatchIf($user->wantsEmails(), $user);
Advanced: For job batching, chaining, and rate limiting, see REFERENCE.md
Events & Listeners
class PostPublished implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Post $post) {}
public function broadcastOn(): array
{
return [new PrivateChannel("user.{$this->post->author_id}")];
}
}
// Dispatch: event(new PostPublished($post));
Testing
Feature Test
class PostControllerTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_create_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/posts', [
'title' => 'Test Post',
'content' => 'Test content for the post.',
]);
$response->assertStatus(201)
->assertJson(['data' => ['title' => 'Test Post']]);
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'author_id' => $user->id,
]);
}
}
Mocking
public function test_order_processing(): void
{
Queue::fake();
$gateway = $this->mock(PaymentGateway::class);
$gateway->shouldReceive('charge')->once()->andReturn(['id' => 'ch_123']);
$this->postJson('/api/orders', [...]);
Queue::assertPushed(ProcessOrder::class);
}
Advanced: For testing commands, complex mocking, see REFERENCE.md
Service Providers
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind interfaces to implementations
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
// Singleton binding
$this->app->singleton(PaymentGateway::class, fn($app) =>
new StripeGateway(config('services.stripe.key'))
);
}
public function boot(): void
{
Model::preventLazyLoading(!app()->isProduction());
}
}
Advanced: For contextual binding and deferred providers, see REFERENCE.md
Task Scheduling
// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
$schedule->command('queue:work --stop-when-empty')
->everyMinute()
->withoutOverlapping();
$schedule->command('reports:generate')
->dailyAt('00:00')
->onOneServer()
->emailOutputOnFailure('admin@example.com');
$schedule->job(new ProcessPendingOrders)
->hourly()
->between('08:00', '22:00');
}
For advanced patterns including multi-tenancy, repository pattern, performance optimization, and debugging, see REFERENCE.md