Laravel Core

General Laravel Standards

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) { ... }