Union types e intersection types no PHP

1. Introdução aos Type Systems no PHP

1.1. Evolução do sistema de tipos: do PHP 7 ao PHP 8.x

O sistema de tipos do PHP passou por uma transformação significativa desde a introdução das declarações de tipo escalar no PHP 7.0. Enquanto o PHP 7 permitia tipos simples como int, string, array e classes, o PHP 8.0 revolucionou o sistema com os union types, e o PHP 8.1 trouxe os intersection types. Essa evolução permitiu que desenvolvedores expressassem restrições de tipo muito mais precisas, reduzindo a necessidade de docblocks e validações manuais.

1.2. Conceito de tipos compostos: união vs. interseção

  • Union types (|): Um valor pode ser de um dos tipos listados. Exemplo: int|string significa "inteiro ou string".
  • Intersection types (&): Um valor deve ser de todos os tipos listados simultaneamente. Exemplo: Countable&Iterator significa "um objeto que implementa ambas as interfaces".

1.3. Benefícios de tipos mais expressivos para segurança e documentação

Tipos compostos eliminam ambiguidades, melhoram a autocompleção em IDEs, reduzem testes de tipo manuais e servem como documentação viva do código. Eles permitem que o próprio interpretador PHP valide contratos em tempo de execução.

2. Union Types: Sintaxe e Funcionalidades

2.1. Declarando tipos com o operador |

function processId(int|string $id): void {
    echo "Processando ID: $id\n";
}

2.2. Uso em parâmetros de funções e valores de retorno

function findUser(int|string $identifier): User|false {
    if (is_int($identifier)) {
        return User::findById($identifier);
    }
    return User::findByEmail($identifier) ?? false;
}

2.3. Union types com void, null e tipos escalares

function logMessage(string $message, ?string $level = null): void {
    echo "[$level] $message\n";
}

// Equivalente moderno:
function logMessageV2(string $message, string|null $level = null): void {
    echo "[$level] $message\n";
}

3. Union Types na Prática

3.1. Exemplos com tipos nativos

function calculateTotal(int|float $price, int $quantity): int|float {
    return $price * $quantity;
}

function formatInput(string|array $data): string {
    if (is_array($data)) {
        return implode(', ', $data);
    }
    return $data;
}

3.2. Combinação com classes e interfaces

interface Logger {}
interface Notifier {}

function process(Logger|Notifier $handler): void {
    if ($handler instanceof Logger) {
        $handler->log('Evento processado');
    }
    if ($handler instanceof Notifier) {
        $handler->send('Notificação enviada');
    }
}

3.3. Validação em tempo de execução

function handleValue(int|string $value): void {
    match (gettype($value)) {
        'integer' => echo "Número: $value",
        'string'  => echo "Texto: $value",
    };
}

4. Intersection Types: Sintaxe e Funcionalidades (PHP 8.1+)

4.1. Declarando tipos com o operador &

function processRenderable(Renderable&HasTitle $item): void {
    echo $item->getTitle();
    echo $item->render();
}

4.2. Restrições: apenas tipos de objeto são permitidos

Intersection types não podem ser usados com tipos escalares. Apenas interfaces, classes e traits são permitidos:

// Inválido:
function test(int&string $x): void {} // Erro!

// Válido:
function test(Countable&ArrayAccess $x): void {}

4.3. Como o compilador verifica a satisfação de múltiplos contratos

O PHP verifica em tempo de execução se o objeto passado implementa todas as interfaces ou classes especificadas:

interface A { function methodA(): void; }
interface B { function methodB(): void; }

function requireAB(A&B $obj): void {
    $obj->methodA();
    $obj->methodB();
}

5. Intersection Types em Cenários Reais

5.1. Garantindo que um objeto implemente múltiplas interfaces

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}
interface Arrayable {
    public function toArray(): array;
}

function exportData(JsonSerializable&Arrayable $data): string {
    return json_encode($data->jsonSerialize());
}

5.2. Uso com generics e classes abstratas

abstract class Entity {
    abstract public function getId(): int;
}
interface Cacheable {
    public function getCacheKey(): string;
}

function cacheEntity(Entity&Cacheable $entity): void {
    $key = $entity->getCacheKey();
    // Lógica de cache...
}

5.3. Exemplo prático: validação de objetos que combinam Countable e Iterator

function processCollection(Countable&Iterator $collection): void {
    echo "Total de itens: " . count($collection) . "\n";

    foreach ($collection as $item) {
        echo "Item: $item\n";
    }
}

// Uso com ArrayIterator (implementa ambas):
$iterator = new ArrayIterator([1, 2, 3]);
processCollection($iterator); // Funciona!

6. Compatibilidade com Nullable Types e Valores Padrão

6.1. Diferença entre ?Type e Type|null em union types

// Equivalente funcional:
function find(?int $id): ?User {}
function find(int|null $id): User|null {}

// Mas union types permitem combinações mais ricas:
function process(int|string|null $input): void {}

6.2. Intersection types com tipos opcionais (limitações)

// Intersection types não aceitam null diretamente:
// function test(Countable&Iterator|null $x): void {} // Inválido!

// Solução: usar union type
function test(Countable&Iterator|null $x): void {} // Válido no PHP 8.2+

6.3. Ordem dos tipos e impacto em parâmetros opcionais

function config(string|int $key = 'default'): void {}
function configV2(int|string $key = 0): void {} // Ordem não importa para valores padrão

7. Limitações, Erros Comuns e Boas Práticas

7.1. Erro de tipo em tempo de compilação vs. runtime

function add(int|float $a, int|float $b): int|float {
    return $a + $b;
}

// Erro em runtime se passar string:
add("10", 20); // TypeError em runtime

7.2. Union types muito amplos e perda de especificidade

// Evite:
function process(mixed $data): void {} // Muito genérico

// Prefira:
function process(int|string|array $data): void {}

7.3. Intersection types com classes concretas (evitar)

// Evite (acoplamento forte):
function process(User&Admin $user): void {}

// Prefira interfaces:
function process(CanEdit&HasPermissions $user): void {}

7.4. Dicas para manutenção e legibilidade

  • Use union types para valores que podem ter múltiplas representações válidas
  • Prefira intersection types com interfaces, não classes concretas
  • Documente casos de uso complexos com exemplos em docblocks
  • Evite union types com mais de 4-5 tipos (considere criar uma classe Value Object)

8. Comparação com Outros Recursos de Tipo no PHP

8.1. Union types vs. mixed e docblocks

// Antes (docblock):
/** @param int|string $id */
function findUser($id) {}

// Agora (nativo):
function findUser(int|string $id): User|null {}

8.2. Intersection types vs. herança múltipla (via traits)

// Intersection types são mais flexíveis que traits:
interface A { function doA(): void; }
interface B { function doB(): void; }

function handle(A&B $obj): void {} // Aceita qualquer objeto que implemente ambos

// Com traits, você precisaria de uma classe específica:
class MyClass {
    use TraitA, TraitB;
}
function handle(MyClass $obj): void {} // Mais restritivo

8.3. Integração com match expression e nullable types

function classify(int|string|null $value): string {
    return match (true) {
        is_null($value) => 'nulo',
        is_int($value)  => "inteiro: $value",
        is_string($value) => "string: $value",
    };
}

Referências