Лучшие решения Битрикса

При разработке более-менее сложных решений я столкнулся с тем, что средствами Битрикса их делать можно, но:

  • сложно: с разрастанием функционала поддержка становится все сложнее,
  • долго: приходится искать хорошие технические решения.

Из готового:

Ниже - практически опробованные варианты и обсуждение.

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;
}