Propriedades readonly e imutabilidade
Esta aula aborda o conceito de imutabilidade em PHP, focando na palavra-chave 'readonly' introduzida no PHP 8.1 para propriedades de classes. Serão explorados objetos imutáveis e value objects, com exemplos práticos de como implementá-los para garantir integridade de dados e previsibilidade no código.
A imutabilidade é um princípio fundamental da programação funcional e orientada a objetos que garante que, uma vez criado, um objeto não possa ter seu estado alterado. No PHP, a partir da versão 8.1, temos a palavra-chave readonly para propriedades de classes, que facilita a criação de objetos imutáveis de forma concisa e segura. Nesta aula, vamos explorar como usar readonly, o que são objetos imutáveis e value objects, e como essas técnicas podem melhorar a qualidade do seu código.
Entender imutabilidade é essencial para escrever código mais previsível, livre de efeitos colaterais e mais fácil de testar. Veremos na prática como aplicar esses conceitos em PHP moderno.
readonly (PHP 8.1)
A partir do PHP 8.1, você pode declarar propriedades de uma classe como readonly. Isso significa que a propriedade só pode ser atribuída uma vez, geralmente no construtor, e não pode ser modificada depois. Essa característica é especialmente útil para criar objetos imutáveis de forma simples, sem a necessidade de escrever getters manualmente ou métodos de modificação proibidos.
Uma propriedade readonly pode ser declarada com ou sem tipo, e pode ser inicializada diretamente na declaração ou no construtor. Após a inicialização, qualquer tentativa de modificar a propriedade resulta em um erro fatal. É importante notar que readonly só pode ser aplicada a propriedades tipadas (com tipo declarado) ou com valor padrão. Além disso, a propriedade não pode ser static.
class Pessoa {
public readonly string $nome;
public readonly int $idade;
public function __construct(string $nome, int $idade) {
$this->nome = $nome;
$this->idade = $idade;
}
}
$pessoa = new Pessoa('João', 30);
echo $pessoa->nome; // João
// $pessoa->nome = 'Maria'; // Erro fatal: Cannot modify readonly property
No exemplo acima, as propriedades $nome e $idade são readonly, definidas no construtor. Qualquer tentativa de alterá-las após a construção resulta em erro. Isso garante que o objeto seja imutável após a criação.
Objetos imutáveis
Um objeto imutável é aquele cujo estado não pode ser alterado após sua criação. Em vez de modificar o objeto, você cria um novo objeto com as alterações desejadas. Isso traz benefícios como segurança em ambientes concorrentes, previsibilidade e facilidade de teste. No PHP, a imutabilidade pode ser alcançada combinando propriedades readonly com a ausência de métodos que modifiquem o estado interno.
Objetos imutáveis são comuns em value objects e em programação funcional. Eles garantem que um objeto, uma vez criado, sempre represente o mesmo valor. Isso evita bugs causados por alterações inesperadas em objetos compartilhados.
class Endereco {
public readonly string $rua;
public readonly string $cidade;
public readonly string $cep;
public function __construct(string $rua, string $cidade, string $cep) {
$this->rua = $rua;
$this->cidade = $cidade;
$this->cep = $cep;
}
// Método que retorna um novo objeto com a rua alterada
public function withRua(string $novaRua): self {
return new self($novaRua, $this->cidade, $this->cep);
}
}
$enderecoOriginal = new Endereco('Rua A', 'São Paulo', '01001-000');
$enderecoModificado = $enderecoOriginal->withRua('Rua B');
echo $enderecoOriginal->rua; // Rua A (imutável)
echo $enderecoModificado->rua; // Rua B (novo objeto)
No exemplo, o método withRua não modifica o objeto original, mas cria um novo. Isso é uma prática comum em objetos imutáveis.
Value objects
Value objects são objetos que representam um valor conceitual, como dinheiro, cor, coordenadas, etc. Eles são imutáveis e se comparam pelo seu conteúdo, não por identidade. Dois value objects com os mesmos valores são considerados iguais. Em PHP, value objects são frequentemente implementados com propriedades readonly e métodos de comparação.
Value objects ajudam a evitar primitives obsession (obsessão por tipos primitivos) e tornam o código mais expressivo. Por exemplo, em vez de passar um array ou string para representar um endereço, você cria uma classe Endereco com validação e comportamento encapsulados.
class Dinheiro {
public readonly float $valor;
public readonly string $moeda;
public function __construct(float $valor, string $moeda) {
if ($valor < 0) {
throw new \InvalidArgumentException('Valor não pode ser negativo');
}
$this->valor = $valor;
$this->moeda = $moeda;
}
public function somar(Dinheiro $outro): self {
if ($this->moeda !== $outro->moeda) {
throw new \InvalidArgumentException('Moedas diferentes');
}
return new self($this->valor + $outro->valor, $this->moeda);
}
public function equals(Dinheiro $outro): bool {
return $this->valor === $outro->valor && $this->moeda === $outro->moeda;
}
}
$dezReais = new Dinheiro(10.0, 'BRL');
$vinteReais = $dezReais->somar(new Dinheiro(10.0, 'BRL'));
echo $vinteReais->valor; // 20.0
Value objects como Dinheiro encapsulam regras de negócio (validação, operações) e garantem que valores inconsistentes não sejam criados.
Exemplos
Vamos ver um exemplo completo que combina todos os conceitos: uma classe Usuario imutável com propriedades readonly e um value object Email para representar o email do usuário.
readonly class Email {
public function __construct(
public string $endereco
) {
if (!filter_var($endereco, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email inválido');
}
}
public function equals(Email $outro): bool {
return $this->endereco === $outro->endereco;
}
}
readonly class Usuario {
public function __construct(
public string $nome,
public Email $email,
public int $idade
) {}
public function withNome(string $novoNome): self {
return new self($novoNome, $this->email, $this->idade);
}
}
$email = new Email('joao@example.com');
$usuario = new Usuario('João', $email, 30);
$usuarioNovo = $usuario->withNome('João Silva');
echo $usuario->nome; // João
echo $usuarioNovo->nome; // João Silva
Neste exemplo, a classe Email é um value object que valida o email na construção. A classe Usuario é imutável, com todas as propriedades readonly. O método withNome retorna um novo usuário com o nome alterado. Isso garante que o objeto original permaneça inalterado.
Boas práticas
Ao trabalhar com imutabilidade em PHP, considere: sempre que possível, declare propriedades como readonly para evitar modificações acidentais; use value objects para representar conceitos do domínio; evite métodos que modifiquem o estado interno; e, se precisar de alterações, crie novos objetos. Além disso, lembre-se de que a imutabilidade pode ter custo de performance devido à criação de novos objetos, mas em muitos casos os benefícios superam esse custo.
Referências
- PHP Manual: Readonly Properties
- PHP 8.1 Release: Readonly Properties
- Martin Fowler: Value Object
- Wikipedia: Immutable Object
- Stitcher.io: PHP 8.1 Readonly Properties
Exercícios
Crie uma classe
Pontoque represente um ponto no plano cartesiano (coordenadas x e y). A classe deve ser imutável, com propriedades readonly. Implemente um métodomoverque retorne um novo ponto com coordenadas alteradas.✓ Resposta:readonly class Ponto { public function __construct( public float $x, public float $y ) {} public function mover(float $dx, float $dy): self { return new self($this->x + $dx, $this->y + $dy); } }Implemente um value object
CPFque valide o formato (apenas números, 11 dígitos) e seja imutável. Inclua um métodoformatadoque retorne o CPF no formato XXX.XXX.XXX-XX.✓ Resposta:readonly class CPF { public function __construct( public string $numero ) { if (!preg_match('/^\d{11}$/', $numero)) { throw new \InvalidArgumentException('CPF deve ter 11 dígitos'); } } public function formatado(): string { return substr($this->numero, 0, 3) . '.' . substr($this->numero, 3, 3) . '.' . substr($this->numero, 6, 3) . '-' . substr($this->numero, 9, 2); } }Escreva uma classe
ContaBancariaimutável com propriedadestitular(string),saldo(float) emoeda(string). Crie um métododepositarque retorne uma nova conta com saldo aumentado.✓ Resposta:readonly class ContaBancaria { public function __construct( public string $titular, public float $saldo, public string $moeda ) {} public function depositar(float $valor): self { return new self($this->titular, $this->saldo + $valor, $this->moeda); } }Crie uma classe
Cor(value object) que represente uma cor RGB (vermelho, verde, azul). As propriedades devem ser inteiros entre 0 e 255. Implemente um métodoescurecerque retorne uma nova cor com cada componente reduzido em 10 (sem ultrapassar 0).✓ Resposta:readonly class Cor { public function __construct( public int $r, public int $g, public int $b ) { if ($r < 0 || $r > 255 || $g < 0 || $g > 255 || $b < 0 || $b > 255) { throw new \InvalidArgumentException('Componentes devem estar entre 0 e 255'); } } public function escurecer(): self { return new self( max(0, $this->r - 10), max(0, $this->g - 10), max(0, $this->b - 10) ); } }Implemente um value object
Dataque armazene dia, mês e ano (inteiros). O objeto deve ser imutável e validar se a data é válida (ex: 29/02 em ano bissexto). Inclua um métodoproximoDiaque retorne a data do dia seguinte.✓ Resposta:readonly class Data { public function __construct( public int $dia, public int $mes, public int $ano ) { if (!checkdate($mes, $dia, $ano)) { throw new \InvalidArgumentException('Data inválida'); } } public function proximoDia(): self { $timestamp = mktime(0, 0, 0, $this->mes, $this->dia, $this->ano); $timestamp = strtotime('+1 day', $timestamp); return new self((int)date('j', $timestamp), (int)date('n', $timestamp), (int)date('Y', $timestamp)); } }