PHP 8.0: Основни нововъведения

От PHP 7.4: бенчмаркове на JIT, типове и чести breaking changes от реални приложения.

Съдържание


PHP 8.0 отваря линия 8.x: JIT в ядрото, по-богата система от типове (union, mixed, static), в синтаксиса — именовани аргументи, match, nullsafe оператор и атрибути, а рантаймът по-малко търпи „тихи“ грешки. Тук акцентът е какво се променя в реални проекти (фреймворци, легаси, разширения), не само списък с фичи.

Named Arguments (Именовани аргументи)

Подаването на аргументи към функция по име премахва нуждата да помните реда им и позволява да пропускате незадължителни параметри. Това прави кода самодокументиращ се.

// Преди: трябваше да подадем всички параметри по ред
setcookie('test', '', time() + 60 * 60 * 2, '/', '', false, true);

// PHP 8.0: посочваме само нужните
setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
    httponly: true
);

Match Expression

По-строга и по-лаконична алтернатива на оператора switch. Връща стойност и използва строго сравнение (===), което предпазва от неочаквани бъгове при преобразуване на типове.


$statusCode = 200;

$statusMessage = match ($statusCode) {
200, 300 => 'Успех или пренасочване',
400, 404 => 'Грешка на клиента',
500 => 'Грешка на сървъра',
default => 'Неизвестен статус',
};

Nullsafe Operator (?->)

Позволява да четете свойства и да извиквате методи във верига. Ако един от елементите е null, цялата верига връща null без да хвърля фатална грешка.


// PHP 7.4
$country = null;
if ($session !== null) {
    $user = $session->user;
    if ($user !== null) {
        $country = $user->getAddress()->country;
    }
}

// PHP 8.0
$country = $session?->user?->getAddress()?->country;

Constructor Property Promotion (Промотиране на свойства в конструктора)

Преди, за да създадете прости DTO (Data Transfer Objects) или Value Objects, трябваше да пишете много еднотипен код: да декларирате свойства, да ги подадете в конструктора и да ги присвоите. PHP 8.0 обединява тези три стъпки в една.

// PHP 7.4: Класическият (и многословен) подход
class UserDTO 
{
    public string $name;
    public string $email;
    protected int $age;

    public function __construct(string $name, string $email, int $age) 
    {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
    }
}

// PHP 8.0: Елегантно и лаконично
class UserDTO 
{
    public function __construct(
        public string $name,
        public string $email,
        protected int $age,
    ) {}
}

Еволюция на системата от типове

В PHP 8.0 системата за типизация стана значително по-строга и изразителна.

Union Types (Обединени типове)

До PHP 8.0, ако една променлива можеше да приема няколко типа данни (например, int или float), разчитахме на PHPDoc. Сега PHP поддържа това нативно.


class Calculator 
{
    private int|float $number;

    public function setNumber(int|float $number): void 
    {
        $this->number = $number;
    }
}

Псевдотип mixed

Новият тип mixed решава проблеми с legacy код. Той е еквивалентен на array|bool|callable|int|float|null|object|resource|string. Обърнете внимание: mixed вече включва null, затова не може да пишете ?mixed или mixed|null — това ще доведе до фатална грешка.

Връщан тип static

За реализация на патерните “Late Static Binding” и Fluent Interfaces е добавен типът за връщана стойност static (преди имаше само self).


class BaseFactory {
    public function create(): static {
        return new static();
    }
}

Интерфейс Stringable

Ако класът реализира магическия метод __toString(), PHP 8.0 автоматично (неявно) му присвоява интерфейса Stringable. Това позволява да използвате string|Stringable в type hint-ове.

Attributes / Анотации (дълбок разбор)

Атрибутите (PHP 8.0) са структурирани метаданни към класове, методи, свойства, параметри и константи. Двигателят ги записва в байткод; четат се чрез Reflection. Това не е магия — нищо не се изпълнява само, докато фреймворкът, рутерът или инструментът ви не ги прочете (аналогия с анотации в Java/C#, но нативно в PHP).

Атрибути срещу PHPDoc

| | PHPDoc (@route, @deprecated) | Атрибути (#[Route]) | |---|-----------------------------------|------------------------| | Парсване | Низ в коментар; нужни са парсери | Синтаксис на езика; без regex по коментари | | Типизация | Неформална; лесно се разминава с кода | Аргументите на конструктора са реални PHP стойности | | Инструменти | Интеграция в IDE е различна | Reflection API е стабилен и бърз |

PHPDoc оставете за човешка документация; атрибутите ползвайте, когато метаданните реално се четат от код (routing, validation hints, сериализация, codegen).

Декларация на клас-атрибут

Всеки клас може да е атрибут, ако е маркиран с вградения #[\Attribute]. Битовата маска ограничава къде е позволен (Attribute::TARGET_*). Ако не зададете цели, по подразбиране са разрешени всички — обикновено е по-добре да ги зададете изрично.

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
    ) {}
}
  • Често класът е final, за да не се наследява „случайно“.
  • Параметрите на конструктора са аргументите при #[Route('/x', methods: [...])].

Един елемент може да има няколко екземпляра от един и същи атрибут, ако в маската добавите Attribute::IS_REPEATABLE — вижте класа Attribute в мануала.

Синтаксис на прилагане

Атрибутите стоят пред декларацията; може няколко:

#[Route('/api/users', methods: ['GET', 'POST'])]
final class UserController {}

#[Route('/items')]
final class ItemController
{
    #[Route('/items/{id}', methods: ['GET'])]
    public function show(int $id): array
    {
        return ['id' => $id];
    }
}

Именуваните аргументи (PHP 8.0) съвпадат с параметрите на конструктора на атрибута.

Как работи в runtime

  1. PHP парсира атрибутите при компилация и ги връзва към reflection-структурата.
  2. Взимате ReflectionClass, ReflectionMethod, ReflectionProperty и т.н.
  3. getAttributes()ReflectionAttribute, после newInstance() → инстанция на вашия клас с аргументите от кода.

Нищо не се вика „само“: bootstrap, DI контейнер или рутерът трябва да извикат Reflection (или библиотека-обвивка).

Четене чрез Reflection

$reflection = new ReflectionClass(UserController::class);

foreach ($reflection->getAttributes(Route::class) as $attr) {
    /** @var Route $route */
    $route = $attr->newInstance();
}

foreach ($reflection->getAttributes() as $attr) {
    $name = $attr->getName();
    $args = $attr->getArguments();
}

За методи — ReflectionMethod::getAttributes(), за параметри — ReflectionParameter::getAttributes().

Краен пример (идея за минимален router)

#[Route('/api/users', methods: ['GET', 'POST'])]
class UserController {}

$rc = new ReflectionClass(UserController::class);
$routeAttrs = $rc->getAttributes(Route::class);
if ($routeAttrs === []) {
    throw new RuntimeException('No Route on ' . UserController::class);
}
$route = $routeAttrs[0]->newInstance();
// регистрирайте $route->path в диспечера...

Къде атрибутите са на място

  • HTTP routing / middleware — път и методи към контролери.
  • Валидация и сериализация — правила върху свойства за хидратор/сериализатор.
  • DI — подсказки за автоматично свързване (често с други атрибути).
  • Тестове и инструменти — групи фикстури, собствени deprecation маркери за CI.

Не вкарвайте бизнес логика само в атрибути: държете ги декларативни; сложната логика — в нормални класове и услуги.

По-разумно сравнение на низове и числа

Това е една от най-коварните промени, които чупят обратната съвместимост. Преди при нестрого сравнение на число с низ PHP конвертираше низа в число. В PHP 8.0, ако низът не е числов, числата се сравняват като низове[cite: 227, 228].


// PHP 7.4
0 == 'foobar' // true [cite: 224]

// PHP 8.0
0 == 'foobar' // false [cite: 226]

Синтактични подобрения

Разрешаване на ::class върху обекти

Вече можете да получите името на класа директно от променлива-обект чрез ::class. Преди за това се използваше функцията get_class().


$object = new \App\Models\User();
// PHP 7: get_class($object);
echo $object::class; // Ще изведе "App\Models\User"

Висящи запетаи (Trailing commas)

Разрешено е да оставяте висяща запетая в списъците с параметри на функции, методи и closure-и. Това прави commit-ите в Git по-чисти.


public function makeRequest(
    string $url,
    array $data,
    array $headers, // <-- висящата запетая вече е позволена [cite: 286]
) { ... }

Нови функции за низове

Вместо strpos() и проверки за !== false са добавени три функции, които връщат строг bool:


$str = "DevSense is awesome";

str_starts_with($str, "DevSense"); // true
str_contains($str, "awesome");     // true
str_ends_with($str, "awesome");    // true



Практичен рецепт: префикс/суфикс без strpos

if (!str_starts_with($path, '/var/www/app/')) {
    throw new InvalidArgumentException('Path escapes allowed root');
}
if (str_ends_with($filename, '.php')) {
    // обработка на качени скриптове и т.н.
}

Функцията get_debug_type()

Новата функция get_debug_type() връща “полезен” тип на променлива (например, App\Models\User вместо просто object, или int вместо integer). Тя е идеална за съставяне на разбираеми съобщения за грешки.

Weak Maps (Слаби карти)

Архитектурен пробив за ORM и кеширане. WeakMap позволява да създавате връзки между обекти така, че тези връзки да не пречат на Garbage Collector-а да премахва обектите от паметта.


class Cache {
    private WeakMap $cache;
    
    public function __construct() {
        $this->cache = new WeakMap();
    }
    
    public function getMetadata(object $obj): array {
        if (!isset($this->cache[$obj])) {
            $this->cache[$obj] = $this->computeExpensiveData($obj);
        }
        return $this->cache[$obj];
    }
}

Веднага щом обектът $obj бъде унищожен в приложението, той автоматично изчезва и от WeakMap, освобождавайки памет.

Throw Expression и обработка на грешки

Операторът throw от инструкция (statement) се превърна в израз (expression).


// Вече може да хвърляте изключения директно в тернарни оператори или при null coalescing
$user = $request->get('user') ?? throw new InvalidArgumentException('User is required');

$callable = fn() => throw new Exception('This should not run');

Глобална промяна в строгостта: в PHP 8.0 операторът за потискане на грешки @ вече не скрива фатални грешки. Освен това, повечето предупреждения на ядрото (Engine Warnings) бяха преобразувани в Error изключения (например, опит да достъпите свойство на не-обект вече “срива” скрипта, вместо просто да запише Warning в логовете).

Non-capturing catches (анонимно прихващане на изключения)

Ако трябва да прихванете изключение, но самият обект на изключението не ви е нужен, променливата вече може да бъде пропусната.


// Преди: бяхме длъжни да декларираме променлива $e
try {
    // код
} catch (Exception $e) {
    Log::error('Нещо се обърка');
}

// PHP 8.0:
try {
    // код
} catch (Exception) {
    Log::error('Нещо се обърка');
}

Type грешки за вградените функции

Повечето вградени функции на PHP вече хвърлят строги изключения TypeError или ValueError при подаване на невалидни параметри, вместо да дават Warning и да връщат null.


// PHP 7.4
strlen([]); // Warning: strlen() expects parameter 1 to be string, array given [cite: 231]

// PHP 8.0
strlen([]); // TypeError: strlen(): Argument #1 ($str) must be of type string, array given [cite: 237]
array_chunk([], -1); // ValueError: array_chunk(): Argument #2 ($length) must be greater than 0

JIT-компилатор и производителност

Най-фундаменталната архитектурна промяна “под капака” на PHP 8.0 е въвеждането на JIT (Just-In-Time) компилатор.

OPcache (преди PHP 8): изходният код се парсваше до OpCodes, а виртуалната машина Zend ги изпълняваше ред по ред.

JIT (PHP 8.0+): анализира OpCodes и компилира “горещите участъци” директно в машинния код на процесора (x86/ARM).

За типични I/O-зависими уеб приложения прирастът е около 1–5%. Но за изчисления (CPU Bound) — от 300% до 500%.

Промени в модулите и ресурсите

Продължава прочистването на ядрото от непрозрачните типове resource. Те се заменят с обекти:

cURL: curl_init() връща клас CurlHandle.

GD: imagecreate() връща GdImage.

Sockets: socket_create() връща Socket.

Управление на паметта: новите обекти се унищожават автоматично от garbage collector-а. Функции като curl_close() вече не носят смислова стойност.

Други промени по разширенията:

JSON вече е “закован” в ядрото (не може да бъде изключен с --disable-json).

XML-RPC е преместен от ядрото в PECL.

Добавена е математическата функция fdiv(), която позволява деление на нула (връща INF, -INF или NAN вместо грешка).

Продължава прочистването на ядрото от непрозрачните типове resource. Те се заменят с обекти:

  • cURL: curl_init() връща CurlHandle.
  • GD: imagecreate() връща GdImage.
  • Sockets: socket_create() връща Socket.
  • Засегнати са и разширенията OpenSSL, XMLWriter, както и функциите XML.

Обратно несъвместими промени (бележки за миграция)

Дори да не използвате новия синтаксис, ъпгрейдът към PHP 8.0 може да “счупи” приложението заради по-строгото поведение в runtime и премахвания. По-долу е кратък чеклист какво си струва да валидирате изрично в CI и на staging.

Език / ключови думи / премахвания

  • match вече е reserved keyword.
  • mixed вече е reserved word (не може да се използва за имена на class/interface/trait и е забранено в namespaces).
  • __autoload() е премахнат. Използвайте spl_autoload_register().
  • create_function() е премахната. Използвайте anonymous functions / closures.
  • each() е премахната. Използвайте foreach или ArrayIterator.
  • Премахнати са дефинициите на константи без значение на регистъра (define('FOO', 'bar', true) вече не се поддържа).
  • Методи със същото име като класа вече не се третират като конструктори (използвайте __construct()).
  • Вече не е позволено да се извикват non-static методи статично (засяга и проверки като is_callable() при използване на име на клас).
  • Кастовете (real) и (unset) са премахнати.

Грешки и диагностика (по-строго в runtime)

  • Assertion failures вече хвърлят по подразбиране (прегледайте assert.*, напр. assert.exception).
  • ini-директивата track_errors е премахната (т.е. php_errormsg вече не е налична; използвайте error_get_last()).
  • Операторът @ вече не потиска фатални грешки. Error handlers не трябва да разчитат на error_reporting() == 0, за да засекат suppression.
  • error_reporting по подразбиране вече е E_ALL (включва E_NOTICE и E_DEPRECATED).
  • display_startup_errors е включен по подразбиране.
  • Много warning-и станаха Error exceptions (напр. писане в property на не-обект, невалидни типове ключове/стрингови offset-и, unpack на не-array/Traversable, достъп до undefined unqualified constants).
  • Много notice-и станаха warnings (undefined variables/properties/array keys, array-to-string conversion, невалидни string offsets и т.н.).

Числови низове и преобразуване на типове

  • Нестрогите сравнения на числа с нечислови низове са променени (виж раздела “По-разумно сравнение на низове и числа” по-горе).
  • “Saner numeric strings”: операции, които преди даваха warning/notice, сега дават warning или хвърлят TypeError (особено аритметични/битови операции с нечислови низове; поведението е по-строго и по-консистентно).
  • Преобразуването float → string вече е независимо от locale.

Масиви / ключове / извиквания / reflection

  • array_key_exists() вече не работи с обекти (използвайте isset() или property_exists()).
  • Невалидни типове ключове за array_key_exists() се обработват по-строго и могат да хвърлят TypeError.
  • Ключовете на масива в call_user_func_array() вече се интерпретират като имена на параметри (поведението може да се промени, ако неволно подавате асоциативни масиви).
  • debug_backtrace() и Exception::getTrace() вече не предоставят references към аргументи.

OOP edge cases

  • Използването на parent в клас без родител вече е фатална compile-time грешка.
  • Сигнатурите на magic methods вече се валидират, ако са декларирани (несъответствията могат да чупят код).

Extensions: notable BC breaks

  • cURL: CURLOPT_POSTFIELDS (и други опции, които приемат масиви) вече не приема обекти “като масив”. Ако сте разчитали на това — направете явен каст (array).
  • Date/Time: mktime() и gmmktime() вече изискват поне един аргумент.
  • DOM: премахнати са редица нереализирани DOM класове и методи (ако сте ги използвали като заглушки/тестови структури — кодът ще се счупи).
  • Exif: read_exif_data() е премахната; използвайте exif_read_data().
  • Filter:
    • FILTER_FLAG_SCHEME_REQUIRED и FILTER_FLAG_HOST_REQUIRED са премахнати (за FILTER_VALIDATE_URL scheme/host и без това винаги са задължителни).
    • INPUT_REQUEST и INPUT_SESSION са премахнати като източници за filter_input().
  • mbstring:
    • mbstring.func_overload е премахната (както и свързаните MB_OVERLOAD_* константи / записи в mb_get_info()).
    • mb_parse_str() вече не може да се извика без да се подаде резултатен масив.
    • модификаторът e за mb_ereg_replace() е премахнат; използвайте mb_ereg_replace_callback().
  • OpenSSL: openssl_seal() и openssl_open() вече изискват аргумента method (старият default "RC4" се счита за небезопасен).
  • PCRE (Regular Expressions): невалидни escape последователности вече не се третират като литерали; модификаторът X вече се игнорира.
  • PDO:
    • default error mode е сменен от silent към exceptions.
    • променени са сигнатурите на някои методи (особено PDO::query() и PDOStatement::setFetchMode()).
  • Phar: metadata в phar вече не се unserialize-ва автоматично (security hardening; код, който разчита на неявен unserialize, трябва да се промени).
  • Reflection:
    • ReflectionClass::newInstance(), ReflectionFunction::invoke() и ReflectionMethod::invoke() преминаха към variadics (...$args).
    • методите Reflection*::export() са премахнати (използвайте cast на reflection обекти към string).
  • SPL:
    • SplFixedArray вече е IteratorAggregate (а не Iterator); премахнати са методи за итерация в полза на getIterator().
    • spl_autoload_register() вече винаги хвърля TypeError при невалидни аргументи (параметърът do_throw на практика се игнорира).
  • Standard library:
    • assert() вече не оценява string аргументи (използвайте assert($a == $b), а не assert('$a == $b')); assert.quiet_eval / ASSERT_QUIET_EVAL са премахнати.
    • parse_str() вече не може да се използва без да се подаде резултатен масив.
    • опцията 'salt' на password_hash() вече не се поддържа (игнорира се с warning).

Изводи (резюме)

Мислете за PHP 8.0 като за обновяване на платформата: по-малко изненади от слаби сравнения, по-ясни откази от вътрешни API и типизация, която най-после отговаря на големите кодови бази. Печалбата е по-малко бъгове само в production и по-гладък път към 8.1+, особено ако чистите deprecations, докато още имате тестове срещу 7.4.