PHP Visibilidade
Até aqui, toda propriedade e todo método começavam com public: qualquer parte do programa podia ler e mudar tudo. Isso abre uma "porta dos fundos": a Ana escreve um método vender que confere o estoque, e alguém simplesmente faz $caneca->estoque = -50. A visibilidade decide quem pode mexer em cada parte de um objeto. É o primeiro pilar da POO na prática: o encapsulamento.
- As três visibilidades:
public,protectedeprivate. - O erro
Cannot access private propertye o que ele protege. - Métodos privados: os "ajudantes internos" de uma classe.
- Getters e setters: ler e mudar com regras.
- A visibilidade assimétrica do PHP 8.4:
public private(set)(todo mundo lê, só a classe muda). - O que é encapsulamento e quando usar cada visibilidade.
O problema: a porta dos fundos
<?php
class Produto
{
public function __construct(
public string $nome,
public int $estoque,
) {
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
echo "Estoque insuficiente!\n";
return;
}
$this->estoque -= $quantidade;
}
}
$caneca = new Produto("Caneca", 10);
$caneca->vender(30); // o método protege...
$caneca->estoque = -50; // ...mas a porta dos fundos está aberta
echo "Estoque: {$caneca->estoque}\n";
$ php problema.php Estoque insuficiente! « o método vender recusou a venda de 30 Estoque: -50 « mas a linha 22 mudou o estoque direto, sem regra nenhuma
O método vender é a porta da frente, com porteiro: ele confere se há estoque. Mas como $estoque é public, qualquer linha do programa pode entrar pelos fundos e pôr um valor absurdo. Num sistema real, isso vira estoque negativo, preço zero, saldo inventado.
Pense num caixa eletrônico: você não abre o cofre e pega o dinheiro. Você usa os botões Sacar e Depositar, e a máquina confere o saldo. O cofre é privado; os botões são públicos.
As três visibilidades
| Palavra | Tradução | Quem pode usar | No dia a dia |
|---|---|---|---|
public | público | Qualquer código: dentro da classe, nas classes filhas e fora ($obj->prop) | A vitrine da loja |
protected | protegido | A própria classe e as classes filhas (veja Herança) | A receita da família: passa para os filhos, não para estranhos |
private | privado | Só o código dentro da própria classe | O cofre |
A visibilidade vale para propriedades, métodos e constantes, e vem sempre na frente: private int $estoque, private function frete(). No construtor com promoção, também: private int $estoque dentro dos parênteses.
private: só a classe mexe
A Ana torna o estoque privado. Para os outros poderem ler o estoque, ela cria um método público getEstoque():
<?php
class Produto
{
private int $estoque;
public function __construct(public string $nome, int $estoque)
{
$this->estoque = $estoque;
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
echo "Estoque insuficiente!\n";
return;
}
$this->estoque -= $quantidade;
}
public function getEstoque(): int
{
return $this->estoque;
}
}
$caneca = new Produto("Caneca", 10);
$caneca->vender(3);
echo "Estoque: " . $caneca->getEstoque() . "\n";
echo $caneca->estoque;
$ php privado.php
Estoque: 7
Fatal error: Uncaught Error: Cannot access private property Produto::$estoque in /home/ana/loja/privado.php:29
Stack trace:
#0 {main}
thrown in /home/ana/loja/privado.php on line 29
Dentro da classe, $this->estoque funciona normalmente (nos métodos vender e getEstoque). Fora dela, a linha 29 tentou ler $caneca->estoque e o PHP parou: Cannot access private property Produto::$estoque ("não é possível acessar a propriedade privada"). Mudar de fora dá o mesmo erro:
<?php
class Produto
{
public function __construct(
public string $nome,
private int $estoque,
) {
}
}
$caneca = new Produto("Caneca", 10);
$caneca->estoque = -50;
$ php privado-escrever.php
Fatal error: Uncaught Error: Cannot access private property Produto::$estoque in /home/ana/loja/privado-escrever.php:12
Stack trace:
#0 {main}
thrown in /home/ana/loja/privado-escrever.php on line 12
A porta dos fundos fechou: o único jeito de mudar o estoque agora é pelo método vender, que tem as regras. (Repare também na promoção com private, no construtor.)
Um método da classe Produto pode ler o private de outro objeto Produto. O "cofre" é da classe, e todo código escrito dentro dela tem a chave:
<?php
class Produto
{
public function __construct(
public string $nome,
private float $preco,
) {
}
public function maisBaratoQue(Produto $outro): bool
{
return $this->preco < $outro->preco;
}
}
$caneca = new Produto("Caneca", 25.90);
$camiseta = new Produto("Camiseta", 49.90);
var_dump($caneca->maisBaratoQue($camiseta));
$ php mesmo-tipo.php bool(true)
Missão: feche a porta dos fundos. Troque public int $estoque por private int $estoque, crie o método getEstoque(): int e mude o echo final para usá-lo. Apague a linha que põe -50 (ela vai dar erro, confira antes de apagar!). A saída deve ser Estoque: 7, sem erros.
<?php
class Produto
{
public function __construct(
public string $nome,
public int $estoque,
) {
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
echo "Estoque insuficiente!\n";
return;
}
$this->estoque -= $quantidade;
}
}
$caneca = new Produto("Caneca", 10);
$caneca->vender(3);
$caneca->estoque = -50;
echo "Estoque: {$caneca->estoque}\n";
<?php
class Produto
{
public function __construct(
public string $nome,
private int $estoque,
) {
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
echo "Estoque insuficiente!\n";
return;
}
$this->estoque -= $quantidade;
}
public function getEstoque(): int
{
return $this->estoque;
}
}
$caneca = new Produto("Caneca", 10);
$caneca->vender(3);
echo "Estoque: " . $caneca->getEstoque() . "\n";
Métodos privados: os ajudantes internos
Métodos também podem ser privados. Eles servem para dividir um cálculo em partes sem expor essas partes para fora. Aqui, o pedido calcula subtotal e frete por dentro, e só mostra o total:
<?php
class Pedido
{
private array $itens = [];
public function adicionar(string $produto, float $preco): void
{
$this->itens[] = ["produto" => $produto, "preco" => $preco];
}
public function total(): float
{
return $this->subtotal() + $this->frete();
}
private function subtotal(): float
{
return array_sum(array_column($this->itens, "preco"));
}
private function frete(): float
{
return $this->subtotal() >= 100 ? 0 : 15;
}
}
$pedido = new Pedido();
$pedido->adicionar("Caneca", 25.90);
$pedido->adicionar("Camiseta", 49.90);
echo "Total: " . number_format($pedido->total(), 2, ",", ".") . "\n";
echo $pedido->frete();
$ php metodo-privado.php
Total: 90,80 « 25,90 + 49,90 = 75,80, mais 15 de frete (abaixo de 100)
Fatal error: Uncaught Error: Call to private method Pedido::frete() from global scope in /home/ana/loja/metodo-privado.php:31
Stack trace:
#0 {main}
thrown in /home/ana/loja/metodo-privado.php on line 31
"Chamada de método privado Pedido::frete() do escopo global" (escopo global = fora de qualquer classe). A vantagem: amanhã a Ana pode mudar a regra do frete ou separar subtotal em dois métodos, e nenhum código de fora quebra, porque ninguém de fora usava esses métodos.
protected: para a família
Fora da classe, protected se comporta igual a private:
<?php
class Produto
{
protected float $preco = 25.90;
}
$c = new Produto();
echo $c->preco;
$ php protegido.php
Fatal error: Uncaught Error: Cannot access protected property Produto::$preco in /home/ana/loja/protegido.php:7
Stack trace:
#0 {main}
thrown in /home/ana/loja/protegido.php on line 7
A diferença só aparece com herança: uma classe filha (class ProdutoDigital extends Produto) pode usar o que é protected na mãe, mas não o que é private. Você vai ver isso na prática em Herança.
O que o var_dump mostra
O var_dump e o print_r mostram tudo, inclusive o privado, com uma marcação. Eles são ferramentas de depuração; a visibilidade protege o código, não esconde dados de quem depura:
<?php
class Conta
{
public string $titular = "Ana";
protected string $agencia = "0001";
private float $saldo = 150.0;
}
var_dump(new Conta());
print_r(new Conta());
echo "\n";
$ php var-dump.php
object(Conta)#1 (3) {
["titular"]=>
string(3) "Ana"
["agencia":protected]=>
string(4) "0001"
["saldo":"Conta":private]=>
float(150)
}
Conta Object
(
[titular] => Ana
[agencia:protected] => 0001
[saldo:Conta:private] => 150
)
private não é segurança de dados
Visibilidade é uma regra para programadores ("não mexa nisso por fora"), não um cofre contra invasores. Uma senha numa propriedade private continua na memória e aparece num var_dump. Senhas se guardam com password_hash (veja Senhas).
Getters e setters
Com as propriedades privadas, como os outros leem e mudam os dados? Por métodos. O costume é chamá-los de getter (de get, "pegar": getPreco()) e setter (de set, "definir": setPreco()). O setter é onde moram as regras:
<?php
class Produto
{
private float $preco;
public function __construct(public string $nome, float $preco)
{
$this->setPreco($preco);
}
public function getPreco(): float
{
return $this->preco;
}
public function setPreco(float $preco): void
{
if ($preco <= 0) {
throw new InvalidArgumentException("Preço inválido: $preco");
}
$this->preco = round($preco, 2);
}
}
$caneca = new Produto("Caneca", 25.9);
$caneca->setPreco(19.999);
echo $caneca->getPreco() . "\n";
$caneca->setPreco(-1);
$ php getset.php
20 « 19.999 arredondado para 2 casas pelo setter
Fatal error: Uncaught InvalidArgumentException: Preço inválido: -1 in /home/ana/loja/getset.php:19
Stack trace:
#0 /home/ana/loja/getset.php(28): Produto->setPreco(-1.0)
#1 {main}
thrown in /home/ana/loja/getset.php on line 19
- private float $preco;
- O dado fica guardado no "cofre".
- getPreco()
- O getter: só devolve o valor.
- setPreco(float $preco)
- O setter: confere a regra (preço maior que zero), arredonda e só então guarda.
- $this->setPreco($preco) no construtor
- O construtor usa o próprio setter: a regra fica escrita num lugar só e vale também na criação.
Um getter e um setter "vazios" (que só leem e gravam, sem regra) não protegem nada: é o mesmo que public, com mais código. Crie setter só quando há regra; e, muitas vezes, um método com nome de ação é melhor que um setter (vender(3) diz mais que setEstoque(7)). Para "todo mundo lê, só a classe muda", o PHP 8.4 tem algo mais curto que o getter: a visibilidade assimétrica, logo abaixo. Para regras na própria propriedade, existem os property hooks.
Visibilidade assimétrica: public private(set) PHP 8.4+
O caso mais comum é: "todo mundo pode ler o estoque, mas só a classe pode mudar". Antes do 8.4, isso exigia uma propriedade private e um getter. No PHP 8.4, a propriedade pode ter duas visibilidades: uma para ler e outra para escrever (set). Daí o nome assimétrica:
<?php
class Produto
{
public private(set) int $estoque = 0;
public function __construct(public string $nome)
{
}
public function repor(int $quantidade): void
{
$this->estoque += $quantidade;
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
throw new RuntimeException("Estoque insuficiente");
}
$this->estoque -= $quantidade;
}
}
$caneca = new Produto("Caneca");
$caneca->repor(10);
$caneca->vender(3);
echo "{$caneca->nome}: {$caneca->estoque} em estoque\n";
$caneca->estoque = 1000;
$ php assimetrica.php
Caneca: 7 em estoque « ler de fora funciona: $caneca->estoque, sem getter
Fatal error: Uncaught Error: Cannot modify private(set) property Produto::$estoque from global scope in /home/ana/loja/assimetrica.php:28
Stack trace:
#0 {main}
thrown in /home/ana/loja/assimetrica.php on line 28
- public
- A visibilidade de leitura: qualquer um lê
$caneca->estoque. - private(set)
- A visibilidade de escrita: só o código da classe muda o valor (nos métodos
reporevender). - int
- O tipo é obrigatório em propriedades com visibilidade assimétrica.
A mensagem do erro é clara: "não é possível modificar a propriedade private(set) Produto::$estoque do escopo global".
As combinações
| Declaração | Quem lê | Quem muda |
|---|---|---|
public private(set) int $x | Todos | Só a classe |
private(set) int $x | Todos (o public fica subentendido) | Só a classe |
public protected(set) int $x | Todos | A classe e as filhas |
protected private(set) int $x | A classe e as filhas | Só a classe |
A escrita nunca pode ser mais aberta que a leitura: private public(set) dá Fatal error: Visibility of property Produto::$estoque must not be weaker than set visibility. E funciona com promoção no construtor:
<?php
class Conta
{
public function __construct(
public private(set) string $titular,
private(set) float $saldo = 0,
) {
}
public function depositar(float $valor): void
{
$this->saldo += $valor;
}
}
$conta = new Conta("Ana");
$conta->depositar(150);
echo "{$conta->titular}: R$ {$conta->saldo}\n";
var_dump($conta);
$conta->titular = "Bruno";
$ php assimetrica-promo.php
Ana: R$ 150
object(Conta)#1 (2) {
["titular"]=>
string(3) "Ana"
["saldo"]=>
float(150)
}
Fatal error: Uncaught Error: Cannot modify private(set) property Conta::$titular from global scope in /home/ana/loja/assimetrica-promo.php:20
Stack trace:
#0 {main}
thrown in /home/ana/loja/assimetrica-promo.php on line 20
Repare: o titular recebeu o valor no construtor (que é código da classe, então pode), e o saldo mudou pelo método depositar. De fora, os dois podem ser lidos, mas não mudados.
Encapsulamento: o pilar por trás de tudo isso
Encapsular é pôr numa cápsula: esconder os detalhes internos de um objeto e oferecer só algumas "portas" públicas, com regras. Na prática:
- Comece com as propriedades não públicas para escrita (
privateouprivate(set)), e abra só o que precisar. - Ofereça métodos com nome de ação (
vender,depositar,adicionar) que conferem as regras. - Deixe
privateos métodos que são só "ajudantes" internos.
O ganho: o objeto nunca fica num estado inválido (estoque negativo, preço zero), e você pode mudar o "como" lá dentro sem quebrar quem usa a classe.
Qual visibilidade usar
| Situação | Use |
|---|---|
Método que os outros devem chamar (vender, total) | public |
| Método ajudante, só para uso interno | private |
| Dado que todos leem, mas só a classe muda | public private(set) (8.4+); antes: private + getter |
| Dado que nunca muda depois de criado | readonly (veja readonly e Property Hooks) |
| Dado interno, que ninguém de fora precisa ver | private |
| Algo que as classes filhas precisam usar ou mudar | protected (com moderação) |
| Dado simples, sem regra nenhuma, num objeto de "transporte" | public (tudo bem!) |
Quando dá errado
Você viu os dois acima. O que significa: código de fora da classe tentou ler ou mudar uma propriedade private (ou protected). Como consertar: use o método público que a classe oferece (getEstoque(), vender()). Se não existe nenhum e você precisa mesmo daquele dado, a mudança é na classe: crie um método, ou use private(set) para liberar só a leitura. Não troque para public só para "fazer o erro sumir".
O que significa: um método private foi chamado de fora da classe (exemplo do pedido, acima). Como consertar: chame o método público que usa o ajudante (total()), ou, se o método deve mesmo ser usado de fora, troque-o para public.
private(set) de fora
<?php
class Carrinho
{
public private(set) array $itens = [];
}
$c = new Carrinho();
$c->itens[] = "Caneca";
$ php assim-array.php
Fatal error: Uncaught Error: Cannot indirectly modify private(set) property Carrinho::$itens from global scope in /home/ana/loja/assim-array.php:7
Stack trace:
#0 {main}
thrown in /home/ana/loja/assim-array.php on line 7
O que significa: acrescentar um item no array também é "modificar" a propriedade, só que de forma indireta (indirectly). Com private(set), nem isso é permitido de fora.
Como consertar: crie um método na classe: public function adicionar(string $item): void { $this->itens[] = $item; }.
<?php
class Produto
{
public private(set) $estoque = 0;
}
$ php assimetrica-sem-tipo.php
Fatal error: Property with asymmetric visibility Produto::$estoque must have type in /home/ana/loja/assimetrica-sem-tipo.php on line 4
Stack trace:
#0 {main}
O que significa: propriedades com visibilidade assimétrica precisam de tipo. Como consertar: public private(set) int $estoque = 0;.
Referência
| Escrita | Significado |
|---|---|
public | Todos acessam |
protected | A classe e as filhas |
private | Só a própria classe |
public private(set) tipo $p PHP 8.4+ | Todos leem; só a classe muda |
public protected(set) tipo $p PHP 8.4+ | Todos leem; a classe e as filhas mudam |
private function x() | Método só para uso interno |
getX() / setX($v) | Costume para ler / mudar com regra |
Missão final
Missão: complete a classe Conta. O saldo deve ser lido por todos, mas mudado só pela classe: declare-o com visibilidade assimétrica (private(set)), começando em 0. Escreva depositar(float $valor): void e sacar(float $valor): void; o sacar lança new RuntimeException("saldo insuficiente") se o valor for maior que o saldo. O código de baixo já está pronto: a saída deve ter Saque recusado: saldo insuficiente e Ana: R$ 120,00.
<?php
class Conta
{
// declare o saldo aqui
public function __construct(public string $titular)
{
}
// escreva depositar e sacar aqui
}
$conta = new Conta("Ana");
$conta->depositar(200);
$conta->sacar(80);
try {
$conta->sacar(500);
} catch (RuntimeException $e) {
echo "Saque recusado: " . $e->getMessage() . "\n";
}
echo "{$conta->titular}: R$ " . number_format($conta->saldo, 2, ",", ".") . "\n";
<?php
class Conta
{
public private(set) float $saldo = 0;
public function __construct(public string $titular)
{
}
public function depositar(float $valor): void
{
$this->saldo += $valor;
}
public function sacar(float $valor): void
{
if ($valor > $this->saldo) {
throw new RuntimeException("saldo insuficiente");
}
$this->saldo -= $valor;
}
}
$conta = new Conta("Ana");
$conta->depositar(200);
$conta->sacar(80);
try {
$conta->sacar(500);
} catch (RuntimeException $e) {
echo "Saque recusado: " . $e->getMessage() . "\n";
}
echo "{$conta->titular}: R$ " . number_format($conta->saldo, 2, ",", ".") . "\n";
Exercícios
1. Só a classe
Complete para que a propriedade só possa ser usada dentro da própria classe:
2. Todos leem, só a classe muda
Complete com a visibilidade assimétrica do PHP 8.4:
3. O getter
Complete o nome do getter da propriedade $preco, seguindo o costume:
4. Quem pode acessar uma propriedade protected?
5. A propriedade é public private(set) int $estoque. O que acontece com echo $caneca->estoque; fora da classe?
6. Por que deixar um método private?
7. Qual declaração dá erro?
Resumo
public= todos;protected= a classe e as filhas;private= só a classe.- Acessar de fora o que não é público dá
Error: Cannot access private property …(ouCall to private method …). - Métodos privados são ajudantes internos; métodos públicos são as "portas" com regras.
- Getters leem; setters mudam com regra. Não crie setters vazios: prefira métodos com nome de ação.
- Visibilidade assimétrica (8.4+):
public private(set)deixa todos lerem e só a classe mudar. Exige tipo. - Encapsulamento: esconda o "como", mostre o "o quê", e o objeto nunca fica num estado inválido.