PHP readonly e Property Hooks
Uma propriedade public aceita qualquer coisa, a qualquer momento, de qualquer lugar: o número de um pedido pode ser trocado por engano, um preço pode virar negativo. O PHP moderno tem duas ferramentas para controlar uma propriedade sem precisar escrever métodos getAlgo()/setAlgo(): o readonly ("somente leitura": o valor é gravado uma vez e nunca mais muda) e os property hooks ("ganchos de propriedade": um código seu roda toda vez que alguém lê ou grava a propriedade).
- Propriedades
readonlyPHP 8.1+: as regras e os erros reais. - Classes
readonlyPHP 8.2+: todas as propriedades de uma vez. - Como "mudar" um objeto readonly: criar uma cópia com
clonee oclonecom novos valores PHP 8.5+, e por que de fora da classe ele dá erro. - Property hooks
getesetPHP 8.4+: validar, limpar e calcular valores. - Qual ferramenta usar em cada caso.
Pré-requisitos: construtores (com promoção de propriedades), visibilidade e exceções. O clone e o __clone aparecem em Métodos Mágicos, mas vamos relembrar aqui. Os exemplos rodam na máquina fictícia da Ana, na pasta ~/poo.
A ideia antes do código: a nota fiscal e o porteiro
Uma nota fiscal, depois de emitida, não se rasura. Se o valor estava errado, emite-se outra nota. Isso é o readonly: o objeto nasce com os valores certos e eles não mudam mais; para ter valores diferentes, cria-se outro objeto.
Já um porteiro fica na entrada e confere quem entra ("tem crachá?") e às vezes ajusta ("assine aqui"). Isso é o hook set: todo valor que entra na propriedade passa por ele. E o hook get é como uma placa que é calculada na hora em que alguém olha.
1. Propriedades readonly PHP 8.1+
Escreva readonly antes do tipo. O jeito mais comum é junto com a promoção no construtor:
<?php
class Pedido
{
public function __construct(
public readonly int $numero,
public readonly string $cliente,
) {}
}
$p = new Pedido(1001, "Ana");
echo "Pedido $p->numero de $p->cliente\n";
$p->numero = 2002;
echo "fim\n";
- public readonly int $numero
public: todo mundo pode ler.readonly: só pode ser gravada uma vez.int: o tipo, que é obrigatório.- new Pedido(1001, "Ana")
- O construtor grava os valores: essa é a "uma vez".
- echo "… $p->numero …"
- Ler funciona normalmente, de qualquer lugar.
- $p->numero = 2002;
- Tentar gravar de novo: erro.
$ php pedido.php
Pedido 1001 de Ana
Fatal error: Uncaught Error: Cannot modify readonly property Pedido::$numero in /home/ana/poo/pedido.php:12
Stack trace:
#0 {main}
thrown in /home/ana/poo/pedido.php on line 12
"Não é possível modificar a propriedade somente leitura Pedido::$numero". O fim não aparece: é um Error que para o programa (a não ser que você o capture com try/catch).
Nem a própria classe pode mudar depois
O readonly não é só "de fora". Depois de gravada, nem um método da classe muda a propriedade:
<?php
class Pedido
{
public function __construct(
public readonly int $numero,
) {}
public function renumerar(int $novo): void
{
$this->numero = $novo;
}
}
$p = new Pedido(1001);
$p->renumerar(2002);
$ php renumerar.php
Fatal error: Uncaught Error: Cannot modify readonly property Pedido::$numero in /home/ana/poo/renumerar.php:10
Stack trace:
#0 /home/ana/poo/renumerar.php(15): Pedido->renumerar(2002)
#1 {main}
thrown in /home/ana/poo/renumerar.php on line 10
As regras do readonly
| Regra | Se desobedecer |
|---|---|
Precisa ter tipo (int, string, array…) | Fatal error: Readonly property Pedido::$numero must have type |
Não pode ter valor padrão (= 1) | Fatal error: Readonly property Pedido::$numero cannot have default value |
| É gravada uma vez só | Cannot modify readonly property Pedido::$numero |
| Só é gravada de dentro da classe (normalmente no construtor) | Cannot modify protected(set) readonly property Pedido::$numero from global scope |
| Precisa ser gravada antes de ser lida | Typed property Pedido::$numero must not be accessed before initialization |
A penúltima linha merece atenção. Desde o PHP 8.4, readonly significa também protected(set): "só a própria classe (e as classes filhas) podem gravar". Veja o que acontece se a Ana deixa o construtor de lado e tenta preencher de fora:
<?php
class Pedido
{
public readonly int $numero;
}
$p = new Pedido();
$p->numero = 5;
$ php de-fora.php
Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Pedido::$numero from global scope in /home/ana/poo/de-fora.php:7
Stack trace:
#0 {main}
thrown in /home/ana/poo/de-fora.php on line 7
"Não é possível modificar a propriedade readonly protected(set) a partir do escopo global" (o "escopo global" é o código fora de qualquer classe). Guarde essa mensagem: ela vai voltar quando falarmos de clone.
Missão: o número do pedido nunca deveria mudar, mas o código troca para 9999 sem reclamar. Rode e veja. Depois coloque readonly na propriedade $numero (deixe o $status como está, porque ele muda de verdade) e rode de novo: o PHP deve recusar a troca com Cannot modify readonly property.
<?php
class Pedido
{
public function __construct(
public int $numero,
public string $status,
) {}
}
$p = new Pedido(1001, "Pendente");
$p->status = "Pago";
echo "Pedido $p->numero: $p->status\n";
$p->numero = 9999;
echo "Pedido $p->numero: $p->status\n";
<?php
class Pedido
{
public function __construct(
public readonly int $numero,
public string $status,
) {}
}
$p = new Pedido(1001, "Pendente");
$p->status = "Pago";
echo "Pedido $p->numero: $p->status\n";
$p->numero = 9999;
echo "Pedido $p->numero: $p->status\n";
Cuidado: readonly não "congela" o que está dentro
Uma propriedade readonly que guarda um objeto não deixa você trocar o objeto, mas o objeto em si continua mudando. Já um array readonly fica realmente travado:
<?php
class Cliente
{
public function __construct(public string $cidade) {}
}
class Pedido
{
public function __construct(
public readonly Cliente $cliente,
public readonly array $itens,
) {}
}
$p = new Pedido(new Cliente("Recife"), ["caderno"]);
$p->cliente->cidade = "Natal";
echo $p->cliente->cidade, "\n";
$p->itens[] = "caneta";
$ php dentro.php
Natal « o Cliente de dentro mudou: ele não é readonly
Fatal error: Uncaught Error: Cannot indirectly modify readonly property Pedido::$itens in /home/ana/poo/dentro.php:18
Stack trace:
#0 {main}
thrown in /home/ana/poo/dentro.php on line 18
O readonly protege a propriedade, não o objeto guardado nela. Se você quer que nada mude, o Cliente também precisa ter propriedades readonly.
2. Classes readonly PHP 8.2+
Quando todas as propriedades devem ser readonly, escreva readonly uma vez, antes de class. É o caso típico de objetos que são só "um valor": um ponto, um preço, uma data, um endereço.
<?php
readonly class Ponto
{
public function __construct(
public int $x,
public int $y,
) {}
}
$p = new Ponto(3, 4);
echo "($p->x, $p->y)\n";
$p->cor = "azul";
$ php ponto.php
(3, 4)
Fatal error: Uncaught Error: Cannot create dynamic property Ponto::$cor in /home/ana/poo/ponto.php:11
Stack trace:
#0 {main}
thrown in /home/ana/poo/ponto.php on line 11
- readonly class Ponto
- Todas as propriedades declaradas viram readonly (e valem as mesmas regras: tipo obrigatório, sem valor padrão).
- $p->cor = "azul"
- Numa classe comum, criar uma propriedade que não existe dá só um aviso
Deprecated. Numa classe readonly é erro: "não é possível criar a propriedade dinâmica".
Sem o readonly na classe, você escreveria public readonly int $x, public readonly int $y. Use a classe readonly quando o objeto inteiro é imutável; use a propriedade readonly quando só algumas partes são fixas (como o Pedido com numero fixo e status que muda).
3. "Mudar" um objeto readonly: criar uma cópia
A Ana tem o preço do caderno, R$ 12,50, num objeto readonly. Vai ter promoção. Ela não muda o preço: ela cria outro objeto Preco com o valor novo (como a nota fiscal nova). O lugar certo para isso é um método da própria classe, que por costume se chama comAlgo() (em inglês, withAlgo()).
O clone com novos valores PHP 8.5+
No PHP 8.5, o clone pode ser escrito como uma função que recebe o objeto e um array com os valores que mudam: clone($objeto, ["propriedade" => novoValor]). Ele copia o objeto e já grava os valores novos na cópia. É o que se chama de "clone with".
<?php
readonly class Preco
{
public function __construct(
public float $valor,
public string $moeda,
) {}
public function comValor(float $valor): static
{
return clone($this, ["valor" => $valor]);
}
}
$a = new Preco(10.0, "BRL");
$b = $a->comValor(12.5);
echo "$a->valor $a->moeda\n";
echo "$b->valor $b->moeda\n";
$ php preco.php 10 BRL « o original não mudou 12.5 BRL « a cópia tem o valor novo e a mesma moeda
- public function comValor(float $valor): static
- Um método que devolve um objeto do mesmo tipo (
static= "a mesma classe deste objeto"). - clone($this, …)
- Copia o objeto atual…
- ["valor" => $valor]
- …e na cópia grava
valorcom o número novo. As outras propriedades (amoeda) vêm iguais.
De fora da classe, dá erro (e isso é bom)
Por que não fazer o clone direto, no código principal, sem o método comValor?
<?php
readonly class Preco
{
public function __construct(
public float $valor,
public string $moeda,
) {}
}
$a = new Preco(10.0, "BRL");
$b = clone($a, ["valor" => 12.5]);
var_dump($b);
$ php clone-fora.php
Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Preco::$valor from global scope in /home/ana/poo/clone-fora.php:10
Stack trace:
#0 /home/ana/poo/clone-fora.php(10): clone(Object(Preco), Array)
#1 {main}
thrown in /home/ana/poo/clone-fora.php on line 10
É a mesma mensagem da seção 1. O clone com valores grava as propriedades obedecendo às mesmas regras de uma gravação comum. Como readonly é protected(set), só código de dentro da classe pode gravar. Por isso o caminho certo é o método comValor(): a classe decide o que pode ser trocado (e pode validar o valor novo antes).
Dá para declarar a propriedade como public public(set) float $valor ("leitura e gravação públicas, mas readonly"). Aí clone($a, ["valor" => 12.5]) funciona fora da classe, e o objeto continua sem poder ser alterado depois de criado. Na maioria dos casos, o método comValor() é mais claro.
Antes do PHP 8.5
Antes do clone com valores, o jeito mais simples era criar um objeto novo pelo construtor: return new static($valor, $this->moeda);. O que não funciona é clonar e depois gravar:
<?php
final class Preco
{
public function __construct(
public readonly float $valor,
) {}
public function comValor(float $valor): static
{
$novo = clone $this;
$novo->valor = $valor;
return $novo;
}
}
$b = (new Preco(10.0))->comValor(12.5);
echo $b->valor, "\n";
$ php clone-antigo.php
Fatal error: Uncaught Error: Cannot modify readonly property Preco::$valor in /home/ana/poo/clone-antigo.php:11
Stack trace:
#0 /home/ana/poo/clone-antigo.php(15): Preco->comValor(12.5)
#1 {main}
thrown in /home/ana/poo/clone-antigo.php on line 11
A cópia já nasce com o valor gravado (copiado do original), então gravar de novo é "modificar". A única exceção é dentro do método __clone: desde o PHP 8.3, ali as propriedades readonly da cópia ainda podem ser regravadas (útil para clonar um objeto de dentro, a "cópia profunda").
Missão: complete o método comValor para devolver uma cópia do preço com o valor novo, usando clone($this, [...]). O preço original deve continuar 12.5.
<?php
readonly class Preco
{
public function __construct(
public float $valor,
public string $moeda,
) {}
public function comValor(float $valor): static
{
// devolva uma cópia de $this com o novo valor
}
}
$caderno = new Preco(12.5, "BRL");
$promocao = $caderno->comValor(9.9);
echo "Antes: $caderno->valor $caderno->moeda\n";
echo "Promoção: $promocao->valor $promocao->moeda\n";
<?php
readonly class Preco
{
public function __construct(
public float $valor,
public string $moeda,
) {}
public function comValor(float $valor): static
{
return clone($this, ["valor" => $valor]);
}
}
$caderno = new Preco(12.5, "BRL");
$promocao = $caderno->comValor(9.9);
echo "Antes: $caderno->valor $caderno->moeda\n";
echo "Promoção: $promocao->valor $promocao->moeda\n";
Se você rodar o código sem completar, o PHP reclama que o método não devolveu nada: Preco::comValor(): Return value must be of type Preco, none returned.
4. Property hooks PHP 8.4+
O readonly resolve "nunca muda". Mas e o preço da Ana, que muda, mas nunca pode ser negativo? Ou o nome do produto, que deveria chegar sem espaços sobrando? Antes do PHP 8.4, a saída era deixar a propriedade private e criar métodos getPreco() e setPreco(). Com os hooks, a regra fica na própria propriedade, e quem usa a classe continua escrevendo $p->preco = 15.
<?php
class Produto
{
public string $nome {
set(string $valor) {
$valor = trim($valor);
if ($valor === "") {
throw new ValueError("O nome não pode ficar vazio");
}
$this->nome = $valor;
}
}
public float $preco {
set(float $valor) {
if ($valor < 0) {
throw new ValueError("Preço negativo: $valor");
}
$this->preco = $valor;
}
}
public string $etiqueta {
get => $this->nome . " - R$ " . number_format($this->preco, 2, ",", ".");
}
}
$p = new Produto();
$p->nome = " Caderno ";
$p->preco = 12.5;
echo "[$p->nome]\n";
echo $p->etiqueta, "\n";
$p->preco = 15;
echo $p->etiqueta, "\n";
$p->preco = -3;
$ php hooks.php
[Caderno] « o set tirou os espaços
Caderno - R$ 12,50 « o get montou a etiqueta na hora
Caderno - R$ 15,00 « e de novo, com o preço novo
Fatal error: Uncaught ValueError: Preço negativo: -3 in /home/ana/poo/hooks.php:17
Stack trace:
#0 /home/ana/poo/hooks.php(35): Produto->$preco::set(-3.0)
#1 {main}
thrown in /home/ana/poo/hooks.php on line 17
- public string $nome { … }
- Em vez de
;no fim, a propriedade ganha um bloco{ }com os hooks. - set(string $valor) { … }
- O hook
setroda toda vez que alguém grava$p->nome = ….$valoré o que está chegando. - $this->nome = $valor;
- Dentro do próprio hook, gravar
$this->nomeguarda o valor de verdade (não chama o hook de novo). Se você não gravar, nada é guardado. - throw new ValueError(…)
- Recusa o valor.
ValueErroré a exceção do PHP para "o tipo está certo, mas o valor não serve". - get => …;
- O hook
getroda toda vez que alguém lê a propriedade. A forma curta=>devolve o resultado da expressão. - $etiqueta
- Só tem
gete não usa$this->etiqueta: é uma propriedade virtual, que não guarda nada; é calculada a cada leitura.
Na linha #0 do erro, repare em Produto->$preco::set(-3.0): o PHP mostra que o erro aconteceu dentro do hook set da propriedade $preco.
A forma curta do set e o $value
Quando o set só transforma o valor, há uma forma curta: set => expressão;. O valor que chega se chama automaticamente $value (em inglês, como o PHP define), e o resultado da expressão é o que fica guardado:
<?php
class Usuario
{
public string $email {
set => strtolower(trim($value));
}
public function __construct(string $email)
{
$this->email = $email;
}
}
$u = new Usuario(" Ana@Exemplo.COM ");
var_dump($u->email);
$u->email = "BIA@exemplo.com";
var_dump($u->email);
$ php email.php string(15) "ana@exemplo.com" « o construtor também passa pelo hook string(15) "bia@exemplo.com"
Hooks também funcionam na promoção do construtor: public string $nome { set => trim($value); }, dentro dos parênteses do __construct.
get e set juntos: uma propriedade calculada
<?php
class Temperatura
{
public float $celsius = 0.0;
public float $fahrenheit {
get => $this->celsius * 9 / 5 + 32;
set(float $valor) {
$this->celsius = ($valor - 32) * 5 / 9;
}
}
}
$t = new Temperatura();
$t->celsius = 25;
echo $t->fahrenheit, "\n";
$t->fahrenheit = 212;
echo $t->celsius, "\n";
var_dump($t);
$ php temperatura.php
77
100 « gravar em fahrenheit mudou o celsius
object(Temperatura)#1 (1) {
["celsius"]=>
float(100) « só o celsius existe de verdade
}
O $fahrenheit é virtual: lê e grava, mas o único valor guardado é o $celsius. O var_dump confirma.
Visibilidade assimétrica: só "ler de fora" PHP 8.4+
Às vezes você não precisa de regra nenhuma: só quer que de fora se possa ler, e só a classe possa gravar, mais de uma vez. Para isso existe public private(set):
<?php
class Conta
{
public private(set) float $saldo = 0.0;
public function depositar(float $valor): void
{
$this->saldo += $valor;
}
}
$c = new Conta();
$c->depositar(50);
echo $c->saldo, "\n";
$c->saldo = 1000000;
$ php conta.php
50
Fatal error: Uncaught Error: Cannot modify private(set) property Conta::$saldo from global scope in /home/ana/poo/conta.php:14
Stack trace:
#0 {main}
thrown in /home/ana/poo/conta.php on line 14
Diferente do readonly, o método depositar pode mudar o saldo quantas vezes quiser. Mais sobre isso em Visibilidade.
Qual usar?
| Você quer… | Use | Exemplo |
|---|---|---|
| Um valor que nunca muda depois de criado | readonly | número do pedido, CPF, data de criação |
| Um objeto inteiro que nunca muda | readonly class | Preco, Ponto, Endereco |
| Uma "versão alterada" de um objeto readonly | método comAlgo() com clone($this, [...]) | $preco->comValor(9.9) |
| Ler de fora, mudar só por dentro (várias vezes) | public private(set) | saldo, estoque, contador |
| Validar ou limpar todo valor gravado | hook set | preço não negativo, e-mail minúsculo |
| Um valor calculado a partir de outros | hook get (virtual) | etiqueta, nome completo, total |
Em PHP anterior ao 8.4 (sem hooks), o equivalente é uma propriedade private com métodos getPreco() e setPreco(), ou os mágicos __get/__set. Em código novo, prefira os hooks: o editor enxerga a propriedade e quem usa a classe não precisa decorar nomes de métodos.
Quando dá errado
<?php
class Pedido
{
public readonly $numero;
}
$ php sem-tipo.php
Fatal error: Readonly property Pedido::$numero must have type in /home/ana/poo/sem-tipo.php on line 4
Stack trace:
#0 {main}
O que significa: "a propriedade readonly precisa ter tipo". Com public readonly int $numero = 1; o erro seria Readonly property Pedido::$numero cannot have default value ("não pode ter valor padrão"): um valor fixo para sempre é uma constante, não uma propriedade.
Como consertar: declare o tipo e grave o valor no construtor.
<?php
class Pedido
{
public readonly int $numero;
}
$p = new Pedido();
echo $p->numero;
$ php antes.php
Fatal error: Uncaught Error: Typed property Pedido::$numero must not be accessed before initialization in /home/ana/poo/antes.php:7
Stack trace:
#0 {main}
thrown in /home/ana/poo/antes.php on line 7
O que significa: "a propriedade tipada não pode ser acessada antes de ser inicializada". A classe não tem construtor, então $numero nunca recebeu valor, e uma readonly não tem valor padrão.
Como consertar: grave toda propriedade readonly no construtor (a promoção public readonly int $numero dentro do __construct já faz isso).
get
<?php
class Produto
{
public string $etiqueta {
get => "x";
}
}
$p = new Produto();
$p->etiqueta = "y";
$ php so-get.php
Fatal error: Uncaught Error: Property Produto::$etiqueta is read-only in /home/ana/poo/so-get.php:9
Stack trace:
#0 {main}
thrown in /home/ana/poo/so-get.php on line 9
O que significa: uma propriedade virtual com só get é "somente leitura": não há onde guardar o valor.
Como consertar: grave nas propriedades de onde ela é calculada (aqui, nome e preco) ou acrescente um set.
<?php
class Produto
{
public readonly string $nome {
get => "a";
}
}
$ php hook-readonly.php
Fatal error: Hooked properties cannot be readonly in /home/ana/poo/hook-readonly.php on line 4
Stack trace:
#0 {main}
O que significa: "propriedades com hooks não podem ser readonly". As duas ideias não se misturam (e por isso uma readonly class também não aceita hooks).
Como consertar: escolha uma. Para "valida uma vez e nunca muda", use readonly e valide no construtor. Para "valida sempre e pode mudar", use o hook.
clone … with não existe
O recurso é conhecido como "clone with", mas a sintaxe é a de uma função: clone($obj, [...]). Escrever clone $a with ["valor" => 1] dá Parse error: syntax error, unexpected identifier "with". E o clone com valores existe só no PHP 8.5; em versões anteriores, use new static(...).
Referência rápida
| Escrita | Significado | Versão |
|---|---|---|
public readonly int $x | Gravada uma vez, de dentro da classe; lida de qualquer lugar | 8.1 |
readonly class Nome | Todas as propriedades são readonly; sem propriedades dinâmicas | 8.2 |
clone($obj, ["x" => 1]) | Copia e grava novos valores (respeitando a visibilidade) | 8.5 |
__clone() regravando readonly | Permitido só dentro do __clone | 8.3 |
public private(set) float $x | Lê de fora, grava só por dentro | 8.4 |
set(tipo $v) { … $this->x = $v; } | Roda a cada gravação | 8.4 |
set => expr($value); | Forma curta: guarda o resultado | 8.4 |
get => expr; | Roda a cada leitura | 8.4 |
Missão final
Missão: o hook set da nota aceita qualquer número, e a Ana ficou com nota 11. Complete o hook para lançar new ValueError("Nota inválida: $valor") quando a nota for menor que 0 ou maior que 10. A saída deve mostrar a mensagem e a nota da Ana deve continuar 9.5.
<?php
class Aluno
{
public float $nota {
set(float $valor) {
// recuse notas menores que 0 ou maiores que 10
$this->nota = $valor;
}
}
public function __construct(public string $nome, float $nota)
{
$this->nota = $nota;
}
}
$ana = new Aluno("Ana", 9.5);
echo "$ana->nome: $ana->nota\n";
try {
$ana->nota = 11;
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
echo "$ana->nome: $ana->nota\n";
<?php
class Aluno
{
public float $nota {
set(float $valor) {
if ($valor < 0 || $valor > 10) {
throw new ValueError("Nota inválida: $valor");
}
$this->nota = $valor;
}
}
public function __construct(public string $nome, float $nota)
{
$this->nota = $nota;
}
}
$ana = new Aluno("Ana", 9.5);
echo "$ana->nome: $ana->nota\n";
try {
$ana->nota = 11;
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
echo "$ana->nome: $ana->nota\n";
Exercícios
1. Somente leitura
Complete para que o CPF possa ser lido, mas nunca alterado depois de criado:
2. Classe inteira
Complete para que todas as propriedades da classe Endereco sejam readonly:
3. Cópia com valor novo (PHP 8.5)
Dentro do método comMoeda(string $moeda), complete para devolver uma cópia com a moeda nova:
4. A classe Preco é readonly. No código principal (fora da classe), a Ana escreve $b = clone($a, ["valor" => 9.9]);. O que acontece?
5. Num hook set => …; (forma curta), como se chama a variável com o valor que está chegando?
6. Uma propriedade só com get => $this->nome . " " . $this->sobrenome; é:
7. O saldo deve ser lido de fora e alterado várias vezes, mas só pelos métodos da classe. Qual declaração usar?
Resumo
readonly(8.1): gravada uma vez, de dentro da classe; precisa de tipo, não tem valor padrão. Mudar dáCannot modify readonly property.- Desde o 8.4, readonly também é
protected(set): gravar de fora dáCannot modify protected(set) readonly property … from global scope. - Readonly protege a propriedade, não o objeto guardado dentro dela.
readonly class(8.2): todas as propriedades readonly e nada de propriedades dinâmicas.- Para "mudar" um objeto readonly, crie outro: um método
comAlgo()comclone($this, ["prop" => $novo])(8.5) ounew static(...). De fora da classe, oclonecom valores esbarra noprotected(set). - Hooks (8.4):
setvalida ou limpa cada gravação;getcalcula na leitura; sóget= propriedade virtual. Hooks não combinam com readonly. public private(set): lê de fora, grava só por dentro, quantas vezes quiser.