PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.1 w praktyce – enumy po miesiącach, serializacja do bazy, Money value object

by Henryk Tews / wtorek, 08 marca 2022 / Opublikowano w PHP

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.

About Henryk Tews

Co możesz przeczytać następne

REST API – zasoby vs akcje, kody HTTP, struktura odpowiedzi, wersjonowanie
PHP 8.3 po premierze – typed constants, json_validate(), clone with w praktyce
Laravel vs Symfony – DI, Doctrine vs Eloquent, kiedy który framework
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}