DEV Community

Cover image for 🚀 Técnicas Avançadas em PHP: Além do Convencional

🚀 Técnicas Avançadas em PHP: Além do Convencional

Cristian Bernardes

Senior PHP | Node.js Developer | Expert em Arquitetura de Software, Laravel, Docker & Microsserviços


📌 Introdução

O ecossistema PHP moderno vai muito além de simplesmente escrever código que funciona. Existe um conjunto de técnicas avançadas, padrões arquiteturais e otimizações que separam desenvolvedores competentes de verdadeiros mestres da linguagem.

Este artigo explora técnicas avançadas que raramente são discutidas na comunidade brasileira, mas que têm impacto direto na qualidade, performance e manutenibilidade de aplicações enterprise. Se você já domina os fundamentos, está pronto para o próximo nível.

"Código elegante não é aquele que simplesmente funciona, mas aquele que evolui com graça ao longo do tempo." - Uncle Bob Martin


1. 🎯 Generator Pipelines para Processamento de Streams

O Poder dos Generators Encadeados

Generators são subestimados pela maioria dos desenvolvedores. Enquanto muitos os usam apenas para economizar memória, poucos exploram seu verdadeiro potencial: criar pipelines de processamento de dados extremamente eficientes e elegantes.

Caso real: Uma aplicação de análise de logs que processava arquivos de 50GB estava consumindo toda a memória do servidor. Ao implementar generator pipelines, o consumo de memória caiu para 2MB constantes, independente do tamanho do arquivo.

Implementação Avançada

<?php

class StreamPipeline
{
    private array $stages = [];

    public function pipe(callable $transformer): self
    {
        $this->stages[] = $transformer;
        return $this;
    }

    public function process(iterable $source): Generator
    {
        $current = $this->ensureGenerator($source);

        foreach ($this->stages as $stage) {
            $current = $stage($current);
        }

        yield from $current;
    }

    private function ensureGenerator(iterable $source): Generator
    {
        if ($source instanceof Generator) {
            return $source;
        }

        return (function() use ($source) {
            yield from $source;
        })();
    }
}

// Funções de transformação reutilizáveis
function filter(callable $predicate): callable
{
    return function(iterable $items) use ($predicate): Generator {
        foreach ($items as $key => $item) {
            if ($predicate($item, $key)) {
                yield $key => $item;
            }
        }
    };
}

function map(callable $transformer): callable
{
    return function(iterable $items) use ($transformer): Generator {
        foreach ($items as $key => $item) {
            yield $key => $transformer($item, $key);
        }
    };
}

function take(int $limit): callable
{
    return function(iterable $items) use ($limit): Generator {
        $count = 0;
        foreach ($items as $key => $item) {
            if ($count++ >= $limit) break;
            yield $key => $item;
        }
    };
}

function chunk(int $size): callable
{
    return function(iterable $items) use ($size): Generator {
        $buffer = [];
        foreach ($items as $item) {
            $buffer[] = $item;
            if (count($buffer) >= $size) {
                yield $buffer;
                $buffer = [];
            }
        }
        if (!empty($buffer)) {
            yield $buffer;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Exemplo Prático: Processamento de Logs

<?php

class LogAnalyzer
{
    private StreamPipeline $pipeline;

    public function __construct()
    {
        $this->pipeline = new StreamPipeline();
    }

    public function analyzeLogFile(string $filepath): array
    {
        $statistics = [
            'total_errors' => 0,
            'error_types' => [],
            'peak_hour' => null
        ];

        $results = $this->pipeline
            ->pipe($this->readLargeFile($filepath))
            ->pipe($this->parseLogLine())
            ->pipe(filter(fn($log) => $log['level'] === 'ERROR'))
            ->pipe(map(fn($log) => $this->enrichLogData($log)))
            ->pipe(chunk(1000))
            ->process([]);

        foreach ($results as $batch) {
            $this->processBatch($batch, $statistics);
        }

        return $statistics;
    }

    private function readLargeFile(string $filepath): callable
    {
        return function() use ($filepath): Generator {
            $handle = fopen($filepath, 'r');
            if (!$handle) {
                throw new RuntimeException("Cannot open file: {$filepath}");
            }

            try {
                while (($line = fgets($handle)) !== false) {
                    yield $line;
                }
            } finally {
                fclose($handle);
            }
        };
    }

    private function parseLogLine(): callable
    {
        return function(Generator $lines): Generator {
            foreach ($lines as $line) {
                if (preg_match('/^\[(.*?)\] (\w+): (.*)$/', $line, $matches)) {
                    yield [
                        'timestamp' => strtotime($matches[1]),
                        'level' => $matches[2],
                        'message' => $matches[3]
                    ];
                }
            }
        };
    }

    private function enrichLogData(array $log): array
    {
        $log['hour'] = date('H', $log['timestamp']);
        $log['error_type'] = $this->classifyError($log['message']);
        return $log;
    }

    private function classifyError(string $message): string
    {
        if (str_contains($message, 'database')) return 'DATABASE';
        if (str_contains($message, 'network')) return 'NETWORK';
        if (str_contains($message, 'timeout')) return 'TIMEOUT';
        return 'GENERAL';
    }

    private function processBatch(array $batch, array &$statistics): void
    {
        $statistics['total_errors'] += count($batch);

        foreach ($batch as $log) {
            $type = $log['error_type'];
            $statistics['error_types'][$type] =
                ($statistics['error_types'][$type] ?? 0) + 1;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefícios Mensuráveis

  • Memória: Consumo constante, independente do volume de dados
  • Performance: Processamento lazy evita operações desnecessárias
  • Manutenibilidade: Pipeline declarativo e fácil de testar
  • Composição: Transformações reutilizáveis em diferentes contextos

2. 🔐 Sealed Classes e Type Narrowing

Garantindo Exhaustividade com Match

Com PHP 8.0+, podemos usar a expressão match combinada com enums e análise estática para garantir que todos os casos possíveis sejam tratados em tempo de compilação.

Caso real: Uma fintech eliminou 12 bugs de produção relacionados a estados não tratados de transações ao implementar sealed classes com verificação exhaustiva.

Implementação

<?php

// PHP 8.1+ com Enums
enum PaymentStatus: string
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case COMPLETED = 'completed';
    case FAILED = 'failed';
    case REFUNDED = 'refunded';
    case CANCELLED = 'cancelled';
}

enum PaymentMethod: string
{
    case CREDIT_CARD = 'credit_card';
    case DEBIT_CARD = 'debit_card';
    case PIX = 'pix';
    case BOLETO = 'boleto';
    case WALLET = 'wallet';
}

abstract class PaymentResult
{
    private function __construct() {}
}

final class PaymentSuccess extends PaymentResult
{
    public function __construct(
        public readonly string $transactionId,
        public readonly float $amount,
        public readonly \DateTimeImmutable $processedAt
    ) {
        parent::__construct();
    }
}

final class PaymentFailure extends PaymentResult
{
    public function __construct(
        public readonly string $errorCode,
        public readonly string $errorMessage,
        public readonly bool $isRetryable
    ) {
        parent::__construct();
    }
}

final class PaymentPending extends PaymentResult
{
    public function __construct(
        public readonly string $referenceId,
        public readonly \DateTimeImmutable $expiresAt
    ) {
        parent::__construct();
    }
}

class PaymentProcessor
{
    public function processPayment(
        float $amount,
        PaymentMethod $method,
        array $data
    ): PaymentResult {
        // Garantia de exhaustividade via match
        return match ($method) {
            PaymentMethod::CREDIT_CARD => $this->processCreditCard($amount, $data),
            PaymentMethod::DEBIT_CARD => $this->processDebitCard($amount, $data),
            PaymentMethod::PIX => $this->processPix($amount, $data),
            PaymentMethod::BOLETO => $this->processBoleto($amount, $data),
            PaymentMethod::WALLET => $this->processWallet($amount, $data),
            // Se adicionarmos um novo método e não tratarmos aqui,
            // PHP 8.0+ lança um UnhandledMatchError
        };
    }

    public function getStatusMessage(PaymentStatus $status): string
    {
        return match ($status) {
            PaymentStatus::PENDING => 'Aguardando processamento',
            PaymentStatus::PROCESSING => 'Processando pagamento...',
            PaymentStatus::COMPLETED => 'Pagamento confirmado',
            PaymentStatus::FAILED => 'Pagamento falhou',
            PaymentStatus::REFUNDED => 'Pagamento estornado',
            PaymentStatus::CANCELLED => 'Pagamento cancelado',
        };
    }

    public function handlePaymentResult(PaymentResult $result): array
    {
        // Type narrowing com match
        return match (true) {
            $result instanceof PaymentSuccess => [
                'status' => 'success',
                'transaction_id' => $result->transactionId,
                'amount' => $result->amount,
                'processed_at' => $result->processedAt->format('c')
            ],
            $result instanceof PaymentFailure => [
                'status' => 'failure',
                'error' => $result->errorMessage,
                'code' => $result->errorCode,
                'can_retry' => $result->isRetryable
            ],
            $result instanceof PaymentPending => [
                'status' => 'pending',
                'reference' => $result->referenceId,
                'expires_at' => $result->expiresAt->format('c')
            ],
        };
    }

    private function processCreditCard(float $amount, array $data): PaymentResult
    {
        // Implementação real
        return new PaymentSuccess(
            uniqid('txn_'),
            $amount,
            new \DateTimeImmutable()
        );
    }

    // Outros métodos de processamento...
    private function processDebitCard(float $amount, array $data): PaymentResult { /* ... */ }
    private function processPix(float $amount, array $data): PaymentResult { /* ... */ }
    private function processBoleto(float $amount, array $data): PaymentResult { /* ... */ }
    private function processWallet(float $amount, array $data): PaymentResult { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Vantagens

  • Segurança de Tipo: Impossível esquecer de tratar um caso
  • Refatoração Segura: Adicionar novos casos gera erros em tempo de compilação
  • Documentação Viva: O código expressa claramente todos os estados possíveis
  • Zero Runtime Overhead: Verificações acontecem em análise estática

3. ⚡ WeakMap para Cache Inteligente

Gerenciamento Automático de Memória

WeakMaps (PHP 8.0+) permitem criar caches que se limpam automaticamente quando objetos não são mais referenciados, eliminando vazamentos de memória em sistemas de longa duração.

Caso real: Um sistema de processamento contínuo que rodava 24/7 tinha memory leaks que forçavam reinicializações diárias. Com WeakMaps, o consumo de memória estabilizou e as reinicializações foram eliminadas.

Implementação

<?php

class SmartCache
{
    private \WeakMap $cache;
    private array $metadata;

    public function __construct()
    {
        $this->cache = new \WeakMap();
        $this->metadata = [];
    }

    public function remember(object $key, callable $factory): mixed
    {
        if (isset($this->cache[$key])) {
            $this->metadata[spl_object_id($key)]['hits']++;
            return $this->cache[$key];
        }

        $value = $factory();
        $this->cache[$key] = $value;

        $this->metadata[spl_object_id($key)] = [
            'cached_at' => time(),
            'hits' => 0
        ];

        return $value;
    }

    public function has(object $key): bool
    {
        return isset($this->cache[$key]);
    }

    public function forget(object $key): void
    {
        unset($this->cache[$key]);
        unset($this->metadata[spl_object_id($key)]);
    }

    public function getStats(): array
    {
        return [
            'entries' => count($this->cache),
            'metadata' => $this->metadata
        ];
    }
}

class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $email
    ) {}
}

class UserRepository
{
    private SmartCache $cache;

    public function __construct(
        private \PDO $db,
        ?SmartCache $cache = null
    ) {
        $this->cache = $cache ?? new SmartCache();
    }

    public function findById(int $id): ?User
    {
        $user = new User($id, ''); // Objeto temporário para chave

        return $this->cache->remember($user, function() use ($id) {
            $stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
            $stmt->execute([$id]);
            $data = $stmt->fetch(\PDO::FETCH_ASSOC);

            return $data ? new User($data['id'], $data['email']) : null;
        });
    }

    public function getCacheStats(): array
    {
        return $this->cache->getStats();
    }
}
Enter fullscreen mode Exit fullscreen mode

Memoization com WeakMap

<?php

class Memoizer
{
    private \WeakMap $results;

    public function __construct()
    {
        $this->results = new \WeakMap();
    }

    public function memoize(object $context, string $method, array $args = []): mixed
    {
        if (!isset($this->results[$context])) {
            $this->results[$context] = [];
        }

        $key = $method . ':' . serialize($args);

        if (!isset($this->results[$context][$key])) {
            $this->results[$context][$key] = $context->$method(...$args);
        }

        return $this->results[$context][$key];
    }
}

class ExpensiveCalculator
{
    private Memoizer $memoizer;

    public function __construct()
    {
        $this->memoizer = new Memoizer();
    }

    public function fibonacci(int $n): int
    {
        return $this->memoizer->memoize($this, 'calculateFibonacci', [$n]);
    }

    private function calculateFibonacci(int $n): int
    {
        if ($n <= 1) return $n;
        return $this->fibonacci($n - 1) + $this->fibonacci($n - 2);
    }

    public function complexCalculation(array $data): float
    {
        return $this->memoizer->memoize($this, 'performCalculation', [$data]);
    }

    private function performCalculation(array $data): float
    {
        // Cálculo complexo e custoso
        return array_sum($data) * log(count($data) + 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

4. 🎭 Named Arguments com Variadic Functions

Flexibilidade com Type Safety

Named arguments (PHP 8.0+) combinados com variadic functions criam APIs extremamente flexíveis sem sacrificar a segurança de tipos.

Implementação de Query Builder Fluente

<?php

class QueryBuilder
{
    private string $table;
    private array $selects = ['*'];
    private array $wheres = [];
    private array $joins = [];
    private array $orderBy = [];
    private ?int $limit = null;
    private ?int $offset = null;

    public function __construct(private \PDO $connection) {}

    public function table(string $table): self
    {
        $this->table = $table;
        return $this;
    }

    public function select(string ...$columns): self
    {
        $this->selects = $columns;
        return $this;
    }

    public function where(
        string $column,
        mixed $operator = null,
        mixed $value = null,
        string $boolean = 'AND'
    ): self {
        // Se apenas 2 argumentos, assume '=' como operador
        if ($value === null) {
            $value = $operator;
            $operator = '=';
        }

        $this->wheres[] = compact('column', 'operator', 'value', 'boolean');
        return $this;
    }

    public function whereIn(string $column, array $values, string $boolean = 'AND'): self
    {
        $this->wheres[] = [
            'column' => $column,
            'operator' => 'IN',
            'value' => $values,
            'boolean' => $boolean
        ];
        return $this;
    }

    public function whereBetween(
        string $column,
        mixed $min,
        mixed $max,
        string $boolean = 'AND'
    ): self {
        $this->wheres[] = [
            'column' => $column,
            'operator' => 'BETWEEN',
            'value' => [$min, $max],
            'boolean' => $boolean
        ];
        return $this;
    }

    public function join(
        string $table,
        string $first,
        string $operator,
        string $second,
        string $type = 'INNER'
    ): self {
        $this->joins[] = compact('table', 'first', 'operator', 'second', 'type');
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $this->orderBy[] = [$column, strtoupper($direction)];
        return $this;
    }

    public function limit(int $limit): self
    {
        $this->limit = $limit;
        return $this;
    }

    public function offset(int $offset): self
    {
        $this->offset = $offset;
        return $this;
    }

    public function get(): array
    {
        $sql = $this->toSql();
        $stmt = $this->connection->prepare($sql);
        $stmt->execute($this->getBindings());
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function first(): ?array
    {
        $this->limit(1);
        $results = $this->get();
        return $results[0] ?? null;
    }

    private function toSql(): string
    {
        $sql = 'SELECT ' . implode(', ', $this->selects);
        $sql .= ' FROM ' . $this->table;

        foreach ($this->joins as $join) {
            $sql .= " {$join['type']} JOIN {$join['table']}";
            $sql .= " ON {$join['first']} {$join['operator']} {$join['second']}";
        }

        if (!empty($this->wheres)) {
            $sql .= ' WHERE ';
            $conditions = [];
            foreach ($this->wheres as $i => $where) {
                if ($i > 0) {
                    $conditions[] = $where['boolean'];
                }

                if ($where['operator'] === 'IN') {
                    $placeholders = implode(',', array_fill(0, count($where['value']), '?'));
                    $conditions[] = "{$where['column']} IN ({$placeholders})";
                } elseif ($where['operator'] === 'BETWEEN') {
                    $conditions[] = "{$where['column']} BETWEEN ? AND ?";
                } else {
                    $conditions[] = "{$where['column']} {$where['operator']} ?";
                }
            }
            $sql .= implode(' ', $conditions);
        }

        if (!empty($this->orderBy)) {
            $sql .= ' ORDER BY ';
            $orders = array_map(
                fn($order) => "{$order[0]} {$order[1]}",
                $this->orderBy
            );
            $sql .= implode(', ', $orders);
        }

        if ($this->limit !== null) {
            $sql .= ' LIMIT ' . $this->limit;
        }

        if ($this->offset !== null) {
            $sql .= ' OFFSET ' . $this->offset;
        }

        return $sql;
    }

    private function getBindings(): array
    {
        $bindings = [];
        foreach ($this->wheres as $where) {
            if (is_array($where['value'])) {
                $bindings = array_merge($bindings, $where['value']);
            } else {
                $bindings[] = $where['value'];
            }
        }
        return $bindings;
    }
}

// Uso com named arguments
$pdo = new \PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$builder = new QueryBuilder($pdo);

$users = $builder
    ->table('users')
    ->select('id', 'name', 'email')
    ->where(column: 'status', value: 'active')
    ->whereBetween(column: 'created_at', min: '2025-01-01', max: '2025-12-31')
    ->whereIn(column: 'role', values: ['admin', 'moderator'])
    ->orderBy(column: 'name', direction: 'ASC')
    ->limit(limit: 10)
    ->get();
Enter fullscreen mode Exit fullscreen mode

5. 🔄 Event Sourcing com Snapshots

Arquitetura Baseada em Eventos

Event Sourcing registra todas as mudanças de estado como uma sequência de eventos, permitindo reconstruir o estado em qualquer ponto no tempo.

Caso real: Uma plataforma de trading implementou Event Sourcing e conseguiu reduzir disputas de clientes em 87%, pois todo histórico de operações era auditável e reconstrutível.

Implementação

<?php

abstract class DomainEvent
{
    public function __construct(
        public readonly string $aggregateId,
        public readonly \DateTimeImmutable $occurredAt,
        public readonly int $version
    ) {}

    abstract public function toArray(): array;
}

class AccountCreated extends DomainEvent
{
    public function __construct(
        string $aggregateId,
        public readonly string $ownerName,
        public readonly string $currency,
        \DateTimeImmutable $occurredAt,
        int $version
    ) {
        parent::__construct($aggregateId, $occurredAt, $version);
    }

    public function toArray(): array
    {
        return [
            'owner_name' => $this->ownerName,
            'currency' => $this->currency
        ];
    }
}

class MoneyDeposited extends DomainEvent
{
    public function __construct(
        string $aggregateId,
        public readonly float $amount,
        public readonly string $description,
        \DateTimeImmutable $occurredAt,
        int $version
    ) {
        parent::__construct($aggregateId, $occurredAt, $version);
    }

    public function toArray(): array
    {
        return [
            'amount' => $this->amount,
            'description' => $this->description
        ];
    }
}

class MoneyWithdrawn extends DomainEvent
{
    public function __construct(
        string $aggregateId,
        public readonly float $amount,
        public readonly string $description,
        \DateTimeImmutable $occurredAt,
        int $version
    ) {
        parent::__construct($aggregateId, $occurredAt, $version);
    }

    public function toArray(): array
    {
        return [
            'amount' => $this->amount,
            'description' => $this->description
        ];
    }
}

class BankAccount
{
    private string $id;
    private string $ownerName;
    private string $currency;
    private float $balance = 0;
    private int $version = 0;
    private array $uncommittedEvents = [];

    public static function create(string $id, string $ownerName, string $currency): self
    {
        $account = new self();
        $account->recordThat(new AccountCreated(
            $id,
            $ownerName,
            $currency,
            new \DateTimeImmutable(),
            1
        ));
        return $account;
    }

    public function deposit(float $amount, string $description): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('Amount must be positive');
        }

        $this->recordThat(new MoneyDeposited(
            $this->id,
            $amount,
            $description,
            new \DateTimeImmutable(),
            $this->version + 1
        ));
    }

    public function withdraw(float $amount, string $description): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('Amount must be positive');
        }

        if ($amount > $this->balance) {
            throw new \RuntimeException('Insufficient funds');
        }

        $this->recordThat(new MoneyWithdrawn(
            $this->id,
            $amount,
            $description,
            new \DateTimeImmutable(),
            $this->version + 1
        ));
    }

    public static function reconstituteFrom(array $events): self
    {
        $account = new self();
        foreach ($events as $event) {
            $account->apply($event);
        }
        return $account;
    }

    private function recordThat(DomainEvent $event): void
    {
        $this->apply($event);
        $this->uncommittedEvents[] = $event;
    }

    private function apply(DomainEvent $event): void
    {
        match (true) {
            $event instanceof AccountCreated => $this->applyAccountCreated($event),
            $event instanceof MoneyDeposited => $this->applyMoneyDeposited($event),
            $event instanceof MoneyWithdrawn => $this->applyMoneyWithdrawn($event),
        };
        $this->version = $event->version;
    }

    private function applyAccountCreated(AccountCreated $event): void
    {
        $this->id = $event->aggregateId;
        $this->ownerName = $event->ownerName;
        $this->currency = $event->currency;
        $this->balance = 0;
    }

    private function applyMoneyDeposited(MoneyDeposited $event): void
    {
        $this->balance += $event->amount;
    }

    private function applyMoneyWithdrawn(MoneyWithdrawn $event): void
    {
        $this->balance -= $event->amount;
    }

    public function getUncommittedEvents(): array
    {
        return $this->uncommittedEvents;
    }

    public function clearUncommittedEvents(): void
    {
        $this->uncommittedEvents = [];
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getBalance(): float
    {
        return $this->balance;
    }

    public function getVersion(): int
    {
        return $this->version;
    }
}

class EventStore
{
    public function __construct(private \PDO $connection) {}

    public function append(string $aggregateId, array $events): void
    {
        $this->connection->beginTransaction();

        try {
            foreach ($events as $event) {
                $stmt = $this->connection->prepare(
                    'INSERT INTO events (aggregate_id, event_type, event_data, occurred_at, version)
                     VALUES (?, ?, ?, ?, ?)'
                );

                $stmt->execute([
                    $event->aggregateId,
                    get_class($event),
                    json_encode($event->toArray()),
                    $event->occurredAt->format('Y-m-d H:i:s'),
                    $event->version
                ]);
            }

            $this->connection->commit();
        } catch (\Exception $e) {
            $this->connection->rollBack();
            throw $e;
        }
    }

    public function getEventsFor(string $aggregateId): array
    {
        $stmt = $this->connection->prepare(
            'SELECT * FROM events WHERE aggregate_id = ? ORDER BY version ASC'
        );
        $stmt->execute([$aggregateId]);

        $events = [];
        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            $events[] = $this->deserializeEvent($row);
        }

        return $events;
    }

    private function deserializeEvent(array $data): DomainEvent
    {
        $eventClass = $data['event_type'];
        $eventData = json_decode($data['event_data'], true);

        // Reconstrói o evento baseado no tipo
        return match ($eventClass) {
            AccountCreated::class => new AccountCreated(
                $data['aggregate_id'],
                $eventData['owner_name'],
                $eventData['currency'],
                new \DateTimeImmutable($data['occurred_at']),
                $data['version']
            ),
            MoneyDeposited::class => new MoneyDeposited(
                $data['aggregate_id'],
                $eventData['amount'],
                $eventData['description'],
                new \DateTimeImmutable($data['occurred_at']),
                $data['version']
            ),
            MoneyWithdrawn::class => new MoneyWithdrawn(
                $data['aggregate_id'],
                $eventData['amount'],
                $eventData['description'],
                new \DateTimeImmutable($data['occurred_at']),
                $data['version']
            ),
        };
    }
}

// Uso
$pdo = new \PDO('mysql:host=localhost;dbname=eventstore', 'user', 'pass');
$eventStore = new EventStore($pdo);

// Criar conta e realizar operações
$account = BankAccount::create('acc-001', 'João Silva', 'BRL');
$account->deposit(1000.00, 'Depósito inicial');
$account->withdraw(250.00, 'Pagamento conta de luz');
$account->deposit(500.00, 'Salário');

// Salvar eventos
$eventStore->append($account->getId(), $account->getUncommittedEvents());
$account->clearUncommittedEvents();

// Reconstruir estado a partir dos eventos
$events = $eventStore->getEventsFor('acc-001');
$reconstructedAccount = BankAccount::reconstituteFrom($events);
echo "Saldo: " . $reconstructedAccount->getBalance(); // 1250.00
Enter fullscreen mode Exit fullscreen mode

6. 🧬 Property Hooks (PHP 8.4+)

Simplificando Getters e Setters

Property hooks permitem adicionar lógica diretamente em propriedades, eliminando a necessidade de métodos getter/setter verbosos.

Implementação

<?php

class Money
{
    public function __construct(
        public float $amount {
            set {
                if ($value < 0) {
                    throw new \InvalidArgumentException('Amount cannot be negative');
                }
                $this->amount = round($value, 2);
            }
        },
        public string $currency {
            set => strtoupper($value)
        }
    ) {}

    public float $formatted {
        get => number_format($this->amount, 2, ',', '.');
    }
}

class User
{
    private array $_permissions = [];

    public function __construct(
        public string $email {
            set {
                if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    throw new \InvalidArgumentException('Invalid email');
                }
                $this->email = strtolower($value);
            }
        },
        public string $password {
            set => password_hash($value, PASSWORD_ARGON2ID)
        },
        public \DateTimeImmutable $createdAt {
            get => $this->createdAt ?? new \DateTimeImmutable()
        }
    ) {}

    public array $permissions {
        get => $this->_permissions,
        set {
            $this->_permissions = array_unique(array_map('strtoupper', $value));
        }
    }

    public bool $isAdmin {
        get => in_array('ADMIN', $this->permissions)
    }
}

class Product
{
    private float $_price;
    private float $_discount = 0;

    public function __construct(
        public string $name,
        float $price
    ) {
        $this->price = $price;
    }

    public float $price {
        get => $this->_price,
        set {
            if ($value <= 0) {
                throw new \InvalidArgumentException('Price must be positive');
            }
            $this->_price = $value;
        }
    }

    public float $discount {
        get => $this->_discount,
        set {
            if ($value < 0 || $value > 100) {
                throw new \InvalidArgumentException('Discount must be between 0 and 100');
            }
            $this->_discount = $value;
        }
    }

    public float $finalPrice {
        get => $this->price * (1 - $this->discount / 100)
    }

    public float $savings {
        get => $this->price - $this->finalPrice
    }
}

// Uso
$product = new Product('Notebook', 5000.00);
$product->discount = 15;

echo $product->finalPrice;  // 4250.00
echo $product->savings;     // 750.00
Enter fullscreen mode Exit fullscreen mode

7. 🎨 Pipeline Pattern com Middleware

Processamento Encadeado Elegante

O Pipeline Pattern permite processar dados através de uma série de etapas independentes, criando fluxos de processamento modulares e testáveis.

Implementação Robusta

<?php

interface PipelineStage
{
    public function process(mixed $payload, \Closure $next): mixed;
}

class Pipeline
{
    private array $stages = [];

    public function pipe(PipelineStage|callable $stage): self
    {
        $this->stages[] = $stage;
        return $this;
    }

    public function send(mixed $payload): mixed
    {
        $pipeline = array_reduce(
            array_reverse($this->stages),
            fn($next, $stage) => fn($payload) => $this->executeStage($stage, $payload, $next),
            fn($payload) => $payload
        );

        return $pipeline($payload);
    }

    private function executeStage(PipelineStage|callable $stage, mixed $payload, \Closure $next): mixed
    {
        if ($stage instanceof PipelineStage) {
            return $stage->process($payload, $next);
        }

        return $stage($payload, $next);
    }
}

// Stages de processamento de pedido
class ValidateOrder implements PipelineStage
{
    public function process(mixed $payload, \Closure $next): mixed
    {
        if (empty($payload['items'])) {
            throw new \RuntimeException('Order must have items');
        }

        if (!isset($payload['customer_id'])) {
            throw new \RuntimeException('Customer ID is required');
        }

        return $next($payload);
    }
}

class CalculateTotals implements PipelineStage
{
    public function process(mixed $payload, \Closure $next): mixed
    {
        $subtotal = array_reduce(
            $payload['items'],
            fn($sum, $item) => $sum + ($item['price'] * $item['quantity']),
            0
        );

        $tax = $subtotal * 0.08; // 8% de imposto
        $shipping = $subtotal > 100 ? 0 : 15;

        $payload['subtotal'] = $subtotal;
        $payload['tax'] = $tax;
        $payload['shipping'] = $shipping;
        $payload['total'] = $subtotal + $tax + $shipping;

        return $next($payload);
    }
}

class ApplyDiscounts implements PipelineStage
{
    public function __construct(private array $discountRules) {}

    public function process(mixed $payload, \Closure $next): mixed
    {
        $discount = 0;

        foreach ($this->discountRules as $rule) {
            if ($rule->applies($payload)) {
                $discount += $rule->calculate($payload);
            }
        }

        $payload['discount'] = $discount;
        $payload['total'] -= $discount;

        return $next($payload);
    }
}

class CheckInventory implements PipelineStage
{
    public function __construct(private InventoryService $inventory) {}

    public function process(mixed $payload, \Closure $next): mixed
    {
        foreach ($payload['items'] as $item) {
            if (!$this->inventory->isAvailable($item['product_id'], $item['quantity'])) {
                throw new \RuntimeException(
                    "Product {$item['product_id']} is not available in requested quantity"
                );
            }
        }

        return $next($payload);
    }
}

class ReserveInventory implements PipelineStage
{
    public function __construct(private InventoryService $inventory) {}

    public function process(mixed $payload, \Closure $next): mixed
    {
        $reservations = [];

        foreach ($payload['items'] as $item) {
            $reservations[] = $this->inventory->reserve(
                $item['product_id'],
                $item['quantity']
            );
        }

        $payload['reservations'] = $reservations;

        return $next($payload);
    }
}

class SaveOrder implements PipelineStage
{
    public function __construct(private \PDO $db) {}

    public function process(mixed $payload, \Closure $next): mixed
    {
        $this->db->beginTransaction();

        try {
            $stmt = $this->db->prepare(
                'INSERT INTO orders (customer_id, subtotal, tax, shipping, discount, total, status)
                 VALUES (?, ?, ?, ?, ?, ?, ?)'
            );

            $stmt->execute([
                $payload['customer_id'],
                $payload['subtotal'],
                $payload['tax'],
                $payload['shipping'],
                $payload['discount'],
                $payload['total'],
                'pending'
            ]);

            $orderId = $this->db->lastInsertId();
            $payload['order_id'] = $orderId;

            foreach ($payload['items'] as $item) {
                $stmt = $this->db->prepare(
                    'INSERT INTO order_items (order_id, product_id, quantity, price)
                     VALUES (?, ?, ?, ?)'
                );

                $stmt->execute([
                    $orderId,
                    $item['product_id'],
                    $item['quantity'],
                    $item['price']
                ]);
            }

            $this->db->commit();

            return $next($payload);
        } catch (\Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}

class NotifyCustomer implements PipelineStage
{
    public function __construct(private EmailService $email) {}

    public function process(mixed $payload, \Closure $next): mixed
    {
        $this->email->send(
            $payload['customer_email'],
            'Pedido Confirmado',
            "Seu pedido #{$payload['order_id']} foi confirmado!"
        );

        return $next($payload);
    }
}

// Uso
class OrderProcessor
{
    public function __construct(
        private \PDO $db,
        private InventoryService $inventory,
        private EmailService $email
    ) {}

    public function processOrder(array $orderData): array
    {
        $pipeline = new Pipeline();

        $result = $pipeline
            ->pipe(new ValidateOrder())
            ->pipe(new CalculateTotals())
            ->pipe(new ApplyDiscounts([]))
            ->pipe(new CheckInventory($this->inventory))
            ->pipe(new ReserveInventory($this->inventory))
            ->pipe(new SaveOrder($this->db))
            ->pipe(new NotifyCustomer($this->email))
            ->send($orderData);

        return $result;
    }
}
Enter fullscreen mode Exit fullscreen mode

8. 🔍 Advanced Error Handling com Result Objects

Substituindo Exceptions por Result Types

Result objects oferecem uma alternativa às exceptions para tratamento de erros, tornando o fluxo de erro explícito e type-safe.

Implementação

<?php

abstract class Result
{
    abstract public function isSuccess(): bool;
    abstract public function isFailure(): bool;
}

final class Success extends Result
{
    public function __construct(private readonly mixed $value) {}

    public function getValue(): mixed
    {
        return $this->value;
    }

    public function isSuccess(): bool
    {
        return true;
    }

    public function isFailure(): bool
    {
        return false;
    }

    public function map(callable $fn): self
    {
        return new self($fn($this->value));
    }

    public function flatMap(callable $fn): Result
    {
        return $fn($this->value);
    }

    public function getOrElse(mixed $default): mixed
    {
        return $this->value;
    }

    public function orElse(callable $fn): self
    {
        return $this;
    }
}

final class Failure extends Result
{
    public function __construct(
        private readonly string $message,
        private readonly ?string $code = null,
        private readonly ?array $context = null
    ) {}

    public function getMessage(): string
    {
        return $this->message;
    }

    public function getCode(): ?string
    {
        return $this->code;
    }

    public function getContext(): ?array
    {
        return $this->context;
    }

    public function isSuccess(): bool
    {
        return false;
    }

    public function isFailure(): bool
    {
        return true;
    }

    public function map(callable $fn): self
    {
        return $this;
    }

    public function flatMap(callable $fn): self
    {
        return $this;
    }

    public function getOrElse(mixed $default): mixed
    {
        return $default;
    }

    public function orElse(callable $fn): Result
    {
        return $fn($this);
    }
}

class UserService
{
    public function __construct(
        private \PDO $db,
        private EmailValidator $emailValidator
    ) {}

    public function createUser(string $email, string $password): Result
    {
        return $this->validateEmail($email)
            ->flatMap(fn($email) => $this->validatePassword($password))
            ->flatMap(fn() => $this->checkEmailAvailability($email))
            ->flatMap(fn() => $this->saveUser($email, $password));
    }

    private function validateEmail(string $email): Result
    {
        if (!$this->emailValidator->isValid($email)) {
            return new Failure(
                'Invalid email format',
                'INVALID_EMAIL',
                ['email' => $email]
            );
        }

        return new Success($email);
    }

    private function validatePassword(string $password): Result
    {
        if (strlen($password) < 8) {
            return new Failure(
                'Password must be at least 8 characters',
                'WEAK_PASSWORD'
            );
        }

        if (!preg_match('/[A-Z]/', $password)) {
            return new Failure(
                'Password must contain uppercase letter',
                'WEAK_PASSWORD'
            );
        }

        if (!preg_match('/[0-9]/', $password)) {
            return new Failure(
                'Password must contain number',
                'WEAK_PASSWORD'
            );
        }

        return new Success($password);
    }

    private function checkEmailAvailability(string $email): Result
    {
        $stmt = $this->db->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
        $stmt->execute([$email]);

        if ($stmt->fetchColumn() > 0) {
            return new Failure(
                'Email already registered',
                'EMAIL_EXISTS',
                ['email' => $email]
            );
        }

        return new Success(true);
    }

    private function saveUser(string $email, string $password): Result
    {
        try {
            $stmt = $this->db->prepare(
                'INSERT INTO users (email, password, created_at) VALUES (?, ?, ?)'
            );

            $stmt->execute([
                $email,
                password_hash($password, PASSWORD_ARGON2ID),
                date('Y-m-d H:i:s')
            ]);

            return new Success([
                'user_id' => $this->db->lastInsertId(),
                'email' => $email
            ]);
        } catch (\PDOException $e) {
            return new Failure(
                'Failed to create user',
                'DATABASE_ERROR',
                ['error' => $e->getMessage()]
            );
        }
    }
}

// Uso
$userService = new UserService($pdo, new EmailValidator());

$result = $userService->createUser('joao@example.com', 'SecurePass123');

if ($result->isSuccess()) {
    $userData = $result->getValue();
    echo "User created successfully! ID: {$userData['user_id']}";
} else {
    $error = $result;
    echo "Error: {$error->getMessage()} (Code: {$error->getCode()})";

    if ($context = $error->getContext()) {
        print_r($context);
    }
}

// Encadeamento com tratamento
$result = $userService->createUser('invalid-email', 'weak')
    ->map(fn($user) => [...$user, 'status' => 'active'])
    ->getOrElse(['status' => 'failed']);
Enter fullscreen mode Exit fullscreen mode

🏆 Conclusão

As técnicas apresentadas neste artigo representam o estado da arte em desenvolvimento PHP moderno. Cada uma delas resolve problemas reais de forma elegante e eficiente:

  • Generator Pipelines para processamento eficiente de grandes volumes de dados
  • Sealed Classes para garantir exhaustividade e type safety
  • WeakMaps para gerenciamento automático de memória
  • Named Arguments para APIs flexíveis e expressivas
  • Event Sourcing para auditoria completa e temporal queries
  • Property Hooks para encapsulamento limpo sem boilerplate
  • Pipeline Pattern para fluxos de processamento modulares
  • Result Objects para tratamento de erros explícito e type-safe

📚 Referências e Recursos

Documentação Oficial

Leitura Adicional

  • "Domain-Driven Design" - Eric Evans
  • "Patterns of Enterprise Application Architecture" - Martin Fowler
  • "PHP: The Right Way" - Modern PHP best practices

Ferramentas Recomendadas

  • PHPStan (Level 9): Análise estática avançada
  • Psalm: Type checking rigoroso
  • Rector: Refatoração automatizada
  • PHP-CS-Fixer: Code style consistency

👨‍💻 Sobre o Autor

Cristian Bernardes

Senior PHP Developer com quase 20 anos de experiência em arquiteturas escaláveis, otimização de performance e desenvolvimento de sistemas críticos. Especialista em Laravel, Docker, microserviços e padrões arquiteturais avançados.

Conecte-se:


💡 Gostou do conteúdo? Compartilhe com sua rede e ajude outros desenvolvedores a elevar suas skills em PHP!

Última atualização: Dezembro de 2025

Top comments (0)