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:

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

This 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:

  1. Parentheses are required: You must wrap intersection types in parentheses when combining them with union types. For example, (A & B) | C is valid, but A & B | C will trigger a compilation error.
  2. 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).
  3. Strict Redundancy Checks: PHP will throw a redundancy error if a type in the DNF expression is redundant. For example, (A & B) | A is redundant because any object satisfying A & B already satisfies A. PHP simplifies this to just A.

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.