Dependency Injection
Prefer Constructor/Method Injection
// Good - Constructor injection
final class TicketService
{
public function __construct(
private TicketRepository $tickets,
private NotificationService $notifications,
) {}
public function close(Ticket $ticket): void
{
$this->tickets->close($ticket);
$this->notifications->send($ticket->user, new TicketClosed($ticket));
}
}
// Good - Method injection (controllers, commands)
public function __invoke(
StoreTicketRequest $request,
CreateTicket $createTicket,
): RedirectResponse {
$ticket = $createTicket->execute(TicketData::from($request));
return redirect()->route('tickets.show', $ticket);
}
Use app() When Injection Isn't Possible
// Acceptable - When DI isn't available
final class ConvertFeetToInches
{
public function execute(float $length, Measurement $from = Measurement::Feet): float
{
if ($from === Measurement::Meters) {
$length = app(ConvertMetersToFeet::class)->execute($length);
}
return $length * 12;
}
}
// Acceptable - In service providers
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Avoid Injecting the Container
// Bad - Injecting container
public function __construct(
private Application $app,
) {}
public function process(): void
{
$service = $this->app->make(SomeService::class);
}
// Good - Inject what you need
public function __construct(
private SomeService $service,
) {}
Facades vs Helpers
Prefer Helper Functions
// Good - Helper functions
session('cart');
config('app.name');
cache('key');
auth()->user();
request()->input('name');
redirect()->route('home');
response()->json($data);
view('welcome');
now();
today();
// Avoid - Longer alternatives
Session::get('cart');
Config::get('app.name');
Cache::get('key');
Auth::user();
Request::input('name');
Use Facades for Fluent APIs
// Good - Facade for chained methods
Cache::tags(['users', 'profiles'])->put($key, $value, $ttl);
Log::channel('slack')->critical('System down');
Storage::disk('s3')->put($path, $contents);
// Good - Facade for static-style calls
DB::transaction(fn () => $this->process());
Route::middleware('auth')->group(fn () => ...);
Quick Reference
| Operation |
Use |
| Simple get/set |
Helper (cache(), session(), config()) |
| Chained methods |
Facade (Cache::tags()->put()) |
| Channel/disk selection |
Facade (Log::channel(), Storage::disk()) |
| Transactions |
Facade (DB::transaction()) |
Laravel Helpers vs PHP Functions
Strings
// Good - Laravel Str helpers
use Illuminate\Support\Str;
Str::contains($haystack, $needle);
Str::startsWith($string, $prefix);
Str::endsWith($string, $suffix);
Str::before($string, $delimiter);
Str::after($string, $delimiter);
Str::slug($title);
Str::uuid();
Str::random(32);
Str::limit($text, 100);
Str::plural($word);
Str::camel($string);
Str::snake($string);
Str::kebab($string);
Str::studly($string);
// Avoid - PHP equivalents
str_contains($haystack, $needle);
str_starts_with($string, $prefix);
substr($string, 0, strpos($string, $delimiter));
Fluent Strings
use Illuminate\Support\Str;
$slug = Str::of($title)
->trim()
->lower()
->replace(' ', '-')
->slug();
$excerpt = Str::of($content)
->stripTags()
->limit(200)
->toString();
Arrays
// Good - Laravel Arr helpers
use Illuminate\Support\Arr;
Arr::get($array, 'user.name', 'default');
Arr::has($array, 'user.email');
Arr::first($array, fn ($value) => $value > 10);
Arr::last($array);
Arr::only($array, ['name', 'email']);
Arr::except($array, ['password']);
Arr::pluck($array, 'name');
Arr::where($array, fn ($value, $key) => $value > 0);
Arr::flatten($array);
Arr::dot($array);
Arr::undot($array);
// Good - data_get for nested access
$name = data_get($response, 'data.user.profile.name');
$names = data_get($users, '*.name');
Collections Over Arrays
// Good - Use collections
$names = collect($users)
->filter(fn ($user) => $user['is_active'])
->map(fn ($user) => $user['name'])
->sort()
->values();
// Avoid - Array functions
$active = array_filter($users, fn ($user) => $user['is_active']);
$names = array_map(fn ($user) => $user['name'], $active);
sort($names);
$names = array_values($names);
Configuration
Access Config Values
// Good - Helper function
$name = config('app.name');
$timeout = config('services.api.timeout', 30);
// Good - Get entire config array
$mail = config('mail');
// Set config at runtime (testing)
config(['app.debug' => true]);
Environment Variables
// Good - Access env only in config files
// config/services.php
return [
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
];
// Then use config() everywhere else
$key = config('services.stripe.key');
// Bad - Using env() outside config
$key = env('STRIPE_KEY'); // Returns null when config is cached
Type-Safe Config
// config/services.php
return [
'api' => [
'timeout' => (int) env('API_TIMEOUT', 30),
'verify_ssl' => (bool) env('API_VERIFY_SSL', true),
'base_url' => env('API_BASE_URL', 'https://api.example.com'),
],
];
Logging
Use Appropriate Levels
use Illuminate\Support\Facades\Log;
// Debug information
Log::debug('User login attempt', ['email' => $email]);
// General information
Log::info('Order placed', ['order_id' => $order->id]);
// Warnings
Log::warning('Payment retry', ['attempt' => $attempt]);
// Errors
Log::error('Payment failed', [
'order_id' => $order->id,
'error' => $exception->getMessage(),
]);
// Critical (immediate attention)
Log::critical('Database connection lost');
Include Context
// Good - Structured context
Log::info('User registered', [
'user_id' => $user->id,
'email' => $user->email,
'plan' => $user->plan,
]);
// Bad - String interpolation
Log::info("User {$user->id} registered with email {$user->email}");
Channel-Specific Logging
Log::channel('slack')->critical('Production error', ['exception' => $e]);
Log::channel('daily')->info('Daily report generated');
Log::stack(['daily', 'slack'])->error('Critical failure');
Caching
Basic Usage
use Illuminate\Support\Facades\Cache;
// Get with default
$value = Cache::get('key', 'default');
$value = cache('key', 'default');
// Get or compute
$users = Cache::remember('users', now()->addHours(1), function () {
return User::all();
});
// Store
Cache::put('key', $value, now()->addMinutes(10));
cache(['key' => $value], now()->addMinutes(10));
// Forever
Cache::forever('key', $value);
// Delete
Cache::forget('key');
// Check existence
if (Cache::has('key')) {
// ...
}
Cache Tags
// Store with tags
Cache::tags(['users', 'profiles'])->put('user.1', $user, $ttl);
// Retrieve tagged
$user = Cache::tags(['users', 'profiles'])->get('user.1');
// Flush by tag
Cache::tags('users')->flush();
Atomic Locks
$lock = Cache::lock('processing-order-'.$order->id, 10);
if ($lock->get()) {
try {
$this->processOrder($order);
} finally {
$lock->release();
}
}
// Or with callback
Cache::lock('processing')->get(function () {
// Lock acquired, auto-released after callback
});
Events
Dispatching Events
// Using event helper
event(new OrderPlaced($order));
// Using Event facade
Event::dispatch(new OrderPlaced($order));
// From model (if using $dispatchesEvents)
$order->save(); // Fires OrderCreated automatically
Event Class
<?php
declare(strict_types=1);
namespace App\Events;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
final class OrderPlaced
{
use Dispatchable;
use SerializesModels;
public function __construct(
public Order $order,
) {}
}
Listener Class
<?php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Notifications\OrderConfirmation;
final class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
$event->order->user->notify(new OrderConfirmation($event->order));
}
}
Queues
Dispatching Jobs
use App\Jobs\ProcessOrder;
// Dispatch to default queue
ProcessOrder::dispatch($order);
// Dispatch with delay
ProcessOrder::dispatch($order)->delay(now()->addMinutes(5));
// Dispatch to specific queue
ProcessOrder::dispatch($order)->onQueue('orders');
// Dispatch to specific connection
ProcessOrder::dispatch($order)->onConnection('redis');
// Chain jobs
Bus::chain([
new ProcessPayment($order),
new UpdateInventory($order),
new SendConfirmation($order),
])->dispatch();
Notifications
Send Notifications
// To a single user
$user->notify(new OrderShipped($order));
// To multiple users
Notification::send($users, new OrderShipped($order));
// On-demand (no user model)
Notification::route('mail', 'admin@example.com')
->route('slack', '#alerts')
->notify(new SystemAlert($message));
Storage
File Operations
use Illuminate\Support\Facades\Storage;
// Store file
Storage::put('file.txt', $contents);
Storage::disk('s3')->put('file.txt', $contents);
// Store uploaded file
$path = $request->file('avatar')->store('avatars', 's3');
// Get file
$contents = Storage::get('file.txt');
$exists = Storage::exists('file.txt');
// Delete
Storage::delete('file.txt');
Storage::delete(['file1.txt', 'file2.txt']);
// URLs
$url = Storage::url('file.txt');
$temporaryUrl = Storage::temporaryUrl('file.txt', now()->addMinutes(5));
Best Practices
Use Laravel's Built-in Features
// Good - Use Carbon
$date = now()->addDays(7);
$formatted = $date->format('Y-m-d');
$diff = $date->diffForHumans();
// Good - Use collections
$filtered = collect($items)->filter(...)->map(...);
// Good - Use validation
$validated = $request->validate([...]);
// Good - Use policies
$this->authorize('update', $ticket);
Avoid Anti-Patterns
// Bad - Business logic in routes
Route::post('/orders', function (Request $request) {
// 50 lines of order processing...
});
// Bad - Raw queries when Eloquent works
DB::select('SELECT * FROM users WHERE active = 1');
// Bad - Manual file includes
require_once 'helpers.php';
// Bad - Global functions
function formatPrice($price) { ... }