languages
June 30, 2026 · 7 min read · 0 views

PHP 8.4: Fibers, Property Hooks, and Asymmetric Visibility—What's New

PHP 8.4 brings lightweight concurrency with Fibers, property hooks for cleaner getters/setters, and asymmetric visibility modifiers. Learn how to adopt these features in production.

PHP 8.4 Ships Major Language Enhancements

PHP 8.4 was released in November 2024, introducing three transformative features that fundamentally improve how developers write concurrent code, manage object state, and control property access. Whether you’re building APIs, microservices, or traditional web applications, these changes are worth understanding immediately.

This post walks through each feature with practical examples, shows how they integrate into real-world codebases, and highlights common migration patterns.

Understanding Fibers: Lightweight Concurrency Without Async/Await

What Are Fibers?

Fibers are a lightweight concurrency primitive that allows you to pause and resume execution within a single thread. Unlike traditional async/await (which PHP still doesn’t have as a language construct), Fibers let you write sequential code that suspends execution at specific points without blocking the entire script.

The classic use case: you need to fetch data from multiple sources concurrently. In PHP 8.3 and earlier, you’d either:

  1. Make sequential HTTP requests (slow)
  2. Use threading/forking (heavy overhead)
  3. Use external job queues (architectural complexity)

With Fibers, you can suspend execution at I/O boundaries and let other Fibers run in the meantime.

Fiber Example: Concurrent API Calls

use Fiber;

$fiber1 = new Fiber(function() {
    echo "Fiber 1 starting\n";
    Fiber::suspend(); // Pause execution here
    echo "Fiber 1 resumed\n";
});

$fiber2 = new Fiber(function() {
    echo "Fiber 2 starting\n";
    Fiber::suspend();
    echo "Fiber 2 resumed\n";
});

$fiber1->start();
$fiber2->start();

$fiber1->resume();
$fiber2->resume();

// Output:
// Fiber 1 starting
// Fiber 2 starting
// Fiber 1 resumed
// Fiber 2 resumed

While the above is synchronous, in real scenarios Fibers integrate with event loops or HTTP client libraries that support suspension. For example, using a Fiber-aware HTTP client:

use Fiber;

class ConcurrentRequests {
    public function fetchMultiple(array $urls): array {
        $results = [];
        $fibers = [];

        foreach ($urls as $url) {
            $fibers[$url] = new Fiber(function() use ($url, &$results) {
                // Simulate async HTTP fetch
                $results[$url] = $this->fetchUrl($url);
            });
        }

        // Start all fibers
        foreach ($fibers as $fiber) {
            $fiber->start();
        }

        return $results;
    }

    private function fetchUrl(string $url): string {
        // In reality, this would use a Fiber-aware HTTP client
        // like Amphp or ReactPHP's HTTP client
        return file_get_contents($url);
    }
}

Frameworks like Amphp, ReactPHP, and Revolt have already added native Fiber support, making concurrent code far more ergonomic.

Property Hooks: Cleaner Getters and Setters

The Problem Property Hooks Solve

In traditional PHP, managing computed or validated properties requires verbose getter/setter methods:

class User {
    private string $email;

    public function getEmail(): string {
        return strtolower($this->email);
    }

    public function setEmail(string $email): void {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email');
        }
        $this->email = $email;
    }
}

This pattern is repetitive and doesn’t scale well when you have many properties. Property hooks let you define get and set behaviors directly on properties:

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

    private string $emailValue;

    private function validateAndSet(string $value): void {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email');
        }
        $this->emailValue = $value;
    }
}

Practical Example: Readonly Computed Properties

class Order {
    public function __construct(
        private float $subtotal,
        private float $taxRate = 0.08,
    ) {}

    // Computed property with only a getter
    public float $total {
        get => $this->subtotal * (1 + $this->taxRate);
    }

    // Property with validation hook
    public float $discountPercent {
        get => $this->discountPercentValue;
        set(float $value) {
            if ($value < 0 || $value > 100) {
                throw new InvalidArgumentException('Discount must be 0-100');
            }
            $this->discountPercentValue = $value;
        }
    }

    private float $discountPercentValue = 0;
}

$order = new Order(100, 0.08);
echo $order->total; // 108
$order->discountPercent = 10; // Valid
$order->discountPercent = 150; // Throws InvalidArgumentException

This eliminates boilerplate while making the intent explicit: properties that are read-only, computed, or require validation.

Asymmetric Visibility: Fine-Grained Access Control

What Changed

Before PHP 8.4, a property’s visibility was symmetric—if it was public, both reads and writes were public. If private, neither could be accessed from outside.

Asymmetric visibility lets you decouple read and write access:

class BankAccount {
    // Public read, private write
    public private(set) float $balance;

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

    public function withdraw(float $amount): void {
        if ($amount > $this->balance) {
            throw new Exception('Insufficient funds');
        }
        $this->balance -= $amount;
    }
}

$account = new BankAccount(1000);
echo $account->balance; // 1000 - readable
$account->balance = 500; // Error: cannot write from outside

Real-World Scenario: API Response Objects

class ApiResponse {
    // Write once during construction, read freely
    public private(set) int $statusCode;
    public private(set) array $data;
    public private(set) ?string $error = null;

    public function __construct(int $statusCode, array $data = [], ?string $error = null) {
        $this->statusCode = $statusCode;
        $this->data = $data;
        $this->error = $error;
    }

    public function isSuccessful(): bool {
        return $this->statusCode >= 200 && $this->statusCode < 300;
    }
}

// Client code
$response = new ApiResponse(200, ['user_id' => 123]);
echo $response->statusCode; // 200
echo json_encode($response->data); // {"user_id":123}
$response->statusCode = 500; // Error: cannot modify

This pattern is invaluable for immutable value objects, config containers, and API responses where you want to expose state but prevent accidental mutation.

Migration Guide: Adopting PHP 8.4

Step 1: Update Your Runtime

Download PHP 8.4 from php.net. Update your Dockerfile, VM, or hosting platform:

FROM php:8.4-fpm

RUN docker-php-ext-install pdo_mysql opcache
COPY php.ini /usr/local/etc/php/

Step 2: Run Your Test Suite

PHP 8.4 has removed deprecated features from 8.3:

  • call_user_func() with class method strings: Use array syntax [$class, $method] instead
  • call_user_func_array() with class methods: Same fix applies
  • DOMDocument::load() with invalid HTML: Now throws ValueError

Run your existing tests without modifications:

composer test

If failures occur, they’ll point to deprecated usage. Address them methodically.

Step 3: Refactor Getters/Setters (Optional)

Not urgent, but property hooks make new code cleaner. Prioritize:

  1. Value objects: Email, Price, UserId
  2. Config/State containers: Response objects, configs
  3. ORM entities: If your framework supports it
// Before (PHP 8.3)
class Price {
    private float $amount;

    public function getAmount(): float { return $this->amount; }
    public function setAmount(float $amount): void {
        if ($amount < 0) throw new Exception('Price must be positive');
        $this->amount = $amount;
    }
}

// After (PHP 8.4)
class Price {
    public float $amount {
        set(float $value) {
            if ($value < 0) throw new Exception('Price must be positive');
            $this->amountValue = $value;
        }
        get => $this->amountValue;
    }
    private float $amountValue;
}

Step 4: Explore Fibers for Async Operations

If your application handles many concurrent I/O operations, Fiber-aware libraries unlock performance gains. Start with:

  • Amphp: Full-featured async runtime with HTTP, DNS, file operations
  • Revolt: Modern event loop library
  • ReactPHP: Mature async framework

Example migration for concurrent database queries:

use Amphp\Sql\Pool;
use Amphp\Mysql\ConnectionConfig;

$config = ConnectionConfig::fromString('mysql://user:pass@localhost/db');
$pool = Pool::create($config);

// Concurrent queries via Fibers
$users = $pool->query('SELECT * FROM users');
$posts = $pool->query('SELECT * FROM posts');

// Both execute concurrently, results available after shortest completes

Common Pitfalls and Solutions

Pitfall 1: Mixing Fiber Suspensions with Output Buffering

Problem: If a Fiber suspends while output buffering is active, subsequent code may see unexpected buffer states.

Solution: Ensure output buffers are flushed before suspension, or use Fiber-safe logging:

// Avoid
ob_start();
echo "Starting operation";
Fiber::suspend();
echo "Resuming";
ob_end_flush();

// Better
logger()->info('Starting operation');
Fiber::suspend();
logger()->info('Resuming');

Pitfall 2: Property Hooks and Serialization

Problem: Properties with hooks may not serialize as expected if the backing field is private.

Solution: Use __serialize() and __unserialize() methods:

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

    public function __serialize(): array {
        return ['email' => $this->emailValue]; // Return backing field
    }

    public function __unserialize(array $data): void {
        $this->emailValue = $data['email'];
    }
}

Pitfall 3: Asymmetric Visibility and Inheritance

Problem: Child classes cannot override visibility modifiers incompatibly.

Solution: Define clear contracts in parent classes:

class Base {
    public private(set) string $id; // Locked: child cannot change
}

class Child extends Base {
    // public public $id; // Error: cannot make less restrictive
}

Debugging Tools for PHP 8.4

For complex Fiber logic, consider:

  • Xdebug 3.3+: Supports Fiber debugging
  • Blackfire: Profiling concurrent code
  • Web request tools: Use Webhook Tester to inspect async request handlers
  • API testing: API Request Builder helps test endpoints powered by Fiber-based frameworks

Why It Matters

These features address long-standing PHP limitations:

  1. Fibers enable high-concurrency applications without external job queues or forking overhead.
  2. Property hooks reduce boilerplate and make object intent explicit, improving maintainability.
  3. Asymmetric visibility enforces encapsulation at the language level, preventing subtle state mutation bugs.

For teams building APIs, microservices, or data-intensive applications, PHP 8.4 represents a maturation of the language toward production-grade concurrency and design patterns.

Getting Started: A Minimal Example

Create a simple Fiber-based task runner:

<?php

use Fiber;

class TaskRunner {
    private array $tasks = [];

    public function addTask(callable $callback): void {
        $this->tasks[] = new Fiber($callback);
    }

    public function run(): void {
        foreach ($this->tasks as $task) {
            $task->start();
        }
    }
}

$runner = new TaskRunner();

$runner->addTask(function() {
    echo "Task 1 executing\n";
});

$runner->addTask(function() {
    echo "Task 2 executing\n";
});

$runner->run();
// Output:
// Task 1 executing
// Task 2 executing

Deploy to production by updating your Docker image, running tests, and gradually adopting new patterns. PHP 8.4 maintains backward compatibility, so migration is incremental.

Recommended Reading

This post was generated with AI assistance and reviewed for accuracy.