PHP Modern Patterns & Advanced Guide (2025)
Research Date: December 1, 2025 Focus: PHP 8.3+, Laravel 11+, Modern Architecture Patterns Difficulty Level: Moderate to Advanced
Executive Summary
This research compiles modern PHP patterns, best practices, and code examples for PHP 8.3+ and Laravel 11+. It covers typed properties, enums, attributes, repository patterns, service layers, dependency injection, advanced Eloquent queries, and comprehensive testing strategies using Pest PHP.
Key Findings:
- PHP 8.3 introduces readonly class cloning, enhanced enums, and standalone types
- PHP 8.4 released November 2024, PHP 8.5 expected November 2025
- Laravel 11+ emphasizes Service-Action architecture for complex workflows
- Repository pattern with interface abstraction remains best practice for data access
- Pest PHP has become the leading testing framework with 30% less code than PHPUnit
- Type safety through union types, intersection types, and readonly properties is now standard
Table of Contents
- Modern PHP 8.3+ Features
- Laravel Advanced Patterns
- Dependency Injection & Service Container
- Database & ORM Patterns
- Testing & Quality Assurance
- WordPress Modern Patterns
1. Modern PHP 8.3+ Features
1.1 Typed Properties and Parameters
PHP Version: 8.0+ Use Case: Ensure type safety at compile time, prevent runtime errors Benefits: IDE autocomplete, early error detection, self-documenting code
<?php
declare(strict_types=1);
namespace App\Models;
use DateTimeImmutable;
class User
{
// Typed properties (PHP 8.0+)
private int $id;
private string $email;
private ?string $phoneNumber = null; // Nullable type
private DateTimeImmutable $createdAt;
private array $roles = []; // Array type
// Promoted constructor properties (PHP 8.0+)
public function __construct(
private string $firstName,
private string $lastName,
private int $age,
) {
$this->createdAt = new DateTimeImmutable();
}
// Return type declarations
public function getFullName(): string
{
return "{$this->firstName} {$this->lastName}";
}
// Void return type
public function setEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email format');
}
$this->email = $email;
}
}
Type Safety Considerations:
- Always use
declare(strict_types=1)at the top of files - Use nullable types (
?Type) instead of type|null for clarity - Avoid mixed types when possible
- Use union types for multiple allowed types
Testing Approach:
test('user requires valid email format', function () {
$user = new User('John', 'Doe', 30);
expect(fn() => $user->setEmail('invalid-email'))
->toThrow(InvalidArgumentException::class);
});
1.2 Union and Intersection Types
PHP Version: 8.0+ (Union), 8.1+ (Intersection) Use Case: Handle multiple types safely, enforce multiple constraints Benefits: Type flexibility with safety, complex type requirements
<?php
declare(strict_types=1);
namespace App\Services;
use App\Contracts\Loggable;
use App\Contracts\Cacheable;
class DataProcessor
{
/**
* Union types - accepts string OR int
* PHP 8.0+
*/
public function processId(string|int $id): string
{
return is_string($id) ? $id : (string) $id;
}
/**
* Union with null - multiple nullable types
*/
public function findUser(string|int|null $identifier): ?User
{
if ($identifier === null) {
return null;
}
return is_int($identifier)
? User::find($identifier)
: User::where('email', $identifier)->first();
}
/**
* Intersection types - must implement BOTH interfaces
* PHP 8.1+
*/
public function processEntity(Loggable&Cacheable $entity): void
{
// Guaranteed to have both log() and cache() methods
$entity->log('Processing started');
$entity->cache();
}
/**
* DNF (Disjunctive Normal Form) types
* PHP 8.2+
* Combines union and intersection
*/
public function handleResource((Loggable&Cacheable)|null $resource): void
{
if ($resource !== null) {
$resource->log('Handling resource');
$resource->cache();
}
}
}
Type Safety Considerations:
- Union types are checked at runtime, not compile time
- Use type guards (
is_string(),is_int()) to narrow types - Intersection types are more strict than union types
- Cannot mix union and intersection freely (use DNF in PHP 8.2+)
Laravel Integration:
// In routes or controllers
Route::get('/users/{id}', function (string|int $id) {
return User::findOrFail($id);
});
// Form requests with union types
class UpdateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'id' => ['required', 'string|integer'],
'status' => ['required', new Enum(UserStatus::class)],
];
}
public function getId(): string|int
{
return $this->input('id');
}
}
Testing Approach:
test('processId handles both string and int', function () {
$processor = new DataProcessor();
expect($processor->processId(123))->toBe('123')
->and($processor->processId('abc'))->toBe('abc');
});
test('intersection type enforces both interfaces', function () {
$entity = new class implements Loggable, Cacheable {
public function log(string $message): void { }
public function cache(): void { }
};
$processor = new DataProcessor();
expect(fn() => $processor->processEntity($entity))->not->toThrow();
});
1.3 Enums and Readonly Classes
PHP Version: 8.1+ (Enums), 8.2+ (Readonly Classes) Use Case: Type-safe constants, immutable value objects Benefits: Prevents invalid states, IDE support, exhaustive matching
<?php
declare(strict_types=1);
namespace App\Enums;
/**
* Basic enum (backed by string)
* PHP 8.1+
*/
enum UserStatus: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
case SUSPENDED = 'suspended';
case DELETED = 'deleted';
/**
* Enum methods - add behavior
*/
public function label(): string
{
return match($this) {
self::ACTIVE => 'Active User',
self::INACTIVE => 'Inactive',
self::SUSPENDED => 'Suspended',
self::DELETED => 'Deleted',
};
}
public function color(): string
{
return match($this) {
self::ACTIVE => 'green',
self::INACTIVE => 'gray',
self::SUSPENDED => 'orange',
self::DELETED => 'red',
};
}
public function canLogin(): bool
{
return $this === self::ACTIVE;
}
/**
* Static constructor
*/
public static function fromString(string $status): self
{
return self::from($status); // Throws ValueError if invalid
}
public static function tryFromString(string $status): ?self
{
return self::tryFrom($status); // Returns null if invalid
}
}
/**
* Enum with interface implementation
*/
enum OrderStatus: string implements \JsonSerializable
{
case PENDING = 'pending';
case PROCESSING = 'processing';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
public function jsonSerialize(): string
{
return $this->value;
}
public function isTerminal(): bool
{
return in_array($this, [self::DELIVERED, self::CANCELLED], true);
}
}
/**
* Pure enum (not backed)
*/
enum Permission
{
case VIEW;
case CREATE;
case UPDATE;
case DELETE;
case ADMIN;
}
Readonly Classes (PHP 8.2+):
<?php
declare(strict_types=1);
namespace App\ValueObjects;
/**
* Readonly class - all properties are readonly
* PHP 8.2+
*/
readonly class Money
{
public function __construct(
public int $amount, // In cents
public string $currency,
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Amount cannot be negative');
}
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException('Currency must be 3-letter ISO code');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Cannot add different currencies');
}
return new self(
$this->amount + $other->amount,
$this->currency
);
}
public function format(): string
{
$formatted = number_format($this->amount / 100, 2);
return "{$this->currency} {$formatted}";
}
}
/**
* Readonly class with enum
*/
readonly class Address
{
public function __construct(
public string $street,
public string $city,
public string $postalCode,
public Country $country, // Enum type
) {}
public function toArray(): array
{
return [
'street' => $this->street,
'city' => $this->city,
'postal_code' => $this->postalCode,
'country' => $this->country->value,
];
}
}
enum Country: string
{
case USA = 'US';
case CANADA = 'CA';
case UK = 'GB';
case GERMANY = 'DE';
}
Laravel Integration:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Enums\UserStatus;
class User extends Model
{
protected $casts = [
'status' => UserStatus::class, // Auto-cast to enum
];
public function activate(): void
{
$this->status = UserStatus::ACTIVE;
$this->save();
}
public function canPerformAction(): bool
{
return $this->status->canLogin();
}
}
// In migrations
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->enum('status', ['active', 'inactive', 'suspended', 'deleted'])
->default('active');
});
// In validation
public function rules(): array
{
return [
'status' => ['required', new Enum(UserStatus::class)],
];
}
Type Safety Considerations:
- Use backed enums (string/int) for database storage
- Enums are singletons - use
===for comparison - Readonly classes prevent accidental mutation
- Cannot use
clonewith readonly (PHP 8.3 allows in __clone)
Testing Approach:
use App\Enums\UserStatus;
use App\ValueObjects\Money;
test('enum provides type safety', function () {
$status = UserStatus::ACTIVE;
expect($status->canLogin())->toBeTrue()
->and($status->label())->toBe('Active User')
->and($status->color())->toBe('green');
});
test('enum handles invalid values', function () {
expect(fn() => UserStatus::from('invalid'))
->toThrow(ValueError::class);
expect(UserStatus::tryFrom('invalid'))->toBeNull();
});
test('readonly class prevents mutation', function () {
$money = new Money(1000, 'USD');
expect(fn() => $money->amount = 2000)
->toThrow(Error::class, 'Cannot modify readonly property');
});
test('readonly class immutable operations', function () {
$money1 = new Money(1000, 'USD');
$money2 = new Money(500, 'USD');
$total = $money1->add($money2);
expect($total->amount)->toBe(1500)
->and($money1->amount)->toBe(1000); // Original unchanged
});
1.4 Attributes (Annotations)
PHP Version: 8.0+ Use Case: Metadata for classes, methods, properties without docblocks Benefits: Native PHP support, reflection API access, framework integration
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
/**
* Custom attribute definition
* PHP 8.0+
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
class Cached
{
public function __construct(
public int $ttl = 3600,
public ?string $key = null,
) {}
}
#[Attribute(Attribute::TARGET_CLASS)]
class Entity
{
public function __construct(
public string $table,
public ?string $connection = null,
) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
class Inject
{
public function __construct(
public string $service,
) {}
}
/**
* Route attribute for controllers
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
public array $middleware = [],
) {}
}
Using Attributes:
namespace App\Services;
use App\Attributes\Cached;
use App\Attributes\Route;
class UserService
{
/**
* Cache method result for 1 hour
*/
#[Cached(ttl: 3600, key: 'user_list')]
public function getAllUsers(): array
{
return User::all()->toArray();
}
/**
* Cache for 5 minutes
*/
#[Cached(ttl: 300)]
public function getUserById(int $id): ?User
{
return User::find($id);
}
}
/**
* Controller with route attributes
*/
#[Entity(table: 'users', connection: 'mysql')]
class UserController
{
#[Route('/api/users', method: 'GET', middleware: ['auth'])]
public function index(): array
{
return User::all()->toArray();
}
#[Route('/api/users/{id}', method: 'GET')]
public function show(int $id): ?User
{
return User::find($id);
}
#[Route('/api/users', method: 'POST', middleware: ['auth', 'admin'])]
public function store(#[Inject('validator')] ValidatorInterface $validator): User
{
// Use injected validator
$data = $validator->validate(request()->all());
return User::create($data);
}
}
Reading Attributes via Reflection:
namespace App\Core;
use ReflectionClass;
use ReflectionMethod;
use App\Attributes\Cached;
use App\Attributes\Route;
class AttributeReader
{
public function getCacheConfig(object $instance, string $method): ?Cached
{
$reflection = new ReflectionMethod($instance, $method);
$attributes = $reflection->getAttributes(Cached::class);
if (empty($attributes)) {
return null;
}
return $attributes[0]->newInstance();
}
public function getRoutes(string $controllerClass): array
{
$reflection = new ReflectionClass($controllerClass);
$routes = [];
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'method' => $route->method,
'handler' => [$controllerClass, $method->getName()],
'middleware' => $route->middleware,
];
}
}
return $routes;
}
}
/**
* Caching interceptor using attributes
*/
class CacheInterceptor
{
public function __construct(
private CacheInterface $cache,
private AttributeReader $reader,
) {}
public function intercept(object $instance, string $method, array $args): mixed
{
$cacheConfig = $this->reader->getCacheConfig($instance, $method);
if ($cacheConfig === null) {
// No caching - call method directly
return $instance->$method(...$args);
}
$cacheKey = $cacheConfig->key ?? $this->generateKey($instance, $method, $args);
// Check cache
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
// Execute and cache
$result = $instance->$method(...$args);
$this->cache->set($cacheKey, $result, $cacheConfig->ttl);
return $result;
}
private function generateKey(object $instance, string $method, array $args): string
{
return sprintf(
'%s::%s:%s',
get_class($instance),
$method,
md5(serialize($args))
);
}
}
Laravel Integration:
// Laravel uses attributes for routes (Laravel 11+)
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Attribute\Route;
use Illuminate\Routing\Attribute\Middleware;
#[Middleware('auth')]
class UserController extends Controller
{
#[Route('GET', '/users')]
public function index(): array
{
return User::all()->toArray();
}
#[Route('POST', '/users')]
#[Middleware('admin')]
public function store(Request $request): User
{
return User::create($request->validated());
}
}
Type Safety Considerations:
- Attributes are validated at instantiation, not declaration
- Use attribute targets to restrict usage (CLASS, METHOD, PROPERTY, etc.)
- Attributes can be repeatable with
Attribute::IS_REPEATABLE - Reflection adds runtime overhead - cache attribute reads
Testing Approach:
use App\Attributes\Cached;
use App\Services\UserService;
test('attribute is correctly defined on method', function () {
$reflection = new ReflectionMethod(UserService::class, 'getAllUsers');
$attributes = $reflection->getAttributes(Cached::class);
expect($attributes)->toHaveCount(1);
$cached = $attributes[0]->newInstance();
expect($cached->ttl)->toBe(3600)
->and($cached->key)->toBe('user_list');
});
test('cache interceptor uses attribute configuration', function () {
$cache = Mockery::mock(CacheInterface::class);
$reader = new AttributeReader();
$interceptor = new CacheInterceptor($cache, $reader);
$service = new UserService();
$cache->shouldReceive('has')->with('user_list')->andReturn(false);
$cache->shouldReceive('set')->with('user_list', Mockery::any(), 3600);
$interceptor->intercept($service, 'getAllUsers', []);
});
1.5 Named Arguments
PHP Version: 8.0+ Use Case: Improve readability, skip optional parameters Benefits: Self-documenting code, flexible parameter order
<?php
declare(strict_types=1);
namespace App\Services;
class EmailService
{
/**
* Send email with many optional parameters
*/
public function send(
string $to,
string $subject,
string $body,
?string $from = null,
?string $replyTo = null,
array $cc = [],
array $bcc = [],
array $attachments = [],
bool $html = true,
int $priority = 3,
): bool {
// Implementation
return true;
}
}
// Traditional way - positional arguments
$email = new EmailService();
$email->send(
'user@example.com',
'Welcome',
'Welcome to our platform',
null, // from
null, // replyTo
[], // cc
[], // bcc
[], // attachments
true, // html
1 // priority - hard to understand
);
// Named arguments - much clearer
$email->send(
to: 'user@example.com',
subject: 'Welcome',
body: 'Welcome to our platform',
priority: 1, // Can skip optional params
html: true,
);
// Can reorder named arguments
$email->send(
body: 'Welcome to our platform',
subject: 'Welcome',
to: 'user@example.com',
);
Named Arguments with Enums:
enum EmailPriority: int
{
case LOW = 5;
case NORMAL = 3;
case HIGH = 1;
case URGENT = 0;
}
class NotificationService
{
public function notify(
string $recipient,
string $message,
EmailPriority $priority = EmailPriority::NORMAL,
bool $immediate = false,
): void {
// Implementation
}
}
// Very readable
$service = new NotificationService();
$service->notify(
recipient: 'admin@example.com',
message: 'System alert',
priority: EmailPriority::URGENT,
immediate: true,
);
Laravel Integration:
// Validation rules with named arguments
validator(
data: $request->all(),
rules: [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8'],
],
messages: [
'email.required' => 'Email is required',
'password.min' => 'Password must be at least 8 characters',
],
);
// Query builder with named arguments
User::where(
column: 'status',
operator: '=',
value: 'active',
)->get();
// More readable without operator
User::where(
column: 'status',
value: 'active',
)->get();
// Cache with named arguments
Cache::remember(
key: "user_{$id}",
ttl: 3600,
callback: fn() => User::find($id),
);
Type Safety Considerations:
- Named arguments are checked at compile time
- Cannot mix required named args with positional args
- Great for reducing parameter object patterns
- Use with constructor property promotion for DTOs
Testing Approach:
test('named arguments improve test readability', function () {
$service = new EmailService();
$result = $service->send(
to: 'test@example.com',
subject: 'Test',
body: 'Test body',
html: false,
priority: 1,
);
expect($result)->toBeTrue();
});
test('can skip optional parameters with named arguments', function () {
$service = new NotificationService();
// Only provide required params
expect(fn() => $service->notify(
recipient: 'user@example.com',
message: 'Hello',
))->not->toThrow();
});
2. Laravel Advanced Patterns
2.1 Service Layer Architecture
Laravel Version: 11+ Use Case: Separate business logic from controllers Benefits: Reusability, testability, maintainability
Architecture Flow:
Controller → Service → Action → Repository → Model
<?php
declare(strict_types=1);
namespace App\Services;
use App\Actions\CreateUserAction;
use App\Actions\UpdateUserAction;
use App\Actions\DeleteUserAction;
use App\Repositories\UserRepositoryInterface;
use App\DTOs\CreateUserData;
use App\DTOs\UpdateUserData;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
/**
* Service layer handles business logic
* Orchestrates actions and repositories
*/
class UserService
{
public function __construct(
private UserRepositoryInterface $userRepository,
private CreateUserAction $createUserAction,
private UpdateUserAction $updateUserAction,
private DeleteUserAction $deleteUserAction,
) {}
/**
* Register new user with email verification
*/
public function registerUser(CreateUserData $data): User
{
return DB::transaction(function () use ($data) {
// Business logic: Hash password
$data->password = Hash::make($data->password);
// Action: Create user
$user = $this->createUserAction->execute($data);
// Business logic: Send welcome email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Business logic: Assign default role
$user->assignRole('user');
return $user;
});
}
/**
* Update user profile with validation
*/
public function updateUserProfile(int $userId, UpdateUserData $data): User
{
$user = $this->userRepository->findOrFail($userId);
// Business logic: Check email uniqueness
if ($data->email && $data->email !== $user->email) {
if ($this->userRepository->existsByEmail($data->email)) {
throw new \DomainException('Email already in use');
}
}
// Action: Update user
return $this->updateUserAction->execute($user, $data);
}
/**
* Deactivate user (soft delete with cleanup)
*/
public function deactivateUser(int $userId): bool
{
return DB::transaction(function () use ($userId) {
$user = $this->userRepository->findOrFail($userId);
// Business logic: Clear sessions
$user->tokens()->delete();
// Business logic: Anonymize personal data
$user->update([
'email' => "deleted_{$user->id}@example.com",
'phone' => null,
]);
// Action: Delete user
return $this->deleteUserAction->execute($user);
});
}
/**
* Get active users with caching
*/
public function getActiveUsers(): Collection
{
return Cache::remember(
key: 'active_users',
ttl: 3600,
callback: fn() => $this->userRepository->getActive(),
);
}
}
Action Classes (Single Responsibility):
namespace App\Actions;
use App\Models\User;
use App\DTOs\CreateUserData;
use App\Repositories\UserRepositoryInterface;
/**
* Action: Single-purpose operation
* No business logic - just execution
*/
class CreateUserAction
{
public function __construct(
private UserRepositoryInterface $userRepository,
) {}
public function execute(CreateUserData $data): User
{
return $this->userRepository->create([
'name' => $data->name,
'email' => $data->email,
'password' => $data->password,
]);
}
}
class UpdateUserAction
{
public function __construct(
private UserRepositoryInterface $userRepository,
) {}
public function execute(User $user, UpdateUserData $data): User
{
return $this->userRepository->update($user, $data->toArray());
}
}
class DeleteUserAction
{
public function __construct(
private UserRepositoryInterface $userRepository,
) {}
public function execute(User $user): bool
{
return $this->userRepository->delete($user);
}
}
DTOs (Data Transfer Objects):
namespace App\DTOs;
use Illuminate\Http\Request;
/**
* Readonly DTO with constructor property promotion
* PHP 8.2+
*/
readonly class CreateUserData
{
public function __construct(
public string $name,
public string $email,
public string $password,
) {}
public static function fromRequest(Request $request): self
{
return new self(
name: $request->input('name'),
email: $request->input('email'),
password: $request->input('password'),
);
}
}
readonly class UpdateUserData
{
public function __construct(
public ?string $name = null,
public ?string $email = null,
public ?string $phone = null,
) {}
public static function fromRequest(Request $request): self
{
return new self(
name: $request->input('name'),
email: $request->input('email'),
phone: $request->input('phone'),
);
}
public function toArray(): array
{
return array_filter([
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
], fn($value) => $value !== null);
}
}
Controller (Thin Layer):
namespace App\Http\Controllers;
use App\Http\Requests\RegisterUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function __construct(
private UserService $userService,
) {}
/**
* Controller only handles HTTP concerns
*/
public function store(RegisterUserRequest $request): JsonResponse
{
$data = CreateUserData::fromRequest($request);
$user = $this->userService->registerUser($data);
return response()->json($user, 201);
}
public function update(UpdateUserRequest $request, int $id): JsonResponse
{
$data = UpdateUserData::fromRequest($request);
$user = $this->userService->updateUserProfile($id, $data);
return response()->json($user);
}
public function destroy(int $id): JsonResponse
{
$this->userService->deactivateUser($id);
return response()->json(null, 204);
}
}
Type Safety Considerations:
- Use readonly DTOs to prevent mutation
- Type-hint service dependencies in controllers
- Return specific types from service methods
- Use exceptions for business rule violations
Testing Approach:
use App\Services\UserService;
use App\DTOs\CreateUserData;
use App\Repositories\UserRepositoryInterface;
test('service layer orchestrates user registration', function () {
$repository = Mockery::mock(UserRepositoryInterface::class);
$createAction = Mockery::mock(CreateUserAction::class);
$service = new UserService(
userRepository: $repository,
createUserAction: $createAction,
updateUserAction: Mockery::mock(UpdateUserAction::class),
deleteUserAction: Mockery::mock(DeleteUserAction::class),
);
$data = new CreateUserData(
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
);
$createAction->shouldReceive('execute')
->once()
->with(Mockery::type(CreateUserData::class))
->andReturn(new User(['id' => 1, 'email' => 'john@example.com']));
Mail::fake();
$user = $service->registerUser($data);
expect($user->email)->toBe('john@example.com');
Mail::assertSent(WelcomeEmail::class);
});
2.2 Repository Pattern Implementation
Laravel Version: Any Use Case: Abstract data access layer, swap implementations Benefits: Testability, database independence, clear contracts
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
/**
* Repository interface (contract)
* Defines data access operations
*/
interface UserRepositoryInterface
{
public function find(int $id): ?User;
public function findOrFail(int $id): User;
public function getAll(): Collection;
public function getActive(): Collection;
public function create(array $data): User;
public function update(User $user, array $data): User;
public function delete(User $user): bool;
public function existsByEmail(string $email): bool;
public function findByEmail(string $email): ?User;
}
/**
* Eloquent implementation
*/
class EloquentUserRepository implements UserRepositoryInterface
{
public function find(int $id): ?User
{
return User::find($id);
}
public function findOrFail(int $id): User
{
return User::findOrFail($id);
}
public function getAll(): Collection
{
return User::all();
}
public function getActive(): Collection
{
return User::where('status', 'active')
->orderBy('created_at', 'desc')
->get();
}
public function create(array $data): User
{
return User::create($data);
}
public function update(User $user, array $data): User
{
$user->update($data);
return $user->fresh();
}
public function delete(User $user): bool
{
return $user->delete();
}
public function existsByEmail(string $email): bool
{
return User::where('email', $email)->exists();
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
}
/**
* Cache decorator (adds caching layer)
*/
class CachedUserRepository implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $repository,
) {}
public function find(int $id): ?User
{
return Cache::remember(
key: "user_{$id}",
ttl: 3600,
callback: fn() => $this->repository->find($id),
);
}
public function findOrFail(int $id): User
{
return $this->find($id) ?? throw new ModelNotFoundException();
}
public function getActive(): Collection
{
return Cache::remember(
key: 'active_users',
ttl: 3600,
callback: fn() => $this->repository->getActive(),
);
}
public function create(array $data): User
{
$user = $this->repository->create($data);
Cache::forget("user_{$user->id}");
Cache::forget('active_users');
return $user;
}
public function update(User $user, array $data): User
{
$updated = $this->repository->update($user, $data);
Cache::forget("user_{$user->id}");
Cache::forget('active_users');
return $updated;
}
public function delete(User $user): bool
{
$result = $this->repository->delete($user);
Cache::forget("user_{$user->id}");
Cache::forget('active_users');
return $result;
}
// Delegate other methods...
public function getAll(): Collection
{
return $this->repository->getAll();
}
public function existsByEmail(string $email): bool
{
return $this->repository->existsByEmail($email);
}
public function findByEmail(string $email): ?User
{
return $this->repository->findByEmail($email);
}
}
Service Provider Registration:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\UserRepositoryInterface;
use App\Repositories\EloquentUserRepository;
use App\Repositories\CachedUserRepository;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Register repository bindings
*/
public function register(): void
{
// Bind interface to implementation
$this->app->bind(
UserRepositoryInterface::class,
EloquentUserRepository::class
);
// Or use singleton
$this->app->singleton(
UserRepositoryInterface::class,
EloquentUserRepository::class
);
// With caching decorator
$this->app->singleton(UserRepositoryInterface::class, function ($app) {
$eloquentRepo = new EloquentUserRepository();
return new CachedUserRepository($eloquentRepo);
});
}
}
Advanced Repository with Specifications:
namespace App\Repositories\Specifications;
use Illuminate\Database\Eloquent\Builder;
/**
* Specification pattern for complex queries
*/
interface Specification
{
public function apply(Builder $query): Builder;
}
class ActiveUsersSpecification implements Specification
{
public function apply(Builder $query): Builder
{
return $query->where('status', 'active');
}
}
class CreatedAfterSpecification implements Specification
{
public function __construct(
private \DateTimeInterface $date,
) {}
public function apply(Builder $query): Builder
{
return $query->where('created_at', '>=', $this->date);
}
}
class HasRoleSpecification implements Specification
{
public function __construct(
private string $role,
) {}
public function apply(Builder $query): Builder
{
return $query->whereHas('roles', fn($q) => $q->where('name', $this->role));
}
}
/**
* Enhanced repository with specifications
*/
class SpecificationUserRepository extends EloquentUserRepository
{
public function findBySpecification(Specification ...$specifications): Collection
{
$query = User::query();
foreach ($specifications as $spec) {
$query = $spec->apply($query);
}
return $query->get();
}
}
// Usage
$repository = new SpecificationUserRepository();
$users = $repository->findBySpecification(
new ActiveUsersSpecification(),
new CreatedAfterSpecification(new DateTime('-30 days')),
new HasRoleSpecification('admin'),
);
Type Safety Considerations:
- Always define interface contracts
- Use dependency injection, not facade pattern
- Return specific types (User, Collection) not mixed
- Use readonly properties in specifications
Testing Approach:
test('repository implements interface contract', function () {
$repository = new EloquentUserRepository();
expect($repository)->toBeInstanceOf(UserRepositoryInterface::class);
});
test('repository can swap implementations', function () {
// Mock repository for testing
$mockRepo = Mockery::mock(UserRepositoryInterface::class);
$mockRepo->shouldReceive('getActive')->andReturn(collect([
new User(['id' => 1, 'name' => 'Test']),
]));
$this->app->instance(UserRepositoryInterface::class, $mockRepo);
$service = app(UserService::class);
$users = $service->getActiveUsers();
expect($users)->toHaveCount(1);
});
test('specification pattern filters users correctly', function () {
User::factory()->create(['status' => 'active']);
User::factory()->create(['status' => 'inactive']);
$repository = new SpecificationUserRepository();
$users = $repository->findBySpecification(
new ActiveUsersSpecification()
);
expect($users)->toHaveCount(1)
->and($users->first()->status)->toBe('active');
});
2.3 Pipeline Pattern
Laravel Version: Any (uses Illuminate\Pipeline) Use Case: Chain operations, middleware-like processing Benefits: Composability, reusability, clean separation
<?php
declare(strict_types=1);
namespace App\Pipelines;
use Illuminate\Support\Facades\Pipeline;
use App\DTOs\CreateUserData;
/**
* Pipeline example: Input sanitization
* Each pipe transforms the data
*/
class UserRegistrationPipeline
{
/**
* Pipes to process user data
*/
protected array $pipes = [
TrimStrings::class,
NormalizeEmail::class,
ValidatePassword::class,
CheckDuplicateEmail::class,
];
public function process(CreateUserData $data): CreateUserData
{
return Pipeline::send($data)
->through($this->pipes)
->thenReturn();
}
}
/**
* Individual pipe: Trim strings
*/
class TrimStrings
{
public function handle(CreateUserData $data, \Closure $next): CreateUserData
{
$trimmed = new CreateUserData(
name: trim($data->name),
email: trim($data->email),
password: $data->password, // Don't trim password
);
return $next($trimmed);
}
}
/**
* Individual pipe: Normalize email
*/
class NormalizeEmail
{
public function handle(CreateUserData $data, \Closure $next): CreateUserData
{
$normalized = new CreateUserData(
name: $data->name,
email: strtolower($data->email),
password: $data->password,
);
return $next($normalized);
}
}
/**
* Individual pipe: Validate password strength
*/
class ValidatePassword
{
public function handle(CreateUserData $data, \Closure $next): CreateUserData
{
if (strlen($data->password) < 8) {
throw new \DomainException('Password must be at least 8 characters');
}
if (!preg_match('/[A-Z]/', $data->password)) {
throw new \DomainException('Password must contain uppercase letter');
}
if (!preg_match('/[0-9]/', $data->password)) {
throw new \DomainException('Password must contain number');
}
return $next($data);
}
}
/**
* Individual pipe: Check duplicate email
*/
class CheckDuplicateEmail
{
public function __construct(
pr
…(truncated)