У п'яти релізах вісімки видно спільний напрямок. PHP додає типи туди, де їх раніше не було (union у 8.0, intersection у 8.1, DNF у 8.2, константи класу у 8.3), і поступово забирає способи змінити стан обʼєкта повз його методи (readonly у 8.1, readonly-класи у 8.2, private(set) у 8.4). Побічний ефект: код стає дружнішим до статичного аналізу, бо значна частина того, що раніше жила в PHPDoc і перевірялась PHPStan, переїхала в сигнатури й перевіряється рушієм.
Щоденний набір із 8.0 у більшості проєктів той самий: match замість довгих switch і if/elseif, promotion у конструкторі, іменовані аргументи для методів із трьома-чотирма опційними параметрами, ?-> там, де звʼязок справді буває порожнім. Union-типи використовуються рідше, ніж можна очікувати: у нормально спроєктованому коді int|string частіше означає, що абстракція протікає, а справжня їхня ніша - межа із зовнішніми даними. З 8.1 у щоденний обіг зайшли enum і readonly. Класичний набір const STATUS_NEW = 'new' перетворюється на backed enum із методами, а DTO між шарами - на final readonly class, після чого питання «хто це зіпсував по дорозі» просто зникає. First-class callable ($this->handle(...), Currency::from(...)) прибрав рядкові callable, які нічим не перевірялись і мовчки ламались при перейменуванні методу.
8.2 і 8.3 виглядають як дрібні релізи, і за кількістю RFC так і є, але саме вони добили обʼєктну модель до робочого стану. readonly class знімає повторення модифікатора на кожній властивості й заразом забороняє динамічні властивості. DNF-типи дозволили нарешті писати (Countable&ArrayAccess)|null: до цього перетин узагалі не можна було зробити нульованим. Типізовані константи класу (const string Draft = 'draft') закривають старий трюк, коли нащадок тихо підміняв константу значенням іншого типу. #[\Override] ловить у компіляції найнеприємніший клас помилок: метод, який ви вважали перевизначенням, а він через одруківку в назві чи зміну сигнатури в базовому класі перетворився на просто ще один метод. У 8.3 також полагодили readonly і __clone: readonly-властивість тепер можна переприсвоїти всередині __clone(), тому глибоке копіювання незмінних обʼєктів більше не впирається в Error.
8.4 - перший реліз, який змінює саме те, як пишеться тіло класу. Property hooks прибирають гетери-однорядковики: public string $title { get => sprintf(...); } дає обчислювану властивість без бекінг-поля, доступну як звичайне поле, тому рефакторинг з простої властивості в обчислювану більше не ламає жоден виклик. Асиметрична видимість (public private(set) int $version) розвʼязує стару проблему сутностей, де вибір був між публічною властивістю, яку хтось ззовні перепише, і приватною з гетером на три рядки. new Foo()->method() без зовнішніх дужок - косметика, зате прибирає найчастіший привід завести зайву змінну. Дужки після імені класу лишились обовʼязковими: new Foo->method() не працює.
Практичну межу задає рядок "php": "^8.х" у composer.json і версія на проді, а не те, скільки фіч ви знаєте. Станом на вересень 2026 PHP 8.1 не отримує навіть security-патчів (закінчилось 31 грудня 2025), у 8.2 security-фаза добігає кінця в грудні 2026, 8.3 підтримується патчами безпеки до кінця 2027, 8.4 в активній підтримці до кінця 2026. Розумний мінімум для нового проєкту - 8.3; 8.4 варто брати одразу, якщо ви пишете багато сутностей і DTO, бо hooks плюс private(set) економлять помітний обсяг механічного коду. Для легасі корисніше рухатись версіями по одній і після кожного стрибка проганяти PHPStan: найбільше часу зʼїдають не нові фічі, а накопичені депрекейти - динамічні властивості з 8.2, ${} в рядках з 8.2, неявний nullable у сигнатурах (function f(Foo $x = null)) з 8.4.
declare(strict_types=1);
enum Currency: string // 8.1: backed enum замість набору констант
{
case Uah = 'UAH';
case Eur = 'EUR';
}
final readonly class Money implements Stringable // 8.2: readonly-клас цілком
{
public function __construct(
public int $minor, // 8.0: promotion прямо в конструкторі
public Currency $currency = Currency::Uah,
) {}
#[\Override] // 8.3: не скомпілюється, якщо перекривати нічого
public function __toString(): string
{
return match ($this->currency) { // 8.0: match, строге порівняння, без break
Currency::Uah => sprintf('%.2f грн', $this->minor / 100),
Currency::Eur => sprintf('%.2f EUR', $this->minor / 100),
};
}
}
final class Order
{
public const string Draft = 'draft'; // 8.3: типізована константа класу
public private(set) int $version = 1; // 8.4: читають усі, пише лише сам клас
public string $title { // 8.4: hook, окремого поля під ним немає
get => sprintf('Замовлення #%d', $this->id);
}
public function __construct(private int $id, private ?Money $total = null) {}
public function applyMeta((Countable&ArrayAccess)|null $meta): void // 8.2: DNF-тип
{
$this->version += $meta?->count() ?? 0; // 8.0: nullsafe обриває весь ланцюг
}
}
echo new Order(id: 7)->title; // 8.0 named args + 8.4 new без зайвих дужок
$codes = array_map(Currency::from(...), ['UAH', 'EUR']); // 8.1: first-class callable
echo new Money(minor: 24_990); // 249.90 грн
Не переказуйте changelog. Візьміть один клас і покажіть, як він виглядав на 7.4 і як виглядає зараз: константи стали backed enum, конструктор схуднув до promotion, DTO став `final readonly`, ланцюжок `if/elseif` став `match`, а гетер на один рядок - property hook. Одразу після цього назвіть версію з composer.json - це показує, що ви думаєте про те, що доступно, а не про те, що модно.