Table of Contents
Директория 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 действия:
- Подключается autoloader для php-классов
- Подключаются внешние файлы для разделения ответственности
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;
}
);
В своей структуре мы решили избавиться от этой проблемы путем разделения ответственности между кодом обработчика события и кодом подписки на событие.
- Файл
events.php
содержит перечень событий и классов-обработчиков подписанных на них. - Мы не пишем код обработчика, а указываем на статический метод класса который является обработчиком события
Содержимое файла /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 Модуль
Для себя мы выделяем два подхода к разработке: “Разработка через модули” (далее модули) и “Разработка через решение”. В чем же отличие “Модуля” от “Решения”, ведь каждый из этих подходов подразумевает какой-то код, структурированный особым образом и все это служит для достижения определенной цели?
Модуль это пере-используемое решение, т.е. общий случай, который можно дополнить или изменить не вмешиваясь в логику его работы. Если модуль не подходит к другим проектам или требуется его значительное переписывание из проекта в проект, то это не модуль. Однако это не единственный критерий - некоторые разработчики любят прятать решение - оформлять его как модуль, но при этом оно не в состоянии жить без “другого” кода.
Как понять, что у вас “решение оформленное под модуль”?
- Для пере-использования недостаточно копировать папку из
*/modules/<your_name>/
на другую установку. - Для обновления вашего модуля нужно выполнять действия руками (заменять файлы, выполнять код или создавать инфоблоки).
Зачастую многие веб-студии поставляют модули, а потом дополнительно поставляют модули, чтобы обновлять их модули (например модуль миграции). Этот подход противоречит подходу “модульности” самого Битрикса, хоть и имеет право быть.