PHP Interfaces
Uma interface é um contrato: uma lista de métodos que uma classe promete ter, sem dizer como eles funcionam. Ela não tem código, não tem propriedades comuns e não pode ser instanciada. Parece pouco, mas é a ferramenta mais usada do PHP moderno para fazer classes diferentes trabalharem juntas, e é o coração do polimorfismo.
- Declarar uma interface com
interfacee cumpri-la comimplements. - Usar a interface como tipo de parâmetro (polimorfismo).
- Uma classe com várias interfaces, e uma interface que estende outra.
- Constantes em interfaces.
- Três interfaces nativas úteis:
Countable,JsonSerializableeStringable. - A diferença entre interface e classe abstrata (com tabela).
A ideia: a tomada
A tomada da parede não sabe se você vai ligar uma geladeira, um carregador ou um liquidificador. Ela só exige um padrão: dois ou três pinos, naquele formato. Qualquer aparelho que siga o padrão funciona, por mais diferente que seja por dentro. E um aparelho pode seguir vários padrões ao mesmo tempo (a tomada brasileira e uma entrada USB).
| Na tomada | No PHP |
|---|---|
| O padrão de pinos | A interface (Pagavel): "tem um método pagar(float $valor): string" |
| Cada aparelho que segue o padrão | Cada classe que implementa a interface (Pix, ValePresente) |
| A tomada da parede | A função que pede o tipo da interface: finalizarCompra(Pagavel $forma) |
| Um aparelho com tomada e USB | Uma classe que implementa várias interfaces |
interface e implements
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
class Pix implements Pagavel
{
public function __construct(private string $chave)
{
}
public function pagar(float $valor): string
{
return "Pix de R$ " . number_format($valor, 2, ",", ".") . " para a chave {$this->chave}";
}
}
class ValePresente implements Pagavel
{
public function __construct(private float $saldo)
{
}
public function pagar(float $valor): string
{
if ($valor > $this->saldo) {
return "Vale recusado: saldo de R$ " . number_format($this->saldo, 2, ",", ".");
}
$this->saldo -= $valor;
return "Vale usado; sobram R$ " . number_format($this->saldo, 2, ",", ".");
}
}
function finalizarCompra(Pagavel $forma, float $total): void
{
echo $forma->pagar($total) . "\n";
}
finalizarCompra(new Pix("ana@exemplo.com"), 79.30);
finalizarCompra(new ValePresente(100), 79.30);
finalizarCompra(new ValePresente(50), 79.30);
var_dump(new Pix("x") instanceof Pagavel);
$ php pagavel.php Pix de R$ 79,30 para a chave ana@exemplo.com Vale usado; sobram R$ 20,70 Vale recusado: saldo de R$ 50,00 bool(true)
- interface Pagavel
- Declara a interface. O nome costuma ser um adjetivo de capacidade (
Pagavel,Reembolsavel,Countable= "contável") ou um substantivo (Notificador). - public function pagar(float $valor): string;
- Só a assinatura, terminando em
;. Todo método de interface épublice não tem corpo. - class Pix implements Pagavel
implements= "implementa", "cumpre". A classe promete ter todos os métodos da interface, com assinaturas compatíveis. O PHP confere ao ler a classe.- function finalizarCompra(Pagavel $forma, …)
- A função aceita qualquer objeto de uma classe que implemente
Pagavel. Ela não sabe o que é Pix ou vale-presente; só sabe que pode chamarpagar().
Repare que Pix e ValePresente não têm nada em comum por dentro: propriedades diferentes, lógica diferente, nenhuma mãe em comum. O que as une é só o contrato. E o instanceof Pagavel responde true, como na herança.
Polimorfismo
O mesmo código ($forma->pagar($total)) produziu três resultados diferentes, um para cada objeto. Isso é polimorfismo ("muitas formas"), o terceiro pilar da POO (O que é POO). O ganho é enorme: amanhã a Ana cria uma classe Cripto implements Pagavel, e a função finalizarCompra funciona com ela sem mudar uma linha.
Missão: crie a classe Boleto que implementa Pagavel. O pagar dela devolve "Boleto de R$ 79,30 gerado" (use number_format($valor, 2, ",", ".")). Rode antes de escrever para ver o erro de classe que não existe.
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
// crie a classe Boleto aqui
function finalizarCompra(Pagavel $forma, float $total): void
{
echo $forma->pagar($total) . "\n";
}
finalizarCompra(new Boleto(), 79.30);
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
class Boleto implements Pagavel
{
public function pagar(float $valor): string
{
return "Boleto de R$ " . number_format($valor, 2, ",", ".") . " gerado";
}
}
function finalizarCompra(Pagavel $forma, float $total): void
{
echo $forma->pagar($total) . "\n";
}
finalizarCompra(new Boleto(), 79.30);
Várias interfaces numa classe
Uma classe só tem uma mãe, mas pode implementar quantas interfaces quiser, separadas por vírgula. Cada interface é uma "capacidade":
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
interface Reembolsavel
{
public function reembolsar(float $valor): string;
}
class Pix implements Pagavel, Reembolsavel
{
public function pagar(float $valor): string
{
return "Pago via Pix: $valor";
}
public function reembolsar(float $valor): string
{
return "Devolvido via Pix: $valor";
}
}
class Boleto implements Pagavel
{
public function pagar(float $valor): string
{
return "Boleto gerado: $valor";
}
}
function devolver(Reembolsavel $forma, float $valor): void
{
echo $forma->reembolsar($valor) . "\n";
}
$pix = new Pix();
$boleto = new Boleto();
echo $pix->pagar(50) . "\n";
devolver($pix, 50);
var_dump($boleto instanceof Reembolsavel);
devolver($boleto, 50);
$ php varias.php
Pago via Pix: 50
Devolvido via Pix: 50
bool(false)
Fatal error: Uncaught TypeError: devolver(): Argument #1 ($forma) must be of type Reembolsavel, Boleto given, called in /home/ana/loja/varias.php on line 43 and defined in /home/ana/loja/varias.php:33
Stack trace:
#0 /home/ana/loja/varias.php(43): devolver(Object(Boleto), 50)
#1 {main}
thrown in /home/ana/loja/varias.php on line 33
O Pix sabe pagar e reembolsar; o boleto só sabe pagar. A função devolver pede Reembolsavel, e o PHP recusa o boleto com um TypeError claro, em vez de deixar o programa tentar chamar um reembolsar() que não existe. Interfaces pequenas e específicas (uma ou duas funções cada) são mais fáceis de combinar do que uma interface gigante.
extends e implements juntos; interface que estende interface
Uma classe pode ter uma mãe e interfaces (o extends vem primeiro). E uma interface pode estender outra, acrescentando métodos:
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
interface Parcelavel extends Pagavel
{
public function parcelas(float $valor, int $vezes): string;
}
abstract class FormaDePagamento
{
abstract public function nome(): string;
}
class Cartao extends FormaDePagamento implements Parcelavel
{
public function nome(): string
{
return "Cartão";
}
public function pagar(float $valor): string
{
return "{$this->nome()}: R$ " . number_format($valor, 2, ",", ".");
}
public function parcelas(float $valor, int $vezes): string
{
return "{$vezes}x de R$ " . number_format($valor / $vezes, 2, ",", ".");
}
}
$cartao = new Cartao();
echo $cartao->pagar(150) . "\n";
echo $cartao->parcelas(150, 3) . "\n";
var_dump($cartao instanceof Pagavel, $cartao instanceof FormaDePagamento);
print_r(class_implements($cartao));
$ php extends-implements.php
Cartão: R$ 150,00
3x de R$ 50,00
bool(true)
bool(true)
Array
(
[Parcelavel] => Parcelavel
[Pagavel] => Pagavel
)
Quem implementa Parcelavel precisa ter parcelas() e pagar() (herdado de Pagavel). A função class_implements lista todas as interfaces do objeto.
Constantes em interfaces
Interfaces não têm propriedades comuns, mas podem ter constantes, que as classes herdam:
<?php
interface RegrasDeFrete
{
public const float GRATIS_ACIMA = 100.0;
public function frete(float $subtotal): float;
}
class Correios implements RegrasDeFrete
{
public function frete(float $subtotal): float
{
return $subtotal >= self::GRATIS_ACIMA ? 0.0 : 15.0;
}
}
echo RegrasDeFrete::GRATIS_ACIMA . "\n";
echo Correios::GRATIS_ACIMA . "\n";
$c = new Correios();
echo $c->frete(75.80) . " / " . $c->frete(120) . "\n";
$ php constantes.php 100 100 15 / 0
A constante pode ser lida pela interface (RegrasDeFrete::GRATIS_ACIMA), pela classe (Correios::GRATIS_ACIMA) ou, dentro da classe, por self::. O tipo float na constante é do PHP 8.3 (veja static).
Uma propriedade comum numa interface dá Fatal error: Interfaces may only include hooked properties. Desde o 8.4, a interface pode exigir uma propriedade que se possa ler, com a sintaxe dos property hooks:
<?php
interface TemNome
{
public string $nome { get; }
}
class Produto implements TemNome
{
public function __construct(public string $nome)
{
}
}
function mostrar(TemNome $coisa): void
{
echo $coisa->nome . "\n";
}
mostrar(new Produto("Caneca"));
$ php hooked.php Caneca
O { get; } quer dizer "tem que dar para ler $coisa->nome". Uma propriedade pública comum já cumpre. Os hooks são explicados em readonly e Property Hooks.
Interfaces nativas úteis
O próprio PHP define várias interfaces. Implementá-las faz seus objetos funcionarem com funções do PHP. Três delas aparecem o tempo todo:
Countable: o objeto funciona com count()
Normalmente, count() só aceita arrays:
<?php
class Carrinho
{
private array $itens = ["Caneca", "Camiseta"];
}
echo count(new Carrinho());
$ php sem-countable.php
Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, Carrinho given in /home/ana/loja/sem-countable.php:6
Stack trace:
#0 {main}
thrown in /home/ana/loja/sem-countable.php on line 6
A mensagem já dá a dica: count() aceita Countable|array. Implementando Countable (um método só: count(): int), o carrinho passa a funcionar com count():
<?php
class Carrinho implements Countable
{
private array $itens = [];
public function adicionar(string $produto, int $quantidade = 1): void
{
$this->itens[$produto] = ($this->itens[$produto] ?? 0) + $quantidade;
}
public function count(): int
{
return array_sum($this->itens);
}
}
$carrinho = new Carrinho();
$carrinho->adicionar("Caneca", 2);
$carrinho->adicionar("Camiseta");
$carrinho->adicionar("Caneca");
echo "Itens no carrinho: " . count($carrinho) . "\n";
$ php countable.php Itens no carrinho: 4 « 2 + 1 + 1 canecas e camisetas: o carrinho decide o que "contar" significa
JsonSerializable: você decide como o objeto vira JSON
O json_encode (de JSON) transforma um objeto comum usando só as propriedades públicas. Com JsonSerializable, você escreve o método jsonSerialize() e devolve exatamente o que quer mostrar:
<?php
class Produto implements JsonSerializable
{
public function __construct(
public string $nome,
public float $preco,
private float $custo,
) {
}
public function jsonSerialize(): array
{
return [
"nome" => $this->nome,
"preco" => $this->preco,
"precoFormatado" => "R$ " . number_format($this->preco, 2, ",", "."),
];
}
}
class ProdutoSimples
{
public function __construct(
public string $nome,
public float $preco,
private float $custo,
) {
}
}
echo json_encode(new ProdutoSimples("Caneca", 25.90, 8.0), JSON_UNESCAPED_UNICODE) . "\n";
echo json_encode(new Produto("Caneca", 25.90, 8.0), JSON_UNESCAPED_UNICODE) . "\n";
echo json_encode([new Produto("Caneca", 25.90, 8.0), new Produto("Adesivo", 3.50, 1.0)], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
$ php json.php
{"nome":"Caneca","preco":25.9}
{"nome":"Caneca","preco":25.9,"precoFormatado":"R$ 25,90"}
[
{
"nome": "Caneca",
"preco": 25.9,
"precoFormatado": "R$ 25,90"
},
{
"nome": "Adesivo",
"preco": 3.5,
"precoFormatado": "R$ 3,50"
}
]
A primeira linha é o objeto comum: o custo (privado) ficou de fora, e não dá para acrescentar nada. As outras usam o jsonSerialize(): um campo novo (precoFormatado), e o custo continua fora. Isso vai ser muito útil em Criar uma API JSON.
Stringable: o objeto pode virar texto
Toda classe que tem o método mágico __toString() (veja Métodos Mágicos) implementa Stringable automaticamente, mesmo sem escrever implements. Use-a como tipo para aceitar "texto ou algo que vira texto":
<?php
class Produto
{
public function __construct(public string $nome, public float $preco)
{
}
public function __toString(): string
{
return "{$this->nome} (R$ " . number_format($this->preco, 2, ",", ".") . ")";
}
}
function etiqueta(string|Stringable $texto): string
{
return "[ " . $texto . " ]";
}
$caneca = new Produto("Caneca", 25.90);
echo $caneca . "\n";
echo etiqueta($caneca) . "\n";
echo etiqueta("Promoção") . "\n";
var_dump($caneca instanceof Stringable);
$ php stringable.php Caneca (R$ 25,90) [ Caneca (R$ 25,90) ] [ Promoção ] bool(true)
O tipo string|Stringable quer dizer "uma string ou um objeto Stringable" (tipos com | estão em Tipos Avançados).
| Interface nativa | Método que você escreve | O que ganha |
|---|---|---|
Countable | count(): int | Funciona com count($obj) |
JsonSerializable | jsonSerialize(): mixed | Controla o resultado de json_encode($obj) |
Stringable | __toString(): string | Funciona com echo e concatenação (automático) |
IteratorAggregate | getIterator(): Iterator | Funciona com foreach ($obj as …) |
ArrayAccess | offsetGet, offsetSet, offsetExists, offsetUnset | Funciona com $obj["chave"] |
Interface × classe abstrata
As duas definem métodos que outras classes precisam ter. A diferença está no que mais elas trazem:
| Interface | Classe abstrata | |
|---|---|---|
| Palavra para usar | implements | extends |
| Quantas uma classe pode ter | Várias | Uma só |
| Métodos com código | Não | Sim |
| Propriedades | Não (só com hooks, 8.4+) | Sim |
| Construtor | Não (na prática) | Sim |
| Constantes | Sim | Sim |
| Visibilidade dos métodos | Só public | public, protected |
| Pode ser instanciada? | Não | Não |
| Relação que expressa | "sabe fazer" (capacidade) | "é um" (família) |
| Exemplo | Pagavel, Countable | Pagamento com total() pronto |
Qual usar? Comece pela interface: ela é mais flexível e não amarra ninguém a uma família. Use uma classe abstrata quando as classes compartilham código de verdade. E dá para combinar: uma interface como contrato e uma classe abstrata que a implementa parcialmente, com o código comum (abstract class PagamentoBase implements Pagavel).
Quando dá errado
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
class Boleto implements Pagavel
{
public function gerar(float $valor): string
{
return "Boleto de $valor";
}
}
$ php faltou.php
Fatal error: Class Boleto contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Pagavel::pagar) in /home/ana/loja/faltou.php on line 7
Stack trace:
#0 {main}
O que significa: a classe disse implements Pagavel, mas não tem o método pagar (tem gerar, que não conta). Para o PHP, os métodos de uma interface são "abstratos", por isso a mensagem é a mesma das classes abstratas.
Como consertar: escreva o método com o nome e a assinatura da interface (public function pagar(float $valor): string).
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
$p = new Pagavel();
$ php instanciar.php
Fatal error: Uncaught Error: Cannot instantiate interface Pagavel in /home/ana/loja/instanciar.php:6
Stack trace:
#0 {main}
thrown in /home/ana/loja/instanciar.php on line 6
O que significa: uma interface é só o contrato; não existe "um Pagavel" sem uma classe concreta. Como consertar: new Pix(…), ou outra classe que implemente a interface.
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
class Pix extends Pagavel
{
}
$ php extends-interface.php
Fatal error: Class Pix cannot extend interface Pagavel in /home/ana/loja/extends-interface.php on line 6
Stack trace:
#0 {main}
O que significa: classe implementa interface; extends é para classe mãe (ou para uma interface estender outra interface). Como consertar: class Pix implements Pagavel.
<?php
interface Pagavel
{
public function pagar(float $valor): string
{
return "pago";
}
}
$ php corpo.php
Fatal error: Interface function Pagavel::pagar() cannot contain body in /home/ana/loja/corpo.php on line 4
Stack trace:
#0 {main}
O que significa: interfaces não têm código. Como consertar: termine a assinatura com ;. Se você quer código comum, use uma classe abstrata ou um trait.
<?php
interface Pagavel
{
private function pagar(float $valor): string;
}
$ php privado.php
Fatal error: Access type for interface method Pagavel::pagar() must be public in /home/ana/loja/privado.php on line 4
Stack trace:
#0 {main}
O que significa: métodos de interface são sempre públicos: a interface descreve o que os outros podem chamar. Do lado da classe, implementar com protected também é erro: Access level to Pix::pagar() must be public (as in class Pagavel). Como consertar: public nos dois lugares.
<?php
interface Pagavel
{
public function pagar(float $valor): string;
}
class Boleto implements Pagavel
{
public function pagar(float $valor, string $vencimento): string
{
return "Boleto de $valor, vence $vencimento";
}
}
$ php assinatura.php
Fatal error: Declaration of Boleto::pagar(float $valor, string $vencimento): string must be compatible with Pagavel::pagar(float $valor): string in /home/ana/loja/assinatura.php on line 9
Stack trace:
#0 {main}
O que significa: quem chama pagar($valor) pela interface não sabe do vencimento; o parâmetro obrigatório a mais quebraria o contrato. Como consertar: receba o vencimento no construtor do Boleto (guardando numa propriedade) ou dê um valor padrão: string $vencimento = "em 3 dias".
Referência
| Escrita | O que faz |
|---|---|
interface X { public function f(): tipo; } | Declara um contrato |
class A implements X | A classe cumpre o contrato X |
class A implements X, Y | Várias interfaces |
class A extends B implements X | Mãe e interface juntas |
interface Y extends X | Interface que acrescenta métodos a outra |
public const tipo NOME = valor; (na interface) | Constante do contrato |
function f(X $obj) | Aceita qualquer objeto que implemente X |
$obj instanceof X, class_implements($obj) | Confere / lista as interfaces |
Missão final
Missão: faça o Carrinho implementar duas interfaces nativas: implements Countable, JsonSerializable. Escreva count(): int (quantos preços há em $this->precos) e jsonSerialize(): array, que devolve ["itens" => count($this->precos), "total" => round(array_sum($this->precos), 2)]. A saída deve ter 3 itens e um JSON com "total":79.3.
<?php
class Carrinho
{
private array $precos = [];
public function adicionar(float $preco): void
{
$this->precos[] = $preco;
}
// escreva count e jsonSerialize aqui
}
$carrinho = new Carrinho();
$carrinho->adicionar(25.90);
$carrinho->adicionar(49.90);
$carrinho->adicionar(3.50);
echo count($carrinho) . " itens\n";
echo json_encode($carrinho) . "\n";
<?php
class Carrinho implements Countable, JsonSerializable
{
private array $precos = [];
public function adicionar(float $preco): void
{
$this->precos[] = $preco;
}
public function count(): int
{
return count($this->precos);
}
public function jsonSerialize(): array
{
return ["itens" => count($this->precos), "total" => round(array_sum($this->precos), 2)];
}
}
$carrinho = new Carrinho();
$carrinho->adicionar(25.90);
$carrinho->adicionar(49.90);
$carrinho->adicionar(3.50);
echo count($carrinho) . " itens\n";
echo json_encode($carrinho) . "\n";
Exercícios
1. Cumprir o contrato
Complete para a classe Pix cumprir a interface Pagavel:
2. Duas interfaces
Complete para a classe implementar Pagavel e Reembolsavel:
3. Funcionar com count()
Qual interface nativa faz um objeto funcionar com count($obj)?
4. O que uma interface não pode ter?
5. Quantas interfaces uma classe pode implementar?
6. Uma classe com __toString() e sem implements nenhum. Quanto vale $obj instanceof Stringable?
7. Classes Pix, Boleto e Cripto não têm código em comum, mas todas precisam de pagar(). O que usar?
Resumo
- Uma interface é um contrato: assinaturas de métodos públicos, sem código. Não pode ser instanciada.
class X implements Interface: a classe promete os métodos; se faltar algum, o PHP recusa a classe antes de rodar.- Use a interface como tipo: a função aceita qualquer classe que a cumpra (polimorfismo).
- Uma classe pode implementar várias interfaces; uma interface pode estender outra; interfaces podem ter constantes.
- Nativas úteis:
Countable(count()),JsonSerializable(json_encode),Stringable(automática com__toString). - Interface = "sabe fazer"; classe abstrata = "é um", com código comum. Na dúvida, comece pela interface.