← Назад в блог

PHP 8.3–8.4 для Bitrix и WordPress: типизация, атрибуты, паттерны

Практический гайд по переходу с PHP 7.4 на 8.3–8.4 в Bitrix и WordPress: union types, named arguments, attributes, match и план постепенной модернизации легаси-кода.

PHP 8.3–8.4 для Bitrix и WordPress: типизация, атрибуты, паттерны

Требования

  • PHP 7.4 (текущий проект) и тестовый стенд с PHP 8.3+
  • Базовое понимание ООП и автозагрузки Composer
  • Доступ к CI/CD или хотя бы к прогону тестов/линтера

PHP 8.3–8.4 для Bitrix и WordPress: типизация, атрибуты и модернизация легаси

Если вы до сих пор на PHP 7.4 в Bitrix/WordPress — вы живёте на пороховой бочке: часть библиотек уже «не разговаривает» с 7.4, а часть уязвимостей закрывают только в новых ветках. Смысл апгрейда не в “модно”, а в том, что вы начинаете ловить баги раньше, а не в проде в 03:00.

PHP 8.3 — про удобства и безопасность типизации (плюс мелкие приятности). (php.net/releases/8.3/) PHP 8.4 — уже про новый уровень модели данных: property hooks и асимметричная видимость свойств. (php.net/releases/8.4/)

Ниже — без философии, только то, что вы реально начнёте применять в Bitrix (D7/ORM/events) и WordPress (WP_Query/hooks).


1) Union types: перестаём притворяться, что всё — “mixed”

Union types появились раньше (PHP 8.0), но именно при миграции с 7.4 они дают максимальную пользу: вы «подсвечиваете» границы легаси-кода и видите, где у вас вечная каша.

Bitrix: аккуратнее с тем, что “то строка, то число, то false”

Классика: опции, свойства, поля инфоблоков, где на выходе что угодно.

Было (7.4-стайл):

/** @return mixed */
function getOption($name) {
    return \Bitrix\Main\Config\Option::get('main', $name);
}

Стало (8.x):

use Bitrix\Main\Config\Option;

function getOption(string $name): string|int|bool|null
{
    $value = Option::get('main', $name, null);
    if ($value === null || $value === '') {
        return null;
    }

    // пример: флажок
    if ($value === 'Y' || $value === 'N') {
        return $value === 'Y';
    }

    // пример: число
    if (ctype_digit($value)) {
        return (int)$value;
    }

    return $value;
}

Профит: дальше по коду вы не делаете «магические» сравнения, а IDE начинает реально помогать с подсказками и проверкой типов.

WordPress: функции/хуки, которые возвращают “строку или WP_Error”

Типичный случай: get_option(), get_post(), хуки фильтров — возвращают то объект, то false/WP_Error. Явный union type убирает догадки и даёт IDE возможность подсказывать поля.

function vp_get_post(int $postId): \WP_Post|\WP_Error
{
    $post = get_post($postId);
    return $post instanceof \WP_Post ? $post : new \WP_Error('not_found', 'Post not found');
}

// Использование — IDE знает тип, можно безопасно разветвить:
$postOrError = vp_get_post(123);
if (is_wp_error($postOrError)) {
    error_log($postOrError->get_error_message());
    return;
}
// здесь $postOrError — гарантированно \WP_Post
echo $postOrError->post_title;

2) Named arguments: меньше “простыней” и меньше ошибок в WP_Query/Bitrix API

Named arguments — не про красоту. Они про то, что вы не перепутаете параметры местами.

WordPress: WP_Query без “угадай, что за массив”

WP_Query принимает массив аргументов. Но “именованные аргументы” отлично заходят там, где у вас свои функции-обёртки.

function vp_query_posts(
    int $limit = 10,
    string $postType = 'post',
    int $paged = 1,
): \WP_Query {
    return new \WP_Query([
        'post_type'      => $postType,
        'posts_per_page' => $limit,
        'paged'          => $paged,
        'no_found_rows'  => true,
    ]);
}

// Читаемо:
$q = vp_query_posts(limit: 12, postType: 'product', paged: 2);

Документация по WP_Query (аргументы/поведение) — Developer Resources.

Bitrix: когда у вас 3–4 флага в методе, именованные аргументы спасают

function vp_syncOrder(int $orderId, bool $dryRun = false, bool $force = false): void
{
    // ...
}

vp_syncOrder(orderId: 123, force: true);

3) Match expressions: нормальный маппинг вместо switch-помойки

match (PHP 8.0) — must-have для маппинга статусов, типов, ролей.

Bitrix: маппинг статуса заказа → стадия сделки

Событие заказа в Bitrix: OnSaleOrderSaved (уже после сохранения сущностей). (API Bitrix24)

use Bitrix\Sale\Order;

function mapOrderStatusToDealStage(string $statusId): string
{
    return match ($statusId) {
        'N' => 'NEW',
        'P' => 'PREPARATION',
        'F' => 'WON',
        'C' => 'LOSE',
        default => 'NEW',
    };
}

WordPress: маппинг режима запроса

function vp_query_args(string $mode): array
{
    return match ($mode) {
        'latest' => ['orderby' => 'date', 'order' => 'DESC'],
        'popular' => ['orderby' => 'comment_count', 'order' => 'DESC'],
        default => ['orderby' => 'date', 'order' => 'DESC'],
    };
}

4) Attributes (#[…]): меньше магии в докблоках, больше структуры

Атрибуты — это метаданные, которые можно читать рефлексией. В Bitrix и WordPress “из коробки” они не везде используются, но внутри вашего кода — это супер-замена разрозненным соглашениям.

4.1. PHP 8.3: #[\Override] — дешёвый способ поймать кривое наследование

В PHP 8.3 появился атрибут #[\Override]. (PHP.Watch) Он делает простую вещь: если вы написали метод “как будто переопределяете”, но реально не переопределяете — получите ошибку.

class BaseHandler {
    public function handle(): void {}
}

class OrderHandler extends BaseHandler {
    #[\Override]
    public function handle(): void {}
}

Где полезно: Bitrix-модули/интеграции, где куча наследования и легко “промазать” сигнатурой.

4.2. WordPress: атрибуты для хуков (красиво, но аккуратно)

В WP сообщество делает библиотеки, которые регистрируют хуки через атрибуты (это не core-фича, а подход). Смысл: вы видите в классе “что цепляется куда”, без простыни add_action().

Пример концепта (самописная реализация, 30 строк рефлексии — и поехали):

#[Attribute(Attribute::TARGET_METHOD)]
class ActionHook {
    public function __construct(
        public string $hook,
        public int $priority = 10,
        public int $acceptedArgs = 1,
    ) {}
}

final class Hooks {
    public static function register(object $instance): void
    {
        $ref = new ReflectionObject($instance);
        foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            foreach ($method->getAttributes(ActionHook::class) as $attr) {
                /** @var ActionHook $meta */
                $meta = $attr->newInstance();
                add_action(
                    $meta->hook,
                    [$instance, $method->getName()],
                    $meta->priority,
                    $meta->acceptedArgs
                );
            }
        }
    }
}

final class SeoTweaks {
    #[ActionHook('wp_head', priority: 1, acceptedArgs: 0)]
    public function addMeta(): void
    {
        echo "<!-- SEO tweaks -->\n";
    }
}

Hooks::register(new SeoTweaks());

Честно: в проде это нужно не всем. Но если у вас большой плагин с 50+ хуками — это реально упорядочивает хаос.

4.3. Bitrix: DTO/валидаторы/маппинг — атрибуты заходят идеально

Bitrix D7 ORM сам по себе атрибуты не “понимает”, но вы можете строить аккуратные слои вокруг:

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {}

final class CreateDealDTO
{
    public function __construct(
        #[Required] public string $title,
        public int $assignedById = 1,
    ) {}
}

function validate(object $dto): array
{
    $errors = [];
    $ref = new ReflectionObject($dto);

    foreach ($ref->getProperties() as $prop) {
        $isRequired = !empty($prop->getAttributes(Required::class));
        if (!$isRequired) continue;

        $prop->setAccessible(true);
        $val = $prop->getValue($dto);
        if ($val === null || $val === '' ) {
            $errors[] = $prop->getName() . ' is required';
        }
    }

    return $errors;
}

5) Fibers: да, они существуют. Нет, они не “ускорят сайт” магически

Fibers (PHP 8.1) — низкоуровневая штука для кооперативной многозадачности. В Bitrix/WordPress в типичном HTTP-запросе вы почти никогда не должны ими лечить проблемы.

Когда fibers реально уместны:

  • CLI-скрипты, воркеры, очереди (где вы контролируете цикл).
  • Интеграции с async IO через библиотеки/рантаймы (ReactPHP, Amp и т.д.).

Мини-идея для воркера: “параллельно” обработать пачку задач, не плодя процессы:

$fibers = array_map(
    fn(int $id) => new Fiber(function () use ($id) {
        // simulate work
        return "done:$id";
    }),
    [1,2,3,4]
);

$results = [];
foreach ($fibers as $fiber) {
    $results[] = $fiber->start();
}

var_dump($results);

Если у вас “gap в backend” (узкие места): начните не с fibers, а с банального:

  • убрать N+1 запросы,
  • включить нормальный кеш,
  • перестать делать внешние HTTP-запросы в критическом пути,
  • вынести тяжёлое в агенты/cron/очередь.

Кстати, если тема автоматизации в Bitrix актуальна — вот базовый материал: https://viku-lov.ru/blog/backend-cron-bitrix-agents-automation


6) PHP 8.4: property hooks и асимметричная видимость — убийцы “геттеров ради геттеров”

6.1. Asymmetric visibility: “читать всем, писать только классу”

Официально: в 8.4 можно разделять видимость get/set. (RFC Asymmetric Visibility)

final class OrderView
{
    public string $status;

    public private(set) int $id;

    public function __construct(int $id, string $status)
    {
        $this->id = $id;      // можно тут
        $this->status = $status;
    }
}

// где-то снаружи:
$view = new OrderView(10, 'N');
echo $view->id;   // ок
$view->id = 99;   // нельзя

Для Bitrix/WordPress это прямой профит в DTO/ViewModel: меньше “случайных” мутаций.

6.2. Property hooks: логика “на свойстве”, а не в 10 методах

PHP 8.4: property hooks. (php.net/releases/8.4/) Это полезно, когда у вас в легаси-коде тонны однотипных getX()/setX().

Условный пример: нормализация телефона:

final class Contact
{
    public string $phone {
        set => preg_replace('~\D+~', '', $value);
    }

    public function __construct(string $phone)
    {
        $this->phone = $phone;
    }
}

$c = new Contact('+49 (151) 123-45-67');
echo $c->phone; // 491511234567

Где реально заходит:

  • Bitrix: сущности интеграций, DTO для CRM/1С, преобразование входных данных.
  • WordPress: настройки плагина, sanitize/normalize рядом с данными.

7) Реальные примеры “modernize” для Bitrix и WordPress

7.1. Bitrix: обработчик события заказа + строгая сигнатура + match

Событие OnSaleOrderSaved документировано в Bitrix24 API.

// /local/php_interface/init.php
use Bitrix\Main\Loader;

Loader::includeModule('sale');

AddEventHandler('sale', 'OnSaleOrderSaved', static function(\Bitrix\Main\Event $event) {
    /** @var \Bitrix\Sale\Order $order */
    $order = $event->getParameter('ENTITY');
    if (!$order) return;

    $statusId = (string)$order->getField('STATUS_ID');
    $dealStage = mapOrderStatusToDealStage($statusId);

    // дальше — обновляете CRM сделку как у вас принято
});

AddEventHandler обычно подключают в /bitrix/php_interface/init.php или /local/php_interface/init.php. (документация Bitrix)

7.2. Bitrix ORM: более предсказуемые типы на границе

Пример ORM-стиля D7 (близко к реальности). (Bitrix D7 ORM)

use Bitrix\Main\Loader;
use Bitrix\Crm\DealTable;

Loader::includeModule('crm');

function getDealTitle(int $dealId): string|null
{
    $row = DealTable::getList([
        'select' => ['ID', 'TITLE'],
        'filter' => ['=ID' => $dealId],
        'limit'  => 1,
    ])->fetch();

    return $row ? (string)$row['TITLE'] : null;
}

7.3. WordPress: WP_Query + “тонкая” обёртка с типами

/**
 * @return array<int, \WP_Post>
 */
function vp_latest_posts(int $limit = 10): array
{
    $q = new \WP_Query([
        'post_type'      => 'post',
        'posts_per_page' => $limit,
        'no_found_rows'  => true,
    ]);

    return $q->posts ?: [];
}

8) План постепенной модернизации легаси-кода (без “переписать всё”)

Шаг 0. Подготовка: стенд и контроль

  • Поднимите второй PHP (8.3/8.4) на staging/локалке.

  • Включите строгие ошибки на стенде:

    • error_reporting(E_ALL);
    • логи в файл
  • Прогоните “карту боли”: какие плагины/модули умирают первыми.

Шаг 1. Совместимость и зависимости

  • Обновите Composer-зависимости под PHP 8.x.
  • В WordPress проверьте плагины/темы на совместимость.
  • В Bitrix проверьте кастомные модули и правки шаблонов.

Шаг 2. Типизация по границам

Не пытайтесь типизировать весь проект. Начните с границ:

  • входные DTO,
  • сервисы интеграции,
  • функции-обёртки над API Bitrix/WP,
  • всё, что “вечно падает”.

Минимальный пример «границы» — обёртка над опцией WordPress с явным возвращаемым типом:

// Было: непонятно, что вернётся
function get_site_option($name) { return get_option($name); }

// Стало: контракт на границе
function get_site_option(string $name): string|int|bool|null
{
    $val = get_option($name);
    if ($val === false) return null;
    return $val;
}

Шаг 3. Рефакторинг “мест, где больно”

Топ-цели:

  • match для маппинга,
  • union types для возвратов,
  • named arguments для читаемости,
  • #[\Override] в местах наследования.

Шаг 4. 8.4-фишки — точечно

  • Asymmetric visibility — почти всегда безопасно и полезно.
  • Property hooks — только там, где реально десятки одинаковых геттеров/сеттеров.

9) 7 признаков, что у вас древний PHP (и как чинить)

  1. Везде array() и “магические массивы” вместо структур
  • ✅ Исправление: DTO + типы возврата + named arguments в обёртках.
  1. Методы возвращают “что угодно”: false|string|array|int
  • ✅ Исправление: union types + нормализация (как в getOption() выше).
  1. switch на 200 строк для статусов/типов
  • ✅ Исправление: match.
  1. Наследование “на честном слове”, постоянно ломают сигнатуры
  • ✅ Исправление: #[\Override] (8.3). (PHP.Watch)
  1. Код полагается на докблоки, а IDE всё равно врёт
  • ✅ Исправление: реальные типы параметров/возвратов + простые DTO.
  1. Тяжёлая логика выполняется в HTTP-запросе
  1. “У нас нет тестов, но мы уверены”
  • ✅ Исправление: хотя бы smoke-тесты/минимальные PHPUnit-тесты на критические сервисы + прогон линтера в CI.

10) Мини-чеклист миграции 7.4 → 8.3/8.4 для Bitrix/WordPress

  • Отдельный стенд с PHP 8.3+
  • Обновлены зависимости/плагины/модули
  • Включены логи и E_ALL на стенде
  • Типизация добавлена на границах (DTO/сервисы/обёртки)
  • match заменил “switch-ад”
  • #[\Override] поставлен в местах наследования
  • 8.4-фишки (asymmetric visibility / hooks) применены точечно и осознанно

Связанные сниппеты

Внутренние ссылки (в тему)

0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!