PHP 8.3–8.4 для Bitrix и WordPress: типизация, атрибуты, паттерны
Практический гайд по переходу с PHP 7.4 на 8.3–8.4 в Bitrix и WordPress: union types, named arguments, attributes, match и план постепенной модернизации легаси-кода.
Требования
- 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 (и как чинить)
- Везде
array()и “магические массивы” вместо структур
- ✅ Исправление: DTO + типы возврата + named arguments в обёртках.
- Методы возвращают “что угодно”:
false|string|array|int
- ✅ Исправление: union types + нормализация (как в
getOption()выше).
- switch на 200 строк для статусов/типов
- ✅ Исправление:
match.
- Наследование “на честном слове”, постоянно ломают сигнатуры
- ✅ Исправление:
#[\Override](8.3). (PHP.Watch)
- Код полагается на докблоки, а IDE всё равно врёт
- ✅ Исправление: реальные типы параметров/возвратов + простые DTO.
- Тяжёлая логика выполняется в HTTP-запросе
- ✅ Исправление: вынос в агенты/cron/очереди. (Про это у тебя уже есть база: https://viku-lov.ru/blog/backend-cron-bitrix-agents-automation)
- “У нас нет тестов, но мы уверены”
- ✅ Исправление: хотя бы 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) применены точечно и осознанно
Связанные сниппеты
- Bitrix: Option::get с union type возврата
- PHP: match для маппинга статуса заказа и режима WP_Query
- WordPress: обёртка над WP_Query с именованными аргументами
- PHP 8.3: атрибут #[\Override] при переопределении методов
Внутренние ссылки (в тему)
- Bitrix отладка и дебаг: https://viku-lov.ru/blog/bitrix-kint-debug-kint-php
- Автоматизация в Bitrix (cron/агенты): https://viku-lov.ru/blog/backend-cron-bitrix-agents-automation
- CI/CD для PHP/Bitrix: https://viku-lov.ru/blog/cicd-php-bitrix-laravel-github-actions



Комментарии