Skip to content

🏗️ Design Patterns

Design patterns are essential for building maintainable, scalable Laravel applications. This section covers the most important patterns used in our backend development.

🏗️ Repository Pattern

The Repository pattern provides an abstraction layer between your application logic and data access logic.

✅ Good Example

php
<?php

namespace App\Repositories;

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

interface UserRepositoryInterface
{
    public function find(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function create(array $data): User;
    public function update(int $id, array $data): bool;
    public function delete(int $id): bool;
}

class UserRepository implements UserRepositoryInterface
{
    public function __construct(
        private User $model
    ) {}

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

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }

    public function create(array $data): User
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): bool
    {
        return $this->model->where('id', $id)->update($data);
    }

    public function delete(int $id): bool
    {
        return $this->model->destroy($id);
    }
}

❌ Bad Example

php
<?php

// Direct model usage in controller - violates separation of concerns
class UserController extends Controller
{
    public function store(Request $request)
    {
        // Business logic mixed with data access
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
        
        // More business logic in controller
        if ($user->email_verified_at) {
            Mail::to($user)->send(new WelcomeEmail($user));
        }
        
        return response()->json($user);
    }
}

🔧 Service Layer Pattern

Services contain business logic and coordinate between different parts of your application.

✅ Good Example

php
<?php

namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

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

    public function createUser(array $data): User
    {
        // Business logic validation
        $this->validateUserData($data);
        
        // Hash password
        $data['password'] = Hash::make($data['password']);
        
        // Create user
        $user = $this->userRepository->create($data);
        
        // Send welcome email
        $this->sendWelcomeEmail($user);
        
        return $user;
    }

    private function validateUserData(array $data): void
    {
        if (empty($data['name']) || empty($data['email'])) {
            throw new \InvalidArgumentException('Name and email are required');
        }
    }

    private function sendWelcomeEmail(User $user): void
    {
        if ($user->email_verified_at) {
            Mail::to($user)->send(new WelcomeEmail($user));
        }
    }
}

❌ Bad Example

php
<?php

// All logic in controller - violates single responsibility
class UserController extends Controller
{
    public function store(Request $request)
    {
        // Validation logic
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);
        
        // Business logic
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
        
        // Email logic
        Mail::to($user)->send(new WelcomeEmail($user));
        
        // Logging logic
        Log::info('User created', ['user_id' => $user->id]);
        
        return response()->json($user);
    }
}

⚡ Action Classes Pattern

Action classes handle single, specific operations with clear responsibilities.

✅ Good Example

php
<?php

namespace App\Actions;

use App\Models\User;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Hash;

class CreateUserAction
{
    public function __construct(
        private UserRepositoryInterface $userRepository
    ) {}

    public function execute(array $data): User
    {
        $this->validateData($data);
        
        $userData = $this->prepareUserData($data);
        
        return $this->userRepository->create($userData);
    }

    private function validateData(array $data): void
    {
        if (empty($data['name']) || empty($data['email'])) {
            throw new \InvalidArgumentException('Name and email are required');
        }
    }

    private function prepareUserData(array $data): array
    {
        return [
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'email_verified_at' => now(),
        ];
    }
}

❌ Bad Example

php
<?php

// Multiple responsibilities in one class
class UserManager
{
    public function createUser(array $data): User
    {
        // Create user
        $user = User::create($data);
        
        // Send email
        Mail::to($user)->send(new WelcomeEmail($user));
        
        // Log activity
        Log::info('User created', ['user_id' => $user->id]);
        
        // Update statistics
        $this->updateUserStats();
        
        // Send notification to admin
        $this->notifyAdmin($user);
        
        return $user;
    }
    
    public function updateUser(int $id, array $data): User
    {
        // Update logic
    }
    
    public function deleteUser(int $id): bool
    {
        // Delete logic
    }
}

🎯 Factory Pattern

Use factories for creating complex objects with different configurations.

✅ Good Example

php
<?php

namespace App\Factories;

use App\Models\User;
use App\Models\Role;

class UserFactory
{
    public static function createAdmin(array $overrides = []): User
    {
        return User::factory()->create(array_merge([
            'role' => 'admin',
            'email_verified_at' => now(),
        ], $overrides));
    }

    public static function createWithRole(Role $role, array $overrides = []): User
    {
        return User::factory()->create(array_merge([
            'role_id' => $role->id,
        ], $overrides));
    }

    public static function createUnverified(array $overrides = []): User
    {
        return User::factory()->create(array_merge([
            'email_verified_at' => null,
        ], $overrides));
    }
}

❌ Bad Example

php
<?php

// Hard-coded user creation without flexibility
class UserController extends Controller
{
    public function createAdmin(Request $request)
    {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'role' => 'admin',
            'email_verified_at' => now(),
        ]);
        
        return response()->json($user);
    }
    
    public function createUser(Request $request)
    {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'role' => 'user',
            'email_verified_at' => null,
        ]);
        
        return response()->json($user);
    }
}

🔄 Observer Pattern

Use observers to handle model events and keep your models clean.

✅ Good Example

php
<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Mail\UserRegistered;

class UserObserver
{
    public function created(User $user): void
    {
        Log::info('User created', ['user_id' => $user->id]);
        
        if ($user->email_verified_at) {
            Mail::to($user)->send(new UserRegistered($user));
        }
    }

    public function updated(User $user): void
    {
        Log::info('User updated', ['user_id' => $user->id]);
    }

    public function deleted(User $user): void
    {
        Log::info('User deleted', ['user_id' => $user->id]);
    }
}

❌ Bad Example

php
<?php

// Business logic in model - violates single responsibility
class User extends Model
{
    protected static function booted()
    {
        static::created(function ($user) {
            // Logging logic
            Log::info('User created', ['user_id' => $user->id]);
            
            // Email logic
            if ($user->email_verified_at) {
                Mail::to($user)->send(new UserRegistered($user));
            }
            
            // Cache logic
            Cache::forget('users_count');
            
            // Statistics logic
            $this->updateUserStatistics();
        });
    }
}

📋 Best Practices Summary

✅ Do's

  • Use Repository Pattern for data access abstraction
  • Implement Service Layer for business logic
  • Create Action Classes for single operations
  • Apply Factory Pattern for object creation
  • Use Observers for model events
  • Keep Controllers Thin - delegate to services/actions
  • Separate Concerns - each class has one responsibility

❌ Don'ts

  • Don't put business logic in controllers
  • Don't access models directly from controllers
  • Don't mix multiple responsibilities in one class
  • Don't put business logic in model events
  • Don't create god classes that do everything
  • Don't skip interfaces for repositories and services

🎯 Implementation Guidelines

  1. Start Simple - Begin with basic patterns and evolve
  2. Consistent Naming - Use clear, descriptive names
  3. Interface Segregation - Create focused interfaces
  4. Dependency Injection - Use Laravel's container
  5. Test Coverage - Write tests for all patterns
  6. Documentation - Document complex patterns

📝 Pattern Evolution: Design patterns should evolve with your application. Start with basic patterns and add complexity as needed.

Built with VitePress