Skip to content

Instantly share code, notes, and snippets.

@mubbi
Last active December 22, 2025 06:06
Show Gist options
  • Select an option

  • Save mubbi/f1008e0d009cb9c156aa84bb1f3e4f0c to your computer and use it in GitHub Desktop.

Select an option

Save mubbi/f1008e0d009cb9c156aa84bb1f3e4f0c to your computer and use it in GitHub Desktop.
Developers Guide for Laravel

Developer Standards Guide

Tech Stack: PHP 8.2+, Laravel 10+, MySQL 8+

This document defines mandatory standards and best practices for building scalable, maintainable, secure, and high‑performance applications using PHP, Laravel, and MySQL. All developers must follow these guidelines consistently.


1. General Engineering Principles

1.1 Core Principles

  • SOLID principles must be followed
  • YAGNI (You Aren't Gonna Need It) - Don't add functionality until it's necessary
  • Separation of Concerns (SoC)
  • Single Responsibility for classes and methods
  • Fail fast, validate early
  • Explicit over implicit
  • Readable code > clever code
  • KISS (Keep It Simple, Stupid) - Prefer simple solutions over complex ones

1.2 Code Quality Expectations

  • Code must be:

    • Readable
    • Testable
    • Extensible
    • Documented
  • No business logic in controllers or views

  • No direct DB queries outside repositories


2. PHP Standards

2.1 PHP Version

  • Minimum PHP 8.2
  • Use strict typing
declare(strict_types=1);

2.2 PSR Standards (Mandatory)

  • PSR-1: Basic Coding Standard
  • PSR-3: Logger Interface (use Monolog)
  • PSR-4: Autoloading
  • PSR-7: HTTP Message Interfaces (when working with HTTP libraries)
  • PSR-11: Container Interface (Laravel's container implements this)
  • PSR-12: Extended Coding Style

2.3 Type Safety

  • Always use:

    • Scalar type hints (int, string, bool, float, array)
    • Return types (including void for methods that return nothing)
    • Property type hints (PHP 7.4+)
    • Union types (PHP 8.0+) when appropriate
    • Nullable types (?Type or Type|null)
declare(strict_types=1);

class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}

    public function handle(User $user): bool
    {
        return $user->isActive();
    }

    public function findById(int $id): ?User
    {
        return $this->repository->findById($id);
    }

    public function process(string|int $identifier): void
    {
        // Process identifier
    }
}

2.4 Modern PHP Features

  • Use PHP 8.2+ features:

    • Readonly properties for immutable DTOs
    • Enums for fixed sets of values
    • Match expressions instead of switch when appropriate
    • Named arguments for clarity
    • Attributes (PHP 8.0+) instead of annotations where applicable
enum UserStatus: string
{
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case SUSPENDED = 'suspended';
}

class UserDTO
{
    public function __construct(
        public readonly string $email,
        public readonly UserStatus $status
    ) {}
}

2.5 PHPStan (Static Analysis)

  • Level 8 or max
  • No ignored errors without justification
  • Use PHPStan extensions for Laravel when available
  • Run PHPStan in CI pipeline

3. Laravel Standards

3.1 Project Structure

3.1.1 Recommended Directory Structure

app/
 ├── Actions/              # Single-purpose business operations
 │   └── User/
 │       ├── CreateUserAction.php
 │       └── UpdateUserAction.php
 ├── Console/
 │   └── Commands/         # Artisan commands
 ├── DTOs/                 # Data Transfer Objects
 │   └── UserDTO.php
 ├── Events/               # Domain events
 │   └── UserRegistered.php
 ├── Exceptions/           # Custom exceptions
 │   └── UserAlreadyExistsException.php
 ├── Http/
 │   ├── Controllers/
 │   │   ├── Api/          # API controllers
 │   │   │   └── V1/
 │   │   └── Web/          # Web controllers (if needed)
 │   ├── Middleware/
 │   ├── Requests/         # Form Requests
 │   │   └── User/
 │   │       ├── StoreUserRequest.php
 │   │       └── UpdateUserRequest.php
 │   └── Resources/        # API Resources
 │       └── UserResource.php
 ├── Jobs/                 # Queue jobs
 │   └── SendWelcomeEmail.php
 ├── Listeners/            # Event listeners
 │   └── SendWelcomeEmailListener.php
 ├── Models/
 │   ├── User.php
 │   └── Traits/           # Model traits
 ├── Observers/            # Model observers
 │   └── UserObserver.php
 ├── Policies/             # Authorization policies
 │   └── UserPolicy.php
 ├── Providers/
 │   ├── AppServiceProvider.php
 │   └── RepositoryServiceProvider.php
 ├── Repositories/
 │   ├── Contracts/         # Repository interfaces
 │   │   └── UserRepositoryInterface.php
 │   └── Eloquent/          # Repository implementations
 │       └── EloquentUserRepository.php
 ├── Services/             # Business logic services
 │   └── UserService.php
 └── Support/              # Helper classes, utilities
     └── Helpers/

3.1.2 Additional Directories

config/                    # Configuration files
database/
 ├── factories/            # Model factories
 ├── migrations/           # Database migrations
 ├── seeders/              # Database seeders
 └── testing/              # Test database setup
routes/
 ├── api.php               # API routes
 ├── web.php               # Web routes
 └── channels.php          # Broadcasting channels
resources/
 ├── lang/                 # Language files
 └── views/                # Blade templates (if using)
tests/
 ├── Feature/              # Feature tests
 ├── Unit/                 # Unit tests
 └── Pest.php              # Pest configuration

3.1.3 Organization Best Practices

  • Group by feature when appropriate (for large applications)
  • Use subdirectories for related classes (e.g., User/ for all user-related classes)
  • Keep flat structure for small to medium applications
  • Consistent naming across directories
  • Separate API and Web controllers if both exist
  • Version API controllers (Api/V1/, Api/V2/)

3.2 Controllers

  • Controllers must be thin

  • No business logic

  • Use Resource Controllers for CRUD operations

  • Only:

    • Request validation (via Form Requests)
    • Authorization (via Policies/Gates)
    • Delegation to services/actions
    • Response formatting
class UserController extends Controller
{
    public function __construct(
        private CreateUserAction $createAction,
        private UpdateUserAction $updateAction
    ) {}

    public function store(StoreUserRequest $request): JsonResponse
    {
        $user = $this->createAction->execute($request->dto());

        return response()->json(
            new UserResource($user),
            201
        );
    }

    public function update(UpdateUserRequest $request, User $user): JsonResponse
    {
        $this->authorize('update', $user);

        $updated = $this->updateAction->execute($user, $request->dto());

        return response()->json(new UserResource($updated));
    }
}

3.3 API Resources

  • Use API Resources for response transformation
  • Separate resources for different contexts (index vs show)
  • Always use resources for API responses
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'email' => $this->email,
            'name' => $this->name,
            'created_at' => $this->created_at?->toIso8601String(),
        ];
    }
}

// For collections
class UserCollection extends ResourceCollection
{
    public $collects = UserResource::class;
}

3.4 Events & Listeners

  • Use Events for decoupled business logic
  • Keep listeners focused and testable
  • Use queued listeners for heavy operations
// Event
class UserRegistered
{
    public function __construct(
        public User $user
    ) {}
}

// Listener
class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        // Send email
    }
}

3.5 Jobs & Queues

  • Use Jobs for asynchronous operations
  • Always implement ShouldQueue for long-running tasks
  • Use ShouldBeUnique for idempotent operations
  • Handle failures gracefully
class ProcessPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private Order $order
    ) {}

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

    public function failed(Throwable $exception): void
    {
        // Handle failure
    }
}

3.6 Model Observers

  • Use Observers for model lifecycle hooks
  • Keep observers focused on single responsibility
  • Avoid business logic in observers
class UserObserver
{
    public function created(User $user): void
    {
        // Log creation
    }

    public function updating(User $user): void
    {
        // Track changes
    }
}

3.7 Advanced Laravel Standards

3.7.1 Service Container & Dependency Injection

  • Always use constructor injection for dependencies
  • Bind interfaces to implementations in service providers
  • Use contextual binding when needed
  • Avoid service locator pattern (app()->make())
// Good: Constructor injection
class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private EmailService $emailService
    ) {}
}

// Service Provider binding
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            UserRepositoryInterface::class,
            EloquentUserRepository::class
        );

        // Singleton binding
        $this->app->singleton(
            PaymentGatewayInterface::class,
            StripePaymentGateway::class
        );

        // Contextual binding
        $this->app->when(OrderController::class)
            ->needs(PaymentGatewayInterface::class)
            ->give(StripePaymentGateway::class);
    }
}

3.7.2 Collections & Array Helpers

  • Use Collections for array manipulation
  • Chain methods for readability
  • Use higher-order messages for cleaner code
  • Lazy collections for large datasets
// Good: Collection methods
$activeUsers = User::all()
    ->filter(fn ($user) => $user->isActive())
    ->map(fn ($user) => $user->name)
    ->values();

// Higher-order messages
$users->each->sendEmail();
$users->map->toArray();

// Lazy collections for large datasets
User::cursor()
    ->filter(fn ($user) => $user->isActive())
    ->each(fn ($user) => $user->sendEmail());

3.7.3 Eloquent Relationships

  • Define relationships in models
  • Use relationship methods instead of manual joins when possible
  • Eager load relationships to avoid N+1
  • Use has() and whereHas() for relationship queries
// Model relationships
class User extends Model
{
    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }

    public function activeOrders(): HasMany
    {
        return $this->orders()->where('status', 'active');
    }
}

// Usage
$users = User::with('orders')->get(); // Eager load
$usersWithOrders = User::has('orders')->get(); // Has relationship
$usersWithActiveOrders = User::whereHas('orders', function ($query) {
    $query->where('status', 'active');
})->get();

3.7.4 Model Scopes & Query Scopes

  • Use query scopes for reusable query logic
  • Use global scopes for default constraints
  • Use local scopes for conditional queries
  • Chain scopes for complex queries
class User extends Model
{
    // Local scope
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }

    public function scopeEmailVerified(Builder $query): Builder
    {
        return $query->whereNotNull('email_verified_at');
    }

    // Global scope
    protected static function booted(): void
    {
        static::addGlobalScope('active', function (Builder $builder) {
            $builder->where('status', '!=', 'deleted');
        });
    }
}

// Usage
User::active()->emailVerified()->get();

3.7.5 Model Accessors & Mutators

  • Use accessors for computed attributes
  • Use mutators for data transformation
  • Use attribute casting for type conversion
  • Use appends for computed attributes in JSON
class User extends Model
{
    protected $casts = [
        'email_verified_at' => 'datetime',
        'metadata' => 'array',
        'is_admin' => 'boolean',
    ];

    protected $appends = ['full_name'];

    // Accessor
    public function getFullNameAttribute(): string
    {
        return "{$this->first_name} {$this->last_name}";
    }

    // Mutator
    public function setEmailAttribute(string $value): void
    {
        $this->attributes['email'] = strtolower($value);
    }
}

3.7.6 Route Model Binding

  • Use route model binding for cleaner routes
  • Customize binding with getRouteKeyName()
  • Use scoped binding for nested resources
  • Handle missing models with custom resolution
// Route
Route::get('/users/{user}', [UserController::class, 'show']);

// Controller
public function show(User $user): JsonResponse
{
    return response()->json(new UserResource($user));
}

// Custom route key
class User extends Model
{
    public function getRouteKeyName(): string
    {
        return 'slug'; // Instead of 'id'
    }
}

// Scoped binding
Route::get('/users/{user}/orders/{order}', function (User $user, Order $order) {
    // $order is automatically scoped to $user
})->scopeBindings();

3.7.7 Middleware Best Practices

  • Create focused middleware (single responsibility)
  • Use middleware groups for organization
  • Apply middleware at route, controller, or global level
  • Use middleware parameters when needed
// Custom middleware
class EnsureUserIsActive
{
    public function handle(Request $request, Closure $next): Response
    {
        if (!$request->user()->isActive()) {
            abort(403, 'User account is inactive');
        }

        return $next($request);
    }
}

// Middleware with parameters
class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string $role): Response
    {
        if (!$request->user()->hasRole($role)) {
            abort(403);
        }

        return $next($request);
    }
}

// Usage
Route::middleware(['auth', 'active', 'role:admin'])->group(function () {
    // Routes
});

3.7.8 Caching Strategies

  • Cache expensive operations
  • Use cache tags (Redis) for grouped invalidation
  • Cache queries with remember()
  • Use cache locks for preventing cache stampede
// Query caching
$users = Cache::remember('users.active', 3600, function () {
    return User::where('status', 'active')->get();
});

// Cache tags (Redis)
Cache::tags(['users', 'active'])
    ->remember('users.active', 3600, fn () => User::active()->get());

Cache::tags(['users'])->flush(); // Invalidate all user caches

// Cache locks (prevent stampede)
$lock = Cache::lock('process-payment', 10);
if ($lock->get()) {
    try {
        // Process payment
    } finally {
        $lock->release();
    }
}

3.7.9 Queue & Job Best Practices

  • Use queues for long-running tasks
  • Implement retry logic with exponential backoff
  • Use job batching for related operations
  • Handle failures gracefully
  • Use job middleware for cross-cutting concerns
class ProcessPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;
    public $backoff = [60, 300, 900]; // Exponential backoff

    public function __construct(
        private Order $order
    ) {}

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

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

// Job batching
Bus::batch([
    new ProcessOrder($order1),
    new ProcessOrder($order2),
    new ProcessOrder($order3),
])->then(function (Batch $batch) {
    // All jobs completed
})->catch(function (Batch $batch, Throwable $e) {
    // First batch job failure
})->finally(function (Batch $batch) {
    // Batch finished
})->dispatch();

3.7.10 Event & Listener Patterns

  • Use events for decoupled architecture
  • Queue listeners for heavy operations
  • Use event subscribers for multiple related listeners
  • Dispatch events after important actions
// Event
class OrderShipped
{
    public function __construct(
        public Order $order
    ) {}
}

// Listener (queued)
class SendShipmentNotification implements ShouldQueue
{
    public function handle(OrderShipped $event): void
    {
        Mail::to($event->order->user)->send(new OrderShippedMail($event->order));
    }
}

// Event subscriber
class OrderEventSubscriber
{
    public function handleOrderShipped(OrderShipped $event): void
    {
        // Handle shipped
    }

    public function handleOrderCancelled(OrderCancelled $event): void
    {
        // Handle cancelled
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            OrderShipped::class => 'handleOrderShipped',
            OrderCancelled::class => 'handleOrderCancelled',
        ];
    }
}

3.7.11 Validation Rules & Custom Rules

  • Create custom rules for reusable validation
  • Use rule objects for complex validation
  • Validate files properly (size, mime types)
  • Use conditional validation with sometimes()
// Custom rule
class StrongPassword implements Rule
{
    public function passes($attribute, $value): bool
    {
        return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $value);
    }

    public function message(): string
    {
        return 'The :attribute must be at least 8 characters with uppercase, lowercase, and numbers.';
    }
}

// Usage
$request->validate([
    'password' => ['required', new StrongPassword()],
]);

// Conditional validation
$request->validate([
    'email' => 'required|email',
    'phone' => 'sometimes|required|phone', // Only if present
]);

3.7.12 API Rate Limiting

  • Implement rate limiting for API endpoints
  • Use different limits for different user types
  • Customize rate limiters for specific needs
  • Return proper headers with rate limit info
// In RouteServiceProvider
protected function configureRateLimiting(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });

    RateLimiter::for('strict', function (Request $request) {
        return Limit::perMinute(10)->by($request->ip());
    });
}

// Usage
Route::middleware(['throttle:api'])->group(function () {
    // Routes
});

Route::middleware(['throttle:strict'])->post('/login', ...);

3.7.13 File Storage

  • Use storage facade for file operations
  • Use disk drivers appropriately (local, s3, etc.)
  • Generate unique filenames to prevent conflicts
  • Validate file uploads properly
// Store file
$path = $request->file('avatar')->store('avatars', 's3');

// Store with custom name
$path = Storage::disk('s3')->putFileAs(
    'avatars',
    $request->file('avatar'),
    $user->id . '.jpg'
);

// Generate temporary URL
$url = Storage::disk('s3')->temporaryUrl(
    $path,
    now()->addMinutes(5)
);

3.7.14 Logging & Debugging

  • Use structured logging with context
  • Use appropriate log levels
  • Never log sensitive data
  • Use Laravel Telescope in development (never in production)
// Structured logging
Log::info('User registered', [
    'user_id' => $user->id,
    'email' => $user->email,
    'ip' => $request->ip(),
]);

// Log with context
Log::channel('slack')->error('Payment failed', [
    'order_id' => $order->id,
    'amount' => $order->total,
    'user_id' => $order->user_id,
]);

3.7.15 Performance Optimization

  • Use eager loading to prevent N+1 queries
  • Select specific columns when possible
  • Use database indexes effectively
  • Cache expensive operations
  • Use lazy loading for large collections
  • Optimize autoloader in production
// Eager load relationships
User::with(['orders', 'profile'])->get();

// Select specific columns
User::select('id', 'name', 'email')->get();

// Lazy loading for large datasets
User::cursor()->each(function ($user) {
    // Process user
});

// Optimize autoloader (run in production)
// composer dump-autoload -o

3.7.16 Production Optimization Checklist

  • Cache configuration: php artisan config:cache
  • Cache routes: php artisan route:cache
  • Cache views: php artisan view:cache
  • Cache events: php artisan event:cache
  • Optimize autoloader: composer dump-autoload -o
  • Disable debug mode: APP_DEBUG=false
  • Use queue workers: Process jobs asynchronously
  • Use OPcache: Enable PHP OPcache
  • Use Redis: For cache and sessions
  • Enable HTTP/2: For faster page loads
  • Use CDN: For static assets
  • Monitor performance: Use APM tools (Laravel Telescope in dev only)
# Production deployment script
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
composer dump-autoload -o

3.7.17 Artisan Commands Best Practices

  • Create custom commands for repetitive tasks
  • Use command signatures for better organization
  • Add command descriptions and help text
  • Use command options and arguments appropriately
  • Test commands with Pest
class SendWeeklyReport extends Command
{
    protected $signature = 'reports:weekly 
                            {--email= : Email to send report to}
                            {--force : Force send even if already sent}';

    protected $description = 'Send weekly report to administrators';

    public function handle(ReportService $service): int
    {
        $email = $this->option('email') ?? config('reports.default_email');
        $force = $this->option('force');

        $this->info('Generating weekly report...');
        
        $service->sendWeeklyReport($email, $force);

        $this->info('Weekly report sent successfully!');

        return self::SUCCESS;
    }
}

3.7.18 Testing Helpers & Utilities

  • Use factories for test data
  • Use database transactions in tests
  • Use HTTP testing helpers for API tests
  • Use model factories for relationships
  • Use test traits for common setup
// Feature test with factories
it('creates a user with orders', function () {
    $user = User::factory()
        ->has(Order::factory()->count(3))
        ->create();

    expect($user->orders)->toHaveCount(3);
});

// HTTP test
it('returns user data', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->getJson("/api/v1/users/{$user->id}");

    $response->assertOk()
        ->assertJsonStructure([
            'data' => ['id', 'name', 'email'],
        ]);
});

3.7.19 Error Handling & Reporting

  • Customize exception handler for consistent error responses
  • Use exception reporting for error tracking
  • Log exceptions with context
  • Use Sentry/Bugsnag for production error tracking
// In App\Exceptions\Handler
public function register(): void
{
    $this->reportable(function (Throwable $e) {
        if (app()->bound('sentry')) {
            app('sentry')->captureException($e);
        }
    });

    $this->renderable(function (ValidationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $e->errors(),
            ], 422);
        }
    });
}

3.7.20 Security Best Practices

  • Use HTTPS in production
  • Set secure cookies: SESSION_SECURE_COOKIE=true
  • Use CSRF protection: Enabled by default
  • Sanitize user input: Always validate and sanitize
  • Use prepared statements: Eloquent does this automatically
  • Hash passwords: Use Hash::make()
  • Rate limit APIs: Prevent abuse
  • Use environment variables: Never hardcode secrets
  • Keep dependencies updated: composer audit
// Secure session configuration
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'strict',

4. Request Validation

  • Always use Form Requests
  • No validation in controllers
  • Use Rule class for complex validation
  • Customize error messages
  • Implement authorization in Form Requests when applicable
class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', User::class);
    }

    public function rules(): array
    {
        return [
            'email' => [
                'required',
                'email:rfc,dns',
                Rule::unique('users')->whereNull('deleted_at'),
            ],
            'password' => [
                'required',
                'string',
                'min:8',
                'regex:/[A-Z]/',
                'regex:/[a-z]/',
                'regex:/[0-9]/',
            ],
            'name' => ['required', 'string', 'max:255'],
            'role' => ['required', Rule::in(['user', 'admin'])],
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique' => 'This email is already registered.',
            'password.regex' => 'Password must contain uppercase, lowercase, and numbers.',
        ];
    }

    public function attributes(): array
    {
        return [
            'email' => 'email address',
        ];
    }
}

5. Service & Repository Pattern

5.1 Why

  • Decouples business logic from framework
  • Enables testing and scalability
  • Note: See Section 7.1 for guidance on when to combine with Actions and DTOs

5.2 Repository Interface

interface UserRepositoryInterface
{
    public function create(array $data): User;
    public function findById(int $id): ?User;
}

5.3 Repository Implementation

class EloquentUserRepository implements UserRepositoryInterface
{
    public function create(array $data): User
    {
        return User::create($data);
    }
}

5.4 Service Layer

  • Services contain business logic
  • Services depend on repositories, not models directly
  • Use dependency injection
  • Keep services focused and testable
class UserService
{
    public function __construct(
        private UserRepositoryInterface $users,
        private EmailService $emailService
    ) {}

    public function register(UserDTO $dto): User
    {
        $user = $this->users->create($dto->toArray());
        
        $this->emailService->sendWelcomeEmail($user);
        
        return $user;
    }

    public function updateProfile(User $user, ProfileDTO $dto): User
    {
        return $this->users->update($user, $dto->toArray());
    }
}

5.5 Dependency Injection

  • Use constructor injection
  • Leverage Laravel's service container
  • Bind interfaces to implementations in service providers
  • Prefer interfaces over concrete classes
// In AppServiceProvider
public function register(): void
{
    $this->app->bind(
        UserRepositoryInterface::class,
        EloquentUserRepository::class
    );
}

6. Actions Pattern (Recommended)

  • Each business operation = one Action
  • Actions are single-purpose, focused operations
  • Actions orchestrate services and handle workflow
  • Use actions for complex operations that span multiple services
  • Actions make controllers even thinner
class CreateUserAction
{
    public function __construct(
        private UserService $userService,
        private EmailService $emailService,
        private EventDispatcher $events
    ) {}

    public function execute(UserDTO $dto): User
    {
        $user = $this->userService->register($dto);
        
        $this->emailService->sendWelcomeEmail($user);
        
        $this->events->dispatch(new UserRegistered($user));
        
        return $user;
    }
}

6.1 When to Use Actions

  • Complex operations involving multiple services
  • Operations that need orchestration (workflow management)
  • Operations with side effects across multiple domains
  • When you want to make controllers extremely thin
  • See Section 7.1 for pattern combination guidelines

6.2 When NOT to Use Actions

  • Simple CRUD operations (use services directly)
  • Operations that are just a single service method call
  • When it adds unnecessary abstraction (follow YAGNI)
  • Simple updates that don't require orchestration

7. DTOs (Data Transfer Objects)

  • Prevent array abuse
  • Strong typing
  • Use readonly properties for immutability
  • Implement toArray() for serialization
  • Use static factory methods for creation
  • Recommended for all data transfer (see Section 7.1 for pattern combinations)
class UserDTO
{
    public function __construct(
        public readonly string $email,
        public readonly string $password,
        public readonly ?string $name = null,
        public readonly UserStatus $status = UserStatus::ACTIVE,
    ) {}

    public static function fromRequest(StoreUserRequest $request): self
    {
        return new self(
            email: $request->validated('email'),
            password: $request->validated('password'),
            name: $request->validated('name'),
        );
    }

    public function toArray(): array
    {
        return [
            'email' => $this->email,
            'password' => $this->password,
            'name' => $this->name,
            'status' => $this->status->value,
        ];
    }
}

7.1 Pattern Combinations & When to Use Them

Following YAGNI principle, use patterns only when they add value. Here's the recommended approach:

7.1.1 Recommended Pattern Stack (Complex Operations)

For complex business operations that involve multiple steps, use all patterns together:

Controller → Action → Service → Repository → Model
            ↓
          DTO

Example: User Registration with Email Verification

// 1. Controller (thin)
class UserController extends Controller
{
    public function store(
        StoreUserRequest $request,
        RegisterUserAction $action
    ): JsonResponse {
        $user = $action->execute($request->dto());
        
        return response()->json(
            new UserResource($user),
            201
        );
    }
}

// 2. Form Request (validation + DTO creation)
class StoreUserRequest extends FormRequest
{
    public function rules(): array { /* ... */ }
    
    public function dto(): UserDTO
    {
        return UserDTO::fromRequest($this);
    }
}

// 3. DTO (type-safe data transfer)
class UserDTO
{
    public function __construct(
        public readonly string $email,
        public readonly string $password,
        public readonly string $name,
    ) {}
    
    public static function fromRequest(StoreUserRequest $request): self
    {
        return new self(
            email: $request->validated('email'),
            password: $request->validated('password'),
            name: $request->validated('name'),
        );
    }
}

// 4. Action (orchestrates the workflow)
class RegisterUserAction
{
    public function __construct(
        private UserService $userService,
        private EmailService $emailService,
    ) {}
    
    public function execute(UserDTO $dto): User
    {
        $user = $this->userService->register($dto);
        $this->emailService->sendVerificationEmail($user);
        
        return $user;
    }
}

// 5. Service (business logic)
class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private HashService $hashService,
    ) {}
    
    public function register(UserDTO $dto): User
    {
        return $this->repository->create([
            'email' => $dto->email,
            'password' => $this->hashService->make($dto->password),
            'name' => $dto->name,
        ]);
    }
}

// 6. Repository (data access)
class EloquentUserRepository implements UserRepositoryInterface
{
    public function create(array $data): User
    {
        return User::create($data);
    }
}

7.1.2 Simplified Pattern Stack (Simple CRUD)

For simple CRUD operations, skip Actions and use Service + Repository:

Controller → Service → Repository → Model
            ↓
          DTO (optional, but recommended)

Example: Simple User Update

// Controller
class UserController extends Controller
{
    public function update(
        UpdateUserRequest $request,
        UserService $service,
        User $user
    ): JsonResponse {
        $this->authorize('update', $user);
        
        $updated = $service->update($user, $request->dto());
        
        return response()->json(new UserResource($updated));
    }
}

// Service (directly handles the operation)
class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}
    
    public function update(User $user, UserDTO $dto): User
    {
        return $this->repository->update($user, [
            'name' => $dto->name,
            'email' => $dto->email,
        ]);
    }
}

7.1.3 Minimal Pattern Stack (Very Simple Operations)

For very simple operations (single model update), you can skip Service layer:

Controller → Repository → Model
            ↓
          DTO (still recommended for type safety)

Example: Toggle User Status

// Controller
class UserController extends Controller
{
    public function toggleStatus(
        UserRepositoryInterface $repository,
        User $user
    ): JsonResponse {
        $this->authorize('update', $user);
        
        $user = $repository->toggleStatus($user);
        
        return response()->json(new UserResource($user));
    }
}

7.1.4 Decision Matrix

Operation Complexity Action Service Repository DTO
Complex (multiple services, orchestration) ✅ Required ✅ Required ✅ Required ✅ Required
Medium (business logic, single service) ❌ Skip ✅ Required ✅ Required ✅ Recommended
Simple (direct data access) ❌ Skip ❌ Skip ✅ Required ✅ Recommended
Very Simple (single field update) ❌ Skip ❌ Skip ✅ Required ⚠️ Optional

7.1.5 Key Guidelines

  1. Always use DTOs for data transfer (prevents array abuse, ensures type safety)
  2. Always use Repositories for data access (decouples from Eloquent)
  3. Use Services when there's business logic (validation, calculations, side effects)
  4. Use Actions only for complex orchestration (multiple services, workflow)
  5. Follow YAGNI - don't add layers you don't need
  6. Start simple - add complexity only when needed

7.1.6 Anti-Pattern: Over-Engineering

Don't do this - Using all patterns for a simple operation:

// Over-engineered: Simple operation with unnecessary Action layer
class UpdateUserNameAction
{
    public function __construct(private UserService $service) {}
    
    public function execute(User $user, UserDTO $dto): User
    {
        return $this->service->updateName($user, $dto->name);
    }
}

class UserService
{
    public function updateName(User $user, string $name): User
    {
        return $this->repository->update($user, ['name' => $name]);
    }
}

Do this instead - Direct service call:

class UserService
{
    public function updateName(User $user, string $name): User
    {
        return $this->repository->update($user, ['name' => $name]);
    }
}

8. Database & MySQL Standards

8.1 Migrations

  • No raw SQL unless required
  • Always reversible (implement down() method)
  • Use descriptive migration names
  • Add indexes in migrations, not later
  • Use foreign key constraints
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')
        ->constrained()
        ->cascadeOnDelete();
    $table->string('status')->index();
    $table->decimal('total', 10, 2);
    $table->timestamps();
    $table->softDeletes();

    $table->index(['user_id', 'status']);
});

Schema::dropIfExists('orders');

8.2 Indexing

  • Index:

    • Foreign keys (automatic in Laravel)
    • Search columns (WHERE, ORDER BY)
    • Unique constraints
    • Composite indexes for multi-column queries
  • Use explain() to analyze query performance

  • Avoid over-indexing (slows writes)

8.3 Eloquent Best Practices

  • Avoid N+1 queries (with(), load() mandatory)

  • Use select() to limit columns when needed

  • Use chunk() or cursor() for large datasets

  • No logic in models except:

    • Accessors
    • Mutators
    • Scopes
    • Relationships
    • Casts
// Good: Eager loading
$users = User::with('orders')->get();

// Good: Chunking
User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // Process
    }
});

// Good: Scopes
class User extends Model
{
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }
}

8.4 Soft Deletes

  • Use soft deletes for audit trails
  • Always check for soft-deleted records in unique validations
  • Use withTrashed() when needed
class User extends Model
{
    use SoftDeletes;

    // In validation
    Rule::unique('users')->whereNull('deleted_at')
}

8.5 Model Factories & Seeders

  • Use Factories for test data
  • Use Seeders for development data
  • Keep factories realistic
class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => Hash::make('password'),
        ];
    }

    public function suspended(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => UserStatus::SUSPENDED,
        ]);
    }
}

8.6 Query Builder vs Eloquent

  • Prefer Eloquent for model operations
  • Use Query Builder for complex queries or when Eloquent is inefficient
  • Always use parameter binding (never raw strings)
// Good: Eloquent
User::where('status', 'active')->get();

// Good: Query Builder for complex queries
DB::table('users')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', DB::raw('COUNT(orders.id) as order_count'))
    ->groupBy('users.id')
    ->get();

8.7 Advanced MySQL Standards

8.7.1 Index Strategy

  • Primary Keys: Always use auto-incrementing integers or UUIDs
  • Foreign Keys: Always indexed (automatic in Laravel)
  • Composite Indexes: Order columns by selectivity (most selective first)
  • Covering Indexes: Include frequently selected columns in indexes
  • Partial Indexes: Use where clause in indexes for filtered queries (MySQL 8.0+)
// Composite index - order matters
$table->index(['status', 'created_at']); // Good if filtering by status first

// Covering index (includes columns in SELECT)
$table->index(['user_id', 'status', 'total']); // If frequently selecting these

// Partial index (MySQL 8.0+)
DB::statement('CREATE INDEX idx_active_users ON users (email) WHERE status = "active"');

8.7.2 Data Types Best Practices

  • Strings: Use appropriate length (VARCHAR(255) vs TEXT)
  • Numbers: Use smallest appropriate type (TINYINT for status, DECIMAL for money)
  • Dates: Use TIMESTAMP or DATETIME appropriately
  • JSON: Use JSON type for structured data (MySQL 5.7+)
  • UUIDs: Use CHAR(36) or BINARY(16) for UUIDs
// Good: Appropriate data types
$table->string('email', 255)->index();
$table->tinyInteger('status')->default(1); // 0-255 range
$table->decimal('price', 10, 2); // Money: 10 digits, 2 decimal places
$table->json('metadata'); // Structured data
$table->uuid('external_id')->index(); // UUID for external systems

8.7.3 Query Optimization

  • Use EXPLAIN: Always analyze slow queries
  • **Avoid SELECT ***: Select only needed columns
  • Limit results: Use limit() and offset() appropriately
  • Use EXISTS: Prefer exists() over count() for existence checks
  • Avoid functions in WHERE: Don't use functions on indexed columns
// Bad: SELECT *
$users = User::all(); // ❌

// Good: Select specific columns
$users = User::select('id', 'name', 'email')->get(); // ✅

// Bad: Function in WHERE
User::whereRaw('YEAR(created_at) = ?', [2024])->get(); // ❌

// Good: Range query
User::whereBetween('created_at', ['2024-01-01', '2024-12-31'])->get(); // ✅

// Bad: Count for existence
if (User::where('email', $email)->count() > 0) { } // ❌

// Good: Exists check
if (User::where('email', $email)->exists()) { } // ✅

8.7.4 Connection Pooling & Transactions

  • Use connection pooling in production
  • Keep transactions short: Don't hold transactions open
  • Use read replicas for read-heavy applications
  • Monitor connection count: Avoid connection exhaustion
// Good: Short transaction
DB::transaction(function () {
    $order = Order::create($data);
    Payment::create(['order_id' => $order->id]);
    // Transaction commits here
});

// Bad: Long transaction
DB::beginTransaction();
// ... long operation ...
// ... external API call ... // ❌ Don't do this in transaction
DB::commit();

8.7.5 Partitioning (Advanced)

  • Partition large tables by date or range
  • Use partitioning for tables with millions of rows
  • Partition by date for time-series data
// Partition by date (requires raw SQL)
DB::statement('
    ALTER TABLE orders 
    PARTITION BY RANGE (YEAR(created_at)) (
        PARTITION p2023 VALUES LESS THAN (2024),
        PARTITION p2024 VALUES LESS THAN (2025),
        PARTITION p2025 VALUES LESS THAN (2026)
    )
');

8.7.6 Full-Text Search

  • Use FULLTEXT indexes for search functionality
  • Use MySQL 5.6+ for InnoDB full-text search
  • Consider Elasticsearch for complex search requirements
// Migration
$table->fullText(['title', 'description']);

// Query
User::whereFullText(['title', 'description'], 'search term')->get();

8.7.7 Database Monitoring

  • Enable slow query log: Monitor queries > 1 second
  • Use query logging in development (never in production)
  • Monitor index usage: Remove unused indexes
  • Track table sizes: Monitor growth patterns
// In AppServiceProvider (development only)
if (app()->environment('local')) {
    DB::listen(function ($query) {
        if ($query->time > 1000) { // Log slow queries
            Log::warning('Slow query', [
                'sql' => $query->sql,
                'time' => $query->time,
            ]);
        }
    });
}

8.7.8 Backup & Recovery

  • Regular backups: Daily at minimum
  • Test restores: Verify backup integrity
  • Point-in-time recovery: Use binary logs
  • Backup before migrations: Always backup before schema changes

8.7.9 Character Sets & Collations

  • Use UTF8MB4: Full UTF-8 support (emojis, special characters)
  • Consistent collation: Use utf8mb4_unicode_ci for case-insensitive
  • Set at database level: Configure in config/database.php
// config/database.php
'mysql' => [
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
],

9. Transactions

  • Mandatory for multi‑step writes
  • Use transactions for data consistency
  • Handle transaction failures appropriately
  • Consider transaction isolation levels when needed
// Basic transaction
DB::transaction(function () {
    $order = $this->orderRepo->create(...);
    $this->paymentRepo->capture($order, ...);
    $this->inventoryRepo->decrement(...);
});

// With return value
$result = DB::transaction(function () {
    return $this->service->processOrder(...);
});

// With manual control
DB::beginTransaction();
try {
    $order = $this->orderRepo->create(...);
    $this->paymentRepo->capture($order, ...);
    DB::commit();
} catch (Throwable $e) {
    DB::rollBack();
    throw $e;
}

10. Testing Standards

10.1 Testing Framework

  • Pest (mandatory)
  • Use Pest's higher-order expectations when possible
  • Organize tests by feature/domain

10.2 Test Types

  • Unit tests: Test individual classes/methods in isolation
  • Feature tests: Test HTTP endpoints and user flows
  • Integration tests: Test interactions between components
// Feature Test
it('creates a user successfully', function () {
    $response = postJson('/api/v1/users', [
        'email' => 'test@example.com',
        'password' => 'Password123',
        'name' => 'Test User',
    ]);

    $response->assertCreated()
        ->assertJsonStructure([
            'data' => ['id', 'email', 'name'],
        ]);

    $this->assertDatabaseHas('users', [
        'email' => 'test@example.com',
    ]);
});

// Unit Test
it('calculates order total correctly', function () {
    $order = new Order(['subtotal' => 100, 'tax' => 10]);
    
    expect($order->total)->toBe(110.0);
});

10.3 Test Organization

  • Follow AAA pattern (Arrange, Act, Assert)
  • Use descriptive test names
  • One assertion per test when possible
  • Use factories for test data
  • Clean up after tests (use RefreshDatabase)
uses(RefreshDatabase::class);

it('updates user email', function () {
    // Arrange
    $user = User::factory()->create();
    $newEmail = 'new@example.com';

    // Act
    $response = putJson("/api/v1/users/{$user->id}", [
        'email' => $newEmail,
    ]);

    // Assert
    $response->assertOk();
    expect($user->fresh()->email)->toBe($newEmail);
});

10.4 Test Data Management

  • Use Factories for model creation
  • Use Seeders sparingly in tests
  • Use RefreshDatabase trait for database tests
  • Use DatabaseTransactions for performance when appropriate
uses(RefreshDatabase::class);

beforeEach(function () {
    $this->user = User::factory()->create();
    $this->actingAs($this->user);
});

10.5 Mocking & Stubs

  • Mock external services (APIs, payment gateways)
  • Use Mockery or Pest's mocking helpers
  • Avoid over-mocking (prefer real implementations when possible)
it('sends email when user registers', function () {
    Mail::fake();

    postJson('/api/v1/users', [...]);

    Mail::assertSent(WelcomeEmail::class);
});

10.6 Coverage

  • Minimum 80% coverage
  • Business logic must be unit tested
  • Critical paths must have feature tests
  • Use --coverage flag to generate reports
  • Focus on meaningful coverage, not just numbers

11. Code Style & Formatting

11.1 Laravel Pint (Mandatory)

./vendor/bin/pint
  • Must pass in CI
  • No custom styles unless approved

12. Error Handling & Logging

12.1 Exceptions

  • Use domain‑specific exceptions
  • Create custom exception classes for business logic errors
  • Use appropriate HTTP status codes
  • Always provide meaningful error messages
// Custom Exception
class UserAlreadyExistsException extends Exception
{
    public function __construct(string $email)
    {
        parent::__construct("User with email {$email} already exists.");
    }

    public function render(Request $request): JsonResponse
    {
        return response()->json([
            'message' => $this->getMessage(),
        ], 409);
    }
}

// Usage
if (User::where('email', $email)->exists()) {
    throw new UserAlreadyExistsException($email);
}

12.2 Exception Handling

  • Use global exception handler for consistent error responses
  • Log exceptions with context
  • Never expose sensitive information in error messages
  • Use different log levels appropriately
// In App\Exceptions\Handler
public function register(): void
{
    $this->renderable(function (UserAlreadyExistsException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => $e->getMessage(),
            ], 409);
        }
    });
}

12.3 Logging

  • Use structured logs with context
  • Use appropriate log levels (debug, info, warning, error, critical)
  • Never log sensitive data (passwords, tokens, credit cards)
  • Use channels for different log types
// Good: Structured logging
Log::info('User registered', [
    'user_id' => $user->id,
    'email' => $user->email,
]);

Log::error('Payment failed', [
    'order_id' => $order->id,
    'amount' => $order->total,
    'error_code' => $exception->getCode(),
]);

// Bad: Logging sensitive data
Log::info('User login', [
    'password' => $request->password, // ❌ Never do this
]);

13. Security Standards

13.1 Secrets & Configuration

  • No secrets in code or version control
  • Use .env for configuration (never commit .env)
  • Use Laravel's config() helper, never env() directly in code
  • Rotate secrets regularly
  • Use different keys for different environments
// Good: Use config
$apiKey = config('services.payment.api_key');

// Bad: Direct env access
$apiKey = env('PAYMENT_API_KEY'); // ❌

13.2 Authentication & Authorization

  • Use Laravel's built-in authentication
  • Always use Policies or Gates for authorization
  • Check authorization in controllers and form requests
  • Use middleware for route-level protection
// Policy
class OrderPolicy
{
    public function update(User $user, Order $order): bool
    {
        return $user->id === $order->user_id;
    }
}

// In Controller
public function update(UpdateOrderRequest $request, Order $order)
{
    $this->authorize('update', $order);
    // ...
}

13.3 Password Security

  • Always hash passwords using Hash facade
  • Use strong password requirements
  • Never store plain text passwords
  • Use password reset tokens securely
// Hashing
$hashed = Hash::make($password);

// Verification
if (Hash::check($password, $user->password)) {
    // Valid
}

13.4 Mass Assignment Protection

  • Always use $fillable or $guarded in models
  • Never use $request->all() without filtering
  • Use Form Requests with validated data
// Model
class User extends Model
{
    protected $fillable = ['name', 'email', 'password'];
    // OR
    protected $guarded = ['id', 'is_admin'];
}

// Controller
public function update(UpdateUserRequest $request, User $user)
{
    $user->update($request->validated()); // ✅ Safe
    // NOT: $user->update($request->all()); // ❌
}

13.5 SQL Injection Prevention

  • Always use Eloquent or Query Builder (parameter binding)
  • Never use raw queries with user input
  • Use whereRaw() only with bindings
// Good: Parameter binding
User::where('email', $email)->first();

// Good: Raw with bindings
DB::select('SELECT * FROM users WHERE email = ?', [$email]);

// Bad: Raw without bindings
DB::select("SELECT * FROM users WHERE email = '{$email}'"); // ❌

13.6 XSS Prevention

  • Always escape output in Blade templates
  • Use {!! !!} only when necessary and data is trusted
  • Sanitize user input
// Blade (auto-escaped)
{{ $user->name }} // ✅ Safe

// Only when trusted
{!! $trustedHtml !!} // Use carefully

13.7 CSRF Protection

  • CSRF protection is enabled by default
  • Always include @csrf in forms
  • Use VerifyCsrfToken middleware for API routes if needed

13.8 Rate Limiting

  • Use Laravel's rate limiting middleware
  • Implement rate limits for authentication endpoints
  • Use different limits for different user types
// In routes
Route::post('/login')->middleware('throttle:5,1'); // 5 attempts per minute

13.9 Input Validation

  • Always validate and sanitize user input
  • Use Form Requests for validation
  • Validate file uploads (type, size)
  • Never trust client-side validation alone

14. Performance & Scalability

14.1 Database Performance

  • Always eager load relationships (with(), load())
  • Use select() to limit columns when fetching large datasets
  • Use chunk() or cursor() for processing large collections
  • Add database indexes for frequently queried columns
  • Use explain() to analyze slow queries
  • Avoid N+1 query problems
// Good: Eager loading
$users = User::with(['orders', 'profile'])->get();

// Good: Chunking
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process
    }
});

// Good: Selecting specific columns
User::select('id', 'name', 'email')->get();

14.2 Caching

  • Cache expensive operations
  • Use appropriate cache drivers (Redis recommended for production)
  • Set reasonable TTL values
  • Invalidate cache when data changes
  • Use cache tags when available
// Simple caching
$users = Cache::remember('users.active', 600, function () {
    return User::where('status', 'active')->get();
});

// Cache with tags (Redis)
Cache::tags(['users', 'active'])
    ->remember('users.active', 600, fn () => ...);

// Cache invalidation
Cache::forget('users.active');
Cache::tags(['users'])->flush();

14.3 Queues & Jobs

  • Use queues for heavy tasks (emails, file processing, API calls)
  • Use appropriate queue drivers (Redis, database, SQS)
  • Implement retry logic for failed jobs
  • Use job batching for related operations
  • Monitor queue performance
// Dispatch job
ProcessPayment::dispatch($order);

// With delay
SendEmail::dispatch($user)->delay(now()->addMinutes(5));

// Job batching
Bus::batch([
    new ProcessOrder($order1),
    new ProcessOrder($order2),
])->dispatch();

14.4 API Performance

  • Avoid synchronous external API calls in request cycle
  • Use HTTP client connection pooling
  • Implement timeouts for external requests
  • Cache API responses when appropriate
  • Use async processing for non-critical API calls
// Use queues for external APIs
CallExternalAPI::dispatch($data);

// Or use async HTTP
Http::timeout(5)->get('https://api.example.com/data');

14.5 Code Optimization

  • Avoid unnecessary loops
  • Use collection methods efficiently
  • Profile code to identify bottlenecks
  • Use Laravel Debugbar in development (never in production)
  • Optimize autoloading with composer dump-autoload -o

15. Observability & Performance Practices

(Laravel / PHP / MySQL / Distributed Systems)


15.1 Observability Principles

If you can't measure it, you can't scale it.

All production systems MUST be observable by default. Observability is not logging alone — it consists of three pillars:

The Three Pillars

  1. Logs – What happened
  2. Metrics – How often / how long / how much
  3. Traces – Where time was spent

Every new feature, background job, or integration must be evaluated against all three pillars.


15.2 Logging Standards

15.2.1 Logging Levels (Mandatory)

Use structured logging with correct severity:

Level Usage
debug Development-only diagnostic info
info Normal business events
warning Recoverable issues
error Failed operations requiring attention
critical System-wide failures

🚫 Never log:

  • Plain text dumps
  • Sensitive data (tokens, passwords, PII)
  • Full request/response bodies in production

15.2.2 Structured Logging (Required)

All logs must be structured (JSON) and include contextual metadata.

Required fields:

  • request_id
  • user_id (if available)
  • service / app_name
  • environment
  • duration_ms (where applicable)
Log::info('Order placed', [
    'order_id' => $order->id,
    'user_id' => auth()->id(),
    'request_id' => request()->header('X-Request-ID'),
]);

15.2.3 Correlation IDs

Every incoming request must have a correlation/request ID:

  • Generated at the edge (NGINX / Load Balancer)

  • Propagated through:

    • HTTP headers
    • Queue jobs
    • External API calls

This enables end-to-end tracing across services.


15.3 Metrics & Monitoring

15.3.1 Key Metrics (Must Track)

Application Metrics
  • Request count
  • Error rate
  • Latency (p50, p95, p99)
  • Queue depth
  • Job failure rate
System Metrics
  • CPU, memory usage
  • Disk I/O
  • Network latency
Business Metrics
  • Orders created
  • Payments processed
  • Failed transactions

15.3.2 Service Level Indicators (SLIs)

Each service must define:

  • Availability SLI
  • Latency SLI
  • Error SLI

Example:

99.9% of API requests respond in < 300ms

15.3.3 Alerts (Actionable Only)

Alerts MUST be:

  • Actionable
  • Time-bound
  • Tied to customer impact

🚫 Do not alert on:

  • Single errors
  • Non-user impacting warnings

✅ Alert on:

  • Error rate spikes
  • Queue backlog growth
  • SLA violations

15.4 Distributed Tracing

15.4.1 Request Tracing

Use distributed tracing to understand:

  • Request flow
  • Bottlenecks
  • External service latency

Trace:

  • Controllers
  • DB queries
  • External HTTP calls
  • Queue jobs

15.4.2 Queue & Job Observability

For each job, track:

  • Execution time
  • Retry count
  • Failure reason
  • Payload size
public function handle(): void
{
    $start = microtime(true);

    try {
        // Job logic
    } finally {
        Log::info('Job executed', [
            'job' => self::class,
            'duration_ms' => (microtime(true) - $start) * 1000,
        ]);
    }
}

15.5 Performance Engineering Guidelines

15.5.1 Performance Budgets

Each endpoint must define:

  • Max response time
  • Max DB queries
  • Max payload size

Example:

  • API endpoint: ≤ 200ms
  • DB queries: ≤ 10
  • Payload: ≤ 50KB

15.5.2 Database Performance

Required Practices
  • Use indexes intentionally
  • Avoid SELECT *
  • Use pagination for large datasets
  • Avoid N+1 queries (use with())
User::with('roles')->paginate(25);
Query Analysis
  • Enable slow query logging
  • Review slow queries regularly
  • Use EXPLAIN for heavy queries

15.5.3 Caching Strategy

Use caching by design, not as a patch.

Cache Types
  • Request-level (memoization)
  • Application-level (Redis)
  • HTTP-level (CDN)
Cache::remember(
    "user:{$id}",
    now()->addMinutes(10),
    fn () => User::findOrFail($id)
);

15.5.4 Async & Background Processing

Move non-critical tasks to queues:

  • Emails
  • Notifications
  • Reports
  • Webhooks

Never block user requests with:

  • External APIs
  • File processing
  • Heavy DB operations

15.6 Load, Stress & Capacity Testing

15.6.1 Load Testing (Mandatory for Major Features)

Before production:

  • Simulate real traffic
  • Identify breaking points
  • Measure response degradation

Test scenarios:

  • Peak traffic
  • Burst traffic
  • Dependency failures

15.6.2 Capacity Planning

Track:

  • Requests/sec
  • Jobs/sec
  • DB connections
  • Queue throughput

Use data to forecast:

  • Infrastructure needs
  • Cost optimization
  • Scaling limits

15.7 Failure Handling & Resilience

15.7.1 Timeouts & Retries

All external calls must define:

  • Timeouts
  • Retry limits
  • Backoff strategy
Http::timeout(3)
    ->retry(3, 200)
    ->get($url);

15.7.2 Graceful Degradation

When dependencies fail:

  • Serve cached data
  • Disable non-essential features
  • Return meaningful errors

15.8 Performance Reviews & Continuous Improvement

15.8.1 Performance as a First-Class Concern

Performance should be reviewed during:

  • Design reviews
  • PR reviews
  • Post-incident reviews

15.8.2 Post-Incident Analysis (Blameless)

After incidents:

  • Identify root cause
  • Improve monitoring
  • Prevent recurrence

No blame. Focus on systems, not people.


15.9 Definition of Done (Observability & Performance)

A feature is NOT DONE unless:

  • Logs are structured
  • Metrics are emitted
  • Alerts are defined
  • Performance budget is respected
  • Failure scenarios are handled

15.10 Architect's Note

Observability is not a tool — it's a design discipline.

Teams that bake observability into their systems ship faster, debug less, and scale with confidence.


16. API Standards

16.1 RESTful Design

  • Use RESTful naming conventions
  • Use appropriate HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Use proper HTTP status codes
  • Version all APIs
// Routes
Route::get('/api/v1/users', [UserController::class, 'index']);
Route::post('/api/v1/users', [UserController::class, 'store']);
Route::get('/api/v1/users/{user}', [UserController::class, 'show']);
Route::put('/api/v1/users/{user}', [UserController::class, 'update']);
Route::delete('/api/v1/users/{user}', [UserController::class, 'destroy']);

16.2 Response Structure

  • Use consistent response structure
  • Always use API Resources for responses
  • Include metadata for paginated responses
  • Return appropriate status codes
// Success response
return response()->json([
    'data' => new UserResource($user),
], 201);

// Paginated response
return UserResource::collection($users)->response()->getData(true);
// Returns: { data: [...], links: {...}, meta: {...} }

// Error response
return response()->json([
    'message' => 'Validation failed',
    'errors' => $validator->errors(),
], 422);

16.3 API Versioning

  • Version all APIs in URL path
  • Maintain backward compatibility when possible
  • Document breaking changes
  • Use semantic versioning (v1, v2, etc.)

16.4 API Documentation

  • Document all API endpoints
  • Use OpenAPI/Swagger or similar
  • Include request/response examples
  • Document authentication requirements
  • Keep documentation up to date

17. CI/CD Requirements

17.1 Mandatory Checks

  • Laravel Pint: Code formatting
  • Pest: Test suite (must pass)
  • PHPStan: Static analysis (level 8+)
  • Coverage: Minimum 80% (must not decrease)

17.2 Pipeline Stages

  1. Lint: Run Pint
  2. Analyze: Run PHPStan
  3. Test: Run Pest with coverage
  4. Build: Verify application builds
  5. Deploy: Automated deployment (if configured)

17.3 Branch Protection

  • No direct push to main/master
  • Require pull request reviews
  • Require status checks to pass
  • Require branches to be up to date
  • Require linear history (no merge commits)

17.4 Pre-commit Hooks (Recommended)

  • Run Pint before commit
  • Run PHPStan on changed files
  • Run relevant tests

18. Documentation

18.1 Code Documentation

  • PHPDoc required for:

    • Public classes and methods
    • Complex logic
    • Non-obvious code
    • Parameters and return types
  • Use meaningful docblocks

/**
 * Creates a new user account.
 *
 * @param UserDTO $dto The user data transfer object
 * @return User The newly created user
 * @throws UserAlreadyExistsException If user with email already exists
 * @throws ValidationException If validation fails
 */
public function createUser(UserDTO $dto): User
{
    // Implementation
}

18.2 API Documentation

  • Document all API endpoints
  • Include request/response examples
  • Document authentication requirements
  • Keep documentation synchronized with code
  • Use tools like Laravel API Documentation or Swagger/OpenAPI

18.3 README Files

  • Maintain up-to-date README.md
  • Include setup instructions
  • Document environment variables
  • Include examples
  • Document common tasks

19. Git & Version Control

19.1 Branch Naming

  • Use descriptive branch names
  • Follow convention: type/description or ticket-number/description
  • Types: feature/, bugfix/, hotfix/, refactor/
feature/user-authentication
bugfix/payment-gateway-error
hotfix/security-patch

19.2 Commit Messages

  • Use clear, descriptive commit messages
  • Follow conventional commits format (optional but recommended)
  • Reference ticket/issue numbers
  • Keep commits focused and atomic
feat: add user registration endpoint

- Implement CreateUserAction
- Add UserDTO
- Add validation rules
- Add tests

Closes #123

19.3 Pull Request Guidelines

  • Keep PRs small and focused
  • One feature/fix per PR
  • Include description and context
  • Link to related tickets/issues
  • Update documentation if needed

20. Composer & Dependencies

20.1 Dependency Management

  • Pin versions in composer.json (avoid *)
  • Use ^ for compatible versions
  • Regularly update dependencies
  • Review security advisories
  • Use composer audit to check for vulnerabilities
{
    "require": {
        "laravel/framework": "^10.0",
        "php": "^8.2"
    }
}

20.2 Autoloading

  • Use PSR-4 autoloading
  • Optimize autoloader in production: composer dump-autoload -o
  • Never commit vendor/ directory
  • Always commit composer.lock

20.3 Package Selection

  • Prefer well-maintained packages
  • Check package statistics (downloads, stars, last update)
  • Review package code quality
  • Prefer Laravel-ecosystem packages when available

21. Environment Configuration

21.1 Environment Files

  • Use .env for local development
  • Use .env.example as template (commit this)
  • Never commit .env file
  • Use different .env files for different environments
  • Document all required environment variables

21.2 Configuration Files

  • Store configuration in config/ directory
  • Use config() helper, never env() directly
  • Cache config in production: php artisan config:cache
  • Use environment-specific config when needed
// config/services.php
return [
    'payment' => [
        'api_key' => env('PAYMENT_API_KEY'),
        'api_url' => env('PAYMENT_API_URL', 'https://api.payment.com'),
    ],
];

// In code
$apiKey = config('services.payment.api_key');

22. Anti‑Patterns (Forbidden)

22.1 Architecture Anti-Patterns

Fat controllers - Controllers with business logic ❌ Business logic in views - Any logic beyond presentation ❌ God objects - Classes that do too much ❌ Anemic domain models - Models with no behavior ❌ Tight coupling - Direct dependencies on concrete classes ❌ Over-engineering - Using all patterns (Action + Service + Repository) for simple operations (violates YAGNI)

22.2 Code Quality Anti-Patterns

Raw queries everywhere - Not using Eloquent/Query Builder ❌ Untyped arrays - Using arrays instead of DTOs ❌ Magic numbers/strings - Hard-coded values without constants ❌ Commented-out code - Dead code should be removed ❌ Copy-paste programming - Duplicated logic

22.3 Database Anti-Patterns

N+1 queries - Not eager loading relationships ❌ Missing indexes - On frequently queried columns ❌ No transactions - For multi-step operations ❌ Raw SQL injection risks - Not using parameter binding ❌ Over-eager loading - Loading unnecessary relationships

22.4 Testing Anti-Patterns

Skipping tests - For "simple" code ❌ Flaky tests - Non-deterministic tests ❌ Testing implementation - Instead of behavior ❌ No test isolation - Tests affecting each other ❌ Mocking everything - Over-mocking

22.5 Security Anti-Patterns

Secrets in code - Hard-coded credentials ❌ No authorization checks - Assuming user permissions ❌ Mass assignment vulnerabilities - Not using fillable/guarded ❌ SQL injection - Raw queries with user input ❌ XSS vulnerabilities - Not escaping output


23. Pull Request (PR) Checklist (Mandatory)

Every Pull Request must include the following checklist and all items must be satisfied before review approval.

23.1 General

  • PR title is clear and descriptive
  • PR scope is small and focused (single responsibility)
  • Linked to ticket / task / issue
  • No unrelated changes included

23.2 Code Quality

  • Code follows PSR-12 and Laravel conventions
  • declare(strict_types=1); used where applicable
  • No duplicated logic
  • No commented-out code
  • Meaningful variable, method, and class names

23.3 Architecture & Design

  • Controllers are thin
  • Business logic resides in Services / Actions
  • Database access only via Repositories
  • DTOs used instead of raw arrays
  • SOLID principles respected
  • No forbidden anti-patterns introduced

23.4 Database & Performance

  • Queries reviewed for N+1 issues
  • Proper indexes added (if applicable)
  • Transactions used for multi-step writes
  • No unnecessary eager loading
  • Caching considered where appropriate

23.5 Testing

  • Tests written using Pest
  • New business logic has unit tests
  • Feature tests updated/added
  • Tests are deterministic (no flaky tests)
  • Coverage does not decrease

23.6 Tooling & CI

  • Laravel Pint passes
  • PHPStan passes (no new errors)
  • CI pipeline is green

23.7 Security

  • No secrets committed
  • Authorization checked (Policies / Gates)
  • Input validated via Form Requests
  • Sensitive data not logged

24. Code Review Rubric

Reviewers must evaluate PRs using the following rubric. Approval requires no Critical issues and all Major issues resolved.

24.1 Severity Levels

Level Description
Critical Must fix before merge
Major Should fix before merge
Minor Can fix later
Nit Style or preference

24.2 Evaluation Criteria

1. Correctness (Critical)

  • Does the code do what the ticket describes?
  • Are edge cases handled?
  • Are error paths covered?

2. Architecture & Design (Critical / Major)

  • Proper separation of concerns
  • No business logic in controllers
  • Correct use of services, actions, repositories
  • Easy to extend without modification

3. Readability & Maintainability (Major)

  • Clear naming
  • Small, focused methods
  • Self-explanatory code
  • Adequate inline comments where needed

4. Performance & Scalability (Major)

  • Efficient database access
  • No unnecessary loops or queries
  • Background jobs used where appropriate
  • Caching strategy considered

5. Testing Quality (Critical)

  • Tests exist and are meaningful
  • Tests assert behavior, not implementation
  • Edge cases tested

6. Security (Critical)

  • Proper authorization
  • Input validation present
  • No mass-assignment vulnerabilities
  • No sensitive data exposure

7. Consistency & Standards (Major)

  • Conforms to this standards guide
  • Matches existing patterns
  • Tooling compliance (Pint, PHPStan, Pest)

25. Review Decision Matrix

Condition Decision
Any Critical issue ❌ Reject
Major issues present 🔄 Changes requested
Only Minor/Nits ✅ Approve

26. Reviewer Responsibilities

  • Be constructive and specific
  • Suggest improvements, not just problems
  • Enforce standards consistently
  • Focus on long-term maintainability
  • Review within 24-48 hours when possible
  • Approve promptly when standards are met

27. Author Responsibilities

  • Respond to all comments
  • Push fixes promptly
  • Do not dismiss feedback without justification
  • Keep PRs up to date with main
  • Request re-review after addressing feedback
  • Be open to suggestions and improvements

28. Final Rule

If it is not tested, typed, documented, reviewed, and formatted — it is not production-ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment