GrabDuck

Первые шаги в Drupal 8. Создание модуля разделов на сайте. | 1С 7.7 8.1, бухгалтерия, ...

:

Долго думал продолжать ли работать с Drupal или уже спрыгнуть на какой-нибудь фреймворк, т.к. постоянные изменения API уже достали. И все таки решил пока остаться. Поэтому взялся разбираться с Drupal 8.

Начал свое изучение с парочки русскоязычных статей и с версии beta2. Спасибо этим статьям, но уже на beta2 некоторые вещи из статей не работали и приходилось подправлять.
Например вызов функции контроллера отвечающая за содержимое в примерах может встречаться типа такой:
_content: '\Drupal\allexx\Controller\Grabber::StartGrabber'
А нынче она выглядит уже вот так:
_controller: '\Drupal\allexx\Controller\Grabber::StartGrabber'
Примеров в интернете пока мало, а на русском языке и подавно, поэтому как реализовать те или иные моменты искал прямо в ядре.

Для чего я полез сразу в создание модулей. Задача у меня была такая, чтобы во-первых создать граббер. Во-вторых организовать на сайте несколько разделов товаров со своим блоком меню. Теоретически можно, конечно, было попробовать реализовать это за счет названий ссылок и на их основе выводить или не выводить блоки. Но во-первых, на данный момент еще нет модуля pathauto, а во-вторых что делать, если посетитель заходит на нейтральную по отношению к разделам страницу (контакты в частности).
Про граббер пока не буду рассказывать, пока что про вторую задачу.

Теперь к практике.
Первое с чем столкнулся при разработке модулей - это не выводится описание ошибок. Чтобы включить эту функцию идем в sites/default/settings.php
Там в конце файла добавляем строку:

$config["system.logging"]["error_level"] = "verbose";

Можно сразу добавить еще одну настройку, которая будет полезна при верстке. После изменения вашей темы по умолчанию нужно каждый раз чистить кэш. Чтобы временно отключить это кэширование редактируем файл sites/default/services.yml. Устанавливаем там значение cache: false, после этого очистить кэш и можно спокойно верстать. Подробности о настройках файла services.yml здесь.

Модуль я свой назвал allexx. Соответственно в папке modules создаем папку allexx.
У меня в этой папке сейчас файлы:

allexx.info.yml
allexx.links.menu.yml
allexx.module
allexx.routing.yml

Мы создаем сейчас просто блоки с разделами и подразделами и для этой задачи нам понадобится лишь один файл allexx.info.yml (остальные файлы мне понадобились для создания граббера). Содержимое файла:

# Комментарии указываются в yml через хэштег.
 
# Название модуля (отображается в списке модулей).
name: Allexx
# Описание модуля. Пишется исключительно на английском.
description: 'Allexx module'
# Объявляем что это модуль.
type: module
# Версия ядра для которого модуль.
core: 8.x
# Версия модуля.
version: 1.0
# Раздел в модулях где будет располагаться наш модуль.
#package: Examples

Это я просто откуда-то содрал. Нужно заявить о нашем модуле, вот этим файлом мы об этом заявили.
Дальше переходим непосредственно к разработке наших блоков. Для этого создаем папку
allexx\src\Plugin\Block
Я делал два блока. Один блок для выбора основных разделов сайта. Второй блок для вывода подразделов соответствующих выбранному основному разделу. Соответственно в папке с блоками создаем два файла:

RazdelyBlock.php
MyCatalogBlock.php

Содежримое файла RazdelyBlock.php

<?php
namespace Drupal\allexx\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Url;
 
/**
 * Provides a 'Demo' block.
 *
 * @Block(
 *   id = "razdely_block",
 *   admin_label = @Translation("Razdely block"),
 * )
 */
 
class RazdelyBlock extends BlockBase {
 
  /**
   * {@inheritdoc}
   */
  public function build() {
    $vid = 'razdely';
    $parent = 0;
    // Загружаем вместе с сущностями (true)
    $tree = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($vid,$parent,1,true);
    $taxonomy_list = array();
    foreach ($tree as $item) {
      //$url = Url::fromRoute('entity.taxonomy_term.canonical',array('taxonomy_term' => $item->tid));
      //$internal_link = \Drupal::l($item->name, $url);
      //$taxonomy_list[] = $internal_link;
      $taxonomy_list[] = entity_view($item,'teaser'); // Рабочий вариант
    }
    $res = array(
      '#theme' => 'item_list',
      '#items' => $taxonomy_list,
    );
    return $res;
 
  }
}

Содежримое файла MyCatalogBlock.php

<?php
 
namespace Drupal\allexx\Plugin\Block;
 
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Url;
 
/**
 * Provides a 'Demo' block.
 *
 * @Block(
 *   id = "my_catalog_block",
 *   admin_label = @Translation("My Catalog block"),
 * )
 */
 
class MyCatalogBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {
    $vid = 'razdely';
    if (!empty($_GET['cur_tid'])) {
      $cur_tid = $_GET['cur_tid'];
    }
    else{
      $cur_tid = 0;
      $current_path = \Drupal::request()->attributes->get('_system_path');
      $path_args = explode('/', $current_path);
      if (($path_args[0] == 'taxonomy') || ($path_args[0] == 'catalog')) {
        $cur_tid = $path_args[2];
      }
      elseif (($path_args[0] == 'node') && (isset($path_args[1]))) {
        $tids = \Drupal::entityManager()->getStorage('taxonomy_term')->getNodeTerms(array($path_args[1]),array($vid));
        if (sizeof($tids) > 0) {
          $cur_tid = key(current($tids));
        }
      }
    }
 
    if ($cur_tid == 0){
      if (!empty($_SESSION['razdel'])) {
        $top_parent = $_SESSION['razdel'];
      }
      else{
        $top_parent = 1;
      }
    }
    else{
      $parents = \Drupal::entityManager()->getStorage('taxonomy_term')->loadAllParents($cur_tid);
      end($parents); // move the internal pointer to the end of the array
      $top_parent = key($parents);
    }
    $_SESSION['razdel'] = $top_parent;
 
    $tree = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($vid,$top_parent);
 
    $taxonomy_list = array();
    $cur_depth = 0;
    foreach ($tree as $item) {
      $active = '';
      $span = '';
      $first = '';
      if ($cur_tid == $item->tid) $active = ' active';
      if ($item->depth == 0) $span = '<span class="list-item"></span>';
      if ($cur_depth != $item->depth) {
        $cur_depth = $item->depth;
        if ($item->depth != 0) $first = ' first';
      }
 
      // Если по таксономии, то по роуту подтягивается алиас
      //$url = Url::fromRoute('entity.taxonomy_term.canonical',array('taxonomy_term' => $item->tid));
      //$internal_link = \Drupal::l($item->name, $url);
 
      if ($item->depth == 0) {
        $internal_link = '<span class="title">'.$item->name.'</span>';
      }
      else {
        $lookup_path = \Drupal::service('path.alias_storage')->lookupPathAlias('catalog/term/'. $item->tid, 'ru');
        if (empty($lookup_path)) {
          $lookup_path = 'catalog/term/'. $item->tid;
        }
        $internal_link = '<a href="/'.$lookup_path.'">'.$item->name.'</a>';
      }
 
      //$taxonomy_list[] = $internal_link;
      $taxonomy_list[] = array(
        '#wrapper_attributes' => array('class' => array('depth-'.$item->depth.$active.$first)),
        '#markup' => $span.$internal_link,
      );
    }
 
    $res = array(
      '#theme' => 'item_list',
      '#items' => $taxonomy_list,
    );
    return $res;
 
    //return $output;
  }
}

Я не стал убирать закомментированные строки, т.к. они пригодятся для общего случая, когда используются ссылки таксономии (taxonomy/term/tid). Я же, чтобы не трогать вывод материалов по стандартным ссылкам таксономии, сделал views с адресом catalog/tid. Этим адресам вручную назначил алиасы. Поэтому в коде мастерил ссылки, в том числе принудительно привязывая алиасы.

В конце кода тоже показал, что сейчас нельзя просто так возвращать строковое значение. Хотя еще в бета 2 я мог возвращать строку и пока тестировал мог смотреть что у меня там получается. А вот в бета 3 уже получил сообщение:

InvalidArgumentException: _content controllers are not allowed to return strings. You can return a render array, a html fragment or a response object.

И еще попутно пришлось подправить ошибку в ядре, т.к. получал вот такое сообщение:

Undefined variable: all_tids in Drupal\taxonomy\TermStorage->getNodeTerms() (line 355 of core\modules\taxonomy\src\TermStorage.php).

Исправляем так:

$results = array();
$all_tids = array(); // XA
foreach ($query->execute() as $term_record) {
  $results[$term_record->node_nid][] = $term_record->tid;
  $all_tids[] = $term_record->tid;
}

Хотел было описать как делал контроллер для граббера с загрузкой материалов с полями-картинками, полями-файлами(множественными), терминами таксономии и т.д. Контроллер то сделал, но описывать пока времени нет. А просто так выкладывать кучу кода вроде как не правильно.