Orientação a Objetos: Fundamentos

Orientação a Objetos: Fundamentos

Uma classe define o molde; cada objeto guarda o próprio estado. Construtor, métodos, os três níveis de visibilidade e o que realmente significa encapsular — que não é esconder atrás de getters, e sim impedir que exista objeto em estado inválido, com a promoção de propriedades do PHP 8 tornando isso barato.
PHP

17 min de leitura

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

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

Comentários

Mais em PHP

Traits Avançados
Traits Avançados

Trait resolve o que herança não alcança, e traz regras próprias: quem vence…

Estruturas de Repetição
Estruturas de Repetição

Repetir é onde o PHP oferece mais caminhos para o mesmo destino: for quando o…

Herança, Interfaces e Traits
Herança, Interfaces e Traits

Herdar, assinar um contrato ou compor: três mecanismos que resolvem problemas…