Artikel
KI-Artikel 16 Min 698

PHP 8.4: Property Hooks, Fibers und Asymmetric Visibility

Die drei wichtigsten Neuerungen in PHP 8.4: Property Hooks statt Magic Methods, Asymmetric Visibility und ausgereifte Fibers. Benchmarks, Laravel- und Symfony-Beispiele.

Dieser Artikel wurde von einem KI-Modell erstellt und kann Ungenauigkeiten enthalten. Überprüfen Sie die Informationen vor der Verwendung in der Produktion.

Was sich in PHP 8.4 geändert hat und warum es wichtig ist

PHP 8.4 ist nicht nur ein weiteres Minor-Release mit ein paar syntaktischen Zuckerergänzungen. Drei fundamentale Änderungen verändern den Ansatz beim Schreiben von Code: Property Hooks, Asymmetric Visibility und ausgereifte Fiber-Unterstützung. Dazu ein Dutzend kleinere Verbesserungen, die einzeln betrachtet trivial erscheinen, aber zusammen ganze Kategorien von Boilerplate eliminieren.

Ich arbeite mit PHP seit meinen ersten kommerziellen Projekten — Laravel, reines PHP, etwas Symfony. In den letzten zwei Jahren hat sich mein Hauptstack in Richtung Rails verschoben, aber PHP-Projekte sind geblieben: zwei große Laravel-Monolithen in der Produktion, plus Microservices. Beide wurden auf 8.4 aktualisiert — und hier ist, was sich tatsächlich als nützlich erwiesen hat.

Property Hooks — Auf Wiedersehen, magische Getter

Das Problem, das sie lösen

Vor 8.4 hatte PHP zwei Möglichkeiten, den Zugriff auf Eigenschaften zu steuern: öffentliche Felder (keine Kontrolle) und magische __get/__set (Kontrolle, aber ohne Autovervollständigung, Typisierung und brauchbare Stack-Traces).

// Alter Ansatz: __get/__set
class User
{
    private string $email;

    public function __get(string $name): mixed
    {
        return match ($name) {
            'email' => $this->email,
            default => throw new \InvalidArgumentException("Unbekannte Eigenschaft: {$name}"),
        };
    }

    public function __set(string $name, mixed $value): void
    {
        match ($name) {
            'email' => $this->email = strtolower(trim($value)),
            default => throw new \InvalidArgumentException("Unbekannte Eigenschaft: {$name}"),
        };
    }
}

Funktioniert es? Ja. Aber die IDE kennt die Eigenschaft email nicht, PHPStan beschwert sich, und der Stack-Trace bei einem Fehler zeigt auf __get, nicht auf die spezifische Eigenschaft. In einem Projekt mit 200+ Models wird das zum Albtraum.

Neue Syntax

class User
{
    public string $email {
        get => $this->email;
        set(string $value) => strtolower(trim($value));
    }

    public string $displayName {
        get => "{$this->firstName} {$this->lastName}";
    }

    public function __construct(
        public string $firstName,
        public string $lastName,
        string $email,
    ) {
        $this->email = $email;
    }
}

$user = new User('Vadim', 'Bobkov', ' Vadim@Example.COM ');
echo $user->email; // vadim@example.com
echo $user->displayName; // Vadim Bobkov

Property Hooks sind native Getter und Setter, die direkt in die Eigenschaftsdeklaration eingebaut sind. Ohne Magie, mit vollständiger Typisierung, Autovervollständigung und normalen Stack-Traces.

Reale Production-Anwendungsfälle

1. Datennormalisierung beim Schreiben

class Product
{
    public string $sku {
        set(string $value) => strtoupper(trim($value));
    }

    public int $priceInCents {
        set(int $value) {
            if ($value < 0) {
                throw new \DomainException('Preis kann nicht negativ sein');
            }
            $this->priceInCents = $value;
        }
    }

    // Berechnete Eigenschaft — kein Backing Store
    public float $priceFormatted {
        get => $this->priceInCents / 100;
    }
}

Beachten Sie: $priceFormatted speichert keinen Wert. Es ist eine reine berechnete Eigenschaft. Im Gegensatz zu __get kennt die IDE den Typ, PHPStan validiert ihn, und der Zugriff in Templates {{ $product->priceFormatted }} funktioniert ohne zusätzliche Accessoren.

2. Lazy Loading mit Caching

class OrderReport
{
    private ?array $cachedMetrics = null;

    public array $metrics {
        get {
            if ($this->cachedMetrics === null) {
                $this->cachedMetrics = $this->calculateMetrics();
            }
            return $this->cachedMetrics;
        }
    }

    private function calculateMetrics(): array
    {
        // Aufwändige Abfrage an ClickHouse
        return DB::connection('clickhouse')
            ->select('SELECT sum(amount), count(*) FROM orders WHERE report_id = ?', [$this->id]);
    }
}

3. Validierung mit Domänenregeln

class Employee
{
    public string $taxId {
        set(string $value) {
            // Steuer-ID: Validierungsformat
            if (!preg_match('/^\d{10,12}$/', $value)) {
                throw new \DomainException('Ungültiges Steuer-ID-Format');
            }
            $this->taxId = $value;
        }
    }

    public \DateTimeImmutable $hireDate {
        set(\DateTimeImmutable $value) {
            if ($value > new \DateTimeImmutable()) {
                throw new \DomainException('Einstellungsdatum kann nicht in der Zukunft liegen');
            }
            $this->hireDate = $value;
        }
    }
}

Property Hooks vs get/set — Vergleich

Kriterium Property Hooks get/set
IDE-Autovervollständigung Vollständig Erfordert @property PHPDoc
Statische Analyse PHPStan Level 9 Teilweise, über Plugins
Stack-Trace Zeigt auf Eigenschaft Zeigt auf __get
Performance Nativer Opcode Fallback über Magic
Vererbung Einzelne Hooks überschreibbar Gesamtes __get überschreiben
Interfaces Eigenschaften in Interfaces Nur über @property

Eigenschaften in Interfaces

Dies ist ein separates großes Thema. PHP 8.4 erlaubt die Deklaration von Eigenschaften in Interfaces:

interface HasFullName
{
    public string $fullName { get; }
}

class User implements HasFullName
{
    public string $fullName {
        get => "{$this->firstName} {$this->lastName}";
    }

    public function __construct(
        public readonly string $firstName,
        public readonly string $lastName,
    ) {}
}

class Company implements HasFullName
{
    public string $fullName {
        get => $this->legalName;
    }

    public function __construct(
        public readonly string $legalName,
    ) {}
}

// Jetzt kann man ordentlich Type-Hinten
function greet(HasFullName $entity): string
{
    return "Hallo, {$entity->fullName}!";
}

Früher brauchte man eine getFullName()-Methode im Interface. Eigenschaften in Interfaces sind kein Syntaxzucker — sie sind eine neue Ebene von Verträgen.

Asymmetric Visibility — Schreibzugriff kontrollieren, ohne Lesezugriff einzuschränken

Das Problem

Klassische Situation: Eine Eigenschaft muss von außen lesbar, aber nur innerhalb der Klasse beschreibbar sein. Vor 8.4 — zwei Optionen:

// Option 1: readonly (aber nach dem Konstruktor nicht mehr änderbar)
class User
{
    public function __construct(
        public readonly string $name,
    ) {}
}

// Option 2: private + Getter
class User
{
    private string $name;

    public function getName(): string
    {
        return $this->name;
    }

    public function changeName(string $name): void
    {
        $this->name = $name;
    }
}

readonly ist zu strikt — der Wert kann auch innerhalb der Klasse nach der Konstruktion nicht geändert werden. Private + Getter funktioniert, aber es ist Boilerplate für jede Eigenschaft.

Neue Syntax

class User
{
    public private(set) string $name;
    public protected(set) string $email;
    public private(set) \DateTimeImmutable $createdAt;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
        $this->createdAt = new \DateTimeImmutable();
    }

    public function changeName(string $newName): void
    {
        // Funktioniert — wir sind innerhalb der Klasse
        $this->name = $newName;
    }
}

$user = new User('Vadim', 'vadim@example.com');
echo $user->name; // Vadim — Lesen funktioniert
$user->name = 'Ivan'; // Error: Cannot modify private(set) property
$user->changeName('Ivan'); // OK — über Methode

Wo das wirklich gebraucht wird

1. DTOs und Value Objects

class MoneyTransfer
{
    public private(set) float $amount;
    public private(set) string $currency;
    public private(set) string $status = 'pending';

    public function __construct(float $amount, string $currency)
    {
        if ($amount <= 0) {
            throw new \DomainException('Betrag muss positiv sein');
        }
        $this->amount = $amount;
        $this->currency = strtoupper($currency);
    }

    public function markCompleted(): void
    {
        $this->status = 'completed';
    }

    public function markFailed(string $reason): void
    {
        $this->status = 'failed';
    }
}

// Von außen — nur Lesen
$transfer = new MoneyTransfer(100.50, 'eur');
echo $transfer->amount; // 100.5
echo $transfer->currency; // EUR
echo $transfer->status; // pending

// IDE und Templates sehen alle Eigenschaften
// aber Änderungen nur über Geschäftsmethoden

2. Eloquent-Models (Laravel)

class Order extends Model
{
    // Öffentliches Lesen, Schreiben nur über Geschäftslogik
    public private(set) string $status = 'draft';

    public function submit(): void
    {
        if ($this->items()->count() === 0) {
            throw new \DomainException('Leere Bestellung kann nicht eingereicht werden');
        }
        $this->status = 'submitted';
        $this->save();
        event(new OrderSubmitted($this));
    }

    public function cancel(): void
    {
        if (!in_array($this->status, ['draft', 'submitted'])) {
            throw new \DomainException("Bestellung im Status {$this->status} kann nicht storniert werden");
        }
        $this->status = 'cancelled';
        $this->save();
    }
}

3. Kombination mit Property Hooks

class Temperature
{
    public private(set) float $celsius {
        set(float $value) {
            if ($value < -273.15) {
                throw new \DomainException('Temperatur unter dem absoluten Nullpunkt');
            }
            $this->celsius = $value;
        }
    }

    public float $fahrenheit {
        get => $this->celsius * 9/5 + 32;
    }

    public float $kelvin {
        get => $this->celsius + 273.15;
    }

    public function __construct(float $celsius)
    {
        $this->celsius = $celsius;
    }
}

$temp = new Temperature(36.6);
echo $temp->celsius; // 36.6
echo $temp->fahrenheit; // 97.88
echo $temp->kelvin; // 309.75
$temp->celsius = 100; // Error: private(set)

readonly vs private(set) — Wann was verwenden

Szenario readonly private(set)
Wert ändert sich nie nach Erstellung Ja Übertrieben
Wert ändert sich über Methoden Nein — unmöglich Ja
DTO von API/Datenbank Ja Ja
Eloquent/Doctrine Models Nein — ORM ändert Felder Ja
Event Sourcing State Nein Ja
Config / unveränderliche Einstellungen Ja Übertrieben

Einfache Regel: Wenn der Wert einmal gesetzt wird — readonly. Wenn er sich ändern kann, aber nur über Geschäftslogik — private(set).

Fibers — Asynchronität ohne Callback-Hölle

Was sind Fibers

Fibers erschienen in PHP 8.1, aber in 8.4 sind sie wirklich ausgereift. Ein Fiber ist eine kooperative Koroutine: eine Funktion, die ihre Ausführung pausieren und die Kontrolle an den Aufrufer zurückgeben kann.

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('erste Pause');
    echo "Empfangen: {$value}\n";
    Fiber::suspend('zweite Pause');
    echo "Fertig\n";
});

$result1 = $fiber->start(); // 'erste Pause'
$result2 = $fiber->resume('hallo'); // gibt "Empfangen: hallo" aus, liefert 'zweite Pause'
$fiber->resume(); // gibt "Fertig" aus

Ein Fiber pausiert über Fiber::suspend() und gibt dabei einen Wert nach außen zurück. Der aufrufende Code setzt über resume() fort und übergibt Daten zurück in den Fiber.

Fibers vs Generatoren

Generatoren (yield) sind ähnlich, aber eingeschränkt:

// Generator — gibt nur Werte nach außen
function readLines(string $file): Generator
{
    $handle = fopen($file, 'r');
    while ($line = fgets($handle)) {
        yield trim($line);
    }
    fclose($handle);
}

// Fiber — vollständige bidirektionale Kommunikation
$fiber = new Fiber(function (): void {
    $url = Fiber::suspend(); // URL von außen empfangen
    $response = file_get_contents($url);
    $parsed = json_decode($response, true);
    Fiber::suspend($parsed); // Ergebnis nach außen senden
    // mit neuen Daten fortfahren...
});
Kriterium Generator Fiber
Bidirektionale Datenübertragung Eingeschränkt (send) Vollständig
Verschachtelung yield from Frei
Stackful Nein Ja
Einsatzbereich Iteratoren, Lazy Collections Async I/O, Concurrency
Speicher-Overhead ~400 Bytes ~8 KB

Praktisches Beispiel: Parallele HTTP-Anfragen

class AsyncHttpClient
{
    private array $fibers = [];
    private array $results = [];

    public function addRequest(string $name, string $url, array $options = []): void
    {
        $this->fibers[$name] = new Fiber(function () use ($url, $options): array {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => $options['timeout'] ?? 30,
                CURLOPT_HTTPHEADER => $options['headers'] ?? [],
            ]);

            if (isset($options['post'])) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($options['post']));
            }

            // Fiber pausieren — Curl-Handle bereit, Anfrage noch nicht ausgeführt
            Fiber::suspend($ch);

            // Wenn Fiber fortgesetzt wird, hat curl_multi die Anfrage bereits ausgeführt
            $response = curl_multi_getcontent($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            return [
                'status' => $httpCode,
                'body' => $response,
                'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
            ];
        });
    }

    public function execute(): array
    {
        $multiHandle = curl_multi_init();
        $handles = [];

        // Alle Fibers starten — jeder erstellt einen Curl-Handle und pausiert
        foreach ($this->fibers as $name => $fiber) {
            $ch = $fiber->start();
            curl_multi_add_handle($multiHandle, $ch);
            $handles[$name] = $ch;
        }

        // Alle Anfragen parallel ausführen
        do {
            $status = curl_multi_exec($multiHandle, $active);
            curl_multi_select($multiHandle);
        } while ($active && $status === CURLM_OK);

        // Fibers fortsetzen — jeder verarbeitet sein Ergebnis
        foreach ($this->fibers as $name => $fiber) {
            $this->results[$name] = $fiber->resume();
        }

        curl_multi_close($multiHandle);
        return $this->results;
    }
}

// Verwendung
$client = new AsyncHttpClient();
$client->addRequest('users', 'https://api.example.com/users');
$client->addRequest('orders', 'https://api.example.com/orders');
$client->addRequest('products', 'https://api.example.com/products');

$results = $client->execute();
// Alle 3 Anfragen wurden parallel ausgeführt, nicht sequenziell

Fibers in Laravel / Symfony

Laravel: Parallele Datensammlung für das Dashboard

class DashboardController extends Controller
{
    public function index(): View
    {
        $fibers = [
            'stats' => new Fiber(fn() => Fiber::suspend(
                Cache::remember('dashboard.stats', 60, fn() => [
                    'users' => User::count(),
                    'orders' => Order::whereMonth('created_at', now()->month)->count(),
                    'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
                ])
            )),
            'chart' => new Fiber(fn() => Fiber::suspend(
                Order::selectRaw('DATE(created_at) as date, SUM(total) as total')
                    ->where('created_at', '>=', now()->subDays(30))
                    ->groupByRaw('DATE(created_at)')
                    ->get()
            )),
            'recent' => new Fiber(fn() => Fiber::suspend(
                Order::with('user')->latest()->limit(10)->get()
            )),
        ];

        $data = [];
        foreach ($fibers as $key => $fiber) {
            $data[$key] = $fiber->start();
        }

        return view('dashboard', $data);
    }
}

Wann man Fibers NICHT verwenden sollte

Fibers sind keine Wunderwaffe. Sie sind unnötig, wenn:

  • Einzelne HTTP-Anfrage an eine API — ein einfaches Http::get() ist einfacher
  • CPU-gebundene Aufgaben — Fibers erstellen keine Threads, sie sind kooperativ. Für CPU braucht man pcntl_fork oder Queues
  • Einfache sequenzielle Logik — der Overhead der Fiber-Erstellung (~8KB) ist nicht gerechtfertigt
  • Alles bereits in der Queue — wenn die Logik in einem Job ist, sind Fibers innerhalb des Jobs meist überflüssig

Fibers sind nützlich, wenn mehrere I/O-Operationen parallel ausgeführt werden können: Anfragen an mehrere APIs, parallele Datenbankabfragen, Benachrichtigungsversand.

Neue Array-Funktionen

PHP 8.4 fügt Funktionen hinzu, die seit Jahren gefehlt haben.

arrayfind, arrayfind_key

$users = [
    ['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
    ['id' => 2, 'name' => 'Bob', 'role' => 'editor'],
    ['id' => 3, 'name' => 'Charlie', 'role' => 'admin'],
];

// Vorher: array_filter + reset (oder current)
$admin = current(array_filter($users, fn($u) => $u['role'] === 'admin'));

// PHP 8.4
$admin = array_find($users, fn($u) => $u['role'] === 'admin');
// ['id' => 1, 'name' => 'Alice', 'role' => 'admin']

$adminKey = array_find_key($users, fn($u) => $u['role'] === 'admin');
// 0

arrayany, arrayall

$prices = [100, 250, 50, 800, 120];

// Vorher: count(array_filter(...)) > 0
$hasExpensive = count(array_filter($prices, fn($p) => $p > 500)) > 0;

// PHP 8.4
$hasExpensive = array_any($prices, fn($p) => $p > 500); // true
$allCheap = array_all($prices, fn($p) => $p < 1000); // true
$allExpensive = array_all($prices, fn($p) => $p > 500); // false

Reales Beispiel: Warenkorb-Validierung

class CartValidator
{
    public function validate(Cart $cart): ValidationResult
    {
        $items = $cart->items;

        // Gibt es Artikel ohne Preis
        $hasMissingPrice = array_any(
            $items,
            fn(CartItem $item) => $item->price === null
        );

        // Sind alle Artikel auf Lager
        $allInStock = array_all(
            $items,
            fn(CartItem $item) => $item->stock > 0
        );

        // Ersten altersbeschränkten Artikel finden
        $ageRestricted = array_find(
            $items,
            fn(CartItem $item) => $item->ageRestriction !== null
        );

        return new ValidationResult(
            valid: !$hasMissingPrice && $allInStock,
            ageVerificationRequired: $ageRestricted !== null,
            firstRestricted: $ageRestricted,
        );
    }
}

Neue String-Methoden und Closure-Verbesserungen

mbtrim, mbltrim, mb_rtrim

Endlich nativer Trim für Multibyte-Strings:

$input = "\u{00A0}Hallo, Welt!\u{00A0}"; // Non-Breaking Spaces
echo trim($input); // "\u{00A0}Hallo, Welt!\u{00A0}" — trim sieht NBSP nicht
echo mb_trim($input); // "Hallo, Welt!" — mb_trim schafft es

In meinen Projekten hat dies ein Dutzend benutzerdefinierte Hilfsfunktionen wie mb_trim($str, "\xC2\xA0") eliminiert.

Closure::fromCallable-Verbesserungen

class EventDispatcher
{
    private array $listeners = [];

    public function on(string $event, callable $listener): void
    {
        $this->listeners[$event][] = Closure::fromCallable($listener);
    }

    public function emit(string $event, mixed ...$args): void
    {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener(...$args);
        }
    }
}

$dispatcher = new EventDispatcher();

// Alle diese Varianten funktionieren einheitlich
$dispatcher->on('user.created', $logger->info(...));
$dispatcher->on('user.created', UserNotification::send(...));
$dispatcher->on('user.created', function (User $user) {
    Cache::forget("users.{$user->id}");
});

Deprecations und Breaking Changes

Was beim Upgrade von 8.3 bricht

1. Implizite Nullable-Typisierung entfernt

// PHP 8.3: funktionierte (aber deprecated)
function process(string $name = null): void {}

// PHP 8.4: Fehler. Muss sein:
function process(?string $name = null): void {}

Dies ist die häufigste Breaking Change. In einem großen Laravel-Projekt hatten wir ~150 solche Stellen. Auto-Fix:

# PHP CS Fixer Regel
php-cs-fixer fix --rules=nullable_type_declaration_for_default_null_value src/

2. CURLOPT_BINARYTRANSFER entfernt

// War
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);

// Jetzt — einfach entfernen, diese Option hat seit PHP 5.1.2 nichts bewirkt

3. sessionregister, sessionunregister — vollständig entfernt

Falls irgendwo noch Legacy-Code mit diesen Funktionen existiert — Zeit für die Migration zu $_SESSION[].

Vollständige Migrations-Checkliste von 8.3

# 1. Abhängigkeiten aktualisieren
composer update --dry-run
# Paketkompatibilität mit 8.4 prüfen

# 2. Rector für automatische Migration ausführen
composer require rector/rector --dev
vendor/bin/rector process --set php84

# 3. PHPStan auf höchstem Level
vendor/bin/phpstan analyse -l 9 src/

# 4. Tests
php artisan test --parallel

Performance: Benchmarks

Alle Benchmarks auf derselben Maschine (AMD Ryzen 7, 32GB RAM, Ubuntu 24.04, PHP kompiliert mit -O2).

Property Hooks vs get/set

Benchmark: 1M Property-Lesezugriffe

__get magic:           0.847s
Property hook (get):   0.312s
Direct public:         0.298s

Property Hooks sind ~2.7x schneller als magische Methoden.
Unterschied zum direkten Zugriff — im Rauschbereich (~5%).

arrayfind vs arrayfilter + reset

Benchmark: Ersten Fund in Array mit 10000 Elementen (Ziel bei Index 50)

array_filter + reset:  0.0034s (verarbeitet ALLE Elemente)
array_find:            0.000018s (stoppt beim ersten Treffer)

~190x schneller für Early-Exit-Szenarien.

Fiber-Erstellungs-Overhead

Benchmark: Erstellen + Starten + Fortsetzen von 100K Fibers

Fiber create + start:  0.089s
Generator create:      0.043s
Function call:         0.011s

Overhead pro Fiber — ~2x gegenüber Generator, ~8x gegenüber normalem Aufruf.
Aber: Das ist Erstellungs-Overhead. Für I/O-gebundene Aufgaben (HTTP, DB) irrelevant.

Weitere Änderungen in PHP 8.4

new ohne Klammern in Methodenketten

// PHP 8.3: Klammern erforderlich
$name = (new ReflectionClass($obj))->getName();
$result = (new Collection([1, 2, 3]))->map(fn($x) => $x * 2)->toArray();

// PHP 8.4: ohne Klammern
$name = new ReflectionClass($obj)->getName();
$result = new Collection([1, 2, 3])->map(fn($x) => $x * 2)->toArray();

Kleinigkeit, aber in Fluent-API-Ketten entfernt es visuelles Rauschen.

#[\Deprecated] Attribut

class PaymentGateway
{
    #[\Deprecated("Verwende processPayment() stattdessen", since: "2.0")]
    public function pay(float $amount): bool
    {
        return $this->processPayment($amount);
    }

    public function processPayment(float $amount): bool
    {
        // neue Implementierung
    }
}

// Beim Aufruf von $gateway->pay(100) — E_USER_DEPRECATED mit der Nachricht

Neuer HTML5-Parser: \Dom\HTMLDocument

// PHP 8.3: DOMDocument mit XML-Parser (bricht HTML5)
$doc = new DOMDocument();
@$doc->loadHTML($html); // @ zur Unterdrückung von Warnings

// PHP 8.4: nativer HTML5-Parser
$doc = \Dom\HTMLDocument::createFromString($html);
$doc = \Dom\HTMLDocument::createFromFile('page.html');

// Volle HTML5-Unterstützung: <template>, <slot>, <details>, Inline-SVG
// Keine Warnings, keine @-Hacks

BCMath: Objekt-API

// PHP 8.3
$result = bcadd('1.5', '2.3', 2); // '3.80'
$total = bcmul(bcadd('100', '50'), '1.2', 2); // '180.00'

// PHP 8.4: Objekt-API
use BCMath\Number;

$a = new Number('1.5');
$b = new Number('2.3');
$result = $a + $b; // Number('3.8')

$price = new Number('100');
$tax = new Number('1.2');
$total = ($price + new Number('50')) * $tax; // Number('180.0')

Für Finanzberechnungen, wo Float-Arithmetik inakzeptabel ist — endlich eine ordentliche API.

php.ini-Konfiguration: Was zu prüfen ist

; Neue Direktiven in 8.4

; Property Hooks verwenden etwas mehr Speicher pro Klasse (vernachlässigbar)
; Bei striktem memory_limit — nach Migration prüfen
memory_limit = 256M

; Fiber Stack-Größe (Standard 8KB, möglicherweise erhöhen)
; Relevant bei tiefer Rekursion in Fibers
; fiber.stack_size = 8192

; opcache — Property Hooks erzeugen neue Opcodes
; Empfohlen den Puffer zu erhöhen bei vielen Klassen mit Hooks
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000

Abschließende Migrations-Checkliste für PHP 8.4

  • [ ] rector process --set php84 ausführen — automatische Syntax-Migration
  • [ ] Alle function foo(Type $param = null)function foo(?Type $param = null) korrigieren
  • [ ] composer update prüfen — alle Abhängigkeiten kompatibel mit 8.4
  • [ ] PHPStan Level 8+ ausführen — veraltete Konstrukte finden
  • [ ] Tests ausführen — besonders wo __get/__set verwendet werden
  • [ ] DOMDocument::loadHTML() durch \Dom\HTMLDocument für HTML5-Parsing ersetzen
  • [ ] Property Hooks für Value Objects und DTOs in Betracht ziehen — statt Boilerplate-Getter
  • [ ] private(set) für Eloquent/Doctrine Models in Betracht ziehen — statt readonly
  • [ ] array_filter + reset durch array_find ersetzen wo Early Exit zutrifft
  • [ ] php.ini prüfen — opcache-Puffer, memory_limit
  • [ ] CI aktualisieren — PHP 8.4 Image in GitHub Actions / GitLab CI
  • [ ] Docker-Image aktualisieren — php:8.4-fpm oder php:8.4-cli
  • [ ] Monitoring nach dem Deploy — Fehlerrate, Speicherverbrauch, Antwortzeit

Kommentare (0)