Dia 16 — Programação orientada a objetos: PHP e MySQL
Até aqui o PHP foi escrito como sequência de comandos e funções soltas. Este dia dá a forma que a arquitetura em camadas exige: `class`, `new`, encapsulamento com `private`, herança e interfaces, para que o model e o controller virem objetos com contrato. É também onde entra o Composer e o `autoload` PSR-4, que é o que faz `use App\Model\Aluno` encontrar o arquivo certo sozinho.
Classes e objetos
Classe é o desenho, objeto é a peça. O new cria a instância, o construtor prepara os dados, e $this dá acesso ao próprio objeto de dentro do método.
Declarando a classe
final class Aluno { public string $nome; public float $nota; public function __construct(string $nome, float $nota) { $this->nome = $nome; $this->nota = $nota; } public function aprovado(float $minima = 7.0): bool { return $this->nota >= $minima; } } $ana = new Aluno('Ana Souza', 8.5); $bruno = new Aluno('Bruno Lima', 6.0);
$ana->nota = 9.0 muda só o $ana. Cada objeto tem os seus valores, mesmo saindo da mesma classe.
__construct roda sozinho no new, e não existe caminho para criar o objeto sem passar por ele. Com tipo declarado no parâmetro, quem chama com o tipo errado recebe TypeError na hora.
$this e o construtor
$this é o próprio objeto. Dentro de aprovado(), $this->nota é a nota daquele aluno, sem a nota precisar ser passada como argumento.
O new aceita argumento nomeado, que evita trocar a ordem por engano:
$carla = new Aluno(nota: 9.25, nome: 'Carla Dias');
A promoção de construtor declara a propriedade e atribui em uma linha, e o valor padrão vem depois do obrigatório:
final class Curso { public function __construct( public string $nome, public int $vagas = 40, public ?string $turno = null, ) { } }
?string aceita null; string puro não aceita, e receber null lança TypeError.
Funções que perguntam sobre o objeto
| Função | Devolve |
|---|---|
get_class($objeto) | o nome da classe, como string |
$objeto instanceof Aluno | true se o objeto é daquela classe, seguindo a herança |
spl_object_id($objeto) | um inteiro que identifica o objeto nesta execução |
method_exists($objeto, 'aprovado') | true se o método existe |
spl_object_id() devolve valores diferentes para objetos diferentes, e o mesmo valor para a mesma variável: é assim que se confirma que são dois objetos independentes.
Objetos entram em array e passam por array_filter(), array_map() e sort() como qualquer outro valor.
__toString(): string é o método que permite echo $objeto. Sem ele, o PHP dá erro ao tentar converter o objeto em string.
Exemplo
<?php declare(strict_types=1); // Classes e objetos: class, new, propriedade, metodo, $this e __construct. echo "=== 1. O que e uma classe e o que e um objeto ===\n"; echo " A classe e o desenho; o objeto e a peca feita a partir do desenho.\n"; echo " Cada objeto tem os proprios valores de propriedade, mesmo saindo\n"; echo " da mesma classe. E por isso que um aluno nao vira o outro ao trocar o\n"; echo " valor da nota.\n"; // --- A classe --- final class Aluno { public string $nome; public float $nota; public function __construct(string $nome, float $nota) { $this->nome = $nome; $this->nota = $nota; } public function aprovado(float $minima = 7.0): bool { return $this->nota >= $minima; } public function situacao(): string { return $this->aprovado() ? 'aprovado' : 'reprovado'; } } $ana = new Aluno('Ana Souza', 8.5); $bruno = new Aluno('Bruno Lima', 6.0); echo "\n=== 2. Dois objetos da mesma classe, dados separados ===\n"; printf(" \$ana: %-12s %.2f %s\n", $ana->nome, $ana->nota, $ana->situacao()); printf(" \$bruno: %-12s %.2f %s\n", $bruno->nome, $bruno->nota, $bruno->situacao()); echo " \$bruno->nota = 9.0 muda so o Bruno:\n"; $bruno->nota = 9.0; printf(" \$ana: %.2f \$bruno: %.2f\n", $ana->nota, $bruno->nota); // --- $this --- echo "\n=== 3. \$this e o proprio objeto ===\n"; echo " Dentro do metodo, \$this aponta para o objeto que chamou.\n"; echo " Por isso aprovado() le \$this->nota de cada aluno, sem receber nada\n"; echo " como argumento.\n"; // --- O construtor --- echo "\n=== 4. O construtor roda sozinho no new ===\n"; echo " \$ana = new Aluno('Ana Souza', 8.5) chama __construct na hora.\n"; echo " Nao existe objeto sem passar por ele: os argumentos ficam obrigatorios\n"; echo " e o tipo declarado (\$nome string, \$nota float) e conferido na entrada.\n"; echo "\n new aceita nomeado, o que evita trocar a ordem dos argumentos:\n"; $carla = new Aluno(nota: 9.25, nome: 'Carla Dias'); printf(" %-12s %.2f\n", $carla->nome, $carla->nota); // --- Propriedade tipada --- echo "\n=== 5. Propriedade com tipo e valor padrao ===\n"; $codigo = <<<'PHP' final class Curso { public function __construct( public string $nome, public int $vagas = 40, public ?string $turno = null, ) { } } PHP; echo $codigo . "\n"; echo " A promocao de construtor declara a propriedade e atribui em uma linha.\n"; echo " ?string aceita null; string puro nao aceita null e lanca TypeError.\n"; // --- get_class, instanceof, spl_object_id --- echo "\n=== 6. Perguntar o que e um objeto ===\n"; printf(" get_class(\$ana): %s\n", get_class($ana)); printf(" \$ana instanceof Aluno: %s\n", var_export($ana instanceof Aluno, true)); printf(" \$ana instanceof DateTime:%s\n", var_export($ana instanceof DateTime, true)); printf(" spl_object_id(\$ana): %d\n", spl_object_id($ana)); printf(" spl_object_id(\$bruno): %d\n", spl_object_id($bruno)); echo " Dois ids diferentes confirmam que sao dois objetos independentes.\n"; // --- O array de objetos --- echo "\n=== 7. Objetos dentro de array, como qualquer outro valor ===\n"; $turma = [new Aluno('Ana Souza', 8.5), new Aluno('Bruno Lima', 6.0), $carla]; foreach ($turma as $aluno) { printf(" %-12s %5.2f %s\n", $aluno->nome, $aluno->nota, $aluno->situacao()); } printf(" aprovados: %d de %d\n", count(array_filter($turma, fn (Aluno $a): bool => $a->aprovado())), count($turma)); // --- __toString --- echo "\n=== 8. __toString permite imprimir o objeto direto ===\n"; final class Moeda { public function __construct(private float $valor) { } public function __toString(): string { return number_format($this->valor, 2, ',', '.'); } } $preco = new Moeda(1250.5); echo " {$preco} reais\n"; echo " Sem __toString, echo em um objeto daria Error: Object of class Moeda\n"; echo " could not be converted to string.\n";
Saída real
=== 1. O que e uma classe e o que e um objeto ===
A classe e o desenho; o objeto e a peca feita a partir do desenho.
Cada objeto tem os proprios valores de propriedade, mesmo saindo
da mesma classe. E por isso que um aluno nao vira o outro ao trocar o
valor da nota.
=== 2. Dois objetos da mesma classe, dados separados ===
$ana: Ana Souza 8.50 aprovado
$bruno: Bruno Lima 6.00 reprovado
$bruno->nota = 9.0 muda so o Bruno:
$ana: 8.50 $bruno: 9.00
=== 3. $this e o proprio objeto ===
Dentro do metodo, $this aponta para o objeto que chamou.
Por isso aprovado() le $this->nota de cada aluno, sem receber nada
como argumento.
=== 4. O construtor roda sozinho no new ===
$ana = new Aluno('Ana Souza', 8.5) chama __construct na hora.
Nao existe objeto sem passar por ele: os argumentos ficam obrigatorios
e o tipo declarado ($nome string, $nota float) e conferido na entrada.
new aceita nomeado, o que evita trocar a ordem dos argumentos:
Carla Dias 9.25
=== 5. Propriedade com tipo e valor padrao ===
final class Curso
{
public function __construct(
public string $nome,
public int $vagas = 40,
public ?string $turno = null,
) {
}
}
A promocao de construtor declara a propriedade e atribui em uma linha.
?string aceita null; string puro nao aceita null e lanca TypeError.
=== 6. Perguntar o que e um objeto ===
get_class($ana): Aluno
$ana instanceof Aluno: true
$ana instanceof DateTime:false
spl_object_id($ana): 1
spl_object_id($bruno): 2
Dois ids diferentes confirmam que sao dois objetos independentes.
=== 7. Objetos dentro de array, como qualquer outro valor ===
Ana Souza 8.50 aprovado
Bruno Lima 6.00 reprovado
Carla Dias 9.25 aprovado
aprovados: 2 de 3
=== 8. __toString permite imprimir o objeto direto ===
1.250,50 reais
Sem __toString, echo em um objeto daria Error: Object of class Moeda
could not be converted to string.
Visibilidade, estáticos e encapsulamento
Visibilidade decide quem enxerga o quê, static decide quem pertence a quem. Encapsulamento é o efeito prático dos dois juntos: a escrita passa por um método que sabe aplicar a regra.
As três visibilidades
| Palavra | Onde é visível |
|---|---|
public | de fora: $obj->propriedade e $obj->metodo() |
protected | dentro da classe e das classes filhas |
private | dentro da própria classe, e só dela |
private no construtor e nos parâmetros vira propriedade private pela promoção de construtor. private readonly acrescenta a regra: só o construtor escreve, depois disso o valor é fixo.
Encapsulamento
final class Conta { private float $saldo = 0.0; public function getSaldo(): float { return $this->saldo; } public function sacar(float $valor): bool { if ($valor <= 0 || $valor > $this->saldo) { return false; } $this->saldo -= $valor; return true; } }
Com public float $saldo, qualquer um escreve $conta->saldo = -1000 e o saldo fica negativo. Com private, a escrita passa por sacar() ou depositar(), que recusam o valor. Tentar escrever de fora gera Error: Cannot access private property.
protected existe para a classe filha alcançar o que o pai escondeu de fora. O filho não alcança o private do pai: precisa de protected, ou de um método público que exponha o valor.
static e constantes
static marca o membro como pertencente à classe. O acesso é com ::, sem objeto, e a propriedade estática é uma só para todos.
final class Registro { private static int $total = 0; public static function total(): int { return self::$total; } } echo Registro::total();
O que não pode ser estático é dado que varia por pessoa. Uma propriedade estática é compartilhada por todos os objetos, então a nota do aluno B sobrescreveria a do aluno A. static vai em contador, cache, registro e configuração.
public const NOME = valor; é acessado com Classe::NOME, e o valor é fixo desde a primeira execução.
self, static e parent
| Palavra | Acessa |
|---|---|
self:: | a classe onde o método foi escrito |
static:: | a classe que foi chamada, com os mesmos métodos |
parent:: | a classe pai, para chamar construtor ou método sobrescrito |
instanceof segue a herança: um ContaCorrente também é Conta. Para comparar a classe exata, o caminho é get_class($objeto) === 'ContaCorrente'.
Exemplo
<?php declare(strict_types=1); // Visibilidade (public, private, protected), membros estaticos, constantes de // classe, encapsulamento com getter e setter, e instanceof. echo "=== 1. As tres visibilidades ===\n"; $visibilidades = [ 'public' => 'visivel de fora: $obj->propriedade e $obj->metodo()', 'protected' => 'visivel so dentro da classe e das classes filhas', 'private' => 'visivel so dentro da propria classe', ]; foreach ($visibilidades as $palavra => $onde) { printf(" %-10s %s\n", $palavra, $onde); } echo "\n=== 2. A classe com encapsulamento de verdade ===\n"; class Conta { private float $saldo = 0.0; public const MOEDA = 'R$'; public const SALDO_MINIMO = 0.0; public function __construct(private readonly string $titular) { } public function getSaldo(): float { return $this->saldo; } public function getTitular(): string { return $this->titular; } public function depositar(float $valor): void { if ($valor <= 0) { throw new InvalidArgumentException('O deposito tem de ser positivo.'); } $this->saldo += $valor; } public function sacar(float $valor): bool { if ($valor <= 0 || $valor > $this->saldo) { return false; } $this->saldo -= $valor; return true; } } $conta = new Conta('Ana Souza'); echo " titular: {$conta->getTitular()} (private: so o getter da leitura)\n"; printf(" saldo: %s %.2f\n", Conta::MOEDA, $conta->getSaldo()); $conta->depositar(500.00); $conta->depositar(150.25); printf(" depois de dois depositos: %s %.2f\n", Conta::MOEDA, $conta->getSaldo()); // --- Por que private, e nao public --- echo "\n=== 3. O que acontece sem encapsular ===\n"; echo " Com public float \$saldo, qualquer um faz \$conta->saldo = -1000.\n"; echo " Com private, a escrita passa por depositar() e sacar(), que recusam\n"; echo " valor invalido. A regra fica no lugar que a sabe aplicar.\n"; var_dump($conta->sacar(200.00)); printf(" depois do saque: %s %.2f\n", Conta::MOEDA, $conta->getSaldo()); var_dump($conta->sacar(99999.00)); printf(" saque maior que o saldo recusado, saldo intacto: %s %.2f\n", Conta::MOEDA, $conta->getSaldo()); try { $conta->depositar(-50); } catch (InvalidArgumentException $e) { echo ' deposito negativo recusado: ' . $e->getMessage() . "\n"; } echo "\n=== 4. protected so aparece na classe filha ===\n"; final class ContaCorrente extends Conta { public function __construct(string $titular, private float $limite = 0.0) { parent::__construct($titular); } public function debitarDoLimite(float $valor): bool { if ($valor <= 0 || $valor > $this->limite) { return false; } $this->limite -= $valor; // acesso a propriedade private da propria classe return true; } } echo " A classe filha alcanca \$this->limite, que e private dela mesma.\n"; echo " Ela nao alcanca \$saldo, que e private da classe pai: para isso,\n"; echo " o pai precisaria de protected ou de um metodo que expusesse o saldo.\n"; // --- Escrever em private de fora da classe --- echo "\n=== 5. A propriedade private na barra a escrita de fora ===\n"; $codigo = <<<'PHP' $conta->saldo = -1000; // Error: Cannot access private property Conta::$saldo PHP; echo $codigo . "\n"; echo " Um unico ponto e um '=' ja produzem Error fatal, e o script para.\n"; // --- Constantes de classe --- echo "\n=== 6. Constantes de classe com const ===\n"; printf(" Conta::MOEDA %s\n", Conta::MOEDA); printf(" Conta::SALDO_MINIMO %s\n", var_export(Conta::SALDO_MINIMO, true)); echo " Classe::CONSTANTE e o mesmo formato de Foo::BAR. Nao ha -> aqui:\n"; echo " o sinal de dois pontos duplo acessa a propria classe, sem objeto.\n"; // --- Estaticos --- echo "\n=== 7. static: metodo que pertence a classe, nao ao objeto ===\n"; final class Registro { private static int $total = 0; public function __construct(private string $nome) { self::$total++; } public static function total(): int { return self::$total; } public static function zerar(): void { self::$total = 0; } } $a = new Registro('Ana'); $b = new Registro('Bruno'); $c = new Registro('Carla'); printf(" apos criar %d registros: Registro::total() = %d\n", 3, Registro::total()); echo " \$a, \$b e \$c nao tem contador proprio: o contador e um so, da classe.\n"; echo " E o mesmo estado que um contador estatico de tela, de visitor ou de\n"; echo " item de carrinho, que precisa ser visto de qualquer lugar.\n"; Registro::zerar(); printf(" depois de Registro::zerar(): %d\n", Registro::total()); echo "\n O que NAO pode ser estatico: estado que pertence a cada objeto.\n"; echo " Uma propriedade estatica e compartilhada por todos, entao a nota do\n"; echo " aluno B sobrescreveria a do aluno A. static vai em contador, cache\n"; echo " e configuracao, nunca em dado que varia por pessoa.\n"; // --- self, static, parent --- echo "\n=== 8. self, static e parent ===\n"; $palavras = [ 'self::' => 'a classe onde o metodo foi escrito (a propria)', 'static::' => 'a classe que foi chamada, com os mesmos metodos (heranca de metodo)', 'parent::' => 'a classe pai, para chamar construtor ou sobrescrever algo', ]; foreach ($palavras as $op => $significado) { printf(" %-10s %s\n", $op, $significado); } // --- instanceof --- echo "\n=== 9. instanceof com heranca ===\n"; $declarado = [$conta, new ContaCorrente('Bruno Lima', 300.0)]; foreach ($declarado as $obj) { printf(" %-16s instanceof Conta: %s\n", get_class($obj), var_export($obj instanceof Conta, true)); } echo " ContaCorrente tambem e Conta, porque instanceof segue a heranca.\n"; echo " Para checar a classe exata, use get_class(\$obj) === 'ContaCorrente'.\n";
Saída real
=== 1. As tres visibilidades === public visivel de fora: $obj->propriedade e $obj->metodo() protected visivel so dentro da classe e das classes filhas private visivel so dentro da propria classe === 2. A classe com encapsulamento de verdade === titular: Ana Souza (private: so o getter da leitura) saldo: R$ 0.00 depois de dois depositos: R$ 650.25 === 3. O que acontece sem encapsular === Com public float $saldo, qualquer um faz $conta->saldo = -1000. Com private, a escrita passa por depositar() e sacar(), que recusam valor invalido. A regra fica no lugar que a sabe aplicar. bool(true) depois do saque: R$ 450.25 bool(false) saque maior que o saldo recusado, saldo intacto: R$ 450.25 deposito negativo recusado: O deposito tem de ser positivo. === 4. protected so aparece na classe filha === A classe filha alcanca $this->limite, que e private dela mesma. Ela nao alcanca $saldo, que e private da classe pai: para isso, o pai precisaria de protected ou de um metodo que expusesse o saldo. === 5. A propriedade private na barra a escrita de fora === $conta->saldo = -1000; // Error: Cannot access private property Conta::$saldo Um unico ponto e um '=' ja produzem Error fatal, e o script para. === 6. Constantes de classe com const === Conta::MOEDA R$ Conta::SALDO_MINIMO 0.0 Classe::CONSTANTE e o mesmo formato de Foo::BAR. Nao ha -> aqui: o sinal de dois pontos duplo acessa a propria classe, sem objeto. === 7. static: metodo que pertence a classe, nao ao objeto === apos criar 3 registros: Registro::total() = 3 $a, $b e $c nao tem contador proprio: o contador e um so, da classe. E o mesmo estado que um contador estatico de tela, de visitor ou de item de carrinho, que precisa ser visto de qualquer lugar. depois de Registro::zerar(): 0 O que NAO pode ser estatico: estado que pertence a cada objeto. Uma propriedade estatica e compartilhada por todos, entao a nota do aluno B sobrescreveria a do aluno A. static vai em contador, cache e configuracao, nunca em dado que varia por pessoa. === 8. self, static e parent === self:: a classe onde o metodo foi escrito (a propria) static:: a classe que foi chamada, com os mesmos metodos (heranca de metodo) parent:: a classe pai, para chamar construtor ou sobrescrever algo === 9. instanceof com heranca === Conta instanceof Conta: true ContaCorrente instanceof Conta: true ContaCorrente tambem e Conta, porque instanceof segue a heranca. Para checar a classe exata, use get_class($obj) === 'ContaCorrente'.
Herança, abstração e interfaces
São quatro relações diferentes, e a escolha entre elas é a que define a estrutura do código. Herança é "é um", interface é "faz", classe abstrata é o modelo, e trait é o código compartilhado sem parentesco.
Herança
extends faz a classe filha herdar o que o pai já tem, e acrescentar o que falta. PHP tem um pai só por classe: não existe herança múltipla de classe.
abstract class Pagamento { public function __construct(protected float $valor) { } abstract public function descricao(): string; // a filha promete cumprir } final class Pix extends Pagamento { public function __construct(float $valor, private string $chave) { parent::__construct($valor); // o construtor do pai roda aqui } public function descricao(): string { return sprintf('Pix de R$ %.2f', $this->valor); } }
parent::__construct() é obrigatório quando o pai tem construtor: sem a chamada, as propriedades do pai ficam vazias. parent:: também serve dentro de um método sobrescrito, para chamar a versão do pai e acrescentar algo.
O polimorfismo é o efeito: o código chama $pagamento->descricao() e recebe a resposta do objeto. O chamador não muda, o comportamento sim.
abstract, interface e final
abstract numa classe impede a instanciação: ela serve de modelo, e new nela dá Error: Cannot instantiate abstract class. abstract num método declara que a filha tem de implementar, e o corpo fica por conta dela.
interface lista o que tem de existir, sem implementar nada. A classe cumpre com implements, e pode implementar várias ao mesmo tempo.
abstract class | interface | |
|---|---|---|
| Pode ter método com corpo | sim | não |
| Pode ter propriedade | sim | não |
| Quantas por classe | uma, por extends | quantas, por implements |
| Serve para | "é um" com código em comum | "faz", sem caminho único |
final impede a extensão ou a sobrescrita: final class não pode ter filha, e final public function não pode ser sobrescrito. Serve para o que não deve mudar, como o formato de um recibo.
Trait
trait RegistroLog { public function registrar(string $mensagem): void { printf("[log] %s (em %s)\n", $mensagem, static::class); } } final class Pedido { use RegistroLog; } final class Cupom { use RegistroLog; }
O trait copia o método para dentro da classe, sem hierarquia. É a resposta para "as duas classes precisam disso" quando o "é um" não se aplica. static::class dentro dele mostra de qual classe o método foi chamado.
A regra da sobrescrita
A filha pode receber os mesmos tipos, ou mais amplos, e devolver o mesmo tipo, ou mais específico. Devolver algo mais amplo quebra quem chamou, e o PHP recusa.
Exemplo
<?php declare(strict_types=1); // Heranca (extends), classe abstrata (abstract), interface (implements), // sobrescrita, parent, final e trait. echo "=== 1. extends: a classe filha aproveita o que o pai ja faz ===\n"; echo " A filha herda metodos e propriedades, e pode acrescentar os dela.\n"; echo " Ela tem exatamente um pai: PHP nao tem heranca multipla de classe.\n"; abstract class Pagamento { public function __construct(protected float $valor) { } abstract public function descricao(): string; // a filha promete cumprir final public function recibo(): string { return "recibo: " . $this->descricao(); } } final class Pix extends Pagamento { public function __construct(float $valor, private string $chave) { parent::__construct($valor); // o construtor do pai roda aqui } public function descricao(): string { return sprintf('Pix de R$ %.2f para a chave %s', $this->valor, $this->chave); } } final class Cartao extends Pagamento { public function __construct(float $valor, private int $parcelas) { parent::__construct($valor); } public function descricao(): string { return sprintf('Cartao de R$ %.2f em %dx', $this->valor, $this->parcelas); } } $pagamentos = [new Pix(150.00, '[email protected]'), new Cartao(89.90, 3)]; echo "\n=== 2. O mesmo metodo, resultado diferente ===\n"; foreach ($pagamentos as $p) { printf(" %-46s %s\n", get_class($p), $p->descricao()); } echo "\n A variavel descricao() e a mesma chamada em dois objetos. O metodo\n"; echo " que responde e o de cada um. E o polimorfismo: o codigo do chamador\n"; echo " nao muda, o comportamento sim.\n"; echo "\n=== 3. parent:: chama o metodo do pai ===\n"; echo " parent::__construct(\$valor) roda o construtor de Pagamento, que e o\n"; echo " que preenche \$this->valor. Sem essa chamada, \$valor ficaria vazio.\n"; echo " parent tambem serve dentro de metodo sobrescrito:\n"; echo " parent::recibo() . 'extra' chama a versao do pai e continua a sua.\n"; // --- final --- echo "\n=== 4. final: metodo ou classe que ninguem estende ===\n"; $codigo = <<<'PHP' final class Cartao extends Pagamento // Error: Class Cartao cannot extend final class Pagamento final class Cartao extends Pagamento { final public function recibo(): string { } // Error: Cannot override final method } PHP; echo $codigo . "\n"; echo " No exemplo, recibo() e final: a classe filha nao pode trocar a forma\n"; echo " do recibo, senao dois pagamentos nao teriam o mesmo formato.\n"; // --- abstract --- echo "\n=== 5. A classe abstrata serve de modelo, nao se instancia ===\n"; $codigo = <<<'PHP' $p = new Pagamento(100); // Error: Cannot instantiate abstract class Pagamento PHP; echo $codigo . "\n"; echo " abstract na e so um metodo sem corpo: a classe inteira nao pode ser\n"; echo " instanciada, e existe para ser herdada. Serve quando a regra vale para\n"; echo " todas as filhas e ninguem deve usar a forma base.\n"; // --- interface --- echo "\n=== 6. interface: o que o codigo precisa saber, e so isso ===\n"; interface Exportavel { public function exportar(): string; public function formato(): string; } final class Relatorio implements Exportavel { public function __construct(private array $linhas) { } public function exportar(): string { return implode("\n", $this->linhas); } public function formato(): string { return 'txt'; } } final class Planilha implements Exportavel { public function exportar(): string { return 'a;1;2'; } public function formato(): string { return 'csv'; } } $exportacoes = [new Relatorio(['id;nome', '1;Ana']), new Planilha()]; foreach ($exportacoes as $e) { printf(" %-12s formato %-4s -> %s\n", get_class($e), $e->formato(), $e->exportar()); } echo "\n Uma classe pode implementar varias interfaces ao mesmo tempo:\n"; echo " final class Planilha implements Exportavel, Enviavel\n"; echo "\n=== 7. abstract e interface, lado a lado ===\n"; $comparacao = [ 'abstract class' => 'pode ter metodo implementado e propriedade; a filha herda o que existe', 'interface' => 'so o que tem de existir; nada e implementado na interface', 'implements' => 'promete cumprir os metodos da interface', 'extends' => 'aproveita a implementacao de uma unica classe', ]; foreach ($comparacao as $palavra => $papel) { printf(" %-15s %s\n", $palavra, $papel); } echo "\n Heranca e para 'e um': um Pix e um Pagamento. Interface e para\n"; echo " 'faz': um pagamento e exportavel, um banco e exportavel.\n"; // --- trait --- echo "\n=== 8. trait: o mesmo codigo em classes que nao tem parentesco ===\n"; trait RegistroLog { public function registrar(string $mensagem): void { printf(" [log] %s (em %s)\n", $mensagem, static::class); } } final class Pedido { use RegistroLog; } final class Cupom { use RegistroLog; } (new Pedido())->registrar('pedido criado'); (new Cupom())->registrar('cupom aplicado'); echo "\n static::class mostra de qual classe o metodo foi chamado, e nao de\n"; echo " qual trait o codigo foi copiado. Sem heranca, sem interface: a trait\n"; echo " e o jeito de compartilhar codigo quando o 'e um' nao se aplica.\n"; // --- sobrescrita e compatibilidade --- echo "\n=== 9. Regra de compatibilidade na sobrescrita ===\n"; echo " O metodo da filha pode receber: os mesmos tipos, ou mais amplos, e\n"; echo " devolver: o mesmo tipo, ou mais especifico.\n"; echo " Devolver algo mais amplo quebra quem chamou, entao o PHP recusa com\n"; echo " um erro de compatibilidade de tipo. A regra vale para o return covariant.\n";
Saída real
=== 1. extends: a classe filha aproveita o que o pai ja faz === A filha herda metodos e propriedades, e pode acrescentar os dela. Ela tem exatamente um pai: PHP nao tem heranca multipla de classe. === 2. O mesmo metodo, resultado diferente === Pix Pix de R$ 150.00 para a chave [email protected] Cartao Cartao de R$ 89.90 em 3x A variavel descricao() e a mesma chamada em dois objetos. O metodo que responde e o de cada um. E o polimorfismo: o codigo do chamador nao muda, o comportamento sim. === 3. parent:: chama o metodo do pai === parent::__construct($valor) roda o construtor de Pagamento, que e o que preenche $this->valor. Sem essa chamada, $valor ficaria vazio. parent tambem serve dentro de metodo sobrescrito: parent::recibo() . 'extra' chama a versao do pai e continua a sua. === 4. final: metodo ou classe que ninguem estende === final class Cartao extends Pagamento // Error: Class Cartao cannot extend final class Pagamento final class Cartao extends Pagamento { final public function recibo(): string { } // Error: Cannot override final method } No exemplo, recibo() e final: a classe filha nao pode trocar a forma do recibo, senao dois pagamentos nao teriam o mesmo formato. === 5. A classe abstrata serve de modelo, nao se instancia === $p = new Pagamento(100); // Error: Cannot instantiate abstract class Pagamento abstract na e so um metodo sem corpo: a classe inteira nao pode ser instanciada, e existe para ser herdada. Serve quando a regra vale para todas as filhas e ninguem deve usar a forma base. === 6. interface: o que o codigo precisa saber, e so isso === Relatorio formato txt -> id;nome 1;Ana Planilha formato csv -> a;1;2 Uma classe pode implementar varias interfaces ao mesmo tempo: final class Planilha implements Exportavel, Enviavel === 7. abstract e interface, lado a lado === abstract class pode ter metodo implementado e propriedade; a filha herda o que existe interface so o que tem de existir; nada e implementado na interface implements promete cumprir os metodos da interface extends aproveita a implementacao de uma unica classe Heranca e para 'e um': um Pix e um Pagamento. Interface e para 'faz': um pagamento e exportavel, um banco e exportavel. === 8. trait: o mesmo codigo em classes que nao tem parentesco === [log] pedido criado (em Pedido) [log] cupom aplicado (em Cupom) static::class mostra de qual classe o metodo foi chamado, e nao de qual trait o codigo foi copiado. Sem heranca, sem interface: a trait e o jeito de compartilhar codigo quando o 'e um' nao se aplica. === 9. Regra de compatibilidade na sobrescrita === O metodo da filha pode receber: os mesmos tipos, ou mais amplos, e devolver: o mesmo tipo, ou mais especifico. Devolver algo mais amplo quebra quem chamou, entao o PHP recusa com um erro de compatibilidade de tipo. A regra vale para o return covariant.
Composer e autoload
Composer resolve duas coisas: baixar biblioteca e carregar classes sem require manual. O arquivo composer.json declara as duas coisas, e composer install gera o carregador.
O fluxo
composer install
O que ele faz: lê o composer.json, baixa as bibliotecas para vendor/, gera vendor/autoload.php e registra o carregador. A partir daí, o projeto precisa de uma linha só:
require __DIR__ . '/vendor/autoload.php'; $repo = new Escola\Repository\AlunoRepository($pdo); // sem require da classe
O composer.json e o composer.lock vão para o git. A pasta vendor/ não: ela é reconstruída com composer install, e versionar biblioteca de terceiro vira conflito de versão em conflito de código.
O composer.json
{ "name": "escola/alunos", "require": { "php": ">=8.2", "vlucas/phpdotenv": "^5.6" }, "autoload": { "psr-4": { "Escola\\": "src/" } } }
^5.6 quer dizer versão 5.6 ou mais nova, dentro do mesmo major. As versões exatas ficam no composer.lock, e é ele que garante que todo mundo instala o mesmo conjunto.
Depois de criar ou renomear uma classe, o autoload precisa ser regerado: composer dump-autoload.
namespace, use e PSR-4
PSR-4 é a regra que converte namespace em caminho: a barra invertida do namespace vira barra no diretório.
| Namespace | Arquivo |
|---|---|
Escola\ | src/ |
Escola\Repository\ | src/Repository/ |
Escola\Repository\AlunoRepository | src/Repository/AlunoRepository.php |
O nome do arquivo tem que casar com o nome da classe, letra por letra.
<?php declare(strict_types=1); namespace Escola\Repository; use PDO; final class AlunoRepository { public function __construct(private PDO $pdo) { } }
namespace declara onde a classe mora. use cria um apelido para um nome de outro espaço de nomes, para não repetir \PDO em cada linha — o use não carrega arquivo, ele só dá o apelido.
O autoload escrito à mão
É o mesmo princípio, e mostra o que o Composer faz por baixo:
spl_autoload_register(static function (string $classe) use ($raiz): void { $caminho = $raiz . '/' . $classe . '.php'; if (is_file($caminho)) { require $caminho; } });
O PHP chama essa função sempre que encontra um nome de classe que ainda não carregou, e a partir daí o new funciona sem require. Com namespace, a conversão é substr() depois do prefixo e str_replace('\\', '/') no resto.
O use ($raiz) no parâmetro da função anônima importa a pasta de fora: sem ele, a função não enxerga a variável.
Exemplo
<?php declare(strict_types=1); // Composer, autoload, PSR-4, namespace e use. // // Roda sem o Composer instalado: a parte de namespace e de classe aparece // como texto, porque um arquivo so pode declarar uma classe sem espaco de // nome, e o exemplo precisa das duas classes lado a lado. O que roda de // verdade e o autoload manual, que segue o mesmo principio que o do Composer: // o PHP pergunta "onde esta esta classe?" e alguem responde com um caminho. echo "=== 1. O problema que o Composer resolve ===\n"; echo " Sem autoload, quem usa uma classe precisa dizer de onde ela vem:\n"; echo " require_once __DIR__ . '/src/Repository/AlunoRepository.php';\n"; echo " Um esquecimento da linha require vira erro em tempo de execucao,\n"; echo " longe do arquivo que usa a classe. O autoload tira esse trabalho.\n"; // --- O que o Composer faz --- echo "\n=== 2. O que o Composer faz na pratica ===\n"; $passos = [ 'composer.json' => 'a lista de bibliotecas e o mapa de autoload do projeto', 'composer install' => 'baixa as bibliotecas em vendor/ e gera o autoload', 'vendor/autoload.php' => 'um arquivo require que registra o carregador automatico', 'require "vendor/autoload.php"' => 'a unica linha de que o projeto precisa', ]; foreach ($passos as $coisa => $faz) { printf(" %-38s %s\n", $coisa, $faz); } echo "\n composer.json e composer.lock vao para o git. A pasta vendor/ nao:\n"; echo " ela e reconstruida com composer install, e versionar biblioteca de\n"; echo " terceiro vira conflito de versao em conflito de codigo.\n"; // --- O composer.json --- echo "\n=== 3. O composer.json com PSR-4 ===\n"; $json = <<<'JSON' { "name": "escola/alunos", "require": { "php": ">=8.2" }, "autoload": { "psr-4": { "Escola\\": "src/" } } } JSON; echo $json . "\n"; echo "\n A regra do PSR-4: a barra invertida do namespace vira barra no caminho.\n"; echo " Escola\\Repository\\AlunoRepository -> src/Repository/AlunoRepository.php\n"; // --- A tabela de correspondencia --- echo "\n=== 4. Como o namespace vira caminho ===\n"; $casos = [ 'Escola\\' => 'src/', 'Escola\\Repository\\' => 'src/Repository/', 'Escola\\Repository\\AlunoRepository' => 'src/Repository/AlunoRepository.php', ]; foreach ($casos as $namespace => $arquivo) { printf(" %-40s %s\n", $namespace, $arquivo); } echo "\n A classe fica dentro do namespace com a mesma barra invertida:\n"; echo " namespace Escola\\Repository; e a pasta e src/Repository/.\n"; echo " O nome do arquivo tem que casar com o nome da classe, letra por letra.\n"; // --- O namespace e o use --- echo "\n=== 5. namespace e use ===\n"; $codigo = <<<'PHP' <?php declare(strict_types=1); namespace Escola\Repository; use PDO; final class AlunoRepository { public function __construct(private PDO $pdo) { } } PHP; echo $codigo . "\n"; echo " namespace Escola\\Repository declara onde a classe mora.\n"; echo " use PDO importa o nome de outro espaco de nome, para nao repetir\n"; echo " \\PDO em cada linha. O use nao carrega arquivo: ele so cria um apelido.\n"; // --- Varias classes no mesmo arquivo --- echo "\n=== 6. Por que o exemplo nao roda o namespace ===\n"; echo " Um arquivo declara um namespace so, e o autoload do PSR-4 espera um\n"; echo " arquivo por classe. Por isso o exemplo mostra o namespace em texto e\n"; echo " roda o autoload de verdade, logo abaixo, com nomes sem espaco de nome.\n"; // --- O autoload manual --- echo "\n=== 7. O mesmo principio, escrito a mao ===\n"; // A classe e gravada num arquivo proprio, num diretorio temporario, para que // o carregador abaixo tenha o que encontrar. Num projeto de verdade, esse // arquivo seria src/Repository/AlunoRepository.php e o carregador do PSR-4 // usaria a barra invertida do namespace virando barra no caminho. $raiz = sys_get_temp_dir() . '/aula-psr4/src/Repository'; if (!is_dir($raiz)) { mkdir($raiz, 0777, true); } $caminhoDaClasse = $raiz . '/AlunoRepository.php'; file_put_contents($caminhoDaClasse, <<<'PHP' <?php declare(strict_types=1); final class AlunoRepository { public function __construct(private array $banco) { } public function todos(): array { return $this->banco; } } PHP); // Este arquivo nao tem namespace, entao a conversao usa so o nome da classe. // Com namespace, seria substr() depois do prefixo e str_replace('\\', '/'). // A pasta entra por use, porque uma funcao sem capture nao enxerga variavel // de fora: sem isso, $raiz chegaria vazia no carregador. spl_autoload_register(static function (string $classe) use ($raiz): void { $caminho = $raiz . '/' . $classe . '.php'; if (is_file($caminho)) { require $caminho; } }); echo " Arquivo criado: {$caminhoDaClasse}\n"; echo " spl_autoload_register grava um carregador: o PHP chama ele sempre que\n"; echo " encontra um nome de classe que ainda nao foi carregado. Com ele, o\n"; echo " new AlunoRepository() abaixo funciona sem nenhum require manual.\n"; echo "\n=== 8. O new sem require: o carregador e acionado pelo proprio new ===\n"; printf(" antes do new, class_exists sem autoload: %s\n", var_export(class_exists('AlunoRepository', false), true)); $repo = new AlunoRepository([ ['id' => 1, 'nome' => 'Ana Souza'], ['id' => 2, 'nome' => 'Bruno Lima'], ]); printf(" depois do new, class_exists sem autoload: %s\n", var_export(class_exists('AlunoRepository', false), true)); printf(" registros carregados do arquivo: %d\n", count($repo->todos())); // O arquivo temporario so serviu para a demonstracao. unlink($caminhoDaClasse); // --- O composer.json completo de um projeto com biblioteca --- echo "\n=== 9. Um composer.json com biblioteca de terceiros ===\n"; $json = <<<'JSON' { "name": "escola/alunos", "require": { "php": ">=8.2", "vlucas/phpdotenv": "^5.6" }, "autoload": { "psr-4": { "Escola\\": "src/" } }, "require-dev": { "phpunit/phpunit": "^11.0" } } JSON; echo $json . "\n"; echo "\n ^5.6 significa: versao 5.6 ou mais nova, dentro do mesmo major.\n"; echo " As versoes ficam no composer.lock; e ele que garante que todo mundo\n"; echo " instala exatamente o mesmo conjunto de bibliotecas.\n";
Saída real
=== 1. O problema que o Composer resolve ===
Sem autoload, quem usa uma classe precisa dizer de onde ela vem:
require_once __DIR__ . '/src/Repository/AlunoRepository.php';
Um esquecimento da linha require vira erro em tempo de execucao,
longe do arquivo que usa a classe. O autoload tira esse trabalho.
=== 2. O que o Composer faz na pratica ===
composer.json a lista de bibliotecas e o mapa de autoload do projeto
composer install baixa as bibliotecas em vendor/ e gera o autoload
vendor/autoload.php um arquivo require que registra o carregador automatico
require "vendor/autoload.php" a unica linha de que o projeto precisa
composer.json e composer.lock vao para o git. A pasta vendor/ nao:
ela e reconstruida com composer install, e versionar biblioteca de
terceiro vira conflito de versao em conflito de codigo.
=== 3. O composer.json com PSR-4 ===
{
"name": "escola/alunos",
"require": {
"php": ">=8.2"
},
"autoload": {
"psr-4": {
"Escola\\": "src/"
}
}
}
A regra do PSR-4: a barra invertida do namespace vira barra no caminho.
Escola\Repository\AlunoRepository -> src/Repository/AlunoRepository.php
=== 4. Como o namespace vira caminho ===
Escola\ src/
Escola\Repository\ src/Repository/
Escola\Repository\AlunoRepository src/Repository/AlunoRepository.php
A classe fica dentro do namespace com a mesma barra invertida:
namespace Escola\Repository; e a pasta e src/Repository/.
O nome do arquivo tem que casar com o nome da classe, letra por letra.
=== 5. namespace e use ===
<?php
declare(strict_types=1);
namespace Escola\Repository;
use PDO;
final class AlunoRepository
{
public function __construct(private PDO $pdo) { }
}
namespace Escola\Repository declara onde a classe mora.
use PDO importa o nome de outro espaco de nome, para nao repetir
\PDO em cada linha. O use nao carrega arquivo: ele so cria um apelido.
=== 6. Por que o exemplo nao roda o namespace ===
Um arquivo declara um namespace so, e o autoload do PSR-4 espera um
arquivo por classe. Por isso o exemplo mostra o namespace em texto e
roda o autoload de verdade, logo abaixo, com nomes sem espaco de nome.
=== 7. O mesmo principio, escrito a mao ===
Arquivo criado: /tmp/aula-psr4/src/Repository/AlunoRepository.php
spl_autoload_register grava um carregador: o PHP chama ele sempre que
encontra um nome de classe que ainda nao foi carregado. Com ele, o
new AlunoRepository() abaixo funciona sem nenhum require manual.
=== 8. O new sem require: o carregador e acionado pelo proprio new ===
antes do new, class_exists sem autoload: false
depois do new, class_exists sem autoload: true
registros carregados do arquivo: 2
=== 9. Um composer.json com biblioteca de terceiros ===
{
"name": "escola/alunos",
"require": {
"php": ">=8.2",
"vlucas/phpdotenv": "^5.6"
},
"autoload": {
"psr-4": {
"Escola\\": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^11.0"
}
}
^5.6 significa: versao 5.6 ou mais nova, dentro do mesmo major.
As versoes ficam no composer.lock; e ele que garante que todo mundo
instala exatamente o mesmo conjunto de bibliotecas.