Директория local уже упрощает жизнь разработчику, но одно ее наличие не решает всех поставленных перед разработкой задач и проблем. Компания 1С-Битрикс продвигает модульную разработку, однако это является актуальным только для продукта “Битрикс: Управление сайтом”, продукт “Битрикс24” поставляется как монолитное приложение под конкретного клиента и подход который разработчики используют при работе с “Битрикс: Управление сайтом” обычно не подходит к “Битрикс24”.

В данной статье мы рассмотрим практики компании “ИТ-Интегратор Фьюжн” для разработки проектов.

Этот подход не претендует на истину в последней инстанции. Выбор любого технического решения должен быть обусловлен исключительно техническими показателями, однако этот подход проверен годами и является хорошей отправной точкой для начала разработки по Битрикс24 в целом.

Типовая структура проекта

Мы уже рассмотрели содержимое директории local которая продиктована разработчиками продукта, теперь рассмотрим содержимое директории local/php_interface которую “ИТ-Интегратор Фьюжн” использует в проектах:

  • /local/php_interface
    • /classes/ - php-классы, подключаемые автолоадером;
    • /console/ - консольные скрипты и приложения;
    • /install - Инсталлеры/Миграторы;
    • /init.php
    • /kernel.php - core-модули (env-файл, servicelocator);
    • /events.php - декларирование подписок на события;
    • /legacy.php - устаревшие механики;
    • /composer.json
    • /vendors/ - директория для подключаемых composer пакетов;

Рассмотрим содержимое каждого скрипта и директории в отдельности.

init.php

Это центральный файл проекта, который подключается для каждого хита, поэтому он должен быть максимально простым и коротким.

Содержимое файла /local/php_interface/init.php:

/**
 * - /local/php_interface/classes/{Path|raw}/{*|raw}.php
 * - /local/php_interface/classes/{Path|ucfirst,lowercase}/{*|ucfirst,lowercase}.php
 */
spl_autoload_register(function($sClassName)
{
    $sClassFile = __DIR__.'/classes';

    if ( file_exists($sClassFile.'/'.str_replace('\\', '/', $sClassName).'.php') )
    {
        require_once($sClassFile.'/'.str_replace('\\', '/', $sClassName).'.php');
        return;
    }

    $arClass = explode('\\', strtolower($sClassName));
    foreach($arClass as $sPath )
    {
        $sClassFile .= '/'.ucfirst($sPath);
    }
    $sClassFile .= '.php';
    if (file_exists($sClassFile))
    {
        require_once($sClassFile);
    }
});

/**
 * Project bootstrap files
 */
foreach( [
    /**
     * File for other kernel data:
     *    Service local integration
     *    Env file with local variables
     *        external service credentials
     *        feature enable flags
     */
    __DIR__.'/kernel.php',

    /**
     * Events subscribe
     */
    __DIR__.'/events.php',

    /**
     * Include composer libraries
     */
    __DIR__.'/vendor/autoload.php',

    /**
     * Include old legacy code
     *   constant initiation etc
     */
    __DIR__.'/legacy.php',
    ]
    as $filePath )
{
    if ( file_exists($filePath) )
    {
        require_once($filePath);
    }
}
unset($filePath);

Мы не поддерживаем концепцию больших init.php файлов, так как его подобное действие вводит чрезмерную сложность в процессе разработки, а так же делает командную разработку более болезненной.

В данном файле концептуально происходит 2 действия:

  1. Подключается autoloader для php-классов
  2. Подключаются внешние файлы для разделения ответственности

Autoloader

Обычно Битрикс24 использует конкретный клиент под конкретные бизнес-задачи, поэтому оформление кода в модули является затратной по времени затеей, ведь проект по сути и есть один большой модуль. Для удобного подключения классов мы используем специальный код, который при вызове класса подключает php-файл с ним в код страницы.

Например, при использовании класса \Fusion\SomeClass\SomeOtherClass\SuperClass будет произведена автоматическая попытка найти его в следующих файлах:

  • /local/php_interface/classes/Fusion/SomeClass/SomeOtherClass/SuperClass.php
  • /local/php_interface/classes/Fusion/Someclass/Someotherclass/Superclass.php

В случае если файл найден, он будет подключен require_once функцией к текущему выполнению страницы.

В самом файле класса должен быть определен корректное пространство имен (namespace) иначе класс не будет подключен.

events.php

Наиболее частой причиной разрастания файл init.php является наличие кода обработчика события непосредственно в подписке на событие.

Плохой пример как это делать в init.php:

$eventManager = \Bitrix\Main\EventManager::getInstance();

// DO NOT DO THIS! Bad practice! Do not store code of your handler with code that subscribe to event 
$eventManager->addEventHandlerCompatible(
    'crm',
    'OnBeforeCrmContactAdd',
    function( &$arFields )
    {
        // Very large code line 1
        // Very large code line 2
        // Very large code line 3
        
        // Very large code line 199
        // Very large code line 200
        // Very large code line 201
        return true;
    }
);

В своей структуре мы решили избавиться от этой проблемы путем разделения ответственности между кодом обработчика события и кодом подписки на событие.

  1. Файл events.php содержит перечень событий и классов-обработчиков подписанных на них.
  2. Мы не пишем код обработчика, а указываем на статический метод класса который является обработчиком события

Содержимое файла /local/php_interface/events.php:

/**
 * This file contains a full list of custom event handlers
 * Code the handlers need NOT be contained in this file. 
 * It needs to be made relevant to the PSR-[0-4] structure, classes
 */

$eventManager = \Bitrix\Main\EventManager::getInstance();

/**
 * For new core of bitrix use
 *     $eventManager->addEventHandler( #module#, #handler#, [#namespace#, #function#]);
 * 
 * For old core of bitrix use
 *     $eventManager->addEventHandlerCompatible( #module#, #handler#, [#namespace#, #function#]);
 */

// ... Your event handlers here

/* End of file. Do not past code after this line! */
unset($eventManager);

Обращаю внимание, что несмотря на отсутствие явных технических отличий между addEventHandler и addEventHandlerCompatible (в одном случае входным аргументом будет \Bitrix\Main\Event или его наследник, а в другом случае параметрами запроса) мы рекомендуем использовать разделение и для старых событий явно использовать Compatible метод. Это позволит не запутаться в тех случаях, когда обработчик модифицирует входящие аргументы.

Пример содержимого файла events.php (вместо строчки с “Your event handlers here”):

$eventManager->addEventHandlerCompatible(
    'crm',
    'OnBeforeCrmContactAdd',
    [
      '\Fusion\Crm\Contact\CheckData',
      'handleBeforeCrmContactAdd'
    ]
);

Файл /local/php_interface/Fusion/Crm/Contact/CheckData.php:

namespace Fusion\Crm\Contact;

class CheckData
{
  /**
   * Handle crm::OnBeforeCrmContactAdd event
   *    - Do some action
   * @params &array $arFields - Changed contact fields 
   * @return bool
   */
  public static function handleBeforeCrmContactAdd( &$arFields )
  {
    // Very large code line 1
    // Very large code line 2
    // Very large code line 3
    
    // Very large code line 199
    // Very large code line 200
    // Very large code line 201
    return true;
  }
}

kernel.php

Стандартом при работе с большими проектами является наличие нескольких сред исполнения кода, например production север и developer сервер и если с переносом кода все более менее понятно, то с переменными окружения (адреса серверов, доступы для внешних сервисов) не все так очевидно.

Для себя мы выделили два наиболее практичных и удобных подходов:

  • Использованием ini-файла (мы называем его .env)
  • Использование механизма options (хранение в b_options таблице, редактирование в административной панели)

Рассмотрим преимущества и недостатки каждого из методов:

Рассматриваемый параметр .env адм. панель
Хранение переменных окружения
Редактирование пользователем *
Перенос через VCS **
Попадают в штатную резервную копию

Легенда таблицы:

✅ - наличие возможности

❌ - отсутствие возможности

* - Под возможностью редактировать подразумевается как наличие в Битрикс24 средства по работе с таблицами, так и возможность разработать административный интерфейс. Редактирование env-файла в данном случае не рекомендуется.

** - Существует возможность переноса изменений через код, однако при проведении обновления нет возможности определить какой из параметров является верным (тот что был введен или пришел при обновлении).

Мы рассмотрим работу с ini-файлом .env, так как создание страниц в публичной или административной панели хорошо изложена в курсах Bitrix Framework и Контент-менеджер.

Bitrix Framework имеет средства для работы с переменными окружения используемых в процессе выполнения страницы и не имеет штатных механик позволяющих загрузить какие-либо другие файлы, чтобы объединить их с реальными env-переменными. Мы будем использовать .env-файл располагаемый выше директории DOCUMENT_ROOT как хранилище конфигурационных данных зависящих от окружения среды.

Файл находится вне DOCUMENT_ROOT директории для обеспечения безопасности. Так как это простой текстовый файл, может возникнуть ситуация, когда файл будет открыт пользователем в браузере. Нахождение его вне корневой директории предотвращает эту проблему.

Помните, что из-за того что файл находится вне директории проекта он не попадает в бекапы формируемые системой, поэтому разработчикам необходимо самостоятельно предусмотреть механизмы поставки, проверки и корректировки данного файла после восстановления

Содержимое файла /local/php_interface/kernel.php:

/**
 * This file store additional requirements for project
 * etc: env data, service locator initialization
 * 
 * @see https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=14032&LESSON_PATH=3913.5062.14032
 */

use \Bitrix\Main;

/**
 * Env section
 * File moved from DOCUMENT_ROOT directory for security reasons
 */
$envPath = dirname($_SERVER['DOCUMENT_ROOT']);

if ( file_exists($envPath.'/.env') )
{
    $env = Main\Context::getCurrent()->getEnvironment();
    $curEnv = $env->getValues();
    $iniParams = \parse_ini_file($envPath.'/.env', true, INI_SCANNER_TYPED);
    foreach ($iniParams as $key => $value)
    {
        $curEnv[$key] = $value;
    }
    $env->set($curEnv);
    unset($curEnv);
    unset($iniParams);
}
unset($envPath);

/**
 * Service locator section
 *   if exist and accepted on platform version
 */
if ( class_exists('\Bitrix\Main\DI\ServiceLocator') )
{
    $serviceLocator = Main\DI\ServiceLocator::getInstance();

    /**
     * service location naming convention:
     *     * must contant vendor.
     *     * must be lowercase
     *         OK: 'fusion.exchange.service', 'sber.payment.service'
     *         BAD: 'FUSION_SOME_SERVICE', 'COOL_SERVICE', 'TaSk.SerViCE.i18n'
     * 
     * Examples:
     * 
     * $serviceLocator->addInstanceLazy('fusion.some.service', [
     *     'constructor' => static function () use ($serviceLocator) {
     *         return new \Fusion\SomeModule\Services\SecondService('foo', 'bar');
     *     }
     * ]);
     * 
     * $serviceLocator->addInstanceLazy('fusion.some.service', [
     *     'className' => \Fusion\SomeModule\Services\SomeService::class,
     * ]);
     * 
     */
    
    // ... Your services here
    
    /* Do not past code after this line in this block*/
    unset($serviceLocator);
}

legacy.php

Помимо событий и явных точек расширения в виде kernel-раздела в некоторых частях системы существуют и другие возможности, например определение функции custom_mail, определение констант и другое. Чтобы придерживаться концепции маленьких файлов и ясной структуры необходимо иметь место для хранения подобных механизмов - им выступает файл legacy.php.

В идеальной среде данный файл должен отсутствовать или не иметь содержимого в нем, однако в реальном мире этот файл является хорошим место для хранения определенных констант в проекте или устаревших функций, например GetGlobalID() поставляемой в некоторых установках.

/**
 * This file contain old legacy code include constant initialization
 * All code in this file in next updates should be moved into new
 * app structure if can.
 * 
 * For constant definitions:
 *     - each constant must contain a reasonable prefix and postfix
 *     - each constant must use english named
 * Example: 
 *     STRUCTURE_IBLOCK_ID is a valid constant
 *     INFOBLOK_VIZUALNOY_STRUCTURI is a invalid
 */

Решение vs Модуль

Для себя мы выделяем два подхода к разработке: “Разработка через модули” (далее модули) и “Разработка через решение”. В чем же отличие “Модуля” от “Решения”, ведь каждый из этих подходов подразумевает какой-то код, структурированный особым образом и все это служит для достижения определенной цели?

Модуль это пере-используемое решение, т.е. общий случай, который можно дополнить или изменить не вмешиваясь в логику его работы. Если модуль не подходит к другим проектам или требуется его значительное переписывание из проекта в проект, то это не модуль. Однако это не единственный критерий - некоторые разработчики любят прятать решение - оформлять его как модуль, но при этом оно не в состоянии жить без “другого” кода.

Как понять, что у вас “решение оформленное под модуль”?

  1. Для пере-использования недостаточно копировать папку из */modules/<your_name>/ на другую установку.
  2. Для обновления вашего модуля нужно выполнять действия руками (заменять файлы, выполнять код или создавать инфоблоки).

Зачастую многие веб-студии поставляют модули, а потом дополнительно поставляют модули, чтобы обновлять их модули (например модуль миграции). Этот подход противоречит подходу “модульности” самого Битрикса, хоть и имеет право быть.