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|stringsignifica "inteiro ou string". - Intersection types (
&): Um valor deve ser de todos os tipos listados simultaneamente. Exemplo:Countable&Iteratorsignifica "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
- PHP Manual: Union Types — Documentação oficial sobre sintaxe e regras de union types no PHP 8.0+.
- PHP Manual: Intersection Types — Documentação oficial sobre intersection types introduzidos no PHP 8.1.
- PHP RFC: Union Types — Proposta técnica original que detalha a implementação e motivações dos union types.
- PHP RFC: Intersection Types — Proposta técnica para intersection types, incluindo discussões sobre restrições e casos de uso.
- Stitcher.io: Union and Intersection Types in PHP — Artigo prático com exemplos detalhados e comparações entre os dois sistemas de tipos.
- PHP.Watch: PHP 8.1 Intersection Types — Guia completo sobre intersection types com exemplos de código e limitações conhecidas.
- Stack Overflow: Union vs Intersection Types in PHP — Discussão da comunidade sobre diferenças conceituais e melhores práticas.