PHP 8.2 DNF Types: Disjunctive Normal Form Explained
PHP 8.2 introduces Disjunctive Normal Form (DNF) types, a feature that allows developers to combine union and intersection types in a standardized, structured way. This article explains how DNF typing works in PHP 8.2, explores the syntax rules required to implement it, and provides practical code examples demonstrating how this addition enables more precise and flexible type declarations.
Understanding the Basics: Union and Intersection Types
To understand DNF types, it is helpful to review the two type-combining features that preceded it in PHP:
- Union Types (PHP 8.0): Allowed a variable to be one
of multiple types, using the OR operator (
|). For example,string|intmeans the value can be a string or an integer. - Intersection Types (PHP 8.1): Allowed a variable to
satisfy multiple type constraints simultaneously, using the AND operator
(
&). For example,Iterable&Countablemeans the value must be both iterable and countable.
Before PHP 8.2, these two features could not be combined in a single type declaration. You could not write a type that was “either (Type A and Type B) OR Type C.” PHP 8.2 solves this limitation with DNF types.
What is Disjunctive Normal Form?
In boolean algebra, a Disjunctive Normal Form is a standardized way of structuring logical expressions: it is a union of intersections.
In PHP terms, this means you can combine intersection types using union operators. When doing so, the intersection types must always be grouped inside parentheses.
The standard pattern for a PHP 8.2 DNF type is:
(TypeA & TypeB) | TypeCThis declaration reads as: “The input must be an object that
implements both TypeA and TypeB, OR it must be
an instance of TypeC.”
Syntax Rules and Constraints
To use DNF types in PHP 8.2, you must follow strict syntax rules:
- Parentheses are required: You must wrap
intersection types in parentheses when combining them with union types.
For example,
(A & B) | Cis valid, butA & B | Cwill trigger a compilation error. - Intersections of Unions are not allowed: You cannot
write
(A | B) & C. You must resolve the expression mathematically into a union of intersections. For example, instead of(A | B) & C, you must write(A & C) | (B & C). - Strict Redundancy Checks: PHP will throw a
redundancy error if a type in the DNF expression is redundant. For
example,
(A & B) | Ais redundant because any object satisfyingA & Balready satisfiesA. PHP simplifies this to justA.
Practical Example: Nullable Intersection Types
The most common and practical use case for DNF types is creating
nullable intersection types. Prior to PHP 8.2, you
could not declare a parameter that must implement two interfaces but
could also be null.
With PHP 8.2, this is now possible:
interface Footwear {
public function wear(): void;
}
interface Waterproof {
public function dry(): void;
}
class Boots implements Footwear, Waterproof {
public function wear(): void {}
public function dry(): void {}
}
// DNF Type allowing an intersection or null
function inspectGear((Footwear & Waterproof) | null $gear): void {
if ($gear !== null) {
$gear->wear();
$gear->dry();
}
}In this example, the inspectGear function will accept an
object that implements both Footwear and
Waterproof, or it will accept null.
Advanced Example: Combining Multiple Intersections
You can also use DNF types to accept different combinations of distinct interfaces.
interface JSONExportable {
public function toJSON(): string;
}
interface YAMLExportable {
public function toYAML(): string;
}
interface Logger {
public function log(string $message): void;
}
// Accepts a logger that can export to JSON, OR a logger that can export to YAML
function serializeLog((Logger & JSONExportable) | (Logger & YAMLExportable) $exporter): void {
// Process the object based on its implemented interfaces
}By allowing these combinations, PHP 8.2 provides the type safety of static analysis and runtime checks without forcing developers to create redundant, empty wrapper interfaces just to satisfy type declarations.