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.

Você vai aprender
  • Declarar uma interface com interface e cumpri-la com implements.
  • 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, JsonSerializable e Stringable.
  • 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 tomadaNo PHP
O padrão de pinosA interface (Pagavel): "tem um método pagar(float $valor): string"
Cada aparelho que segue o padrãoCada classe que implementa a interface (Pix, ValePresente)
A tomada da paredeA função que pede o tipo da interface: finalizarCompra(Pagavel $forma)
Um aparelho com tomada e USBUma 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 é public e 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 chamar pagar().

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

Propriedades em interface? Só com hooks PHP 8.4+

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 nativaMétodo que você escreveO que ganha
Countablecount(): intFunciona com count($obj)
JsonSerializablejsonSerialize(): mixedControla o resultado de json_encode($obj)
Stringable__toString(): stringFunciona com echo e concatenação (automático)
IteratorAggregategetIterator(): IteratorFunciona com foreach ($obj as …)
ArrayAccessoffsetGet, offsetSet, offsetExists, offsetUnsetFunciona 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:

InterfaceClasse abstrata
Palavra para usarimplementsextends
Quantas uma classe pode terVáriasUma só
Métodos com códigoNãoSim
PropriedadesNão (só com hooks, 8.4+)Sim
ConstrutorNão (na prática)Sim
ConstantesSimSim
Visibilidade dos métodosSó publicpublic, protected
Pode ser instanciada?NãoNão
Relação que expressa"sabe fazer" (capacidade)"é um" (família)
ExemploPagavel, CountablePagamento 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

Class … contains 1 abstract method … (Interface::metodo)
<?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).

Cannot instantiate interface
<?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.

Class … cannot extend 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.

Interface function … cannot contain body
<?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.

Access type for interface method … must be public
<?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.

Assinatura diferente da interface
<?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

EscritaO que faz
interface X { public function f(): tipo; }Declara um contrato
class A implements XA classe cumpre o contrato X
class A implements X, YVárias interfaces
class A extends B implements XMãe e interface juntas
interface Y extends XInterface 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:

class Pix { … }

2. Duas interfaces

Complete para a classe implementar Pagavel e Reembolsavel:

class Pix implements { … }

3. Funcionar com count()

Qual interface nativa faz um objeto funcionar com count($obj)?

class Carrinho implements { … }

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.