PHP 8.1 wyszło w grudniu 2021. Mam za sobą kilka miesięcy używania enumeracji, readonly properties i intersection types w projektach produkcyjnych. Czas na uczciwe podsumowanie: co weszło do kodu naturalnie, co zaskakuje i gdzie są granice tych nowości w kontekście Magento 2 i starszych baz kodu.
Enumy – gdzie weszły od razu
Najszybciej enumy zastąpiły klasy ze stałymi reprezentującymi skończony zestaw wartości. To był bolesny anty-wzorzec który wszyscy znamy:
<?php
// Stary sposób - brak gwarancji typów, IDE nie pomaga
class OrderStatus
{
const PENDING = 'pending';
const PROCESSING = 'processing';
const SHIPPED = 'shipped';
const CANCELLED = 'cancelled';
}
// Można przekazać dowolny string - brak walidacji w runtime
function updateStatus(int $orderId, string $status): void
{
// Co jeśli ktoś przekaże 'panding' przez literówkę?
}
updateStatus(42, OrderStatus::PENDING); // ok
updateStatus(42, 'panding'); // też "ok" - błąd ujawni się dopiero w bazie lub logice
<?php
// PHP 8.1 - enum gwarantuje poprawność w kompilacji i runtime
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
self::Pending => 'Oczekuje',
self::Processing => 'W realizacji',
self::Shipped => 'Wysłane',
self::Cancelled => 'Anulowane',
};
}
public function canTransitionTo(self $next): bool
{
return match($this) {
self::Pending => $next === self::Processing || $next === self::Cancelled,
self::Processing => $next === self::Shipped || $next === self::Cancelled,
self::Shipped => false,
self::Cancelled => false,
};
}
}
function updateStatus(int $orderId, OrderStatus $status): void
{
// Niepoprawna wartość jest niemożliwa - PHP odrzuci ją wcześniej
}
updateStatus(42, OrderStatus::Pending); // ok
updateStatus(42, 'panding'); // TypeError - IDE też podkreśli błąd
Enumy i baza danych – serializacja
Najczęstsze pytanie: jak przechowywać enumy w bazie? Backed enum przechowujesz jako wartość (->value), odczytujesz przez from() lub tryFrom():
<?php
declare(strict_types=1);
class OrderRepository
{
public function save(Order $order): void
{
$this->db->execute(
'UPDATE orders SET status = ? WHERE id = ?',
[
$order->getStatus()->value, // enum -> string dla bazy
$order->getId(),
]
);
}
public function getById(int $id): Order
{
$row = $this->db->fetchRow('SELECT * FROM orders WHERE id = ?', [$id]);
$order = new Order();
$order->setId((int) $row['id']);
// string z bazy -> enum, tryFrom() nie rzuca wyjątku gdy wartość nieznana
$status = OrderStatus::tryFrom($row['status']);
if ($status === null) {
throw new \RuntimeException("Unknown order status: {$row['status']}");
}
$order->setStatus($status);
return $order;
}
}
Enumy w Magento 2 – aktualna kompatybilność
Magento 2.4.4+ oficjalnie wspiera PHP 8.1, więc enumy możesz używać w własnych modułach. Kilka rzeczy na które uważać:
<?php
// Enum NIE może być wstrzyknięty przez DI jako argument konstruktora
// ObjectManager nie potrafi "stworzyć" enuma - enum to wartość, nie serwis
// BŁĘDNIE:
class MyService
{
public function __construct(
private OrderStatus $defaultStatus // DI nie wie jak to zainicjować!
) {}
}
// POPRAWNIE - przekaż wartość enuma przez argument w di.xml lub ustaw domyślną
class MyService
{
private OrderStatus $defaultStatus;
public function __construct()
{
$this->defaultStatus = OrderStatus::Pending;
}
}
// Lub przez factory/metodę
class MyService
{
public function process(Order $order, OrderStatus $targetStatus): void
{
if (!$order->getStatus()->canTransitionTo($targetStatus)) {
throw new \LogicException('Invalid status transition');
}
// ...
}
}
Readonly properties w praktyce – value objects
Readonly properties weszły naturalnie do wszystkich nowych value objects i DTO. Szczególnie w kombinacji z constructor promotion:
<?php
declare(strict_types=1);
// Immutable Money value object
final class Money
{
public function __construct(
public readonly int $amount, // w groszach - int, nie float (brak błędów zaokrąglenia)
public readonly string $currency
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Amount cannot be negative');
}
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException('Currency must be 3-letter ISO code');
}
}
public function add(self $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Cannot add different currencies');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function multiply(float $factor): self
{
return new self((int) round($this->amount * $factor), $this->currency);
}
public function format(): string
{
return number_format($this->amount / 100, 2, ',', ' ') . ' ' . $this->currency;
}
public function equals(self $other): bool
{
return $this->amount === $other->amount && $this->currency === $other->currency;
}
}
$price = new Money(2999, 'PLN'); // 29.99 PLN
$tax = $price->multiply(0.23); // podatek 23%
$total = $price->add($tax);
echo $total->format(); // 36,89 PLN
$price->amount = 1000; // Error: Cannot modify readonly property
Fibers – pierwsze realne zastosowania
Po kilku miesiącach Fibers znalazły realne zastosowanie głównie w bibliotekach asynchronicznych. W kontekście Magento 2 mogą być przydatne przy skryptach CLI które równolegle przetwarzają wiele requestów HTTP do zewnętrznych API:
<?php
// Uproszczony przykład równoległego pobierania danych przez Fibers
$fibers = [];
foreach ($productSkus as $sku) {
$fiber = new Fiber(function() use ($sku): array {
// Symulacja async HTTP - w praktyce użyj ReactPHP lub Amp
return $this->apiClient->fetchProduct($sku);
});
$fibers[$sku] = $fiber;
$fiber->start();
}
$results = [];
foreach ($fibers as $sku => $fiber) {
if ($fiber->isTerminated()) {
$results[$sku] = $fiber->getReturn();
}
}
W praktyce dla prawdziwej asynchroniczności w CLI użyj biblioteki Amp v3 lub ReactPHP które są zbudowane na Fibers i oferują dojrzałe API.
Co nie weszło tak płynnie
Intersection types są przydatne ale sporadycznie – trzeba trafić na sytuację gdzie naprawdę potrzebujesz wymagać dwóch interfejsów jednocześnie. W codziennym kodzie Magento 2 rzadko tak jest.
Enumy w Doctrine ORM (jeśli używasz poza Magento) wymagają dedykowanego typu przez EnumType – nie mapują się automatycznie. To dodatkowy krok który trzeba zaplanować przy projektowaniu.
Podsumowanie
PHP 8.1 po kilku miesiącach to przede wszystkim enumy i readonly properties używane na co dzień. Enumy wyeliminowały klasy ze stałymi i dodały walidację typów tam gdzie wcześniej był surowy string. Readonly properties upraszczają value objects – niezmienność jest teraz wymuszana przez język, nie przez konwencję. Fibers i intersection types to nowości z wąższym, bardziej specjalistycznym zastosowaniem.
