Что изменилось в PHP 8.4 и почему это важно
PHP 8.4 — не очередной минорный релиз с парой сахарных конструкций. Здесь три фундаментальных изменения, которые меняют подход к написанию кода: property hooks, asymmetric visibility и зрелая работа с Fibers. Плюс десяток мелочей, которые по отдельности незаметны, но в сумме убирают целые категории бойлерплейта.
Я работаю с PHP с первых коммерческих проектов — Laravel, чистый PHP, немного Symfony. Последние два года основной стек сместился в сторону Rails, но PHP-проекты никуда не делись: два крупных Laravel-монолита в продакшене, плюс микросервисы. Обновление до 8.4 прошло на обоих — и вот что реально пригодилось.
Property Hooks — прощайте, магические геттеры
Проблема, которую решает
До 8.4 у PHP было два способа контролировать доступ к свойствам: публичные поля (без контроля) и магические __get/__set (с контролем, но без автокомплита, типизации и вменяемого стектрейса).
// Старый подход: __get/__set
class User
{
private string $email;
public function __get(string $name): mixed
{
return match ($name) {
'email' => $this->email,
default => throw new \InvalidArgumentException("Unknown property: {$name}"),
};
}
public function __set(string $name, mixed $value): void
{
match ($name) {
'email' => $this->email = strtolower(trim($value)),
default => throw new \InvalidArgumentException("Unknown property: {$name}"),
};
}
}
Работает? Да. Но IDE не знает о свойстве email, PHPStan ругается, а стектрейс при ошибке указывает на __get, а не на конкретное свойство. В проекте с 200+ моделями это превращается в ад.
Новый синтаксис
class User
{
public string $email {
get => $this->email;
set(string $value) => strtolower(trim($value));
}
public string $displayName {
get => "{$this->firstName} {$this->lastName}";
}
public function __construct(
public string $firstName,
public string $lastName,
string $email,
) {
$this->email = $email;
}
}
$user = new User('Vadim', 'Bobkov', ' Vadim@Example.COM ');
echo $user->email; // vadim@example.com
echo $user->displayName; // Vadim Bobkov
Property hooks — это нативные геттеры и сеттеры, встроенные прямо в объявление свойства. Без магии, с полной типизацией, автокомплитом и нормальными стектрейсами.
Реальные кейсы из production
1. Нормализация данных при записи
class Product
{
public string $sku {
set(string $value) => strtoupper(trim($value));
}
public int $priceInCents {
set(int $value) {
if ($value < 0) {
throw new \DomainException('Price cannot be negative');
}
$this->priceInCents = $value;
}
}
// Вычисляемое свойство — нет backing store
public float $priceFormatted {
get => $this->priceInCents / 100;
}
}
Обратите внимание: $priceFormatted не хранит значение. Это чистый computed property. В отличие от __get, IDE знает его тип, PHPStan проверяет, а при доступе в шаблоне {{ $product->priceFormatted }} всё работает без дополнительных аксессоров.
2. Lazy loading с кэшированием
class OrderReport
{
private ?array $cachedMetrics = null;
public array $metrics {
get {
if ($this->cachedMetrics === null) {
$this->cachedMetrics = $this->calculateMetrics();
}
return $this->cachedMetrics;
}
}
private function calculateMetrics(): array
{
// Тяжёлый запрос к ClickHouse
return DB::connection('clickhouse')
->select('SELECT sum(amount), count(*) FROM orders WHERE report_id = ?', [$this->id]);
}
}
3. Валидация с доменными правилами
class Employee
{
public string $taxId {
set(string $value) {
// ИНН: 10 или 12 цифр
if (!preg_match('/^\d{10}(\d{2})?$/', $value)) {
throw new \DomainException('Invalid INN format');
}
$this->taxId = $value;
}
}
public \DateTimeImmutable $hireDate {
set(\DateTimeImmutable $value) {
if ($value > new \DateTimeImmutable()) {
throw new \DomainException('Hire date cannot be in the future');
}
$this->hireDate = $value;
}
}
}
Property Hooks vs get/set — сравнение
| Критерий | Property Hooks | get/set |
|---|---|---|
| Автокомплит IDE | Полный | Требует @property PHPDoc |
| Статический анализ | PHPStan level 9 | Частично, через плагины |
| Стектрейс | Указывает на свойство | Указывает на __get |
| Производительность | Нативный opcode | Fallback через magic |
| Наследование | Можно переопределять отдельные хуки | Переопределяешь весь __get |
| Интерфейсы | Свойства в интерфейсах | Только через @property |
Свойства в интерфейсах
Это отдельная большая тема. PHP 8.4 позволяет объявлять свойства в интерфейсах:
interface HasFullName
{
public string $fullName { get; }
}
class User implements HasFullName
{
public string $fullName {
get => "{$this->firstName} {$this->lastName}";
}
public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {}
}
class Company implements HasFullName
{
public string $fullName {
get => $this->legalName;
}
public function __construct(
public readonly string $legalName,
) {}
}
// Теперь можно типизировать
function greet(HasFullName $entity): string
{
return "Hello, {$entity->fullName}!";
}
Раньше для этого нужен был метод getFullName() в интерфейсе. Свойства в интерфейсах — это не сахар, это новый уровень контрактов.
Asymmetric Visibility — контролируй запись, не закрывая чтение
Проблема
Классическая ситуация: свойство нужно читать извне, но записывать только изнутри класса. До 8.4 — два варианта:
// Вариант 1: readonly (но нельзя менять после конструктора)
class User
{
public function __construct(
public readonly string $name,
) {}
}
// Вариант 2: private + getter
class User
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function changeName(string $name): void
{
$this->name = $name;
}
}
readonly слишком строгий — нельзя изменить значение даже внутри класса после конструктора. Private + getter — работает, но это бойлерплейт на каждое свойство.
Новый синтаксис
class User
{
public private(set) string $name;
public protected(set) string $email;
public private(set) \DateTimeImmutable $createdAt;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
$this->createdAt = new \DateTimeImmutable();
}
public function changeName(string $newName): void
{
// Работает — мы внутри класса
$this->name = $newName;
}
}
$user = new User('Vadim', 'vadim@example.com');
echo $user->name; // Vadim — чтение работает
$user->name = 'Ivan'; // Error: Cannot modify private(set) property
$user->changeName('Ivan'); // OK — через метод
Где это реально нужно
1. DTO и Value Objects
class MoneyTransfer
{
public private(set) float $amount;
public private(set) string $currency;
public private(set) string $status = 'pending';
public function __construct(float $amount, string $currency)
{
if ($amount <= 0) {
throw new \DomainException('Amount must be positive');
}
$this->amount = $amount;
$this->currency = strtoupper($currency);
}
public function markCompleted(): void
{
$this->status = 'completed';
}
public function markFailed(string $reason): void
{
$this->status = 'failed';
}
}
// Снаружи — только чтение
$transfer = new MoneyTransfer(100.50, 'usd');
echo $transfer->amount; // 100.5
echo $transfer->currency; // USD
echo $transfer->status; // pending
// IDE и шаблоны видят все свойства
// но изменить можно только через бизнес-методы
2. Eloquent-модели (Laravel)
class Order extends Model
{
// Публичное чтение, запись только через бизнес-логику
public private(set) string $status = 'draft';
public function submit(): void
{
if ($this->items()->count() === 0) {
throw new \DomainException('Cannot submit empty order');
}
$this->status = 'submitted';
$this->save();
event(new OrderSubmitted($this));
}
public function cancel(): void
{
if (!in_array($this->status, ['draft', 'submitted'])) {
throw new \DomainException("Cannot cancel order in status: {$this->status}");
}
$this->status = 'cancelled';
$this->save();
}
}
3. Комбинация с property hooks
class Temperature
{
public private(set) float $celsius {
set(float $value) {
if ($value < -273.15) {
throw new \DomainException('Temperature below absolute zero');
}
$this->celsius = $value;
}
}
public float $fahrenheit {
get => $this->celsius * 9/5 + 32;
}
public float $kelvin {
get => $this->celsius + 273.15;
}
public function __construct(float $celsius)
{
$this->celsius = $celsius;
}
}
$temp = new Temperature(36.6);
echo $temp->celsius; // 36.6
echo $temp->fahrenheit; // 97.88
echo $temp->kelvin; // 309.75
$temp->celsius = 100; // Error: private(set)
readonly vs private(set) — когда что
| Сценарий | readonly | private(set) |
|---|---|---|
| Значение не меняется после создания | Да | Избыточно |
| Значение меняется через методы | Нет — нельзя | Да |
| DTO из API/базы | Да | Да |
| Eloquent/Doctrine модели | Нет — ORM меняет поля | Да |
| Event Sourcing state | Нет | Да |
| Config / immutable settings | Да | Избыточно |
Правило простое: если значение задаётся один раз — readonly. Если может меняться, но только через бизнес-логику — private(set).
Fibers — асинхронность без callback-ада
Что такое Fibers
Fibers появились в PHP 8.1, но в 8.4 они стали по-настоящему зрелыми. Fiber — это кооперативный корутин: функция, которая может приостановить своё выполнение и вернуть управление вызывающему коду.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('first pause');
echo "Received: {$value}\n";
Fiber::suspend('second pause');
echo "Finished\n";
});
$result1 = $fiber->start(); // 'first pause'
$result2 = $fiber->resume('hello'); // prints "Received: hello", returns 'second pause'
$fiber->resume(); // prints "Finished"
Fiber приостанавливается через Fiber::suspend(), возвращая значение наружу. Вызывающий код продолжает через resume(), передавая данные обратно в fiber.
Fibers vs Generators
Генераторы (yield) похожи, но ограничены:
// Generator — только yield значения наружу
function readLines(string $file): Generator
{
$handle = fopen($file, 'r');
while ($line = fgets($handle)) {
yield trim($line);
}
fclose($handle);
}
// Fiber — полноценная двусторонняя коммуникация
$fiber = new Fiber(function (): void {
$url = Fiber::suspend(); // получить URL снаружи
$response = file_get_contents($url);
$parsed = json_decode($response, true);
Fiber::suspend($parsed); // отдать результат наружу
// продолжить с новыми данными...
});
| Критерий | Generator | Fiber |
|---|---|---|
| Двусторонняя передача данных | Ограниченно (send) | Полная |
| Вложенность | yield from | Свободная |
| Stackful | Нет | Да |
| Использование | Итераторы, ленивые коллекции | Async I/O, concurrency |
| Overhead памяти | ~400 bytes | ~8 KB |
Практический пример: параллельные HTTP-запросы
class AsyncHttpClient
{
private array $fibers = [];
private array $results = [];
public function addRequest(string $name, string $url, array $options = []): void
{
$this->fibers[$name] = new Fiber(function () use ($url, $options): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $options['timeout'] ?? 30,
CURLOPT_HTTPHEADER => $options['headers'] ?? [],
]);
if (isset($options['post'])) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($options['post']));
}
// Приостановить fiber — curl handle готов, но запрос ещё не выполнен
Fiber::suspend($ch);
// Когда fiber возобновится, curl_multi уже выполнил запрос
$response = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
});
}
public function execute(): array
{
$multiHandle = curl_multi_init();
$handles = [];
// Стартуем все fibers — каждый создаёт curl handle и приостанавливается
foreach ($this->fibers as $name => $fiber) {
$ch = $fiber->start();
curl_multi_add_handle($multiHandle, $ch);
$handles[$name] = $ch;
}
// Выполняем все запросы параллельно
do {
$status = curl_multi_exec($multiHandle, $active);
curl_multi_select($multiHandle);
} while ($active && $status === CURLM_OK);
// Возобновляем fibers — каждый обрабатывает свой результат
foreach ($this->fibers as $name => $fiber) {
$this->results[$name] = $fiber->resume();
}
curl_multi_close($multiHandle);
return $this->results;
}
}
// Использование
$client = new AsyncHttpClient();
$client->addRequest('users', 'https://api.example.com/users');
$client->addRequest('orders', 'https://api.example.com/orders');
$client->addRequest('products', 'https://api.example.com/products');
$results = $client->execute();
// Все 3 запроса выполнились параллельно, а не последовательно
Fibers в Laravel / Symfony
Laravel: параллельный сбор данных для дашборда
class DashboardController extends Controller
{
public function index(): View
{
$fibers = [
'stats' => new Fiber(fn() => Fiber::suspend(
Cache::remember('dashboard.stats', 60, fn() => [
'users' => User::count(),
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
])
)),
'chart' => new Fiber(fn() => Fiber::suspend(
Order::selectRaw('DATE(created_at) as date, SUM(total) as total')
->where('created_at', '>=', now()->subDays(30))
->groupByRaw('DATE(created_at)')
->get()
)),
'recent' => new Fiber(fn() => Fiber::suspend(
Order::with('user')->latest()->limit(10)->get()
)),
];
$data = [];
foreach ($fibers as $key => $fiber) {
$data[$key] = $fiber->start();
}
return view('dashboard', $data);
}
}
Symfony: асинхронная обработка сообщений
class OrderProcessor
{
public function processBatch(array $orderIds): array
{
$fibers = [];
foreach ($orderIds as $id) {
$fibers[$id] = new Fiber(function () use ($id): array {
$order = $this->orderRepository->find($id);
// Проверка склада
$stockCheck = $this->stockService->check($order->items);
Fiber::suspend(); // уступить процессор
// Расчёт доставки
$shipping = $this->shippingService->calculate($order);
Fiber::suspend();
// Отправка уведомления
$this->notifier->send($order->user, new OrderConfirmation($order));
return [
'order_id' => $id,
'stock_ok' => $stockCheck,
'shipping_cost' => $shipping->cost,
'status' => 'processed',
];
});
}
// Запуск всех fibers
foreach ($fibers as $fiber) {
$fiber->start();
}
// Round-robin возобновление
$results = [];
$active = true;
while ($active) {
$active = false;
foreach ($fibers as $id => $fiber) {
if (!$fiber->isTerminated()) {
$result = $fiber->resume();
if ($fiber->isTerminated()) {
$results[$id] = $fiber->getReturn();
}
$active = true;
}
}
}
return $results;
}
}
Когда НЕ использовать Fibers
Fibers — не серебряная пуля. Они не нужны, когда:
- Один HTTP-запрос к API — обычный
Http::get()проще - CPU-bound задачи — Fibers не создают потоки, они кооперативные. Для CPU нужен
pcntl_forkили очереди - Простая последовательная логика — overhead на создание Fiber (~8KB) не оправдан
- Всё уже в очереди — если логика в
Job, Fibers внутри джобы обычно избыточны
Fibers полезны, когда есть несколько I/O-операций, которые можно выполнить параллельно: запросы к нескольким API, параллельные запросы к базе, отправка уведомлений.
Новые функции массивов
PHP 8.4 добавил функции, которых не хватало годами.
arrayfind, arrayfind_key
$users = [
['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
['id' => 2, 'name' => 'Bob', 'role' => 'editor'],
['id' => 3, 'name' => 'Charlie', 'role' => 'admin'],
];
// Раньше: array_filter + reset (или current)
$admin = current(array_filter($users, fn($u) => $u['role'] === 'admin'));
// PHP 8.4
$admin = array_find($users, fn($u) => $u['role'] === 'admin');
// ['id' => 1, 'name' => 'Alice', 'role' => 'admin']
$adminKey = array_find_key($users, fn($u) => $u['role'] === 'admin');
// 0
arrayany, arrayall
$prices = [100, 250, 50, 800, 120];
// Раньше: count(array_filter(...)) > 0
$hasExpensive = count(array_filter($prices, fn($p) => $p > 500)) > 0;
// PHP 8.4
$hasExpensive = array_any($prices, fn($p) => $p > 500); // true
$allCheap = array_all($prices, fn($p) => $p < 1000); // true
$allExpensive = array_all($prices, fn($p) => $p > 500); // false
Реальный пример: валидация корзины
class CartValidator
{
public function validate(Cart $cart): ValidationResult
{
$items = $cart->items;
// Есть ли товары без цены
$hasMissingPrice = array_any(
$items,
fn(CartItem $item) => $item->price === null
);
// Все ли товары в наличии
$allInStock = array_all(
$items,
fn(CartItem $item) => $item->stock > 0
);
// Найти первый товар с ограничением по возрасту
$ageRestricted = array_find(
$items,
fn(CartItem $item) => $item->ageRestriction !== null
);
return new ValidationResult(
valid: !$hasMissingPrice && $allInStock,
ageVerificationRequired: $ageRestricted !== null,
firstRestricted: $ageRestricted,
);
}
}
Новые методы строк и Closure
mbtrim, mbltrim, mb_rtrim
Наконец-то нативный trim для мультибайтовых строк:
$input = "\u{00A0}Привет, мир!\u{00A0}"; // non-breaking spaces
echo trim($input); // "\u{00A0}Привет, мир!\u{00A0}" — trim не видит NBSP
echo mb_trim($input); // "Привет, мир!" — mb_trim справляется
В моих проектах это убрало десяток кастомных хелперов вида mb_trim($str, "\xC2\xA0").
Closure::fromCallable улучшения
class EventDispatcher
{
private array $listeners = [];
public function on(string $event, callable $listener): void
{
// PHP 8.4: первоклассные callable синтаксис
$this->listeners[$event][] = Closure::fromCallable($listener);
}
public function emit(string $event, mixed ...$args): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$args);
}
}
}
$dispatcher = new EventDispatcher();
// Все эти варианты работают единообразно
$dispatcher->on('user.created', $logger->info(...));
$dispatcher->on('user.created', UserNotification::send(...));
$dispatcher->on('user.created', function (User $user) {
Cache::forget("users.{$user->id}");
});
Deprecations и breaking changes
Что ломается при обновлении с 8.3
1. Неявная nullable типизация удалена
// PHP 8.3: работало (но deprecated)
function process(string $name = null): void {}
// PHP 8.4: ошибка. Нужно:
function process(?string $name = null): void {}
Это самый массовый breaking change. В крупном Laravel-проекте у нас было ~150 таких мест. Автофикс:
# PHP CS Fixer правило
php-cs-fixer fix --rules=nullable_type_declaration_for_default_null_value src/
2. CURLOPT_BINARYTRANSFER удалён
// Было
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
// Стало — просто удалить, эта опция ничего не делала с PHP 5.1.2
3. sessionregister, sessionunregister — полностью удалены
Если где-то ещё жив legacy-код с этими функциями — пора мигрировать на $_SESSION[].
4. Класс Date (E_DEPRECATED для конструктора DateTime с мутацией)
// Deprecated: создание DateTime без аргументов в некоторых контекстах
$date = new DateTime();
$date->modify('+1 day'); // мутирует объект
// Рекомендовано:
$date = new DateTimeImmutable();
$tomorrow = $date->modify('+1 day'); // новый объект
Полный чеклист для миграции с 8.3
# 1. Обновить зависимости
composer update --dry-run
# Проверить совместимость пакетов с 8.4
# 2. Запустить Rector для автоматической миграции
composer require rector/rector --dev
vendor/bin/rector process --set php84
# Rector автоматически исправит:
# - nullable type declarations
# - removed constants
# - deprecated function replacements
# 3. PHPStan на максимальном уровне
vendor/bin/phpstan analyse -l 9 src/
# 4. Тесты
php artisan test --parallel
Rector-правила для PHP 8.4
// rector.php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/src'])
->withSets([LevelSetList::UP_TO_PHP_84])
->withRules([
// Добавить property hooks где есть __get/__set
\Rector\Php84\Rector\Class_\PropertyHookFromMagicGetSetRector::class,
]);
Производительность: бенчмарки
Все бенчмарки — на одной машине (AMD Ryzen 7, 32GB RAM, Ubuntu 24.04, PHP compiled with -O2).
Property Hooks vs get/set
Benchmark: 1M property reads
__get magic: 0.847s
Property hook (get): 0.312s
Direct public: 0.298s
Property hooks в ~2.7x быстрее магических методов.
Разница с прямым доступом — в пределах шума (~5%).
arrayfind vs arrayfilter + reset
Benchmark: find first in array of 10000 elements (target at index 50)
array_filter + reset: 0.0034s (обрабатывает ВСЕ элементы)
array_find: 0.000018s (останавливается на первом совпадении)
~190x быстрее для early exit сценариев.
Fiber creation overhead
Benchmark: create + start + resume 100K fibers
Fiber create + start: 0.089s
Generator create: 0.043s
Function call: 0.011s
Overhead на Fiber — ~2x от генератора, ~8x от обычного вызова.
Но: это overhead на создание. Для I/O-bound задач (HTTP, DB) это нерелевантно.
Laravel и Symfony — конкретные примеры адаптации
Laravel: Request validation с property hooks
class CreateOrderRequest extends FormRequest
{
public array $validated {
get => parent::validated();
}
// Типизированный доступ к полям
public string $customerEmail {
get => $this->validated['customer_email'];
}
public array $items {
get => $this->validated['items'];
}
public float $totalAmount {
get => collect($this->items)->sum('price');
}
public function rules(): array
{
return [
'customer_email' => ['required', 'email'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'exists:products,id'],
'items.*.price' => ['required', 'numeric', 'min:0.01'],
];
}
}
// В контроллере:
class OrderController extends Controller
{
public function store(CreateOrderRequest $request): JsonResponse
{
// Вместо $request->validated()['customer_email']
$order = Order::create([
'email' => $request->customerEmail,
'items' => $request->items,
'total' => $request->totalAmount,
]);
return response()->json($order, 201);
}
}
Laravel: Resource с asymmetric visibility
class UserResource extends JsonResource
{
public private(set) array $computed;
public function __construct(User $resource)
{
parent::__construct($resource);
$this->computed = [
'full_name' => "{$resource->first_name} {$resource->last_name}",
'avatar_url' => $resource->avatar
? Storage::url($resource->avatar)
: null,
'is_active' => $resource->last_login_at?->isAfter(now()->subDays(30)),
];
}
public function toArray($request): array
{
return [
'id' => $this->id,
'email' => $this->email,
...$this->computed,
'role' => $this->whenLoaded('role', fn() => $this->role->name),
'created_at' => $this->created_at->toISOString(),
];
}
}
Symfony: DTO с property hooks для API Platform
#[ApiResource]
class ProductOutput
{
public string $name {
get => $this->name;
}
public private(set) string $slug {
set(string $value) => Str::slug($value);
}
public Money $price {
get => new Money($this->priceInCents, $this->currency);
}
public string $formattedPrice {
get => $this->price->format();
}
public bool $isAvailable {
get => $this->stock > 0 && $this->visible;
}
public function __construct(
string $name,
public private(set) int $priceInCents,
public private(set) string $currency,
public private(set) int $stock,
public private(set) bool $visible = true,
) {
$this->name = $name;
$this->slug = $name;
}
}
Другие изменения в PHP 8.4
new без скобок в цепочке вызовов
// PHP 8.3: нужны скобки
$name = (new ReflectionClass($obj))->getName();
$result = (new Collection([1, 2, 3]))->map(fn($x) => $x * 2)->toArray();
// PHP 8.4: без скобок
$name = new ReflectionClass($obj)->getName();
$result = new Collection([1, 2, 3])->map(fn($x) => $x * 2)->toArray();
Мелочь, но в цепочках fluent API это убирает визуальный шум.
#[\Deprecated] атрибут
class PaymentGateway
{
#[\Deprecated("Use processPayment() instead", since: "2.0")]
public function pay(float $amount): bool
{
return $this->processPayment($amount);
}
public function processPayment(float $amount): bool
{
// новая реализация
}
}
// При вызове $gateway->pay(100) — E_USER_DEPRECATED с сообщением
Раньше для этого нужен был trigger_error() внутри метода или PHPDoc @deprecated. Атрибут — это нативный, машиночитаемый способ.
Новый HTML5-парсер: \Dom\HTMLDocument
// PHP 8.3: DOMDocument с XML-парсером (ломает HTML5)
$doc = new DOMDocument();
@$doc->loadHTML($html); // @ для подавления warnings
// PHP 8.4: нативный HTML5-парсер
$doc = \Dom\HTMLDocument::createFromString($html);
$doc = \Dom\HTMLDocument::createFromFile('page.html');
// Полная поддержка HTML5: <template>, <slot>, <details>, SVG inline
// Без warnings, без хаков с @
Для парсинга HTML в скраперах и тестах — это огромный шаг вперёд.
BCMath: объектный API
// PHP 8.3
$result = bcadd('1.5', '2.3', 2); // '3.80'
$total = bcmul(bcadd('100', '50'), '1.2', 2); // '180.00'
// PHP 8.4: объектный API
use BCMath\Number;
$a = new Number('1.5');
$b = new Number('2.3');
$result = $a + $b; // Number('3.8')
$price = new Number('100');
$tax = new Number('1.2');
$total = ($price + new Number('50')) * $tax; // Number('180.0')
Для финансовых расчётов, где float-арифметика недопустима, — наконец-то нормальный API.
Конфигурация php.ini: что проверить
; Новые директивы в 8.4
; Property hooks используют больше памяти на класс (незначительно)
; Если у вас жёстко ограничена memory_limit — проверьте после миграции
memory_limit = 256M
; Fiber stack size (default 8KB, может потребоваться увеличить)
; Актуально если fibers вызывают глубокую рекурсию
; fiber.stack_size = 8192
; opcache — property hooks создают новые opcodes
; Рекомендуется увеличить буфер если много классов с hooks
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
Итоговый чеклист миграции на PHP 8.4
- [ ] Запустить
rector process --set php84— автоматическая миграция синтаксиса - [ ] Исправить все
function foo(Type $param = null)→function foo(?Type $param = null) - [ ] Проверить
composer update— все зависимости совместимы с 8.4 - [ ] Запустить PHPStan level 8+ — поймать deprecated конструкции
- [ ] Прогнать тесты — особенно там, где используются
__get/__set - [ ] Заменить
DOMDocument::loadHTML()на\Dom\HTMLDocumentдля HTML5-парсинга - [ ] Рассмотреть property hooks для Value Objects и DTO — вместо бойлерплейт-геттеров
- [ ] Рассмотреть
private(set)для Eloquent/Doctrine моделей — вместоreadonly - [ ] Заменить
array_filter + resetнаarray_findгде есть early exit - [ ] Проверить
php.ini— opcache буферы, memory_limit - [ ] Обновить CI — PHP 8.4 image в GitHub Actions / GitLab CI
- [ ] Обновить Docker-образ —
php:8.4-fpmилиphp:8.4-cli - [ ] Мониторинг после деплоя — error rate, memory usage, response time
Комментарии (0)