23 Union Types und Intersection Types

PHP hat in seinen neueren Versionen signifikante Verbesserungen im Typsystem eingeführt, um die Codequalität und -sicherheit zu erhöhen. Besonders hervorzuheben sind die Union Types (ab PHP 8.0) und die Intersection Types (ab PHP 8.1), die eine präzisere Typangabe ermöglichen.

23.1 Union Types

Union Types erlauben es, mehrere mögliche Typen für einen Parameter, Rückgabewert oder eine Eigenschaft zu definieren. Diese Typen werden mit dem Pipe-Symbol (|) verknüpft.

23.1.1 Grundlegende Syntax

function verarbeiteWert(int|float $zahl): string|bool {
    if ($zahl > 0) {
        return "Positive Zahl: " . $zahl;
    }
    
    return false;
}

In diesem Beispiel: - Der Parameter $zahl kann entweder vom Typ int oder float sein - Die Rückgabe kann entweder ein string oder ein bool sein

23.1.2 Anwendungsfälle für Union Types

  1. Optionale Typen (Nullable Types)

Vor PHP 8.0 wurden nullable Typen mit einem vorangestellten Fragezeichen markiert:

// Vor PHP 8.0
function getName(?string $name): ?string {
    return $name;
}

Mit Union Types ist dies äquivalent zu:

// Ab PHP 8.0
function getName(string|null $name): string|null {
    return $name;
}

Beide Schreibweisen sind gültig und führen zum gleichen Ergebnis. Das Fragezeichen ist eine kürzere Syntax für |null.

  1. Mehrere primitive Typen
function konfigurationsWert(string|int|float|bool $wert): void {
    // Verarbeite Konfigurationswert unterschiedlicher Typen
}
  1. Verschiedene Objekttypen
function verarbeiteZahlung(Kreditkarte|PayPal|Rechnung $zahlungsmethode): Transaktion {
    return $zahlungsmethode->prozessieren();
}
  1. Arrays mit union types kombinieren
function filterListe(array $liste): array<int|string> {
    // Filtert eine Liste und gibt nur Elemente zurück, die int oder string sind
}

23.1.3 Unterstützte Typen in Union Types

Union Types können alle in PHP verfügbaren Typen kombinieren:

Seit PHP 8.2 ist es auch möglich, null und false separat zu definieren, während vorher nur der komplette bool-Typ verwendet werden konnte.

// Ab PHP 8.2
function findElement(array $haystack, mixed $needle): int|false {
    $index = array_search($needle, $haystack);
    return $index;
}

23.2 Intersection Types

Intersection Types wurden in PHP 8.1 eingeführt und ermöglichen es, Typen zu definieren, die gleichzeitig mehrere Typen (Interfaces oder Klassen) implementieren müssen. Sie werden mit dem Ampersand-Symbol (&) verknüpft.

23.2.1 Grundlegende Syntax

function prozessiereSpezialObjekt(Serializable&Countable $objekt): void {
    // $objekt muss sowohl Serializable als auch Countable implementieren
    echo "Anzahl der Elemente: " . count($objekt);
    $serialized = serialize($objekt);
}

23.2.2 Einschränkungen bei Intersection Types

23.2.3 Typische Anwendungsfälle für Intersection Types

  1. Mehrere Interfaces erzwingen
function speichereLogEintrag(JsonSerializable&Stringable $logEintrag): void {
    file_put_contents(
        'log.txt',
        (string)$logEintrag . PHP_EOL,
        FILE_APPEND
    );
    
    file_put_contents(
        'log.json',
        json_encode($logEintrag->jsonSerialize()) . PHP_EOL,
        FILE_APPEND
    );
}
  1. Einschränkung von Generics

Obwohl PHP keine echten Generics unterstützt, können Intersection Types dazu beitragen, die Typensicherheit zu verbessern:

/**
 * @template T of Iterator&Countable
 * @param T $collection
 * @return int
 */
function countIterator(Iterator&Countable $collection): int {
    return count($collection);
}
  1. Traits und Interfaces kombinieren
interface Repository {
    public function findById(int $id): ?object;
}

trait Cacheable {
    private array $cache = [];
    
    public function isCached(int $id): bool {
        return isset($this->cache[$id]);
    }
}

class UserRepository implements Repository {
    use Cacheable;
    
    // ...
}

function mitCacheArbeiten(Repository&Cacheable $repo, int $id): ?object {
    if ($repo->isCached($id)) {
        return $repo->findById($id);
    }
    
    return null;
}

23.3 Kombination von Union und Intersection Types

Ab PHP 8.1 können Union Types und Intersection Types miteinander kombiniert werden. Dabei hat das &-Zeichen Vorrang vor dem |-Zeichen:

// Eine Funktion, die entweder ein Objekt akzeptiert, das Countable und Iterator implementiert,
// oder ein einfaches Array
function zähleElemente(array|(Countable&Iterator) $items): int {
    if (is_array($items)) {
        return count($items);
    }
    
    // $items implementiert sowohl Countable als auch Iterator
    return count($items);
}

Wenn die Reihenfolge der Auswertung unklar sein könnte, sollten Klammern verwendet werden, um die Gruppierung explizit zu machen.

23.4 Behandlung von Typprüfungen zur Laufzeit

Sobald Union oder Intersection Types definiert sind, führt PHP Typprüfungen zur Laufzeit durch:

function process(int|string $value): void {
    // ...
}

process(42);        // OK
process("Hello");   // OK
process(null);      // TypeError
process([]);        // TypeError

Wenn ein Wert nicht einem der erwarteten Typen entspricht, wirft PHP eine TypeError-Exception.

23.5 Best Practices

  1. Präzise, aber nicht zu restriktiv

Union Types sollten spezifisch genug sein, um Typfehler zu erkennen, aber nicht so restriktiv, dass sie legitime Anwendungsfälle verhindern.

// Besser:
function getId(int|string $id): void { /* ... */ }

// Statt:
function getId(mixed $id): void { /* ... */ }
  1. Verwendung für API-Stabilität

Union Types sind besonders nützlich für öffentliche APIs, um Kompatibilität zu gewährleisten und gleichzeitig Typ-Sicherheit zu bieten:

public function setConfig(array|ConfigObject $config): void {
    if (is_array($config)) {
        $config = new ConfigObject($config);
    }
    
    $this->config = $config;
}
  1. Weniger Typumwandlungen

Mit Union Types können oft Typumwandlungen vermieden werden:

// Ohne Union Types
function processValue($value): string {
    return (string) $value;
}

// Mit Union Types
function processValue(int|float|string $value): string {
    return is_string($value) ? $value : (string) $value;
}
  1. Kombination mit Type-Checking

Ergänzen Sie Union Types mit expliziten Typprüfungen:

function process(int|float|string $value): string {
    if (is_string($value)) {
        return $value;
    }
    
    if (is_int($value)) {
        return "Ganzzahl: $value";
    }
    
    return "Fließkommazahl: $value";
}

23.6 Herausforderungen und Einschränkungen

  1. Keine Generics

PHP unterstützt immer noch keine echten Generics, was die Verwendung von typisierten Arrays einschränkt. PHPDoc-Annotationen können diese Lücke teilweise schließen:

/**
 * @param array<int|string> $values
 * @return array<int, string>
 */
function transformValues(array $values): array {
    // ...
}
  1. Keine Disjunkten Unions

PHP kann nicht ausdrücken, dass ein Typ genau eine von mehreren Möglichkeiten sein muss, ohne die anderen zu sein (disjunkte Unions):

// Es gibt keine Möglichkeit zu definieren, dass ein Wert entweder ein int ODER ein string sein muss,
// aber nicht beides (was mit "mixed" möglich wäre)
  1. Keine Pipe-Zeichen in Dokumentationsblöcken

Bei der Verwendung von Pipe-Zeichen in PHPDoc-Blöcken muss beachtet werden, dass diese eine andere Bedeutung haben als in Typdeklarationen:

/**
 * @param int|string $value  // In PHPDoc ist dies eine Union
 */
function example(int|string $value): void { // Tatsächliche PHP-Union-Typdeklaration
    // ...
}