Лучшие решения Битрикса
При разработке более-менее сложных решений я столкнулся с тем, что средствами Битрикса их делать можно, но:
- сложно: с разрастанием функционала поддержка становится все сложнее,
- долго: приходится искать хорошие технические решения.
Из готового:
- болванка под компонент с контроллером
- вспомогательная библиотека для работы с Битриксом
Ниже - практически опробованные варианты и обсуждение.
Exception
Выброс исключений - хороший способ организации логики и архитектуры. Статья об этом.
Пример 1
BASeoEngine.php
- часть кода инфоблока, которая выводит на странице SEO-текст. Этот текст может быть прописан для статических страниц прямо в настройках инфоблока, для динамических - в свойствах элемента другого инфоблока. BASeoEngine
сам решает, откуда брать тексты.
- BASeoEngine.php
class BASeoEngine { // контролер public function execute() { if (trim(strip_tags($this->arParams['ANNOUNCE']))) // если у инфоблока есть параметр "Анонс" - берем его + полный текст { $this->getPageText(); } else { try { $this->getBlockText(); // пытаемся получить для динамических страниц } catch (Exception $e) { if ($e->getCode() == self::NO_ELEMENT) // нет элемента - берем полный текст из настроек инфоблока { $this->getPageText(); } else // элемент есть, но нет SEO-текста для него { throw new Exception('Uncatched exception', 0, $e); } } } return array ( 'ANNOUNCE' => $this->announce, 'FULL_TEXT' => $this->fulltext, 'TEXT_IS_PRESENT' => trim(strip_tags($this->announce)) !='' || trim(strip_tags($this->fulltext)) != '' ); } }
Пример 2
Сообщения об ошибках удобно выводить в отдельном шаблоне. Например, проверить авторизацию, и в случае неуспеха - вывести сообщение и форму входа.
- component.php
if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die(); require_once('BACassaAdd.php'); $engine = new BACassaAdd($arParams); $arResult = $engine->execute(); $this->IncludeComponentTemplate($arResult['TEMPLATE']); // подключаем нужный шаблон
- BACassaAdd.php
class BACassaAdd { public function execute() { try { $this->accessGranted(); } catch (Exception $e) { return array( 'TEMPLATE' => 'error', // шаблон ошибки 'TEXT' => $e->getMessage() ); } } private function accessGranted() { global $USER; if(!$USER->IsAuthorized()) return false; } }
Active Record
Штатными средствами с элементами возможно работать как массивами.
Но работать с массивами неудобно. Есть классы. Если мы работаем с классом, который в итоге должен мапиться на базу данных, хорошее решение - это использовать Active Record
. Это позволяет:
- создать полноценный класс => все достоинства ООП
- загружать и сохранять объект в базу данных
- Active Record считается анти-паттерном, однако:
- Битрикс имеет только один тип хранилища БД
- именно так можно меньшей кровью решить вопрос сохранения «прямо на месте»
Пример Active Record в Битриксе
- SberbankOrder.php
// Платежка Сбербанка - Active Record class SberbankOrder { private $config, $details = array( "ID" => 0, // обязательные свойства "IBLOCK_SECTION_ID" => false, "PROPERTY_VALUES"=> array( // свойства, добавляемые отдельно 'AMOUNT' => 0, // стоимость - в рублях (так хранится в БД), в классе - в копейках, преобразование в момент сохранения/загрузки. 'EXTEND_ID' => '', // внешний ID 'PAY_URI' => '', // URI по которому производится оплата 'STATUS' => 0, // статус 'RESP' => '' // ответ Сбербанка по API ) ); // сохраняем конфиг (DI-контейнер) public function __construct($config) { $this->config = $config; } // создание нового объекта public function create() { $element = new CIBlockElement; global $USER; $details =& $this->details; $details['NAME'] = "Новый заказ"; $details['ACTIVE'] = "Y"; $details['CREATED_BY'] = $USER->GetID(); $details['MODIFIED_BY'] = $USER->GetID(); $details['IBLOCK_ID'] = $this->config->module['BLOCK_ID']; if(!($details['ID'] = $element->Add($details))) throw new Exception("Невозможно создать заказ"); $description = preg_replace('~\{orderId\}~ui', $details['ID'], $this->config->module['DESCRIPTION']); $details['NAME'] = $details['PREVIEW_TEXT'] = $description; $this->save(); } // загрузить объект по ID public function load($orderId) { $arFilter = array('ID' => $orderId, 'IBLOCK_ID' => $this->config->module['BLOCK_ID']); $arSelect = array("ID", "CODE", "NAME", "PROPERTY_AMOUNT", "PROPERTY_EXTEND_ID", "PROPERTY_PAY_URI", "PROPERTY_STATUS", "PROPERTY_RESP"); $item = CIBlockElement::GetList(array(), $arFilter, false, false, $arSelect); if($item->SelectedRowsCount() != 1) throw new Exception("Такого заказа не существует"); $result = $item->Fetch(); $details =& $this->details; $details['ID'] = $result['ID']; $details['PROPERTY_VALUES']['AMOUNT'] = $result['PROPERTY_AMOUNT_VALUE'] * 100; // переводим в копейки $details['PROPERTY_VALUES']['EXTEND_ID'] = $result['PROPERTY_EXTEND_ID_VALUE']; $details['PROPERTY_VALUES']['PAY_URI'] = $result['PROPERTY_PAY_URI_VALUE']; $details['PROPERTY_VALUES']['STATUS'] = $result['PROPERTY_STATUS_ENUM_ID']; $details['PROPERTY_VALUES']['RESP'] = $result['PROPERTY_RESP_VALUE']; $this->save(); } // сохранить объект public function save() { $element = new CIBlockElement; $rawDetails = $this->details; $rawDetails['PROPERTY_VALUES']['AMOUNT'] = $rawDetails['PROPERTY_VALUES']['AMOUNT'] / 100; // переводим в рубли if(!($element->Update($rawDetails['ID'], $rawDetails))) throw new Exception("Невозможно сохранить заказ"); } // установить свойства public function setProperties(array $properties) { if(count($properties) > 0) foreach($properties as $key => $value) { $this->details['PROPERTY_VALUES'][$key] = $value; } $this->save(); return $this; } // получить ID public function getId() { return $this->details['ID']; } // прочие методы объекта }
Dependency Injection
В рамках крупной задачи этот паттерн только запутал понимание организации работы системы. Смешались Битрикс-массивы, DI, схожие, но разные классы. Посмотрим в дальнейшем.
DI-контейнер
Пожалуй, самый удачный паттерн.
Так можно указать в компоненте на файл с настройками:
- index.php
<?$APPLICATION->IncludeComponent( "vendor:pay.gateway", "", Array( "BLOCK_ID" => "14", "CONFIG" => "/merchant/real.json" ) );?>
Так загрузить где-то в компоненте:
- component.php
public function initConfig($moduleConfig) { $configFileName = $_SERVER['DOCUMENT_ROOT'].'/'.ltrim($moduleConfig['CONFIG'], '/'); if(!file_exists($configFileName)) throw new Exception("Error loading config $configFileName"); $this->config = json_decode(file_get_contents($configFileName)); if(!is_object($this->config)) throw new Exception("Wrong config $configFileName"); $this->config->module = $moduleConfig; }
Так передать в другой объект и сохранить в объекте:
- di.php
public function __construct($config) { $this->config = $config; }