Пять идей чистого кода в Битрикс проекте
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