Пять идей чистого кода в Битрикс проекте

21 мая 2025 г.

Максим Лавриненко, директор по информационным технологиям

  • Разработка крупных интернет-магазинов
  • Техподдержка и доработки веб-проектов

Содержание статьи

Привет! Хочу поделиться несколькими способами улучшить качество вашего Битрикс проекта. Вкратце:

I. Внедряйте зависимости в компоненты
II. Сделайте `$arParams` и `$arResult` объектами
III. Подключайте компоненты, ссылаясь на их классы
IV. Используйте штатную маршрутизацию с Page-компонентами
V. Используйте ORM со своими моделями

I. Внедряйте зависимости в компоненты

Классы компонентов отвечают за подготовку данных для своих шаблонов, но часто туда просачивается какая-то сторонняя логика, которую можно переиспользовать где-то ещё. Выделяйте её в уместные сервисы, потом внедряйте сервисы в компоненты как зависимости.

1. Подключите контейнер зависимостей `composer require php-di/php-di`

2. Создайте функцию `ProjectScope\container`, которая будет возвращать синглтон контейнера

namespace ProjectScope;

use DI\Container;
use DI\ContainerBuilder;

function container(): Container
{
    static $container = null;
    if ($container === null) {
        $container = (new ContainerBuilder())
            ->addDefinitions(__DIR__ . '/../config/di.php')
            ->useAttributes(true)
            ->build();
    }

    return $container;
}

3. Настройте нужные сервисы в `config/di.php` согласно документации PHP-DI
4. Используйте контейнер в ваших компонентах:

use DI\Attribute\Inject;
use ExampleScope\Catalog\Product\ProductRepository;

use function ProjectScope\container;

class ProductCard extends CBitrixComponent
{
    #[Inject] private ProductRepository $productRepo;

    public function executeComponent(): void
    {
        container()->injectOn($this);

        $this->arResult['PRODUCT'] = $this->productRepo->get($this->arParams['PRODUCT_ID']);
    }
}

Теперь компоненты не будут распухать лишней логикой и всё будет разложено "по полочкам", что упростит поддержку.

II. Сделайте `$arParams` и `$arResult` объектами

Стандартные `$arParams` и `$arResult` содержат что угодно. Никто вам не подскажет какие данные можно оттуда получить, да и статический анализ тоже не сработает. Чтобы решить эту проблему, передавайте в свои компоненты параметры в виде объекта, а в самом компоненте тоже пишите в arResult объект. Например, в директории компонента:

1. Создайте директорию `./src` рядом с `class.php`
2. Разместите там классы `final readonly ProductCardParams implements ArrayAccess` и `final ProductCardResult`
3. Добавьте в `composer.json` в `autoload.classmap` сканирование таких директорий, например: `www/local/components/project/*/src` и выполните `composer dump-autoload`
4. Сообщите классу компонента о типе arParams

/** @property ProductCardParams $arParams */
class ProductCard extends CBitrixComponent {}

5. При подключении компонента передайте ему в качестве параметра новый экземпляр `ProductCardParams`
6. В `executeComponent` пишите в `$this->arResult` объект `ProductCardResult`
7. В шаблонах добавьте аннотацию `@var ProductCardResult $arResult`

Теперь при подключении компонента и в шаблонах вы будете чётко видеть набор доступных свойств, а статический анализ подскажет, если где-то не сошлись типы.

III. Подключайте компоненты, ссылаясь на их классы

Можете ли вы легко найти все места в коде, где используется компонент `catalog.section`? Поиск по вхождению не очень помогает, ведь найдётся ещё и `catalog.section.list`. Ненадёжно всё это.

А ещё, только сам класс компонента знает, какие параметры он использует - это его интерфейс. Поэтому дадим возможность подключать компонент через его собственный статичный метод:

ProductCard::include('.default', new ProductCardParams(/* ... */));

Внутри `include` нужно будет подключить компонент как обычно, определив его имя по `static::class`, но придётся решить ещё одну внезапную проблему: Битрикс не сможет найти класс `ProductCard` при подключении компонента, т.к. класс уже автозагружен, а механизм определения класса компонента работает по принципу "какой последний класс объявили после подключения class.php?". Как следствие, придётся через рефлексию влезть в `CBitrixComponent::__classes_map` и дополнить его вручную. Итоговый вид `include`-метода дан ниже.

$componentClass = self::class;
$componentFile = (new ReflectionClass($componentClass))->getFileName();
$componentName = resolveComponentName($componentFile);

$reflection = new ReflectionClass(CBitrixComponent::class);
$componentClassesMap = $reflection->getProperty('__classes_map');
$componentClassesMap->setValue($componentClassesMap->getValue() + [
    str_replace(
        [
            $_SERVER['DOCUMENT_ROOT'],
            // Учитываем возможные симлинки.
            realpath($_SERVER['DOCUMENT_ROOT']),
            '/class.php'
        ],
        '',
        $componentFile
    ) => $componentClass,
]);

global $APPLICATION;
$APPLICATION->IncludeComponent($componentName, $template, $params);

Конечно, эту логику не нужно дублировать в каждом `include`-методе, её стоит выделить в отдельную функцию. `resolveComponentName` предлагается написать самостоятельно (я не жадный, просто в проекте с наработками на эту тему использовался не универсальный подход и делиться им смысла не вижу).

Теперь найти все места подключения компонента, сделанного подобным образом, проще простого. Нажатие одной горячей клавиши и вы перепрыгнули из места подключения компонента в его класс — удобно.

IV. Используйте штатную маршрутизацию с компонентами

В Битрикс появилась возможность роутинга. И даже даётся вот такой пример:

$routes->get('/countries/', new PublicPageController('/countries.php'));

Но имеет смысл упростить себе жизнь и под каждую страницу создавать компонент, а в роутинге лишь на него ссылаться:

1. Создайте наследника `PublicPageController`, чтобы упростить механизм подключения:

namespace ProjectScope;

use Bitrix\Main\Routing\Controllers\PublicPageController;
use Bitrix\Main\Routing\Route;
use Closure;

final class Page extends PublicPageController
{
    public function __construct(
        public readonly Closure $includer,
        public readonly string $template = '/local/routes/page.php'
    ) {
        parent::__construct($template);
    }

    public function includePageComponent(Route $route): void
    {
        $values = $route->getParametersValues()->getValues();
        ($this->includer)(...$values);
    }
}

2. Создайте `/local/routes/page.php`

use Bitrix\Main\Routing\Route;
use ProjectScope\Page;

/**
 * @var Route $route
 * @var Page $controller
 */
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/header.php';
$controller->includePageComponent($route);
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/footer.php';

3. Создавайте любое количество маршрутов на базе `ProjectScope\Page`, например:

use ProjectScope\Page;

$routes->name('product')->get(
    '/products/{id}',
    new Page(static fn (string $id) => ProductCard::include(new ProductCardParams($id)))
);

Теперь настраивать маршруты просто и удобно, а полный список страниц легко считать из директории `local/components/pages`, если вы, конечно, решите все компоненты-страницы размещать именно так.

V. Используйте ORM инфоблоков со своими моделями

Упростите взаимодействие с элементами инфоблоков и позвольте статическому анализу проверять код, работая с вашими собственными моделями:

1. Сгенерируйте служебный файл с аннотациями
2. Создайте собственные классы для самой модели, для её DataManager'а и для коллекции
3. В модели объекта добавляйте собственные методы для более удобного получения данных или какого-то сценария их модификации:

$article->publish(); // вместо $article->setStatus(ArticleStatus::PUBLISHED);

4. В собственных коллекциях добавляйте методы для удобного получения или обработки данных из набора объектов:

// Вот так будет выглядеть код, который хочет получить все email-адреса авторов статей из коллекции:
$emails = $articles->toAuthors()->toEmails();

Без наследования это невозможно и лучше делать это сразу, иначе в тот момент, когда вам в моменте нужно будет выбрать подобные данные, то скорее всего, вы пойдёте простейшим путём и сделаете два цикла:

// Вот так будет выглядеть код, который хочет получить все email-адреса авторов статей из коллекции без собственных моделей:
$authors = [];
foreach ($articles as $article) {
    $authors[] = $article->getAuthor();
}

$emails = [];
foreach ($authors as $author) {
    $emails[] = $author->getEmail();
}

$emails = array_unique($emails);

Проект без собственных моделей рискует со временем утонуть в таком вот коде, дублирующемся от места к месту.

Подытожим

Внедрение зависимостей позволит избежать распухания компонентов, объектное представление `$arParams` и `$arResult` облегчит статический анализ и использование этих переменных, подключение компонентов через классы упростит навигацию по коду, маршрутизация через компоненты-страницы упорядочит структуру, а использование ORM с собственными моделями сделает работу с данными удобнее.

Эти практики не требуют кардинальной перестройки проекта и могут внедряться постепенно: сначала для новых компонентов и объектов, а затем рефакторингом старых, когда освоитесь и найдётся подходящий момент. Удачи!

1