graph TB
A[HTTP Request] --> B[PHP-FPM Worker]
B --> C[Front Controller]
C --> D[Middleware Stack]
D --> E[Controller]
E --> F[Service Layer]
F --> G[Repository / Data Mapper]
G --> H[(Database)]
H --> I[Entity / DTO]
I --> J[Transformer]
J --> K[HTTP Response]
D --> L[Auth / CSRF Guard]
// Enums with methods (PHP 8.1+)
enum UserRole: string
{
case Admin = 'admin';
case Editor = 'editor';
case Viewer = 'viewer';
public function permissions(): array
{
return match($this) {
self::Admin => ['read', 'write', 'delete', 'manage'],
self::Editor => ['read', 'write'],
self::Viewer => ['read'],
};
}
}
// Readonly + named args + constructor promotion
final class UserRegistrationDto
{
public function __construct(
public readonly string $email,
public readonly string $name,
public readonly UserRole $role = UserRole::Viewer,
public readonly ?string $invitationCode = null,
) {}
}
$dto = new UserRegistrationDto(email: 'user@example.com', name: 'Jane');
// Attributes (PHP 8.0+)
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class RequirePermission
{
public function __construct(public readonly UserRole $role) {}
}
#[RequirePermission(UserRole::Admin)]
final class AdminDashboardController { / ... / }
// Nullsafe operator & match expression
$country = $user?->getAddress()?->getCountry()?->getName();
$msg = match($statusCode) { 200 => 'ok', 404 => 'not found', default => 'unknown' };
// Fibers (PHP 8.1+)
$fiber = new Fibers\Fiber(fn() => Fibers\Fiber::uspend('ready'));
$r = $fiber->start(); // 'ready'
// JIT: opcache.jit_buffer_size=128M opcache.jit=1255 (tracing)
classDiagram
class UserService {
-users: UserRepositoryInterface
+register(dto): User
+deactivate(id): void
}
class UserRepositoryInterface {
<>
+find(id): ?User
+save(user): void
}
class DoctrineUserRepository {
-em: EntityManagerInterface
}
UserService --> UserRepositoryInterface
DoctrineUserRepository ..|> UserRepositoryInterface
// Domain Entity (persistence-ignorant)
final class User
{
private function __construct(
private readonly int $id,
private string $email,
private UserRole $role,
private bool $isActive = true,
) {}
public static function register(UserRegistrationDto $dto): self
{
return new self(id: 0, email: $dto->email, role: $dto->role);
}
public function deactivate(): void
{
$this->isActive ?: throw new LogicException('Already deactivated.');
$this->isActive = false;
}
}
interface UserRepositoryInterface
{
public function find(int $id): ?User;
public function findByEmail(string $email): ?User;
public function save(User $user): void;
}
// Service Layer with DI
final class UserService
{
public function __construct(
private readonly UserRepositoryInterface $users,
private readonly EventDispatcherInterface $dispatcher,
) {}
public function register(UserRegistrationDto $dto): User
{
$this->users->findByEmail($dto->email)
?? throw new DomainException("Email {$dto->email} already registered.");
$user = User::register($dto);
$this->users->save($user);
$this->dispatcher->dispatch(new UserRegisteredEvent($user));
return $user;
}
}
// PSR-11 Container binding
app()->singleton(UserRepositoryInterface::class, DoctrineUserRepository::class);
| Aspect | Active Record (Eloquent) | Data Mapper (Doctrine) |
|-------------|----------------------------|----------------------------|
| Save method | $user->save() | $em->flush() |
| Testability | Coupled to DB | Pure entity unit tests |
| Best for | CRUD apps, rapid dev | Enterprise, DDD, CQRS |
// ❌ $db->query("SELECT * FROM users WHERE email = '{$_POST['email']}'");
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]); // ✅ PDO
$qb->select('u')->from(User::class, 'u')->where('u.email = :email') // ✅ Doctrine
->setParameter('email', $email);
echo htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$response->headers->set('Content-Security-Policy',
"default-src 'self'; script-src 'self' 'nonce-{$nonce}'");
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// Verify: hash_equals($_SESSION['csrf_token'], $_POST['_csrf_token'])
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Strict');
session_regenerate_id(true); // on privilege escalation
$fingerprint = hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
($_SESSION['fingerprint'] ?? '') === $fingerprint || (session_destroy() && exit);
$hash = password_hash($pw, PASSWORD_ARGON2ID, ['memory_cost'=>65536,'time_cost'=>4]);
password_verify($input, $hash) || throw new AuthException();
// Rehash: password_needs_rehash($hash, PASSWORD_ARGON2ID)
function safeUpload(array $file, string $dir): string
{
$file['error'] === UPLOAD_ERR_OK || throw new RuntimeException('Upload error');
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
in_array($mime, ['image/jpeg','image/png','image/webp'], true) || throw new RuntimeException('Bad MIME');
$name = bin2hex(random_bytes(16)) . '.' . pathinfo($file['name'], PATHINFO_EXTENSION);
move_uploaded_file($file['tmp_name'], rtrim($dir,'/') . '/' . $name) || throw new RuntimeException('Move failed');
chmod($dir . '/' . $name, 0644);
return $name;
}
opcache.validate_timestamps=0 ; no auto-reload in prod
opcache.save_comments=1 ; annotations need this
open_basedir=/var/www/html ; restrict file access
| PSR | Title | Key Points |
|-------|-----------------|-----------------------------------------|
| PSR-3 | Logger | Psr\Log\LoggerInterface |
| PSR-4 | Autoloading | Namespace → directory mapping |
| PSR-7 | HTTP Messages | Request/Response interfaces |
| PSR-11| Container | get($id), has($id) |
| PSR-12| Coding Style | 4 spaces, final by default |
| PSR-15| Middleware | MiddlewareInterface, RequestHandler|
// Laravel: Eloquent with enum casting, scopes, eager loading
final class Order extends Model
{
protected $casts = ['status' => OrderStatus::class, 'total' => 'decimal:2'];
public function scopeActive(Builder $q): Builder { return $q->where('status','pending'); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
}
Order::with('customer', 'items.product')->active()->get(); // eager load
// Symfony: attribute-based handler
#[AsMessageHandler]
final class ProcessPaymentHandler
{
public function __construct(private readonly PaymentGateway $gateway) {}
public function __invoke(ProcessPayment $cmd): void { $this->gateway->charge($cmd->amount); }
}
graph LR
subgraph "Eloquent AR"
A1[Model] -->|save| A2[(DB)]
end
subgraph "Doctrine DM"
B1[Entity] --> B2[EM]
B2 -->|flush| B3[(DB)]
end
Order::with('customer')->get(); // eager load — avoid N+1
Order::chunk(200, fn($c) => process($c)); // chunk large sets
Order::cursor(); // generator — memory efficient
User::pluck('email'); // select column only
$table->index(['status','created_at']); // composite index
Schema::create('orders', function (Blueprint $t): void {
$t->id();
$t->foreignId('customer_id')->constrained()->cascadeOnDelete();
$t->decimal('total', 12, 2)->index();
$t->string('status', 30)->index();
$t->timestamps();
$t->index(['status', 'created_at']);
});
Order::factory()->count(500)->create(); // seeding
// PHPUnit — typed mocks + data providers
final class UserServiceTest extends TestCase
{
public function testRegisterThrowsOnDuplicate(): void
{
$repo = $this->createMock(UserRepositoryInterface::class);
$repo->method('findByEmail')->willReturn($this->createStub(User::class));
$this->expectException(DomainException::class);
(new UserService($repo, $this->createMock(EventDispatcherInterface::class)))
->register(new UserRegistrationDto(email:'dup@x.com', name:'D'));
}
#[DataProvider('roles')]
public function testPermissions(UserRole $r, int $n): void
{ $this->assertCount($n, $r->permissions()); }
public static function roles(): array
{ return [[UserRole::Admin, 4], [UserRole::Viewer, 1]]; }
}
// Pest — fluent
it('registers', fn() => postJson('/api/register', $payload)->assertCreated());
it('rejects unauth', fn() => getJson('/api/me')->assertUnauthorized());
jobs:
test:
services: { mysql: { image: mysql:8.4 } }
steps:
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3', coverage: xdebug }
- run: composer install --no-interaction
- run: php artisan migrate --env=testing
- run: vendor/bin/pest --coverage --min=80
// Retry with exponential backoff
function retry(callable $op, int $max = 3, int $baseMs = 200): mixed
{
for ($i = 1; $i <= $max; $i++) {
try { return $op(); } catch (RetryableException $e) {
if ($i === $max) throw $e;
usleep($baseMs 2 ($i - 1) 1000);
}
}
}
// Integer overflow guard (financial)
function safeCentsMul(int $c, int $q): int
{ $r = $c * $q; ($r <= PHP_INT_MAX && $r >= 0) || throw new ArithmeticError('Overflow'); return $r; }
// Defensive input
function sanitizePage(mixed $p, int $max = 1000): int
{ return filter_var($p, FILTER_VALIDATE_INT, ['options'=>['min_range'=>1,'max_range'=>$max]]) ?: 1; }
// Exhaustive match on enum — compiler enforces all cases
function handleStatus(PaymentStatus $s): PaymentResult
{ return match($s) { Succeeded=>confirmed(), Declined=>failed(), Timeout=>retryable() }; }
; Production OPcache + JIT
opcache.enable=1
opcache.memory_consumption=512
opcache.max_accelerated_files=60000
opcache.validate_timestamps=0
opcache.jit_buffer_size=128M
opcache.jit=1255
; PHP-FPM pool
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.max_spare_servers = 20
pm.max_requests = 1000 ; prevent memory leaks
request_terminate_timeout = 30
# Profiling
blackfire run php artisan test:performance
XHProf: php -d xhprof.enabled=1 script.php
graph LR
A[Git Push] --> B[CI Test+Build]
B -->|pass| C[Docker Build]
C --> D[Push Registry]
D --> E[Rolling Deploy]
E -->|ok| F[Live Traffic]
E -->|fail| G[Auto Rollback]
FROM composer:2.7 AS build
COPY . . && RUN composer install --no-dev --optimize-autoloader
FROM php:8.3-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache
COPY --from=build /app /var/www/html
COPY docker/opcache.ini /usr/local/etc/php/conf.d/
USER app
EXPOSE 9000
function envOrFail(string $key): string
{
$v = getenv($key);
($v !== false && $v !== '') || throw new RuntimeException("Missing env: {$key}");
return $v;
}
RELEASE="/var/www/releases/$(date +%Y%m%d%H%M%S)"
git clone --depth 1 -b main /repo.git "$RELEASE"
cd "$RELEASE" && composer install --no-dev --optimize-autoloader
php artisan migrate --force && php artisan route:cache && php artisan config:cache
ln -sfn "$RELEASE" /var/www/current # atomic switch
sudo systemctl reload php8.3-fpm # graceful reload
ls -dt /var/www/releases/* | tail -n +6 | xargs rm -rf
/**
* Charges a customer via the payment gateway with idempotent retry.
*
* @param Money $amount Non-negative, smallest currency unit
* @param string $cardToken Single-use token from Stripe.js
* @param string $idempotencyKey Prevents duplicate charges
* @return PaymentResult Confirmed with transaction ID
* @throws PaymentDeclinedException When card is declined
* @throws GatewayTimeoutException After 3 failed retries
*/
public function charge(Money $amount, string $cardToken, string $idempotencyKey): PaymentResult
# ADR-001: Adopt Doctrine ORM as Data Mapper
Status: Accepted
Context
Complex domain needs persistence-ignorant entities and high testability.
Decision
Use Doctrine Data Mapper over Eloquent Active Record.
Consequences
✅ Pure domain, CQRS-ready ❌ Steeper learning curve
| Feature | Ver | Example |
|------------------|-----|----------------------------------------|
| enum | 8.1 | enum S:string { case A='a'; } |
| readonly | 8.1 | public readonly int $id |
| match | 8.0 | match($v) { 1=>'a', default=>'b' } |
| Named args | 8.0 | new User(name: 'Jane') |
| Nullsafe ?. | 8.0 | $obj?->method() |
| Attributes | 8.0 | #[Route('/api')] |
| Fibers | 8.1 | Fiber::uspend($val) |
| Ctor promotion | 8.0 | __construct(private int $id) |
| never type | 8.1 | function fail(): never { exit(1); } |
| Intersection | 8.1 | Countable&Iterator |
| JIT | 8.0 | opcache.jit=1255 |