PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP 8.3 po premierze – typed constants, json_validate(), clone with w praktyce

by Henryk Tews / wtorek, 28 listopada 2023 / Opublikowano w PHP

PHP 8.3 wyszło oficjalnie 23 listopada 2023. W maju pisałem o zapowiedziach – teraz mam kilka dni na sprawdzenie finalnej wersji. Nie ma tu rewolucji jak w PHP 8.0 czy enumów z 8.1, ale kilka zmian wchodzi do codziennego kodu od razu. Typed constants i json_validate() to te które stosuję natychmiast.

Typed Class Constants – pierwsze wrażenia

Typed constants weszły do kodu dokładnie tak jak spodziewałem się po RFC. Największy zysk to natychmiastowe wykrycie literówek i błędów typów przy stałych:

<?php

declare(strict_types=1);

// Przed PHP 8.3 - chaos możliwy
interface PaymentStatusInterface
{
    const PENDING   = 'pending';
    const PAID      = 1;         // int zamiast string - brak błędu
    const FAILED    = false;     // bool - kompletna dowolność
    const REFUNDED  = null;      // null - PHP milczy
}

// PHP 8.3 - typy wymuszają spójność
interface PaymentStatusInterface
{
    const string PENDING   = 'pending';
    const string PAID      = 'paid';
    const string FAILED    = 'failed';
    const string REFUNDED  = 'refunded';
    const string CANCELLED = 'cancelled';
}

// PHPStan i IDE od razu ostrzegają jeśli próbujesz
// przypisać int tam gdzie oczekiwany string

// Działa z wszystkimi typami PHP - skalarnymi i klasami
class HttpStatus
{
    const int OK                    = 200;
    const int CREATED               = 201;
    const int BAD_REQUEST           = 400;
    const int UNAUTHORIZED          = 401;
    const int NOT_FOUND             = 404;
    const int INTERNAL_SERVER_ERROR = 500;
}

class Config
{
    const array ALLOWED_CURRENCIES = ['PLN', 'EUR', 'USD', 'GBP'];
    const float DEFAULT_TAX_RATE   = 0.23;
    const bool  DEBUG_MODE         = false;
}

// Typed constants w interfejsach dziedziczą po sobie
interface BaseConfigInterface
{
    const string VERSION = '1.0.0';
}

interface ExtendedConfigInterface extends BaseConfigInterface
{
    const string VERSION = '2.0.0'; // nadpisanie - ok, typ się zgadza
    // const int VERSION = 2; // Error: type mismatch
}

json_validate() – oszczędność pamięci przy webhookach

Zacząłem używać json_validate() przy walidacji payloadów webhooków zanim je przetwarzam. Różnica pamięciowa jest mierzalna przy dużych payloadach:

<?php

declare(strict_types=1);

// Middleware do walidacji webhooków
class WebhookMiddleware
{
    public function handle(string $rawBody, string $signature): array
    {
        // Najpierw sprawdź sygnaturę (tanie - tylko HMAC)
        if (!$this->verifySignature($rawBody, $signature)) {
            throw new \RuntimeException('Invalid webhook signature', 401);
        }

        // Sprawdź poprawność JSON PRZED dekodowaniem (tanie - bez alokacji)
        if (!json_validate($rawBody)) {
            throw new \InvalidArgumentException('Invalid JSON payload', 400);
        }

        // Teraz bezpiecznie dekoduj (wiemy że JSON jest poprawny)
        return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
    }

    private function verifySignature(string $payload, string $signature): bool
    {
        $expected = hash_hmac('sha256', $payload, config('webhook.secret'));
        return hash_equals($expected, $signature);
    }
}

// Benchmark dla 1000 walidacji 100KB JSON
$largePayload = json_encode(array_fill(0, 5000, [
    'id'    => random_int(1, 99999),
    'sku'   => 'SKU-' . random_int(1000, 9999),
    'price' => round(mt_rand(100, 100000) / 100, 2),
    'name'  => 'Produkt testowy ' . random_int(1, 999),
]));

// json_validate - tylko walidacja, bez alokacji tablicy wyniku
$start = microtime(true);
$memBefore = memory_get_usage();
for ($i = 0; $i < 1000; $i++) {
    json_validate($largePayload);
}
echo sprintf(
    "json_validate: %.2fms, delta RAM: %s\n",
    (microtime(true) - $start) * 1000,
    number_format(memory_get_usage() - $memBefore)
);

// json_decode - walidacja + alokacja tablicy (natychmiast zwolniona przez GC)
$start = microtime(true);
$memBefore = memory_get_usage();
for ($i = 0; $i < 1000; $i++) {
    $data = json_decode($largePayload, true);
    json_last_error() === JSON_ERROR_NONE;
    unset($data);
}
echo sprintf(
    "json_decode:   %.2fms, delta RAM: %s\n",
    (microtime(true) - $start) * 1000,
    number_format(memory_get_usage() - $memBefore)
);
// json_validate ~35% szybszy, zużywa negligible RAM

Clone with – czyste value objects

Clone with weszło do produkcyjnego kodu przy pierwszej okazji. Eliminuje metody withX() w value objects bez żadnych kompromisów:

<?php

declare(strict_types=1);

readonly class ProductPrice
{
    public function __construct(
        public float $amount,
        public string $currency,
        public float $taxRate,
        public bool $includesTax
    ) {}

    public function gross(): float
    {
        return $this->includesTax
            ? $this->amount
            : $this->amount * (1 + $this->taxRate);
    }

    public function net(): float
    {
        return $this->includesTax
            ? $this->amount / (1 + $this->taxRate)
            : $this->amount;
    }
}

$basePrice = new ProductPrice(100.0, 'PLN', 0.23, false);

// PHP 8.3 - clone with zastępuje withCurrency(), withAmount() itp.
$eurPrice   = clone $basePrice with { currency: 'EUR', amount: 23.50 };
$grossBased = clone $basePrice with { includesTax: true, amount: 123.0 };

echo $basePrice->gross();  // 123.0
echo $eurPrice->amount;    // 23.50
echo $eurPrice->currency;  // EUR

// Szczególnie użyteczne przy transformacji DTO w pipeline przetwarzania
readonly class ImportRow
{
    public function __construct(
        public string $sku,
        public string $name,
        public float $price,
        public ?string $error = null,
        public bool $processed = false
    ) {}
}

function validateRow(ImportRow $row): ImportRow
{
    if (empty($row->sku)) {
        return clone $row with { error: 'SKU is required' };
    }

    if ($row->price < 0) {
        return clone $row with { error: 'Price cannot be negative' };
    }

    return clone $row with { processed: true };
}

$rows = [
    new ImportRow('', 'Produkt bez SKU', 9.99),
    new ImportRow('SKU-001', 'Poprawny produkt', 29.99),
    new ImportRow('SKU-002', 'Ujemna cena', -5.0),
];

$processed = array_map('validateRow', $rows);
// Każdy row jest immutable - validateRow tworzy nową kopię z modyfikacją

array_find() i array_all() / array_any() w praktyce

<?php

declare(strict_types=1);

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

// array_find - pierwszy pasujący element
$firstPending = array_find(
    $orders,
    fn(array $order): bool => $order['status'] === 'pending'
);
echo $firstPending['id']; // 1

// array_find_key - klucz zamiast wartości
$cancelledKey = array_find_key(
    $orders,
    fn(array $order): bool => $order['status'] === 'cancelled'
);
echo $cancelledKey; // 3

// array_any - czy jakikolwiek element spełnia warunek
$hasHighValue = array_any(
    $orders,
    fn(array $order): bool => $order['total'] > 300.0
);
var_dump($hasHighValue); // true

// array_all - czy wszystkie elementy spełniają warunek
$allProcessed = array_all(
    $orders,
    fn(array $order): bool => in_array($order['status'], ['shipped', 'delivered', 'cancelled'], true)
);
var_dump($allProcessed); // false (pending i processing nie pasują)

// Praktyczny przykład - walidacja zamówień przed zbiorczym przetworzeniem
function canBulkProcess(array $orders): bool
{
    return array_all(
        $orders,
        fn($order) => $order['status'] === 'pending' && $order['total'] > 0
    );
}

Deprecacje które warto znać

<?php

// 1. Statyczne wywołanie metody niestatycznej na instancji
class Formatter
{
    public function format(string $value): string
    {
        return strtoupper($value);
    }
}

$f = new Formatter();
$f::format('test'); // PHP 8.3: Deprecated - użyj $f->format('test')

// 2. Negative zero w round()
round(-0.5, 0); // -0.0 w PHP 8.3, zachowanie normalizowane

// 3. range() z niekompatybilnymi typami
range(0, 5.0);   // PHP 8.3: Deprecated - mieszanie int i float
range(0.0, 5.0); // ok
range(0, 5);     // ok

// Sprawdź projekt szybkim PHPStan run:
// vendor/bin/phpstan analyse --level=8 src/

Podsumowanie

PHP 8.3 to solidna aktualizacja bez przełomowych nowości. Typed constants i json_validate() wchodzą do kodu natychmiast – pierwsze przy porządkowaniu klas ze stałymi konfiguracyjnymi, drugie przy obsłudze webhooków i zewnętrznych API. Clone with eliminuje boilerplate w value objects. Migracja z PHP 8.2 jest bezbolesna jeśli projekt ma PHPStan na poziomie 6+ – wszystkie deprecacje zostaną wyłapane przed runtime.

About Henryk Tews

Co możesz przeczytać następne

PHPUnit – testy jednostkowe, mocki, data providers, testy w Magento 2
GraphQL Federation – subgrafy, gateway, Apollo Router, integracja z Magento 2
AI workflow 2026 – 2 lata później, co działa, co nie, Claude 200k w praktyce
  • 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}