PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.1 in practice – enums after a month, database storage, Money value object

by Henryk Tews / Tuesday, 08 March 2022 / Published in PHP

PHP 8.1 was released in November 2021. After a month of migrating modules and writing new code with the new features I have a clear view of what matters in practice. Enums are the biggest quality-of-life improvement – I show how I store them in the database, use them in match expressions, and build a Money value object that shows readonly properties and enums working together.

Enums in practice – database storage

<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending    = 'pending';
    case Processing = 'processing';
    case Complete   = 'complete';
    case Cancelled  = 'cancelled';

    public function label(): string
    {
        return match($this) {
            self::Pending    => 'Awaiting payment',
            self::Processing => 'In progress',
            self::Complete   => 'Completed',
            self::Cancelled  => 'Cancelled',
        };
    }

    public function canTransitionTo(self $new): bool
    {
        return match($this) {
            self::Pending    => in_array($new, [self::Processing, self::Cancelled]),
            self::Processing => in_array($new, [self::Complete, self::Cancelled]),
            self::Complete,
            self::Cancelled  => false, // final states
        };
    }
}

// Storing in the database - use ->value
$statusValue = OrderStatus::Processing->value; // 'processing'

// Reading from the database
$fromDb = OrderStatus::from($row['status']);    // throws on invalid value
$safe   = OrderStatus::tryFrom($row['status']); // returns null on invalid

// In Magento ResourceModel
class OrderStatusMapper
{
    public function toDb(OrderStatus $status): string
    {
        return $status->value;
    }

    public function fromDb(string $value): OrderStatus
    {
        return OrderStatus::tryFrom($value)
            ?? throw new \UnexpectedValueException("Invalid status: {$value}");
    }
}

Enum in source model for admin dropdowns

<?php

// Magento source model built from enum - zero duplication
class OrderStatusSource implements \Magento\Framework\Data\OptionSourceInterface
{
    public function toOptionArray(): array
    {
        return array_map(
            fn(OrderStatus $s) => ['value' => $s->value, 'label' => $s->label()],
            OrderStatus::cases()
        );
    }
}
// Result: [['value' => 'pending', 'label' => 'Awaiting payment'], ...]

Money value object – readonly + enum

<?php

declare(strict_types=1);

enum Currency: string
{
    case PLN = 'PLN';
    case EUR = 'EUR';
    case USD = 'USD';
    case GBP = 'GBP';

    public function symbol(): string
    {
        return match($this) {
            self::PLN => 'zł',
            self::EUR => '€',
            self::USD => '$',
            self::GBP => '£',
        };
    }
}

final class Money
{
    // readonly: assigned once in constructor, immutable thereafter
    public function __construct(
        public readonly int $amount,         // in smallest unit (pence, grosz)
        public readonly Currency $currency,
    ) {
        if ($amount < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative');
        }
    }

    public function add(self $other): self
    {
        $this->assertSameCurrency($other);
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function subtract(self $other): self
    {
        $this->assertSameCurrency($other);
        if ($other->amount > $this->amount) {
            throw new \InvalidArgumentException('Result would be negative');
        }
        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 applyTax(float $rate): self
    {
        return $this->multiply(1 + $rate);
    }

    public function applyDiscount(float $percent): self
    {
        return $this->multiply(1 - ($percent / 100));
    }

    public function format(): string
    {
        return number_format($this->amount / 100, 2, ',', ' ')
            . ' ' . $this->currency->symbol();
    }

    public function equals(self $other): bool
    {
        return $this->amount === $other->amount
            && $this->currency === $other->currency;
    }

    private function assertSameCurrency(self $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException(
                "Currency mismatch: {$this->currency->value} vs {$other->currency->value}"
            );
        }
    }
}

// Usage
$price    = new Money(9999, Currency::PLN);    // 99.99 PLN
$shipping = new Money(1499, Currency::PLN);    // 14.99 PLN
$total    = $price->add($shipping);            // 114.98 PLN
$withTax  = $total->applyTax(0.23);            // 141.43 PLN
$final    = $withTax->applyDiscount(10);       // 127.29 PLN (10% off)

echo $final->format(); // 127,29 zł

Readonly on existing PHP 7.x patterns

<?php

// Before PHP 8.1 - private property + getter
class ProductId
{
    private int $value;

    public function __construct(int $value)
    {
        if ($value <= 0) throw new \InvalidArgumentException('ID must be positive');
        $this->value = $value;
    }

    public function getValue(): int { return $this->value; }
    public function equals(self $other): bool { return $this->value === $other->value; }
}

// PHP 8.1 - public readonly eliminates the getter
class ProductId
{
    public readonly int $value;

    public function __construct(int $value)
    {
        if ($value <= 0) throw new \InvalidArgumentException('ID must be positive');
        $this->value = $value;
    }

    public function equals(self $other): bool { return $this->value === $other->value; }
}

$id = new ProductId(42);
echo $id->value;         // 42 - direct access, no getter needed
// $id->value = 99;     // Error: Cannot modify readonly property

Summary

PHP 8.1 after a month in production proves its worth daily. Enums eliminate the “class full of constants” antipattern and bring type safety to state machines. Readonly properties with constructor promotion reduce value object boilerplate to almost nothing. The Money + Currency example shows how these two features reinforce each other – an immutable, type-safe financial value object in about 50 lines.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
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 Always active
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.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}