Appearance
🏗️ 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
- Start Simple - Begin with basic patterns and evolve
- Consistent Naming - Use clear, descriptive names
- Interface Segregation - Create focused interfaces
- Dependency Injection - Use Laravel's container
- Test Coverage - Write tests for all patterns
- Documentation - Document complex patterns
📝 Pattern Evolution: Design patterns should evolve with your application. Start with basic patterns and add complexity as needed.