PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.4 premiera – property hooks w prod, asymmetric visibility, BcMath\Number, array_find

by Henryk Tews / wtorek, 26 listopada 2024 / Opublikowano w PHP

PHP 8.4 wyszło oficjalnie 21 listopada 2024. Pisałem o RC w lipcu i wrześniu – teraz mam kilka dni z finalną wersją i pierwsze projekty które uruchamiam na 8.4. Property hooks, asymmetric visibility, Lazy Objects, BcMath\Number – wszystko wylądowało dokładnie jak RFC obiecywało. Czas na uczciwy bilans: co od razu wchodzi do kodu, co zaskakuje i jak gładka jest migracja z 8.3.

Property Hooks w produkcji – co poszło inaczej niż planowałem

<?php

declare(strict_types=1);

// Zakładałem że przepiszę wszystkie Value Objects na property hooks.
// W praktyce - tylko nowy kod. Migracja istniejącego to za duże ryzyko regresu.

// Nowy kod - przepisałem DTO importu produktów
readonly class ProductImportRow
{
    public string $sku {
        set(string $v) {
            if (empty(trim($v))) {
                throw new \InvalidArgumentException('SKU cannot be empty');
            }
            $this->sku = strtoupper(trim($v));
        }
    }

    public float $price {
        set(float $v) {
            if ($v < 0) {
                throw new \InvalidArgumentException("Price cannot be negative: {$v}");
            }
            $this->price = round($v, 2);
        }
    }

    public string $status {
        get => match($this->rawStatus) {
            '1', 'active', 'enabled' => 'active',
            '0', 'inactive', 'disabled' => 'inactive',
            default => 'inactive',
        };
    }

    // Niespodzianki: readonly class + set hook = błąd!
    // Musiałem zmienić na zwykłą klasę z private(set)
    // dla właściwości które mają set hook

    public function __construct(
        string $sku,
        float $price,
        private readonly string $rawStatus = '1',
        public readonly string $name = '',
        public readonly int $qty = 0
    ) {
        $this->sku   = $sku;   // wywołuje set hook
        $this->price = $price; // wywołuje set hook
    }
}

$row = new ProductImportRow('  sku-001  ', 29.999, '1', 'Widget Pro', 50);
echo $row->sku;    // SKU-001 (trimmed, uppercased)
echo $row->price;  // 30.0 (rounded)
echo $row->status; // active (computed)

Asymmetric Visibility – najczęściej używam w ten sposób

<?php

declare(strict_types=1);

// Wzorzec który wszedł do każdej nowej klasy agregatu:
// publiczny odczyt dla wszystkich, zapis tylko wewnątrz

class ImportBatch
{
    public private(set) int $processed = 0;
    public private(set) int $failed = 0;
    public private(set) int $skipped = 0;
    public private(set) \DateTimeImmutable $startedAt;
    public private(set) ?\DateTimeImmutable $finishedAt = null;
    public private(set) string $status = 'pending';

    public function __construct(
        public readonly string $batchId,
        public readonly int $totalItems
    ) {
        $this->startedAt = new \DateTimeImmutable();
        $this->status    = 'running';
    }

    public function recordSuccess(): void
    {
        $this->processed++;
        $this->checkCompletion();
    }

    public function recordFailure(string $reason): void
    {
        $this->failed++;
        $this->checkCompletion();
    }

    public function recordSkip(): void
    {
        $this->skipped++;
        $this->checkCompletion();
    }

    private function checkCompletion(): void
    {
        $done = $this->processed + $this->failed + $this->skipped;
        if ($done >= $this->totalItems) {
            $this->finishedAt = new \DateTimeImmutable();
            $this->status     = $this->failed > 0 ? 'completed_with_errors' : 'completed';
        }
    }

    public function progressPercent(): float
    {
        if ($this->totalItems === 0) return 100.0;
        return round(($this->processed + $this->failed + $this->skipped) / $this->totalItems * 100, 1);
    }
}

// Użycie w serwisie importu
$batch = new ImportBatch('import-' . uniqid(), count($products));

foreach ($products as $product) {
    try {
        $this->importProduct($product);
        $batch->recordSuccess();
    } catch (SkipException) {
        $batch->recordSkip();
    } catch (\Exception $e) {
        $batch->recordFailure($e->getMessage());
    }
}

// Tylko odczyt z zewnątrz - settery niemożliwe
echo "{$batch->processed}/{$batch->totalItems} ({$batch->progressPercent()}%)\n";
$batch->processed = 999; // Error: Cannot modify private(set) from outside

BcMath\Number – nareszcie finansowa arytmetyka bez bólu

<?php

declare(strict_types=1);

use BcMath\Number;

// Stary kod w każdym module płatności i cen
class OldPriceCalculator
{
    public function calculateTotal(array $items, float $taxRate, ?float $discount): string
    {
        $subtotal = '0';
        foreach ($items as $item) {
            $subtotal = bcadd($subtotal, bcmul((string) $item['price'], (string) $item['qty'], 4), 4);
        }

        if ($discount) {
            $subtotal = bcsub($subtotal, (string) $discount, 4);
        }

        $tax   = bcmul($subtotal, (string) $taxRate, 4);
        $total = bcadd($subtotal, $tax, 4);

        return bcround($total, 2);
    }
}

// PHP 8.4 - BcMath\Number
class NewPriceCalculator
{
    public function calculateTotal(array $items, Number $taxRate, ?Number $discount): Number
    {
        $subtotal = new Number('0');

        foreach ($items as $item) {
            $subtotal += new Number((string) $item['price']) * new Number((string) $item['qty']);
        }

        if ($discount !== null) {
            $subtotal -= $discount;
        }

        $tax = $subtotal * $taxRate;
        return ($subtotal + $tax)->round(2);
    }
}

$calc = new NewPriceCalculator();

$items    = [['price' => '29.99', 'qty' => 2], ['price' => '9.99', 'qty' => 1]];
$taxRate  = new Number('0.23');
$discount = new Number('5.00');

$total = $calc->calculateTotal($items, $taxRate, $discount);
echo $total; // (29.99*2 + 9.99 - 5.00) * 1.23 = 85.25...

// BcMath\Number vs float - klasyczny przykład
$f1 = 0.1 + 0.2;
echo $f1; // 0.30000000000000004 - błąd float

$n1 = new Number('0.1') + new Number('0.2');
echo $n1; // 0.3 - dokładne

// Porównania
var_dump(new Number('29.99') > new Number('29.98')); // true
var_dump(new Number('10.00') === new Number('10'));   // false (różne typy)
var_dump(new Number('10.00') == new Number('10'));    // true (wartość)

Nowe array_find/all/any – w końcu dostępne

<?php

declare(strict_types=1);

// Te funkcje były dyskutowane od PHP 8.3 - trafiły do 8.4
// Dzisiaj zastąpiłem je w 3 miejscach i kod stał się czytelniejszy

$orders = [
    ['id' => 1, 'status' => 'pending',    'total' => 150.0],
    ['id' => 2, 'status' => 'processing', 'total' => 320.0],
    ['id' => 3, 'status' => 'shipped',    'total' => 89.50],
    ['id' => 4, 'status' => 'cancelled',  'total' => 45.0],
];

// Przed
$firstHighValue = null;
foreach ($orders as $order) {
    if ($order['total'] > 200) {
        $firstHighValue = $order;
        break;
    }
}

// PHP 8.4
$firstHighValue = array_find($orders, fn($o) => $o['total'] > 200);
// ['id' => 2, 'status' => 'processing', 'total' => 320.0]

// Walidacja czy wszystkie w dobrym stanie
$allActive = array_all(
    $orders,
    fn($o) => !in_array($o['status'], ['cancelled', 'refunded'], true)
);
var_dump($allActive); // false - jest cancelled

// Sprawdź czy jest jakiekolwiek duże zamówienie
$hasHighValue = array_any($orders, fn($o) => $o['total'] > 300);
var_dump($hasHighValue); // true

// Znajdź klucz zamiast wartości
$cancelledKey = array_find_key($orders, fn($o) => $o['status'] === 'cancelled');
echo $cancelledKey; // 3

Checklist migracji na PHP 8.4

# 1. Sprawdź deprecacje które stają się błędami w 8.4
# Najważniejsze: implicit nullable parameters
# function foo(string $x = null) {} // ERROR w 8.4 - zmień na ?string

# 2. PHPStan z 8.4
vendor/bin/phpstan analyse --level=8 --php-version=8.4 src/

# 3. Rector - automatyczna naprawa
vendor/bin/rector process src --php-version=8.4

# 4. Sprawdź zewnętrzne zależności
composer outdated --direct

# 5. Uruchom testy na PHP 8.4 RC zanim upgrade produkcji
docker run --rm -v $(pwd):/app php:8.4-cli \
    php /app/vendor/bin/phpunit

# 6. Implicit nullable - najczęstszy problem
# Stary: function getData(string $filter = null): array
# Nowy:  function getData(?string $filter = null): array
grep -r "function.*= null" src/ | grep -v "?.*=" # znajdź problematyczne przypadki

Magento 2 i PHP 8.4 – czy już?

Magento 2.4.7 oficjalnie wspiera PHP 8.3. PHP 8.4 dla Magento będzie prawdopodobnie w 2.4.8 (2025). Oznacza to:

  • Własny kod modułów można pisać pod PHP 8.4 już teraz (property hooks w custom klasy domenowe)
  • Platforma (rdzeń Magento + zewnętrzne moduły) wymaga 8.3 do oficjalnego wsparcia
  • Możesz uruchomić Magento 2.4.7 na PHP 8.4 – najczęściej działa, ale bez gwarancji
  • Dla bezpieczeństwa na produkcji: zostań na 8.3 z Magento do czasu oficjalnego wsparcia 8.4

Podsumowanie

PHP 8.4 to najlepsze wydanie PHP w historii jeśli patrzeć na gęstość wartościowych zmian. Property hooks i asymmetric visibility wchodzą do nowego kodu natychmiast. BcMath\Number to koniec ery bcadd/bcmul chainingu przy obliczeniach finansowych. Lazy Objects uzupełniają DI toolkit bez potrzeby generowania klas Proxy. Migracja z 8.3 jest gładka – główna pułapka to implicit nullable parameters które teraz są twardym błędem.

About Henryk Tews

Co możesz przeczytać następne

PHP 8.3 po premierze – typed constants, json_validate(), clone with w praktyce
PHP 7.4 w praktyce – pułapki typed properties, hydratacja, zapowiedź PHP 8.0
PHP 8.0 – premiera i pierwsze wrażenia z nowych funkcji
  • 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}