Padrões de projeto em PHP
Esta aula apresenta quatro padrões de projeto essenciais em PHP: Factory, Strategy, Observer e Repository. Cada padrão é explicado com seu contexto, estrutura e implementação prática em código, além de exercícios para fixação. O objetivo é capacitar o aluno a aplicar esses padrões para criar sistemas mais flexíveis, testáveis e de fácil manutenção.
Padrões de projeto são soluções reutilizáveis para problemas comuns no desenvolvimento de software. Eles representam práticas consolidadas pela comunidade, que ajudam a criar códigos mais organizados, flexíveis e de fácil manutenção. Nesta aula, vamos explorar quatro padrões fundamentais no contexto do PHP: Factory, Strategy, Observer e Repository. Cada um resolve um tipo específico de problema, e conhecê-los é essencial para qualquer desenvolvedor PHP que deseje escrever código profissional e escalável.
Antes de mergulharmos nos padrões, é importante entender que eles não são receitas prontas, mas sim diretrizes que devem ser adaptadas ao contexto do seu projeto. O uso exagerado de padrões pode gerar complexidade desnecessária, portanto, avalie sempre se o padrão realmente resolve um problema que você está enfrentando. Vamos começar!
Factory
O padrão Factory (Fábrica) é um padrão criacional que fornece uma interface para criar objetos em uma superclasse, mas permite que as subclasses alterem o tipo de objeto que será criado. Em termos práticos, em vez de instanciar diretamente uma classe com new, você delega a criação a um método de fábrica. Isso centraliza a lógica de criação, facilitando a manutenção e a extensão do código.
Um exemplo comum é quando temos diferentes tipos de conexão com banco de dados (MySQL, PostgreSQL, SQLite) e queremos selecionar a conexão correta com base em uma configuração. A Factory nos permite encapsular essa decisão em um único lugar, evitando espalhar condicionais pelo código.
Vamos ver um exemplo de Factory simples:
interface DatabaseConnection {
public function connect(): void;
}
class MySQLConnection implements DatabaseConnection {
public function connect(): void {
echo "Conectando ao MySQL\n";
}
}
class PostgreSQLConnection implements DatabaseConnection {
public function connect(): void {
echo "Conectando ao PostgreSQL\n";
}
}
class DatabaseFactory {
public static function create(string $type): DatabaseConnection {
return match ($type) {
'mysql' => new MySQLConnection(),
'pgsql' => new PostgreSQLConnection(),
default => throw new \InvalidArgumentException("Tipo de banco desconhecido: $type"),
};
}
}
// Uso
$connection = DatabaseFactory::create('mysql');
$connection->connect();
Neste exemplo, a classe DatabaseFactory centraliza a criação de conexões. Se amanhã precisarmos adicionar um novo tipo de banco, basta adicionar um novo case no match e uma nova classe que implemente a interface. O código cliente não precisa ser alterado, o que está de acordo com o princípio Open/Closed (aberto para extensão, fechado para modificação).
Strategy
O padrão Strategy é um padrão comportamental que permite definir uma família de algoritmos, colocá-los em classes separadas e torná-los intercambiáveis. Ele é útil quando você tem várias formas de realizar uma operação e quer selecionar uma delas em tempo de execução, sem usar condicionais complexas.
Um exemplo clássico é a aplicação de impostos em um e-commerce. Dependendo do país ou do tipo de produto, o cálculo do imposto varia. Em vez de preencher a classe de pedido com if/else, você define uma interface TaxStrategy e implementa cada regra em uma classe separada.
Veja o código:
interface TaxStrategy {
public function calculate(float $price): float;
}
class BrazilTax implements TaxStrategy {
public function calculate(float $price): float {
return $price * 0.2; // 20% de imposto
}
}
class USATax implements TaxStrategy {
public function calculate(float $price): float {
return $price * 0.1; // 10% de imposto
}
}
class Order {
private float $total;
private TaxStrategy $taxStrategy;
public function __construct(float $total, TaxStrategy $taxStrategy) {
$this->total = $total;
$this->taxStrategy = $taxStrategy;
}
public function calculateTax(): float {
return $this->taxStrategy->calculate($this->total);
}
}
// Uso
$order = new Order(100.0, new BrazilTax());
echo $order->calculateTax(); // 20.0
O padrão Strategy permite que o comportamento da classe Order seja alterado simplesmente trocando a estratégia passada no construtor. Isso torna o código mais testável, pois podemos testar cada estratégia isoladamente, e mais flexível, pois novas regras podem ser adicionadas sem modificar as classes existentes.
Observer
O padrão Observer é um padrão comportamental que define uma dependência de um-para-muitos entre objetos, de forma que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente. É muito utilizado em sistemas de eventos, como notificações, logs ou atualizações de interface.
Em PHP, o SPL (Standard PHP Library) já fornece as interfaces SplSubject e SplObserver, facilitando a implementação. Vamos criar um exemplo onde um pedido, ao ter seu status alterado, notifica observadores (como um serviço de e-mail e um sistema de auditoria).
class Order implements \SplSubject {
private string $status;
private \SplObjectStorage $observers;
public function __construct() {
$this->observers = new \SplObjectStorage();
$this->status = 'pending';
}
public function attach(\SplObserver $observer): void {
$this->observers->attach($observer);
}
public function detach(\SplObserver $observer): void {
$this->observers->detach($observer);
}
public function notify(): void {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setStatus(string $status): void {
$this->status = $status;
$this->notify();
}
public function getStatus(): string {
return $this->status;
}
}
class EmailNotifier implements \SplObserver {
public function update(\SplSubject $subject): void {
echo "Enviando e-mail: pedido com status {$subject->getStatus()}\n";
}
}
class AuditLogger implements \SplObserver {
public function update(\SplSubject $subject): void {
echo "Registrando auditoria: status alterado para {$subject->getStatus()}\n";
}
}
// Uso
$order = new Order();
$order->attach(new EmailNotifier());
$order->attach(new AuditLogger());
$order->setStatus('paid');
// Saída:
// Enviando e-mail: pedido com status paid
// Registrando auditoria: status alterado para paid
Repare que o objeto Order não precisa conhecer os detalhes de cada observador, apenas que eles implementam a interface SplObserver. Isso desacopla o sujeito dos observadores, permitindo adicionar novos observadores sem modificar o sujeito. Esse padrão é a base de muitos sistemas de eventos em frameworks como o Laravel.
Repository
O padrão Repository é um padrão estrutural que atua como uma camada de mediação entre a lógica de negócio e o acesso a dados. Ele encapsula a lógica de consulta e persistência, fornecendo uma interface uniforme para trabalhar com entidades. Isso isola o restante da aplicação dos detalhes de implementação do banco de dados, tornando o código mais testável e fácil de mudar de persistência (ex.: de MySQL para MongoDB).
Um repositório geralmente oferece métodos como findById, findAll, save e delete. Em vez de espalhar consultas com Query Builder ou Eloquent pelo código, você centraliza tudo no repositório.
Exemplo com PDO:
class User {
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email
) {}
}
interface UserRepository {
public function findById(int $id): ?User;
public function findAll(): array;
public function save(User $user): void;
}
class PDOUserRepository implements UserRepository {
public function __construct(private \PDO $pdo) {}
public function findById(int $id): ?User {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$data = $stmt->fetch(\PDO::FETCH_ASSOC);
return $data ? new User($data['id'], $data['name'], $data['email']) : null;
}
public function findAll(): array {
$stmt = $this->pdo->query('SELECT * FROM users');
$users = [];
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
$users[] = new User($row['id'], $row['name'], $row['email']);
}
return $users;
}
public function save(User $user): void {
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
$stmt->execute([':name' => $user->name, ':email' => $user->email]);
}
}
// Uso
$pdo = new \PDO('mysql:host=localhost;dbname=test', 'root', '');
$userRepo = new PDOUserRepository($pdo);
$user = $userRepo->findById(1);
O padrão Repository é amplamente utilizado em frameworks modernos. No Laravel, o Eloquent já oferece uma implementação de repositório, mas você pode criar seus próprios repositórios para abstrair ainda mais a camada de dados. Ele também facilita a implementação de testes, pois você pode criar repositórios falsos (fakes) que não dependem de banco de dados.
Boas Práticas e Observações Finais
Ao aplicar esses padrões, tenha em mente os princípios SOLID, especialmente o da Responsabilidade Única e o Open/Closed. Cada padrão deve ser usado para resolver um problema específico, e não por modismo. Comece com uma implementação simples e evolua conforme a necessidade.
Além disso, é importante manter a consistência na nomenclatura e na estrutura. Padrões de projeto são uma linguagem comum entre desenvolvedores; usá-los corretamente facilita a comunicação e a manutenção do código.
Por fim, estude os padrões em conjunto com os recursos da linguagem. O PHP moderno oferece muitas facilidades, como arrow functions e match, que podem simplificar a implementação de alguns padrões.
Referências
- PHP Manual - Padrões de Projeto
- PHP Manual - SplSubject
- PHP Manual - SplObserver
- Refactoring Guru - Factory Method
- Refactoring Guru - Strategy
- Refactoring Guru - Observer
- Martin Fowler - Repository
Exercícios
- Crie uma classe
ShapeFactoryque retorne objetos de formas geométricas (Círculo, Quadrado, Triângulo) com base em uma string. Cada forma deve ter um métodoarea(). - Implemente o padrão Strategy para um sistema de pagamento que aceita Pix, Cartão de Crédito e Boleto. Cada estratégia deve ter um método
pay(float $amount). - Usando as interfaces SPL, crie um sistema onde um
Stocknotifica observadores quando o estoque de um produto fica baixo. - Crie um repositório para a entidade
Product(id, nome, preço) usando PDO. Implemente os métodosfindById,findAllesave. - Refatore o código abaixo para usar um Factory para criar diferentes tipos de notificação (Email, SMS, Push). O código atual usa if/else.
interface Shape { public function area(): float; }
class Circle implements Shape {
public function __construct(private float $radius) {}
public function area(): float { return pi() * $this->radius ** 2; }
}
class Square implements Shape {
public function __construct(private float $side) {}
public function area(): float { return $this->side ** 2; }
}
class Triangle implements Shape {
public function __construct(private float $base, private float $height) {}
public function area(): float { return ($this->base * $this->height) / 2; }
}
class ShapeFactory {
public static function create(string $type, array $params): Shape {
return match ($type) {
'circle' => new Circle($params['radius']),
'square' => new Square($params['side']),
'triangle' => new Triangle($params['base'], $params['height']),
default => throw new \InvalidArgumentException("Forma desconhecida: $type"),
};
}
}
interface PaymentStrategy {
public function pay(float $amount): void;
}
class PixPayment implements PaymentStrategy {
public function pay(float $amount): void { echo "Pagando R$ {$amount} via Pix\n"; }
}
class CreditCardPayment implements PaymentStrategy {
public function pay(float $amount): void { echo "Pagando R$ {$amount} via Cartão de Crédito\n"; }
}
class BoletoPayment implements PaymentStrategy {
public function pay(float $amount): void { echo "Pagando R$ {$amount} via Boleto\n"; }
}
class PaymentContext {
public function __construct(private PaymentStrategy $strategy) {}
public function setStrategy(PaymentStrategy $strategy): void { $this->strategy = $strategy; }
public function pay(float $amount): void { $this->strategy->pay($amount); }
}
class Stock implements \SplSubject {
private \SplObjectStorage $observers;
private int $quantity;
public function __construct(private string $product) {
$this->observers = new \SplObjectStorage();
$this->quantity = 0;
}
public function attach(\SplObserver $observer): void { $this->observers->attach($observer); }
public function detach(\SplObserver $observer): void { $this->observers->detach($observer); }
public function notify(): void {
foreach ($this->observers as $observer) { $observer->update($this); }
}
public function setQuantity(int $quantity): void {
$this->quantity = $quantity;
if ($quantity < 10) { $this->notify(); }
}
public function getProduct(): string { return $this->product; }
public function getQuantity(): int { return $this->quantity; }
}
class LowStockAlert implements \SplObserver {
public function update(\SplSubject $subject): void {
echo "Alerta: estoque baixo para {$subject->getProduct()} ({$subject->getQuantity()} restantes)\n";
}
}
class Product {
public function __construct(
public readonly ?int $id,
public readonly string $name,
public readonly float $price
) {}
}
interface ProductRepository {
public function findById(int $id): ?Product;
public function findAll(): array;
public function save(Product $product): void;
}
class PDOProductRepository implements ProductRepository {
public function __construct(private \PDO $pdo) {}
public function findById(int $id): ?Product {
$stmt = $this->pdo->prepare('SELECT * FROM products WHERE id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ? new Product($row['id'], $row['name'], $row['price']) : null;
}
public function findAll(): array {
$stmt = $this->pdo->query('SELECT * FROM products');
$products = [];
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
$products[] = new Product($row['id'], $row['name'], $row['price']);
}
return $products;
}
public function save(Product $product): void {
if ($product->id === null) {
$stmt = $this->pdo->prepare('INSERT INTO products (name, price) VALUES (:name, :price)');
$stmt->execute([':name' => $product->name, ':price' => $product->price]);
} else {
$stmt = $this->pdo->prepare('UPDATE products SET name = :name, price = :price WHERE id = :id');
$stmt->execute([':name' => $product->name, ':price' => $product->price, ':id' => $product->id]);
}
}
}
interface Notification {
public function send(string $message): void;
}
class EmailNotification implements Notification {
public function send(string $message): void { echo "Enviando e-mail: $message\n"; }
}
class SMSNotification implements Notification {
public function send(string $message): void { echo "Enviando SMS: $message\n"; }
}
class PushNotification implements Notification {
public function send(string $message): void { echo "Enviando push: $message\n"; }
}
class NotificationFactory {
public static function create(string $type): Notification {
return match ($type) {
'email' => new EmailNotification(),
'sms' => new SMSNotification(),
'push' => new PushNotification(),
default => throw new \InvalidArgumentException("Tipo de notificação desconhecido: $type"),
};
}
}
// Uso: $notification = NotificationFactory::create('email'); $notification->send('Olá');