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.
