Readonly properties e classes readonly

1. Introdução às Readonly Properties

A mutabilidade acidental em objetos é uma das fontes mais comuns de bugs em sistemas orientados a objetos. Quando uma propriedade pode ser alterada em qualquer ponto do ciclo de vida do objeto, o rastreamento de estado se torna complexo e propenso a erros. O PHP 8.1 introduziu uma solução elegante para esse problema: as readonly properties.

Readonly properties são propriedades que só podem receber valor uma única vez, geralmente no construtor. Após a inicialização, qualquer tentativa de modificar seu valor resulta em um erro fatal. Essa característica torna o código mais previsível e seguro, especialmente em contextos onde a imutabilidade é desejada, como em Data Transfer Objects (DTOs) e Value Objects.

2. Sintaxe e Comportamento Básico

A declaração de uma propriedade readonly utiliza a palavra-chave readonly antes do tipo:

class Usuario {
    public readonly string $nome;
    public readonly int $idade;

    public function __construct(string $nome, int $idade) {
        $this->nome = $nome;
        $this->idade = $idade;
    }
}

$usuario = new Usuario('João', 30);
echo $usuario->nome; // João
// $usuario->nome = 'Maria'; // Erro: Cannot modify readonly property

Regras fundamentais:
- A atribuição só pode ocorrer uma vez, no construtor ou na declaração inline
- Não é possível definir valores padrão que dependam de expressões dinâmicas
- Propriedades readonly devem ser tipadas (exceto em classes readonly do PHP 8.2)

class Config {
    // Válido: valor constante
    public readonly string $versao = '1.0';

    // Inválido: expressão dinâmica
    // public readonly string $timestamp = time();

    public function __construct() {
        // Atribuição no construtor também é válida
        // $this->timestamp = time();
    }
}

3. Readonly Properties com Tipos e Promoção de Construtor

O PHP 8.1 permite combinar readonly com constructor property promotion, criando uma sintaxe concisa para DTOs imutáveis:

class ProdutoDTO {
    public function __construct(
        public readonly int $id,
        public readonly string $nome,
        public readonly float $preco,
        public readonly ?string $descricao = null
    ) {}
}

$produto = new ProdutoDTO(1, 'Notebook', 2500.00, 'Notebook de última geração');
echo $produto->nome; // Notebook

Essa abordagem elimina a necessidade de métodos getters e setters, tornando o código mais limpo e expressivo.

4. Limitações e Comportamentos Específicos

Proibição de unset e reassignação:

$produto = new ProdutoDTO(1, 'Mouse', 150.00);
unset($produto->nome); // Erro: Cannot unset readonly property

Comportamento com objetos mutáveis:
Readonly não impede a mutação interna de objetos armazenados:

class Pedido {
    public function __construct(
        public readonly array $itens
    ) {}
}

$pedido = new Pedido(['item1', 'item2']);
$pedido->itens[] = 'item3'; // Funciona! A referência do array não mudou
echo count($pedido->itens); // 3

Interação com herança:
Propriedades readonly podem ser herdadas, mas não podem ser sobrescritas com diferentes níveis de visibilidade ou tipos incompatíveis:

class Base {
    public readonly string $nome;
}

class Derivada extends Base {
    // Não é possível redefinir $nome como writable
    // public string $nome; // Erro
}

5. Classes Readonly (PHP 8.2)

O PHP 8.2 expandiu o conceito com classes readonly. Ao declarar uma classe como readonly, todas as suas propriedades tornam-se automaticamente readonly:

readonly class Configuracao {
    public string $host;
    public int $porta;
    public string $usuario;

    public function __construct(string $host, int $porta, string $usuario) {
        $this->host = $host;
        $this->porta = $porta;
        $this->usuario = $usuario;
    }
}

$config = new Configuracao('localhost', 3306, 'admin');
// $config->host = 'novo-host'; // Erro: propriedade readonly

Restrições importantes:
- Propriedades não podem ter tipos opcionais (nullable) sem valor padrão
- Propriedades dinâmicas são proibidas
- Não é possível declarar propriedades sem tipo (untyped)

readonly class Exemplo {
    // public $untyped; // Erro: propriedade untyped não permitida
    public string $nome;
    public ?string $apelido = null; // Permitido com valor padrão
}

6. Readonly Classes e Herança

Classes readonly podem estender outras classes readonly, mas não classes comuns:

readonly class Base {
    public string $nome;
}

readonly class Derivada extends Base {
    public string $sobrenome;
}

// class Comum extends Base {} // Erro: não pode estender classe readonly

Sobrescrita de propriedades:
Propriedades readonly podem ser sobrescritas em subclasses, desde que mantenham o mesmo tipo e visibilidade:

readonly class Animal {
    public string $especie;
}

readonly class Cachorro extends Animal {
    public string $raca;

    public function __construct(string $especie, string $raca) {
        parent::__construct($especie);
        $this->raca = $raca;
    }
}

7. Casos de Uso e Boas Práticas

Value Objects imutáveis:

readonly class Endereco {
    public function __construct(
        public string $rua,
        public string $cidade,
        public string $cep
    ) {}
}

$endereco = new Endereco('Rua A', 'São Paulo', '01001-000');
// $endereco->cidade = 'Rio'; // Garantia de imutabilidade

Configurações que não devem mudar:

readonly class AppConfig {
    public function __construct(
        public string $dbHost = 'localhost',
        public int $dbPort = 3306,
        public bool $debug = false
    ) {}
}

Performance e segurança em aplicações concorrentes:
Em ambientes com múltiplas threads ou processos (como aplicações ReactPHP ou Swoole), objetos readonly eliminam race conditions relacionadas à mutação de estado.

8. Comparação com Alternativas e Evolução

Diferenças entre readonly, final e constantes de classe:

Característica readonly final const
Escopo Propriedade Classe/método Classe
Atribuição única Sim N/A Sim
Valor dinâmico Sim (construtor) N/A Não
Herança Permite sobrescrita controlada Impede herança Permite sobrescrita

Evolução do PHP:
Antes do PHP 8.1, a imutabilidade era alcançada com propriedades privadas e métodos getters:

// Antes do PHP 8.1
class UsuarioAntigo {
    private string $nome;

    public function __construct(string $nome) {
        $this->nome = $nome;
    }

    public function getNome(): string {
        return $this->nome;
    }
}

// Com PHP 8.1+
class UsuarioNovo {
    public function __construct(
        public readonly string $nome
    ) {}
}

Expectativas futuras:
A comunidade PHP aguarda funcionalidades como deep readonly (imutabilidade profunda para objetos aninhados) e suporte a readonly em parâmetros de métodos. Essas features estão em discussão para versões futuras do PHP.

Referências