Orientação a Objetos é o paradigma que organiza o código em torno de entidades — objetos — que combinam dados e comportamento em uma única unidade. É o modelo dominante no desenvolvimento PHP profissional: frameworks como Laravel, Symfony e Doctrine são construídos inteiramente sobre OOP.
Neste artigo cobrimos os fundamentos: classes, objetos, propriedades, métodos, construtores, visibilidade e os conceitos de encapsulamento que tornam o código orientado a objetos mais seguro e expressivo do que o código procedural.
O que é uma classe
Uma classe é um molde — ela descreve como um objeto deve ser. Um objeto é uma instância desse molde — um exemplar concreto criado a partir dele:
<?php
declare(strict_types=1);
// Classe — o molde
class Produto
{
// Propriedades — os dados que cada objeto carrega
public string $nome;
public float $preco;
public int $estoque;
}
// Objeto — uma instância criada a partir do molde
$teclado = new Produto();
$teclado->nome = "Teclado Mecânico";
$teclado->preco = 350.0;
$teclado->estoque = 15;
$mouse = new Produto(); // outro objeto, independente do $teclado
$mouse->nome = "Mouse Gamer";
$mouse->preco = 180.0;
$mouse->estoque = 42;
echo $teclado->nome; // Teclado Mecânico
echo $mouse->preco; // 180.0
// Cada objeto tem seus próprios dados — são independentes
$teclado->preco = 320.0; // muda apenas o $teclado
echo $mouse->preco; // 180.0 — não foi afetado
Construtor — inicializando objetos
O construtor é um método especial chamado automaticamente quando um objeto é criado com new. Ele garante que o objeto começa em um estado válido:
<?php
declare(strict_types=1);
class Produto
{
public string $nome;
public float $preco;
public int $estoque;
// __construct é chamado automaticamente pelo new
public function __construct(string $nome, float $preco, int $estoque = 0)
{
// Validação no construtor — objeto nasce válido ou não nasce
if ($preco < 0) {
throw new InvalidArgumentException("Preço não pode ser negativo.");
}
$this->nome = $nome;
$this->preco = $preco;
$this->estoque = $estoque;
}
}
// Agora o new exige os dados obrigatórios
$teclado = new Produto("Teclado Mecânico", 350.0, 15);
echo $teclado->nome; // Teclado Mecânico
// Tentativa de criar produto inválido — lança exceção
// $invalido = new Produto("Teste", -10.0); // InvalidArgumentException
Promoção de propriedades no construtor (PHP 8)
O PHP 8 introduziu uma sintaxe muito mais compacta que declara e atribui propriedades diretamente nos parâmetros do construtor:
<?php
declare(strict_types=1);
class Produto
{
// PHP 8: public/protected/private no parâmetro declara e atribui
// automaticamente a propriedade — elimina muito código repetitivo
public function __construct(
public readonly string $nome, // readonly — não pode ser modificado após o construtor
public float $preco,
public int $estoque = 0,
) {
if ($this->preco < 0) {
throw new InvalidArgumentException("Preço não pode ser negativo.");
}
}
}
$p = new Produto("Monitor 4K", 2500.0, 5);
echo $p->nome; // Monitor 4K
echo $p->preco; // 2500.0
// $p->nome = "Outro"; // Error: Cannot modify readonly property
Métodos — o comportamento do objeto
Métodos são funções definidas dentro de uma classe. Eles operam sobre os dados do próprio objeto através de $this:
<?php
declare(strict_types=1);
class Produto
{
public function __construct(
public readonly string $nome,
public float $preco,
public int $estoque = 0,
) {}
// Método de consulta — retorna informação
public function estaDisponivel(): bool
{
return $this->estoque > 0;
}
// Método de ação — modifica o estado do objeto
public function adicionarEstoque(int $quantidade): void
{
if ($quantidade <= 0) {
throw new InvalidArgumentException("Quantidade deve ser positiva.");
}
$this->estoque += $quantidade;
}
public function vender(int $quantidade): void
{
if ($quantidade > $this->estoque) {
throw new RuntimeException("Estoque insuficiente.");
}
$this->estoque -= $quantidade;
}
// Método de formatação — representação legível
public function resumo(): string
{
$status = $this->estaDisponivel() ? "disponível" : "esgotado";
return "{$this->nome} — R$ {$this->preco} ({$status})";
}
}
$monitor = new Produto("Monitor 4K", 2500.0, 3);
echo $monitor->resumo(); // Monitor 4K — R$ 2500.0 (disponível)
$monitor->vender(3);
echo $monitor->estaDisponivel(); // false (bool)
echo $monitor->resumo(); // Monitor 4K — R$ 2500.0 (esgotado)
Visibilidade: public, protected, private
A visibilidade controla quem pode acessar propriedades e métodos. É o mecanismo central do encapsulamento:
<?php
declare(strict_types=1);
class ContaBancaria
{
// private — acessível apenas dentro desta classe
private float $saldo;
private array $historico = [];
// public — acessível de qualquer lugar
public string $titular;
public function __construct(string $titular, float $depositoInicial = 0.0)
{
$this->titular = $titular;
$this->saldo = 0.0;
if ($depositoInicial > 0) {
$this->depositar($depositoInicial);
}
}
public function depositar(float $valor): void
{
$this->validarValor($valor);
$this->saldo += $valor;
$this->registrar("Depósito", $valor);
}
public function sacar(float $valor): void
{
$this->validarValor($valor);
if ($valor > $this->saldo) {
throw new RuntimeException("Saldo insuficiente.");
}
$this->saldo -= $valor;
$this->registrar("Saque", $valor);
}
// Método getter — acesso controlado ao saldo
public function getSaldo(): float
{
return $this->saldo;
}
public function getHistorico(): array
{
return $this->historico;
}
// private — usado internamente, não exposto ao mundo externo
private function validarValor(float $valor): void
{
if ($valor <= 0) {
throw new InvalidArgumentException("Valor deve ser positivo.");
}
}
private function registrar(string $tipo, float $valor): void
{
$this->historico[] = [
"tipo" => $tipo,
"valor" => $valor,
"data" => date("d/m/Y H:i"),
];
}
}
$conta = new ContaBancaria("Ana Silva", 1000.0);
$conta->depositar(500.0);
$conta->sacar(200.0);
echo $conta->getSaldo(); // 1300.0
// $conta->saldo = 99999; // Error: Cannot access private property
// $conta->validarValor(-10); // Error: Cannot call private method
Propriedades e métodos estáticos
Membros estáticos pertencem à classe, não a uma instância específica. Eles existem independentemente de qualquer objeto criado:
<?php
declare(strict_types=1);
class Configuracao
{
// static — existe na classe, não no objeto
private static array $valores = [];
private static int $chamadas = 0;
// Método estático — chamado na classe, não no objeto
public static function definir(string $chave, mixed $valor): void
{
self::$valores[$chave] = $valor;
}
public static function obter(string $chave, mixed $padrao = null): mixed
{
self::$chamadas++;
return self::$valores[$chave] ?? $padrao;
}
public static function totalChamadas(): int
{
return self::$chamadas;
}
}
// Chamada com :: (double colon / paamayim nekudotayim)
// Não precisa criar objeto
Configuracao::definir("app.nome", "Minha Aplicação");
Configuracao::definir("app.versao", "1.0.0");
echo Configuracao::obter("app.nome"); // Minha Aplicação
echo Configuracao::obter("app.debug", false); // false — padrão
echo Configuracao::totalChamadas(); // 2
// Constante de classe — imutável, compartilhada entre todas as instâncias
class Moeda
{
const BRL = "BRL";
const USD = "USD";
const EUR = "EUR";
// PHP 8.3: constante tipada
// const string SIMBOLO_BRL = "R$";
}
echo Moeda::BRL; // BRL
Encapsulamento — o princípio por trás da visibilidade
O encapsulamento não é apenas esconder dados — é garantir que o objeto sempre esteja em um estado válido e que mudanças de estado passem por regras de negócio:
<?php
declare(strict_types=1);
// ✗ Sem encapsulamento — o objeto pode ser corrompido
class PedidoAberto
{
public string $status = "pendente";
public float $total = 0.0;
public array $itens = [];
}
$pedido = new PedidoAberto();
$pedido->status = "cancelado"; // permitido mesmo sem itens
$pedido->total = -500.0; // total negativo — estado inválido!
// ✓ Com encapsulamento — o objeto protege seu próprio estado
class Pedido
{
private string $status = "pendente";
private float $total = 0.0;
private array $itens = [];
// Transição de estado passa por validação
public function cancelar(): void
{
if ($this->status === "entregue") {
throw new RuntimeException("Pedido entregue não pode ser cancelado.");
}
$this->status = "cancelado";
}
public function adicionarItem(string $nome, float $preco, int $qtd): void
{
if ($this->status !== "pendente") {
throw new RuntimeException("Só é possível adicionar itens a pedidos pendentes.");
}
$this->itens[] = ["nome" => $nome, "preco" => $preco, "qtd" => $qtd];
$this->total += $preco * $qtd;
}
public function getStatus(): string { return $this->status; }
public function getTotal(): float { return $this->total; }
public function getItens(): array { return $this->itens; }
}
O método mágico __toString
O PHP tem vários métodos mágicos — métodos com nomes especiais que são chamados em situações específicas. O __toString é invocado quando o objeto é usado em contexto de string:
<?php
declare(strict_types=1);
class Produto
{
public function __construct(
public readonly string $nome,
public float $preco,
) {}
// Chamado quando o objeto é tratado como string
public function __toString(): string
{
return "{$this->nome} (R$ " . number_format($this->preco, 2, ',', '.') . ")";
}
}
$p = new Produto("Teclado", 350.0);
echo $p; // Teclado (R$ 350,00) — __toString chamado automaticamente
echo "Produto: $p"; // Produto: Teclado (R$ 350,00) — interpolação também chama
$s = (string) $p; // cast explícito também chama __toString
Boas práticas em OOP
Nomeie classes com substantivos, métodos com verbos. Pedido, Usuario, Produto — não GerenciadorDePedido. Métodos: calcularTotal(), validarEmail(), cancelar().
Prefira readonly em propriedades imutáveis. Se uma propriedade não deve mudar após a criação, declare-a readonly. O PHP 8.1 introduziu readonly para propriedades individuais; o PHP 8.2 para classes inteiras.
Nunca deixe objetos em estado inválido. O construtor deve validar os dados e lançar exceção se os dados não permitem criar um objeto válido. É melhor falhar na criação do que ter um objeto corrompido rodando pelo sistema.
Limite o número de dependências públicas. Quanto menos o código externo precisar conhecer sobre o interior de uma classe, mais fácil é mudar essa classe no futuro.
A parte fácil da orientação a objetos é a sintaxe — classe, construtor, método, visibilidade — e ela se aprende em uma tarde. A parte que leva tempo é decidir quais objetos devem existir e o que cada um garante sobre si mesmo. Uma classe que aceita qualquer valor e conta com quem a usa para verificar depois não é melhor que um array; uma que recusa nascer inválida elimina, de uma vez, todas as checagens espalhadas pelo resto do sistema. É essa troca que a orientação a objetos oferece, e o resto da série parte dela.
Fontes e leituras recomendadas
-
Classes e Objetos — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.oop5.php Documentação completa do sistema OOP do PHP — todas as funcionalidades com exemplos.
-
Promoção de propriedades no construtor — php.net https://www.php.net/manual/pt_BR/language.oop5.decon.php#language.oop5.decon.constructor.promotion Documentação específica da promoção de propriedades introduzida no PHP 8.0.
-
Propriedades readonly — RFC php.net https://wiki.php.net/rfc/readonly_properties_v2 A proposta que introduziu
readonlyno PHP 8.1, com motivação, design e exemplos. -
MARTIN, Robert C. Clean Code. Capítulo 10: Classes. https://www.oreilly.com/library/view/clean-code-a/9780136083238/ O capítulo sobre classes do Clean Code — tamanho ideal, responsabilidade única e coesão aplicados a OOP.
-
PHP: The Right Way — Object-Oriented Programming https://phptherightway.com/#object-oriented_programming Seção sobre boas práticas OOP na comunidade PHP moderna.
-
GAMMA, E. et al. Design Patterns. Addison-Wesley, 1994. https://www.oreilly.com/library/view/design-patterns-elements/0201633612/ O livro que formalizou os padrões de design OOP. Referência para os artigos futuros do Módulo 3.
Exercícios
Exercício 1
A propriedade $saldo é private, e mesmo assim este método lê o saldo de outro objeto. Isso é um furo no encapsulamento?
<?php
class Conta
{
public function __construct(private float $saldo) {}
public function maiorQue(Conta $outra): bool
{
return $this->saldo > $outra->saldo; // funciona
}
}
Ver resposta
✓ Resposta: Não é furo: em PHP, como em Java e C++, a visibilidade é por classe, não por objeto. Código escrito dentro de Conta pode acessar membros privados de qualquer instância de Conta, porque quem precisa ser protegido é o mundo externo, não um objeto do outro. E isso é necessário na prática — sem essa regra, nenhum método de comparação, cópia ou soma entre objetos da mesma classe poderia ser escrito sem expor o estado com um getter público, o que enfraqueceria o encapsulamento em vez de reforçá-lo. A distinção que costuma faltar é entre private e protected: private é visível só na classe que declarou — nem as subclasses enxergam, e uma subclasse que declare uma propriedade de mesmo nome cria outra propriedade, convivendo com a primeira; protected estende a visibilidade aos descendentes. A recomendação usual é começar tudo private e promover a protected só quando uma subclasse concreta precisar — o contrário, abrir por precaução, transforma detalhe interno em contrato que você não pode mais mudar. Vale notar ainda que a propriedade aqui é declarada na assinatura do construtor: a constructor property promotion, do PHP 8.0, que declara, recebe e atribui em um lugar só.
Exercício 2
Qual classe cada método instancia quando chamado como Relatorio::criar() e Relatorio::criarSelf()?
<?php
class Documento
{
public static function criar(): static { return new static(); }
public static function criarSelf(): self { return new self(); }
}
class Relatorio extends Documento {}
Ver resposta
✓ Resposta: Relatorio::criar() devolve um Relatorio; Relatorio::criarSelf() devolve um Documento. A diferença é o late static binding: self é resolvido onde o código foi escrito, e aponta para sempre a Documento; static é resolvido onde a chamada foi feita, e acompanha a subclasse. Daí a regra prática: em método de fábrica que deve funcionar para descendentes, use new static() — com self, toda subclasse recebe um objeto da classe-mãe, o que costuma aparecer como um erro de tipo dezenas de linhas adiante, ou pior, como comportamento faltando. A mesma distinção vale em outros lugares: static::class devolve o nome da classe concreta, enquanto self::class devolve o da classe que escreveu a linha, e é por isso que static::class é o que se usa em log dentro de trait ou de classe-base. Vale acrescentar que static como tipo de retorno só existe a partir do PHP 8.0 — antes era preciso documentar a intenção em docblock e torcer. E há o caso em que self é mesmo o certo: quando o método deve produzir a classe-base independentemente de quem chama, geralmente porque a subclasse não é substituível ali.
Exercício 3
Uma classe de valor foi declarada com readonly para ser imutável. Por que a segunda linha falha e a terceira não?
<?php
final class Carrinho
{
public function __construct(public readonly array $itens) {}
}
$c = new Carrinho(['caneta']);
$c->itens[] = 'caderno'; // Error
$copia = [...$c->itens, 'caderno']; // ok
Ver resposta
✓ Resposta: A segunda linha lança Error: Cannot indirectly modify readonly property. readonly (PHP 8.1) impede qualquer escrita na propriedade depois da inicialização, e acrescentar um item ao array é escrita — arrays em PHP têm semântica de valor, então $c->itens[] = ... significaria substituir o array inteiro por outro. A terceira linha não falha porque não escreve nada: ela lê o array e monta um novo, deixando o original intacto. E é exatamente esse o padrão de trabalho com objeto imutável — em vez de alterar, produzir uma nova instância: public function com(string $item): static { return new static([...$this->itens, $item]); }. Há um limite importante que readonly não cobre: se a propriedade guardar um objeto, a referência fica travada, mas o estado interno desse objeto continua alterável — $pedido->cliente->nome = 'outro' passa sem reclamar. Imutabilidade profunda exige que os objetos aninhados também sejam imutáveis, e não há palavra-chave que garanta isso por você. Desde o PHP 8.2 é possível marcar a classe inteira como readonly, o que aplica o modificador a todas as propriedades e evita esquecer uma; e desde o 8.3 uma subclasse pode reinicializar propriedade readonly ao clonar, o que tornou o padrão "com" bem menos verboso.
Exercício 4
Este código imprime o objeto direto. Em que situação ele quebra, e qual é a forma segura de escrever isso?
<?php
echo "Pedido: " . $pedido;
Ver resposta
✓ Resposta: Quebra sempre que $pedido for um objeto sem o método mágico __toString(): o PHP lança Error: Object of class X could not be converted to string. Repare que é Error, não Exception — não é capturado por um catch (Exception). A forma segura tem duas partes. A primeira é declarar a intenção no tipo: se a classe deve poder virar texto, ela implementa Stringable (interface do PHP 8.0, implementada automaticamente por qualquer classe com __toString), e quem recebe declara string|Stringable $valor. Assim o compromisso aparece na assinatura, e não como surpresa em tempo de execução. A segunda é não depender de conversão implícita em ponto crítico: $pedido->codigo() é mais explícito do que "$pedido", e quem lê não precisa procurar o __toString para saber o que vai sair. Dois cuidados fecham o assunto: __toString pode lançar exceção desde o PHP 7.4 (antes, lançar de dentro dele causava erro fatal irrecuperável), mas continua sendo má ideia colocar lógica que falhe ali, porque o método é chamado em lugares inesperados — interpolação, comparação frouxa, mensagem de log. E ele não deve formatar para exibição: uma classe Dinheiro cujo __toString devolve "R$ 1.234,56" fica presa a uma localidade, e o formato pertence à camada de apresentação.
Exercício 5
Desafio: modele uma classe Temperatura que não permita existir em estado inválido (abaixo do zero absoluto) e ofereça conversão entre escalas. Explique por que getters e setters não são a resposta.
Ver resposta
✓ Resposta: A chave é impedir o estado inválido na construção, em vez de vigiá-lo depois:
<?php
declare(strict_types=1);
final class Temperatura implements Stringable
{
private const ZERO_ABSOLUTO = -273.15;
// private: ninguém constrói pela escala errada por engano
private function __construct(public readonly float $celsius)
{
if ($celsius < self::ZERO_ABSOLUTO) {
throw new InvalidArgumentException(
sprintf('%.2f °C está abaixo do zero absoluto', $celsius)
);
}
}
// Construtores nomeados: a escala fica explícita na chamada
public static function emCelsius(float $c): self { return new self($c); }
public static function emFahrenheit(float $f): self { return new self(($f - 32) / 1.8); }
public static function emKelvin(float $k): self { return new self($k + self::ZERO_ABSOLUTO); }
public function fahrenheit(): float { return $this->celsius * 1.8 + 32; }
public function kelvin(): float { return $this->celsius - self::ZERO_ABSOLUTO; }
// Transformação devolve outra instância — o objeto nunca muda
public function mais(float $graus): self
{
return new self($this->celsius + $graus);
}
public function __toString(): string
{
return number_format($this->celsius, 1, ',', '.') . ' °C';
}
}
$t = Temperatura::emFahrenheit(451);
echo $t, ' = ', round($t->kelvin(), 2), ' K'; // 232,8 °C = 505,93 K
O motivo de getter e setter não serem a resposta é que eles não encapsulam nada — apenas escondem a propriedade atrás de dois métodos com o mesmo poder. Uma classe com getCelsius() e setCelsius() é, para todos os efeitos, uma propriedade pública com mais digitação: qualquer parte do sistema ainda pode deixá-la em estado inválido, e a validação no setter só adia o problema, porque agora existe um objeto válido que pode se tornar inválido a qualquer momento. Encapsular de verdade é garantir a invariante: se a temperatura nunca pode estar abaixo do zero absoluto, então nenhum Temperatura nesse estado deve conseguir existir, nunca — e a única porta de entrada é o construtor.
Três detalhes do desenho merecem nota. O construtor é private e a criação passa por construtores nomeados, o que elimina a ambiguidade de new Temperatura(451) — 451 em qual escala? A propriedade é public readonly em vez de private com getter: leitura livre não quebra invariante nenhuma, e um getter que só devolve o valor é cerimônia sem função. E mais() devolve uma nova instância em vez de alterar a atual, o que torna o objeto seguro de compartilhar — duas partes do sistema podem guardar a mesma Temperatura sem que uma surpreenda a outra.