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.

Você vai aprender
  • As três visibilidades: public, protected e private.
  • O erro Cannot access private property e 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

PalavraTraduçãoQuem pode usarNo dia a dia
publicpúblicoQualquer código: dentro da classe, nas classes filhas e fora ($obj->prop)A vitrine da loja
protectedprotegidoA própria classe e as classes filhas (veja Herança)A receita da família: passa para os filhos, não para estranhos
privateprivadoSó o código dentro da própria classeO 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.)

Privado é por classe, não por objeto

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.
Não crie getter e setter para tudo

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 repor e vender).
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çãoQuem lêQuem muda
public private(set) int $xTodosSó a classe
private(set) int $xTodos (o public fica subentendido)Só a classe
public protected(set) int $xTodosA classe e as filhas
protected private(set) int $xA classe e as filhasSó 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:

  1. Comece com as propriedades não públicas para escrita (private ou private(set)), e abra só o que precisar.
  2. Ofereça métodos com nome de ação (vender, depositar, adicionar) que conferem as regras.
  3. Deixe private os 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çãoUse
Método que os outros devem chamar (vender, total)public
Método ajudante, só para uso internoprivate
Dado que todos leem, mas só a classe mudapublic private(set) (8.4+); antes: private + getter
Dado que nunca muda depois de criadoreadonly (veja readonly e Property Hooks)
Dado interno, que ninguém de fora precisa verprivate
Algo que as classes filhas precisam usar ou mudarprotected (com moderação)
Dado simples, sem regra nenhuma, num objeto de "transporte"public (tudo bem!)

Quando dá errado

Cannot access private property / protected property

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".

Call to private method … from global scope

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.

Mudar um array 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; }.

Property with asymmetric visibility … must have type
<?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

EscritaSignificado
publicTodos acessam
protectedA classe e as filhas
privateSó 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:

float $custo;

2. Todos leem, só a classe muda

Complete com a visibilidade assimétrica do PHP 8.4:

public int $estoque = 0;

3. O getter

Complete o nome do getter da propriedade $preco, seguindo o costume:

public function (): float

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 … (ou Call 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.