📚 BusinessExplain 🏠 Home 📊 Analytics
← Back to Skills

Php Mastery

💻 Coding & Engineering 8 min read v1.0.0
📋 Contents

PHP Mastery — Production-Grade Development Guide

Architecture Overview

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]

1. Modern PHP 8+ (Attributes, Enums, Fibers, Readonly, Match, Named Args, JIT)

// 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)


2. OOP & Design Patterns (DI, Repository, Service Layer, AR vs DM)

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 |


3. PHP Security (SQLi, XSS, CSRF, Session Hijacking, File Uploads, password_hash, OPcache)

SQL Injection — Always Parameterize

// ❌ $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);

XSS — Escape Output + CSP Header

echo htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$response->headers->set('Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-{$nonce}'");

CSRF — Token Per Session

$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// Verify: hash_equals($_SESSION['csrf_token'], $_POST['_csrf_token'])

Session Hijacking Prevention

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);

Password Hashing

$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)

File Upload Security

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 Hardening

opcache.validate_timestamps=0    ; no auto-reload in prod
opcache.save_comments=1          ; annotations need this
open_basedir=/var/www/html       ; restrict file access

4. Frameworks (Laravel Ecosystem, Symfony Components, PSR Standards)

| 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); }
}


5. Database & ORM (Eloquent, Doctrine, Query Optimization, Migrations, Seeding)

graph LR
    subgraph "Eloquent AR"
        A1[Model] -->|save| A2[(DB)]
    end
    subgraph "Doctrine DM"
        B1[Entity] --> B2[EM]
        B2 -->|flush| B3[(DB)]
    end

Query Optimization

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

Migration + Seeding

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

6. Testing (PHPUnit, Pest, Mocking, CI)

// 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());

CI (GitHub Actions)

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

7. Edge Cases (Boundary Conditions, Retry, Error Handling)

// 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() }; }


8. Performance (OPcache, Profiling, Query Optimization, PHP-FPM Tuning)

; 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


9. Deployment (CI/CD, Docker, Env Config, Systemd, Rollback)

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]

Docker (Multi-stage)

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

Env Config — Fail Fast

function envOrFail(string $key): string
{
    $v = getenv($key);
    ($v !== false && $v !== '') || throw new RuntimeException("Missing env: {$key}");
    return $v;
}

Zero-Downtime Deploy

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

10. Documentation (PHPDoc, API Docs, ADRs)

PHPDoc

/**
 * 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 Template

# 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

PHP 8+ Quick Reference

| 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 |

💬 Ask about Php Mastery