Skip to content

Latest commit

 

History

History
766 lines (586 loc) · 15.4 KB

File metadata and controls

766 lines (586 loc) · 15.4 KB

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));
    }
}

Register in EventServiceProvider

protected $listen = [
    OrderPlaced::class => [
        SendOrderConfirmation::class,
        UpdateInventory::class,
        NotifyWarehouse::class,
    ],
];

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();

Job Class

<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Order;
use App\Services\PaymentService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

final class ProcessOrder implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 120;

    public function __construct(
        public Order $order,
    ) {}

    public function handle(PaymentService $payment): void
    {
        $payment->process($this->order);
    }

    public function failed(Throwable $exception): void
    {
        Log::error('Order processing failed', [
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);
    }
}

Unique Jobs

use Illuminate\Contracts\Queue\ShouldBeUnique;

final class ProcessOrder implements ShouldQueue, ShouldBeUnique
{
    public function uniqueId(): string
    {
        return (string) $this->order->id;
    }

    public function uniqueFor(): int
    {
        return 60; // Seconds
    }
}

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));

Notification Class

<?php

declare(strict_types=1);

namespace App\Notifications;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

final class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public Order $order,
    ) {}

    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject(__('notifications.order_shipped.subject'))
            ->line(__('notifications.order_shipped.line1', ['order' => $this->order->number]))
            ->action(__('notifications.order_shipped.action'), route('orders.show', $this->order))
            ->line(__('notifications.order_shipped.line2'));
    }

    public function toArray(object $notifiable): array
    {
        return [
            'order_id' => $this->order->id,
            'message' => __('notifications.order_shipped.message'),
        ];
    }
}

Service Providers

When to Create a Service Provider

  • Binding interfaces to implementations
  • Registering event listeners
  • Configuring third-party packages
  • Bootstrapping application services

Structure

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripeGateway;
use Illuminate\Support\ServiceProvider;

final class PaymentServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(PaymentGateway::class, StripeGateway::class);

        $this->app->singleton(StripeClient::class, function ($app) {
            return new StripeClient(config('services.stripe.secret'));
        });
    }

    public function boot(): void
    {
        // Bootstrap services after all providers registered
    }
}

Error Handling

Report vs Render

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions): void {
    // Don't report certain exceptions
    $exceptions->dontReport([
        ValidationException::class,
        AuthenticationException::class,
    ]);

    // Custom reporting
    $exceptions->report(function (PaymentFailedException $e): void {
        Log::channel('payments')->error($e->getMessage(), [
            'order_id' => $e->order->id,
        ]);
    });

    // Custom rendering
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Not found'], 404);
        }
    });
})

Custom Exceptions

<?php

declare(strict_types=1);

namespace App\Exceptions;

use Exception;

final class InsufficientFundsException extends Exception
{
    public function __construct(
        public readonly float $available,
        public readonly float $required,
    ) {
        parent::__construct("Insufficient funds: {$available} available, {$required} required");
    }

    public function render(): Response
    {
        return response()->json([
            'message' => $this->getMessage(),
            'available' => $this->available,
            'required' => $this->required,
        ], 422);
    }

    public function report(): void
    {
        Log::warning('Insufficient funds', [
            'available' => $this->available,
            'required' => $this->required,
        ]);
    }
}

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