Исключения, Active Record и конфигурация без тяжёлого DI в проектах на 1С‑Битрикс
Вводная: сложность «чистым» ядром
На задачах чуть сложнее каталога плюс корзины штатными средствами 1С‑Битрикс всё ещё можно обойтись, но цена очевидна: растёт количество веток, исключительных случаев и времени на поиск узнаваемого решения. Готовые каркасы из сообщества помогают с компонентом с контроллером или с адаптером над API инфоблоков — они не обязательны, просто экономят несколько первых недель прототипа. Ниже — практика, которая у меня работала именно поверх платформы, без привязки к конкретному стороннему модулю.
Исключения вместо бесконечных флагов
Удобнее выбросить узнаваемое исключение, чем тащить цепочку if (!$ok) через половину вызова. Особенно когда один и тот же сценарий должен понимать контроллер компонента, агент или консольный скрипт. В примере сервис пытается прочесть текст для страницы: сначала из параметров секции инфоблока, затем подставляет текст элемента, а при отсутствии нужного узла откатывается назад через код ошибки константы.
final class NarrativeCopyResolver {
public const MISSING_NODE = 20404;
private string $snippet = "";
private string $body = "";
public function build(array $sectionParams): array {
if ($this->sectionHasAnnouncement($sectionParams)) {
$this->hydrateFromSection($sectionParams);
return $this->payload();
}
try {
$this->hydrateFromRelatedElement();
} catch (\Throwable $e) {
if ((int)$e->getCode() === self::MISSING_NODE) {
$this->hydrateFromSection($sectionParams);
} else {
throw $e;
}
}
return $this->payload();
}
private function sectionHasAnnouncement(array $sectionParams): bool {
return trim(strip_tags((string)($sectionParams["ANNOUNCE"] ?? ""))) !== "";
}
private function hydrateFromRelatedElement(): void {/* выбор CIBlockElement::GetList */}
private function hydrateFromSection(array $sectionParams): void {/* чтение ANNOUNCE / DETAIL_TEXT */}
private function payload(): array {
return [
"snippet" => $this->snippet,
"body" => $this->body,
"visible" => trim(strip_tags($this->snippet . $this->body)) !== "",
];
}
}Сообщение для пользователя удобно отдавать отдельным шаблоном компонента: сервис возвращает имя нужного файла или флаг ошибки — а $this->IncludeComponentTemplate() выбирается по этому ключу без дублирования HTML во всех исключительных кейсах.
Active Record и инфоблок
Стандартные выборки по инфоблоку возвращают массивы, которые хочется упаковать в объект. Если хранилище всё равно MySQL через ядро, Active Record здесь воспринимается как упрощающая прослойка, хотя теория называет его спорным. Важнее дисциплина: свойства синхронизируются перед Add и Update, суммы переводятся в нужные единицы именно при границе БД.
class LedgerBookingSlot {
private object $merchant;
private array $blob = [
"NAME" => "Новая заявка",
"ACTIVE" => "Y",
"PROPERTY_VALUES" => [
"AMOUNT_MINOR" => 0,
"REFERENCE" => "",
"STATUS" => 0,
],
];
public function __construct(object $merchant) {
$this->merchant = $merchant;
}
public function createDraft(): void {
global $USER;
$el = new CIBlockElement();
$row = &$this->blob;
$row["IBLOCK_ID"] = (int)$this->merchant->blockId;
$row["CREATED_BY"] = $USER->GetID();
$row["MODIFIED_BY"] = $USER->GetID();
$minor = $row["PROPERTY_VALUES"]["AMOUNT_MINOR"];
$row["PROPERTY_VALUES"]["AMOUNT_MINOR"] = intdiv((int)$minor, 100);
$id = (int)$el->Add($row);
if ($id <= 0) {
throw new RuntimeException("CIBlockElement::Add вернул 0");
}
$this->blob["ID"] = $id;
}
public function persist(): void {
$el = new CIBlockElement();
$row = $this->blob;
$row["PROPERTY_VALUES"]["AMOUNT_MINOR"] = intdiv((int)$row["PROPERTY_VALUES"]["AMOUNT_MINOR"], 100);
if (!$el->Update((int)$row["ID"], $row)) {
throw new RuntimeException("CIBlockElement::Update не сохранил");
}
}
}В реальном проекте между вызовами я бы вынес дубль преобразования суммы в маленький приватный метод и добавил загрузку GetList(); блок выше задаёт именно паттерн, а не готовое решение платёжной ведомости.
Dependency Injection в чистом виде против «тонкого контекста»
Полномасштабный DI-контейнер в смеси с битриксовыми глобалями и массивами параметров может ухудшить читаемость: одни службы живут через провайдер, другие напрямую тянут $USER, третья половина — это $arParams. Я чаще останавливался на прозрачной передаче конфигурационного файла и явном конструкторе у зависимого класса — без автоматического графа.
$APPLICATION->IncludeComponent(
"partner:payments.bridge",
"",
[
"LEDGER_IBLOCK_ID" => 14,
"SETTINGS_PATH" => "/upload/payments/live.json",
],
false,
);
// в методе класса компонента
public function hydrateSettings(array $params): void {
$diskPath = $_SERVER["DOCUMENT_ROOT"] . '/' . ltrim((string)$params["SETTINGS_PATH"], "/");
if (!is_readable($diskPath)) {
throw new InvalidArgumentException("Нет конфигурации {$diskPath}");
}
$decoded = json_decode((string)file_get_contents($diskPath), false, 512, JSON_THROW_ON_ERROR);
$decoded->runtime = [
"LEDGER_IBLOCK_ID" => (int)$params["LEDGER_IBLOCK_ID"],
];
$this->settings = $decoded;
}Сторона, которая реально платит по заказам, получает объект настроек в конструкторе и не знает про пути на диске — это сохраняет границу между инфраструктурой компонента и доменным действием без лишнего контейнерного DSL.
Сжатый вывод
- Исключения с осмысленными кодами и отдельные шаблоны компонента уменьшают вложенность и дубль разметки.
- Active Record допустим для инфоблоковой записи при жёстко описанном преобразовании свойств между доменной моделью и полями
$fieldsAPI. - Пара JSON в параметрах плюс строго проверенный файл часто понятнее, чем второй параллельный механизм внедрения зависимостей только ради паттерна.
Не хотите копаться сами?
Починю за 1-3 дня. Без предоплаты — оплата по результату.
15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии