projects/org/@biz-lideravto/it/docs/site/MODULES_GUIDE.md

Руководство по модулям: использовать, доработать, создать

← Назад в оглавление

Версия: 1.0
Дата: 2026-02-27
Проект: new.lideravto.ru — Drupal 11.3.3 + Commerce 3.x
Аудитория: разработчики Drupal


Текущее состояние: 9 модулей в /tst/

Все модули находятся в /it/lider-drupal/tst/ и ещё не задеплоены в production (/app/).
205 тестов написаны и проходят.

tst/modules/custom/
├── dru_lider_catalog        284 KB  — каталог товаров
├── dru_lider_models         120 KB  — модели грузовиков
├── dru_lider_importer        76 KB  — импорт CSV
├── dru_lider_parts           72 KB  — запчасти и OEM
├── dru_lider_compatibility   64 KB  — матрица совместимости
├── dru_lider_seo             28 KB  — SEO и редиректы
├── dru_lider_setup           20 KB  — установка
├── dru_lider_products        16 KB  — расширение Commerce
└── dru_lider_frontend         4 KB  — шаблон и меню

Схема зависимостей

dru_lider_setup
    └── инициализирует всё окружение

dru_lider_models
    └── taxonomy: бренды, модели, поколения, системы

dru_lider_parts
    └── зависит от: dru_lider_models
    └── OEM нормализация, хранение

dru_lider_catalog
    └── зависит от: dru_lider_parts, dru_lider_models
    └── content type lider_part, страницы деталей

dru_lider_products
    └── зависит от: dru_lider_catalog
    └── Commerce Product + Variation

dru_lider_compatibility
    └── зависит от: dru_lider_catalog, dru_lider_models
    └── связи деталь  модель

dru_lider_importer
    └── зависит от: все выше
    └── CSV  Drupal entities

dru_lider_seo
    └── зависит от: dru_lider_catalog, dru_lider_compatibility
    └── canonical, redirects, sitemap

dru_lider_frontend
    └── зависит от: все выше (данные из БД)
    └── шаблоны, меню, вёрстка

Детальный анализ каждого модуля


dru_lider_setup

Что делает:
Устанавливает начальную конфигурацию Drupal для проекта. Создаёт vocabulary, типы контента, поля, начальные taxonomy terms для брендов и моделей. Запускается один раз при деплое.

Статус: ✅ Готов к использованию

Что делать: Использовать как есть. Запуск:

drush en dru_lider_setup
drush lider:setup:install

Ключевые команды:
- drush lider:setup:install — полная установка
- drush lider:setup:check — проверка конфигурации


dru_lider_models

Что делает:
Taxonomy vocabularies для структуры грузовых автомобилей:
- lider_brands — бренды (Scania, Volvo, Mercedes...)
- lider_models — модели (R5, FH, Actros...)
- lider_generations — поколения (5-я серия, MP4...)
- lider_systems — системы (Двигатель, КПП, Мосты...)
- lider_nodes — узлы (Форсунка, Насос, Балансир...)

Статус: ✅ Готов, небольшая доработка

Что добавить:
- Поле canonical_priority в lider_brands — порядок приоритета моделей для выбора canonical URL (массив: ["R", "G", "P", "F"] для Scania)
- Поле engine_family в lider_models — к какому семейству двигателей относится (для donor expansion)

Оценка доработки: 15 минут

Файл конфига после доработки: it/data/references/canonical_priority.yaml

# canonical_priority.yaml — порядок выбора canonical модели
scania:
  priority: [R, G, P, F, L]
  description: "R  магистральный, самый массовый"

volvo:
  priority: [FH, FM, FMX, FL, FE]
  description: "FH  флагманский тягач"

mercedes:
  priority: [Actros, Arocs, Antos, Axor, Atego]
  description: "Actros  основной магистральный"

man:
  priority: [TGX, TGS, TGM, TGL, TGA]
  description: "TGX  тяжёлый магистральный"

daf:
  priority: [XF, CF, LF]
  description: "XF  флагман DAF"

renault:
  priority: [T, C, K, D, Master]
  description: "T  основной магистральный"

iveco:
  priority: [Stralis, Trakker, Eurocargo, Daily]
  description: "Stralis  основная магистральная линейка"

dru_lider_parts

Что делает:
Entity lider_oem для хранения OEM-номеров:
- Поля: oem_original, oem_normalized, oem_canonical
- Метод normalize() — реализация алгоритма нормализации
- Связь с lider_part node

Статус: ✅ Готов, мелкая доработка

Что добавить:
- Поле brand_hint — подсказка для нормализации (например, "Mercedes" помогает определить A-prefix)
- Метод detectBrand() по паттерну OEM

Оценка доработки: 10 минут


dru_lider_catalog

Что делает:
Content type lider_part — основная страница детали:
- Все поля карточки товара
- Отображение: блок основных данных, блок совместимости, блок кросс-номеров
- Breadcrumbs, page title

Статус: ✅ Готов к использованию

Что добавить:
- Поле needs_review (boolean) для флага проверки наименования — 5 минут


dru_lider_products

Что делает:
Commerce Product типа lider_spare_part:
- Product variation с полями: SKU (= OEM), цена (RUB), состояние (новый/аналог/оригинал)
- Add to cart форма
- Интеграция с Drupal Commerce checkout

Статус: ✅ Готов к использованию

Что делать: Использовать как есть.


dru_lider_compatibility

Что делает:
Entity lider_compatibility — связи деталь ↔ модель:
- Поля: part_id, brand_id, model_id, source, confidence
- Создаёт записи из прямых данных CSV

Статус: ⚠️ Частично готов — есть прямые связи, нет расширения

Что добавить:

A. Donor Expansion — расширение по донорам

Новый сервис DonorExpander:

// dru_lider_compatibility/src/Service/DonorExpander.php

class DonorExpander {
  public function expand(string $oem, string $system): int {
    // 1. Найти прямые совместимости для OEM
    // 2. Для каждой совместимости найти агрегат (двигатель/коробка)
    // 3. Найти все другие грузовики с тем же агрегатом
    // 4. Создать записи совместимости с source=donor_expansion
    // Вернуть: количество созданных записей
  }
}

Файл доноров it/data/references/donors.yaml:

engines:
  dc13_xpi_5:
    name: "Scania DC13 XPI (5 серия)"
    trucks: [scania_r5, scania_g5, scania_p5, scania_f5]
    years: "2010-2016"
  dc13_xpi_6:
    name: "Scania DC13 XPI (6 серия)"
    trucks: [scania_r6, scania_g6, scania_p6, scania_s]
    years: "2017+"
  d13_fh4:
    name: "Volvo D13 (FH4)"
    trucks: [volvo_fh4, volvo_fm4, volvo_fmx4]
    years: "2013-2020"
  d13_fh5:
    name: "Volvo D13 (FH5)"
    trucks: [volvo_fh5, volvo_fm5]
    years: "2021+"
  om471_mp4:
    name: "Mercedes OM471 (MP4)"
    trucks: [mercedes_actros_mp4, mercedes_arocs]
    years: "2012+"
  d2676_euro6:
    name: "MAN D2676 Euro 6"
    trucks: [man_tgx_e6, man_tgs_e6, man_tgm_e6]
    years: "2013+"

gearboxes:
  grs895:
    name: "Scania GRS895"
    trucks: [scania_r5, scania_g5, scania_p5]
  grs905:
    name: "Scania GRS905"
    trucks: [scania_r6, scania_g6]
  i_shift_fh4:
    name: "Volvo I-Shift (FH4)"
    trucks: [volvo_fh4, volvo_fm4]

axles:
  rs1344:
    name: "Volvo RS1344 (задний мост)"
    trucks: [volvo_fh3, volvo_fh4, renault_t]
    note: "AB Volvo концерн  общий мост"

Drush команда: drush lider:import:compatibility --source=donors

B. Парсинг комментариев

Метод parseTextCompatibility(string $text): array — на базе регулярных выражений из TZ_IMPORT.md.

Оценка доработки: 60 минут (DonorExpander + donors.yaml + парсинг комментариев)


dru_lider_importer

Что делает:
Batch-импорт из CSV. Читает файл → нормализует OEM → создаёт nodes + Commerce products.

Статус: ⚠️ Частично готов — есть insert, нет delta и валидации

Что добавить:

A. Дельта-логика — сравнение нового прайса с текущей базой
- Читать oem_normalized из БД в memory (bulk query)
- Сравнивать каждую строку CSV — NEW / UPDATED / DISAPPEARED
- Сохранять только изменённое (не писать UNCHANGED)

B. NamingValidator сервис

// dru_lider_importer/src/Service/NamingValidator.php
class NamingValidator {
  const STOP_WORDS = ['Кронштейн', 'Болт', 'Гайка', ...];

  public function validate(string $name, string $node = '', string $system = ''): ValidationResult {
    // Вернуть: исправленное имя + флаг needs_review
  }
}

C. Отчёт импорта
- Подсчёт статистики по всем статусам
- Форматированный вывод в CLI
- Сохранение в lider_import_log

Оценка доработки: 45 минут


dru_lider_seo

Что делает:
- Canonical tags (<link rel="canonical" href="self">)
- URL aliases по шаблону /zapchasti/{brand}/{model_or_any}/{system}/{name}-{oem}/
- XML Sitemap (все страницы деталей — они все canonical)
- 301 редиректы для format-error OEM

Правило URL:
- 1 бренд, 1 модель → /scania/r5/...
- 1 бренд, 2+ модели → /scania/any/...
- 2+ бренда → /priority-brand/any/... (Scania → Volvo → Mercedes → MAN → DAF → Renault → Iveco → Liebherr)

Статус: ⚠️ Частично готов — базовый canonical, нужен алгоритм ANY, нужен OemNormalizerSubscriber

Что добавить:

A. OemNormalizerSubscriber — НОВЫЙ КОМПОНЕНТ, ВЫСОКИЙ ПРИОРИТЕТ

Динамический обработчик format-error редиректов. Один класс вместо 2 400 статических правил. Обрабатывает только ошибки формата OEM (A-prefix, PE-suffix, 742-prefix, дефисы) → 301 на canonical URL.

// dru_lider_seo/src/EventSubscriber/OemNormalizerSubscriber.php

namespace Drupal\dru_lider_seo\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\RedirectResponse;

class OemNormalizerSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents(): array {
    return [KernelEvents::REQUEST => ['onRequest', 30]];
  }

  public function onRequest(RequestEvent $event): void {
    $path = $event->getRequest()->getPathInfo();

    // Проверяем что путь — страница детали
    if (!preg_match('|^/zapchasti/[^/]+/[^/]+/[^/]+/[^/]+-([^/]+)/$|', $path, $matches)) {
      return;
    }

    $oem_from_url = $matches[1];
    $oem_normalized = $this->normalizeOem($oem_from_url);

    // Если OEM уже нормализован — ничего не делаем
    if ($oem_from_url === $oem_normalized) {
      return;
    }

    // Ищем canonical страницу для нормализованного OEM
    $canonical_path = $this->lookupCanonicalPath($oem_normalized);
    if (!$canonical_path) {
      return; // OEM не найден в базе — пусть 404 обработает Drupal
    }

    // 301 на canonical
    $event->setResponse(new RedirectResponse($canonical_path, 301));
  }

  private function normalizeOem(string $oem): string {
    // Кириллица → латиница
    $map = ['А'=>'A','В'=>'B','Е'=>'E','О'=>'O','Р'=>'R','С'=>'C','Х'=>'X'];
    $oem = strtr(strtoupper($oem), $map);
    // Удалить разделители
    $oem = preg_replace('/[\s\-\.\,]/', '', $oem);
    // A-prefix Mercedes
    if (preg_match('/^A(\d{10})$/', $oem, $m)) $oem = $m[1];
    // PE-suffix DAF
    if (str_ends_with($oem, 'PE') && strlen($oem) > 4) $oem = substr($oem, 0, -2);
    // 742-prefix Renault/Volvo
    if (preg_match('/^742(.{7,})$/', $oem, $m)) $oem = $m[1];
    return $oem;
  }

  private function lookupCanonicalPath(string $oem_normalized): ?string {
    // Запрос в БД: найти canonical URL alias для нормализованного OEM
    // SELECT alias FROM path_alias WHERE oem_canonical = :oem AND is_canonical = 1
    return \Drupal::service('dru_lider_seo.canonical_resolver')
      ->getPathByOem($oem_normalized);
  }
}

Регистрация в dru_lider_seo.services.yml:

dru_lider_seo.oem_normalizer_subscriber:
  class: Drupal\dru_lider_seo\EventSubscriber\OemNormalizerSubscriber
  arguments: ['@dru_lider_seo.canonical_resolver']
  tags:
    - { name: event_subscriber }

B. CanonicalUrlBuilder — URL по правилу ANY

Алгоритм в CanonicalUrlBuilder:

public function buildCanonicalUrl(array $compatible_brands_models): string {
  // Определяем приоритетный бренд
  $brand = $this->selectTopPriorityBrand($compatible_brands_models);

  // Если для этого бренда только одна модель → конкретная модель в URL
  $models_for_brand = $this->getModelsForBrand($compatible_brands_models, $brand);
  $model_slug = count($models_for_brand) === 1
    ? $models_for_brand[0]->slug
    : 'any';

  return "/zapchasti/{$brand->slug}/{$model_slug}/...";
}

private function selectTopPriorityBrand(array $brands_models): BrandTerm {
  // Приоритет: scania > volvo > mercedes > man > daf > renault > iveco > liebherr
  $priority = ['scania'=>1,'volvo'=>2,'mercedes'=>3,'man'=>4,'daf'=>5,'renault'=>6,'iveco'=>7,'liebherr'=>8];
  usort($brands_models, fn($a,$b) => ($priority[$a->brand->slug]??99) <=> ($priority[$b->brand->slug]??99));
  return $brands_models[0]->brand;
}

Оценка доработки: 30 минут


dru_lider_frontend

Что делает:
Twig шаблоны для страниц, CSS стили. Пока минимальный скелет.

Статус: ⚠️ Базовый скелет — нужна реализация

Что сделать:

A. Верхнее меню по брендам
- Автогенерируется из taxonomy lider_brands
- Горизонтальное, 8 брендов
- Выбранный бренд подсвечен

B. Левое контекстное меню
- На странице бренда: список моделей этого бренда
- На странице модели: список систем
- На странице системы: список узлов
- Хлебные крошки на каждом уровне

C. Карточка товара — три блока:

Блок 1: Основные данные

<div class="part-main">
  <h1>{{ node.title }}</h1>
  <div class="part-oem">OEM: {{ node.field_oem_canonical }}</div>
  <div class="part-price">{{ product.price }}</div>
  {{ form.add_to_cart }}
</div>

Блок 2: Совместимые модели (из lider_compatibility)

<div class="part-compatibility">
  <h3>Совместимые модели</h3>
  {% for brand, models in compatibility %}
    <div class="brand-group">
      <strong>{{ brand }}</strong>:
      {% for model in models %}
        <a href="/zapchasti/{{ brand.slug }}/{{ model.slug }}/">{{ model.name }}</a>
      {% endfor %}
    </div>
  {% endfor %}
</div>

Блок 3: Аналоги (кросс-номера, отфильтрованные по текущей модели)

<div class="part-crosses">
  <h3>Аналоги других производителей</h3>
  {#
    cross_parts передаётся уже отфильтрованным по current_model_id:
    - На /scania/r5/... → только кроссы совместимые с R5
    - На /scania/g5/... → только кроссы совместимые с G5
    Кроссы несовместимые с текущей моделью не показываются.
  #}
  {% if cross_parts %}
    {% for cross in cross_parts %}
      {% if cross.has_page %}
        <a href="{{ cross.url }}">{{ cross.manufacturer }}: {{ cross.oem }}</a>
      {% else %}
        <span>{{ cross.manufacturer }}: {{ cross.oem }}</span>
      {% endif %}
    {% endfor %}
  {% else %}
    <p class="no-crosses">Аналоги для данной модели не найдены</p>
  {% endif %}
</div>

PHP (Block plugin / Controller) — передача отфильтрованных кросс-OEM:

// Контекст модели — из URL (может быть 'any' или конкретный slug)
$model_slug = $this->routeMatch->getParameter('model');
$current_model_id = ($model_slug !== 'any')
  ? $this->modelResolver->getIdBySlug($model_slug)
  : null;

// Запрос: кросс-OEM основного + фильтр по модели
$cross_parts = $this->crossRefService->getCrossesForModel(
  oem_canonical: $oem_canonical,
  model_id: $current_model_id   // null (any) → все кроссы
);

На странице /brand/any/... (current_model_id = null) показываются все кроссы. На странице /brand/r5/... — только кроссы совместимые с R5.

Оценка: 45 минут


Новые компоненты — создать с нуля

OemNormalizerSubscriber

NamingValidator

DonorExpander


Итоговая таблица работ

Модуль / Компонент Действие Что именно Оценка
dru_lider_setup Использовать Без изменений
dru_lider_models Доработать Поля canonical_priority, engine_family 15 мин
dru_lider_parts Доработать Поле brand_hint 10 мин
dru_lider_catalog Доработать Поле needs_review 5 мин
dru_lider_products Использовать Без изменений
dru_lider_compatibility Доработать DonorExpander, парсинг комментариев 60 мин
dru_lider_importer Доработать Дельта-логика, NamingValidator, отчёт 45 мин
dru_lider_seo Доработать OemNormalizerSubscriber, canonical по узлу 30 мин
dru_lider_frontend Реализовать Меню (верх + лево), карточка товара 45 мин
OemNormalizerSubscriber Создать EventSubscriber (код готов) 20 мин
NamingValidator Создать Сервис валидации 15 мин
DonorExpander Создать Сервис расширения 30 мин
ИТОГО ~4,5 часа

Порядок выполнения

Волна 1 (независимые, можно параллельно):
1. Доработка dru_lider_models — поле canonical_priority (15 мин)
2. Создание donors.yaml и canonical_priority.yaml (20 мин)
3. Доработка dru_lider_parts — brand_hint (10 мин)

Волна 2 (после Волны 1):
4. Создание NamingValidator (15 мин)
5. Доработка dru_lider_importer — дельта + валидация + отчёт (45 мин)
6. Создание DonorExpander (30 мин)

Волна 3 (после Волны 2):
7. Создание OemNormalizerSubscriber (20 мин)
8. Доработка dru_lider_seo — canonical алгоритм (30 мин)
9. Реализация dru_lider_frontend — меню + карточка (45 мин)

Волна 4:
10. Деплой всего в /app/ и первый тестовый импорт


Документ подготовлен для проекта new.lideravto.ru
Дата: 2026-02-27


← Назад в оглавление