Introduction
Intersection types, introduced in PHP 8.1, require a value to satisfy multiple type constraints simultaneously. While union types say "A or B", intersection types say "A and B" — the value must implement all specified interfaces.
Key Concepts
- Intersection Type: A type declaration using
&that requires all listed types, e.g.Countable&Traversable. - DNF Types (PHP 8.2+): Disjunctive Normal Form allows combining union and intersection types, e.g.
(A&B)|null.
Real World Context
Intersection types solve a real problem: you want to accept an object that implements multiple interfaces without creating a new interface just for that combination. This is common in service layers where a dependency must be both loggable and serializable, or both countable and iterable.
Deep Dive
Basic Syntax
Intersection types use the ampersand & between types:
php<?php // Countable and Iterator are built-in PHP interfaces function process(Countable&Iterator $collection): void { foreach ($collection as $item) { // Can iterate (Iterator) } echo "Count: " . $collection->count(); // Can count (Countable) }
The parameter $collection must implement both Countable and Iterator. Passing an object that only implements one of them would cause a TypeError.
Combining Interfaces
This pattern shines when you need to guarantee multiple capabilities:
php<?php interface Renderable { public function render(): string; } interface Cacheable { public function getCacheKey(): string; } function renderAndCache(Renderable&Cacheable $component): string { $key = $component->getCacheKey(); return $component->render(); }
Both methods are guaranteed to exist on $component, with no need for instanceof checks.
Intersection vs Union
The key difference is logical AND vs OR:
php<?php // Union: value implements A OR B (either one suffices) function union(A|B $value): void {} // Intersection: value implements A AND B (both required) function intersection(A&B $value): void {}
Union types are more permissive; intersection types are more restrictive.
DNF Types (PHP 8.2+)
Disjunctive Normal Form combines union and intersection types in a structured way:
php<?php // (A&B)|C means: (both A and B) OR just C function process((Countable&Iterator)|null $data): void { if ($data === null) { return; } // $data implements both Countable and Iterator } // More complex DNF function handle((A&B)|(C&D) $value): void { // Value is either (A and B) or (C and D) }
DNF types must follow a specific structure: intersections grouped in parentheses, combined with unions at the top level.
Practical Example
Here is a complete example showing intersection types in a real scenario:
php<?php interface Loggable { public function getLogContext(): array; } interface Jsonable { public function toJson(): string; } class Event implements Loggable, Jsonable { public function __construct( public string $type, public array $data ) {} public function getLogContext(): array { return ['type' => $this->type, 'data' => $this->data]; } public function toJson(): string { return json_encode($this->data); } } function logAndSerialize(Loggable&Jsonable $item): void { $context = $item->getLogContext(); $json = $item->toJson(); }
The logAndSerialize function can safely call methods from both interfaces without any runtime checks.
Common Pitfalls
- Using intersection with scalar types — Intersection types only work with class/interface types. You cannot write
int&stringbecause no value can be both simultaneously. - Forgetting parentheses in DNF —
(A&B)|nullis valid DNF, butA&B|nullis a syntax error. Always wrap intersection groups in parentheses when combining with unions.
Best Practices
- Use intersection types to avoid marker interfaces — Instead of creating
interface RenderableAndCacheable extends Renderable, Cacheable {}, useRenderable&Cacheabledirectly in the type declaration. - Combine with nullable for optional parameters —
(Countable&Iterator)|nullcleanly expresses an optional parameter that, when provided, must satisfy both constraints.
Summary
- Intersection types use
&to require all listed interfaces simultaneously. - They only work with class and interface types, not scalars.
- DNF types (PHP 8.2) combine unions and intersections:
(A&B)|C. - They eliminate the need for combined marker interfaces.
- Use parentheses when mixing intersection and union types.
Code Examples
<?php
declare(strict_types=1);
// Stringable and JsonSerializable are built-in PHP interfaces
// No need to declare them — just implement them
// Intersection type ensures both serialization capabilities
function export(Stringable&JsonSerializable $item): array {
return [
'text' => (string) $item,
'json' => json_encode($item->jsonSerialize()),
];
}
class Product implements Stringable, JsonSerializable {
public function __construct(
public string $name,
public float $price
) {}
public function __toString(): string {
return "{$this->name} (\${$this->price})";
}
public function jsonSerialize(): mixed {
return ['name' => $this->name, 'price' => $this->price];
}
}
$product = new Product('Widget', 29.99);
$exported = export($product);
// ['text' => 'Widget ($29.99)', 'json' => '{"name":"Widget","price":29.99}']
?>