PHP 8.4 wychodzi w listopadzie 2024, ale RC1 jest dostępne już teraz. Przez kilka tygodni testowałem property hooks i asymmetric visibility na realnych projektach – data transfer objects, value objects, moduły Magento. Czas na uczciwy raport: co weszło do kodu od razu, co wymaga ostrożności i gdzie są niespodzianki.
Property Hooks – pierwsze wrażenia po RC1
<?php
declare(strict_types=1);
// Przed PHP 8.4 – ~40 linii boilerplate
class Email
{
private string $value;
public function __construct(string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email: {$email}");
}
$this->value = mb_strtolower(trim($email));
}
public function getValue(): string { return $this->value; }
public function getDomain(): string { return explode('@', $this->value)[1]; }
}
// PHP 8.4 – ta sama funkcjonalność, ~15 linii
class Email
{
public string $value {
set(string $email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email: {$email}");
}
$this->value = mb_strtolower(trim($email));
}
}
// Computed property - tylko get, bez set
public string $domain {
get => explode('@', $this->value)[1];
}
public function __construct(string $email)
{
$this->value = $email; // wywołuje hook set
}
}
$email = new Email(' JAN@EXAMPLE.COM ');
echo $email->value; // jan@example.com
echo $email->domain; // example.com
$email->domain = 'test'; // Error: Cannot set hook-only property
Niespodzianki w RC1
<?php
declare(strict_types=1);
// 1. Operator ++ i -- działają przez get+set - bez niespodzianek
class Counter
{
public int $count = 0 {
set(int $v) {
if ($v < 0) throw new \RangeException('Cannot be negative');
$this->count = $v;
}
}
}
$c = new Counter();
$c->count++; // wywołuje get (0) + set (1) - działa
$c->count += 5; // get (1) + set (6) - działa
$c->count = -1; // RangeException - działa
// 2. readonly class + property hooks = błąd kompilacji
// readonly class nie może mieć set hook - sprzeczność
// readonly class Foo { public string $bar { set => ... } } // Fatal Error
// 3. serialize() i var_export() - zachowują się jak bez hooków
// Blackfire i Xdebug wyświetlają wartości właściwości poprawnie
// 4. Refleksja - ReflectionProperty::hasHook() nowe API
$rf = new \ReflectionProperty(Email::class, 'value');
var_dump($rf->hasHook(\PropertyHookType::Set)); // true
var_dump($rf->hasHook(\PropertyHookType::Get)); // false
// 5. Interface hooks - interfejsy mogą deklarować wymagane hooki
interface HasPrice
{
public float $price { get; set; }
}
// Implementacja MUSI zapewnić get i set dla $price
class Product implements HasPrice
{
public float $price {
get => $this->price;
set(float $v) {
$this->price = max(0.0, round($v, 2));
}
}
}
Asymmetric Visibility w praktyce
<?php
declare(strict_types=1);
// Wzorzec który najczęściej stosowałem: domain aggregate z publicznym odczytem
class ShoppingCart
{
public private(set) float $subtotal = 0.0;
public private(set) int $itemCount = 0;
public private(set) array $items = [];
public private(set) bool $locked = false;
public function __construct(
public readonly string $cartId
) {}
public function addItem(string $sku, float $price, int $qty): void
{
if ($this->locked) {
throw new \LogicException('Cart is locked for checkout');
}
$this->items[] = compact('sku', 'price', 'qty');
$this->subtotal += $price * $qty;
$this->itemCount += $qty;
}
public function lock(): void
{
$this->locked = true; // ok - wewnątrz klasy
}
}
$cart = new ShoppingCart('cart-abc123');
$cart->addItem('SKU-001', 29.99, 2);
$cart->addItem('SKU-002', 9.99, 1);
echo $cart->subtotal; // 69.97 - public odczyt ok
echo $cart->itemCount; // 3 - public odczyt ok
$cart->subtotal = 0; // Error: Cannot modify private(set) from outside
$cart->locked = true; // Error: Cannot modify private(set) from outside
Praktyczne porównanie: property hooks vs asymmetric visibility
| Chcę… | Użyj | Przykład |
|---|---|---|
| Walidować wartość przy zapisie | set hook | Email, Price, Quantity |
| Obliczać wartość dynamicznie | get hook | $order->total, $url->domain |
| Publiczny odczyt, prywatny zapis | public private(set) | $order->status, $cart->total |
| Wartość niezmienialną po init | readonly | $order->id, $user->createdAt |
| Walidację + prywatny zapis | private(set) + set hook | Złożone domain objects |
Chaining new – drobna ale wygodna zmiana
<?php
// Przed PHP 8.4 - nawiasy wymagane przy chainingu po new
$result = (new QueryBuilder())
->from('orders')
->where('status', '=', 'pending')
->build();
// PHP 8.4 - nawiasy przy new opcjonalne
$result = new QueryBuilder()
->from('orders')
->where('status', '=', 'pending')
->build();
// Szczególnie czytelne przy prostych obiektach
$email = new Email('jan@example.com')->normalized();
$request = new HttpRequest('GET', '/api/products')->withHeader('Accept', 'application/json');
Kiedy migrować?
# Sprawdź kompatybilność projektu z PHP 8.4 już teraz na RC
docker run --rm -v $(pwd):/app php:8.4-rc-cli \
php -l /app/src/Model/Order.php
# PHPStan z PHP 8.4
vendor/bin/phpstan analyse \
--level=8 \
--php-version=8.4 \
src/
# Deprecacje usunięte w PHP 8.4 na które warto uważać:
# - implicitly nullable parameters (function f(string $x = null) -> wymaga ?string)
# - GMP i BCMath zmiany API
Podsumowanie
PHP 8.4 RC1 potwierdza że property hooks i asymmetric visibility są dopracowane. Property hooks natychmiast wchodzą do nowych Value Objects – eliminują boilerplate getterów/setterów przy zachowaniu enkapsulacji. Asymmetric visibility rozwiązuje realne ograniczenie readonly w klasach domenowych. Warto przetestować migrację na RC żeby wykryć problemy zanim przyjdzie final release w listopadzie.
