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.
- Наследование “на честном слове”, постоянно ломают сигнатуры
- Код полагается на докблоки, а 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

Top comments (0)