Drupal → Drupal 8/9 Dev FAQ
Обновляемый список коротких вопросов и ответов по Drupal 8/9.
Все консольные команды рассчитаны на выполнение из корня друпала. Пользователям Windows перед выполнением команд в cmd.exe нужно заменить разделитель директорий с /
на \
, т.е. вместо vendor/bin/drush
писать vendor\bin\drush
. Так же пользователям Windows надо брать в двойные кавычки аргументы с символом ^
, например composer require "foo/bar:^1.0"
.
Содержание
- Работа с Composer
- Общие вопросы
- Работа с сущностями и полями (Entity API, Field API)
- Темизация (Theming API, Render API)
- Работа с Twig
- Работа с формами (Form API, Ajax API)
- Работа с базой данных (Database API)
- Работа с сервисом entity.query (Entity Query API)
- Работа с меню и адресами, навигация, роутинг (Menu API, Url, Routing API)
- Работа с Views
- Работа с JavaScript
- Работа с многоязычностью (Language API, Translation, i18n, Multilingual)
Работа с Composer
cd path/to/drupal wget -O composer.phar https://getcomposer.org/composer-stable.phar
После этого можно пользоваться композером через команду
php composer.phar
, например:php composer.phar require drupal/devel
composer create-project drupal/recommended-project
Способ 2 (папка vendor будет в web root):
composer create-project drupal/legacy-project
Подробнее
composer update drupal/core* --with-all-dependencies vendor/bin/drush updb
Для версий меньше 8.8.0 сначала обновляемся до 8.8.0 по статье никлана, а потом до актуальной версии способом выше.
Перед обновлением обязательно делаем бэкап файлов и базы!
composer require drupal/devel
RC версия:
composer require "drupal/field_group:^3.0-rc2"
Dev версия:
composer require drupal/devel:1.x-dev
Замечание 1: надо всегда следить за тем, какую версию скачивает композер и какая актуальная на странице модуля. Он может легко скачать модуль на пару версий меньше, если например в composer.json есть "prefer-stable": true
, а у модуля доступна только rc версия.
composer update drupal/devel --with-all-dependencies
Иногда модуль содержит несколько под-модулей и в каждом свой
composer.json
. Такие модули можно обновлять по маске:composer update drupal/commerce* --with-all-dependencies
Если надо обновить модуль на новую мажорную версию, то необходимо явно указать её в require:
composer require "drupal/devel:^3.0"
Если надо обновить со стабильной до dev версии:
composer require drupal/devel:4.x-dev
vendor/bin/drush pm-uninstall devel composer remove drupal/devel
prohibits
, которая выдаст причины:composer prohibits drupal/core 8.9.2
composer outdated
Только drupal модули:
composer outdated drupal/*
composer show drupal/core
symfony/process
composer why symfony/process
composer show --all drupal/devel
Общие вопросы
vendor/bin/drush site-install standard --db-url=mysql://root:root@localhost/drupal --account-pass=admin
Подробнее.
// src/Controller/ModulenameController.php namespace Drupal\modulename\Controller; use Drupal\Core\Controller\ControllerBase; class ModulenameController extends ControllerBase { public function helloWorld() { return [ '#markup' => 'Hello, World!', ]; } }
# modulename.routing.yml modulename.hello_world: path: '/hello/world' defaults: _controller: '\Drupal\modulename\Controller\ModulenameController::helloWorld' _title: 'Hello, World!' requirements: _permission: 'access content'
Или с помощью drush:
vendor/bin/drush generate controller
// src/Plugin/Block/HelloWorldBlock.php namespace Drupal\modulename\Plugin\Block; use Drupal\Core\Block\BlockBase; /** * @Block( * id = "hello_world_block", * admin_label = @Translation("Hello world block"), * ) */ class HelloWorldBlock extends BlockBase { public function build() { return [ '#title' => 'It\'s block title', // Optional '#markup' => 'Hello, World!', ]; } }
Или с помощью drush:
vendor/bin/drush generate block
use Drupal\Core\Cache\UncacheableDependencyTrait; /** * @Block(...) */ class MyBlock extends BlockBase { use UncacheableDependencyTrait; // <--- public function build() { ... } }
Способ 2:
/** * @Block(...) */ class MyBlock extends BlockBase { public function build() { ... } public function getCacheMaxAge() { return 0; // <--- } }
Стоит помнить, что отключение кэширование блока отключит кэширование страницы на которой этот блок выводится.
class MyBlock extends BlockBase { ... /** * {@inheritdoc} */ public function access(AccountInterface $account, $return_as_object = FALSE) { $access = FALSE; if (...) { $access = TRUE; } return $return_as_object ? AccessResult::allowedIf($access) : $access; } }
$site_mail = \Drupal::config('system.site')->get('mail'); // Вернёт строку "site@example.com"
Подробнее.
$site_config = \Drupal::config('system.site'); $site_name = $site_config->get('name'); $site_slogan = $site_config->get('slogan'); $site_mail = $site_config->get('mail');
Способ 1 - на странице admin/config/development/performance
нажать кнопку "Clear all caches".
Способ 2 - выполнить в консоли:
vendor/bin/drush cache-rebuild
Способ 3 - выполнить php функцию:
drupal_flush_all_caches();
// Для отступов используется два пробела function example() { $foo = 'bar'; } // Между бинарными операторами ставится пробел $a = 1 + 2; // Good $a=1+2; // Bad // Блок elseif/else начинается с новой строки if (...) { ... } elseif (...) { ... } else { ... } // Массивы создаются с помощью короткого синтаксиса $array = ['foo', 'bar']; // Good $array = array('foo', 'bar'); // Bad // Строки заключаются в одиночные кавычки $string = 'foo'; // Good $string = "foo"; // Bad // Переменные именуются с помощью нижнего подчёркивания $my_variable = 'foo'; // Good $myVariable = 'foo'; // Bad // Названия классов именуются в CamelCase class MyFirstClass { } // Переменные класса именуются в camelCase class MyFirstClass { public $myFirstVariable; }
Подробнее.
\Drupal::messenger()->addMessage('Hello World!');
use Drupal\Component\Utility\Timer; Timer::start('test'); sleep(1); debug(Timer::read('test') . ' ms');
\Drupal::logger('modulename')->info('Hello World!'); \Drupal::logger('modulename')->error('It\'s error');
Список доступных методов. Подробнее про Logging API.
// MODULENAME.module /** * Implements hook_page_attachments(). */ function MODULENAME_page_attachments(array &$page) { $page['#attached']['library'][] = 'MODULENAME/LIBRARYNAME'; }
Подробнее.
/** @var \Drupal\Core\Session\AccountProxyInterface $current_user */ $current_user = \Drupal::currentUser();
Функция
\Drupal::currentUser()
возвращает прокси-объект AccountProxy, которого для большинства случаев будет достаточно, но если нужна именно сущность пользователя, то:$current_user_uid = \Drupal::currentUser()->id(); /** @var \Drupal\user\Entity\User $current_user */ $current_user = \Drupal\user\Entity\User::load($current_user_uid);
if (\Drupal::currentUser()->isAuthenticated()) { ... } if (\Drupal::currentUser()->isAnonymous()) { ... }
if (\Drupal::currentUser()->hasPermission('administer site configuration')) { ... }
if (in_array('administrator', \Drupal::currentUser()->getRoles())) { // Current user has role "administrator" } if (array_intersect(['administrator', 'editor'], \Drupal::currentUser()->getRoles())) { // Current user has role "administrator" or "editor" }
// $_GET $nid = \Drupal::request()->query->get('nid'); $all_get_params = \Drupal::request()->query->all(); // $_POST $nid = \Drupal::request()->request->get('nid'); $all_post_params = \Drupal::request()->request->all(); // $_COOKIE $nid = \Drupal::request()->cookies->get('nid'); $all_cookie_params = \Drupal::request()->cookies->all();
$escaped_string = \Drupal\Component\Utility\Html::escape('<b>Hello</b>'); // Переменная будет содержать "<b>Hello</b>"
Подробнее.
$string = new \Drupal\Component\Render\FormattableMarkup('My name is: @name', [ '@name' => $name, ]);
@variable
— текст будет пропущен через Html::escape()
.%variable
— текст будет пропущен через Html::escape()
и обёрнут в <em></em>
.:variable
— текст будет пропущен через Html::escape()
и UrlHelper::stripDangerousProtocols()
.Чтобы запретить обрабатывать текст с помощью Html::escape()
нужно передать в плэйсхолдер объект MarkupInterface
:
$string = new \Drupal\Component\Render\FormattableMarkup('My name is: @name', [ '@name' => \Drupal\Core\Render\Markup::create('<b>Dries</b>'), ]);
$truncated_text = \Drupal\Component\Utility\Unicode::truncate($text, 128);
$string = \Drupal\Component\Serialization\Json::encode(['foo' => 'bar']); // Вернёт строку {"foo":"bar"} $array = \Drupal\Component\Serialization\Json::decode('{"foo":"bar"}'); // Вернёт массив ['foo' => 'bar']
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */ $date_formatter = \Drupal::service('date.formatter'); $formatted_date = $date_formatter->format(1558730206, 'short'); $formatted_date = $date_formatter->format(1558730206, 'custom', 'd.m.Y');
vendor/bin/drush config-import --partial --source=modules/modulename/config/install
\Drupal::service('plugin.manager.mail')->mail()
, куда передаём необходимые переменные:\Drupal::service('plugin.manager.mail')->mail( 'modulename', 'example_mail_key', 'to@gmail.com', 'en', ['myvar' => 123] );
Плюс реализуем хук
hook_mail()
, в котором формируем заголовок и текст письма:/** * Implements hook_mail(). */ function MODULENAME_mail(string $key, array &$message, array $params) { if ($key == 'example_mail_key') { $message['subject'] = 'Example email subject'; $message['body'][] = 'Example email body. myvar = ' . $params['myvar']; } }
Можно обойтись без реализации хука
hook_mail()
, если указать модуль system
и специальным образом сформировать массив $params
:\Drupal::service('plugin.manager.mail')->mail('system', 'example_mail_key', 'example@gmail.com', 'en', [ 'context' => [ 'subject' => 'Subject', 'message' => \Drupal\Core\Render\Markup::create('Message'), ], ]);
/** * Implements hook_mail_alter(). */ function MODULENAME_mail_alter(array &$message) { if ($message['module'] == 'commerce' && $message['key'] == 'order_receipt') { $message['subject'] = 'New subject'; $message['body'] = ['New body']; } }
Подробнее
$ip = \Drupal::request()->getClientIp();
$current_timestamp = \Drupal::time()->getCurrentTime(); // Вернёт число 1598281486
$transliterated_string = \Drupal::transliteration()->transliterate('Привет Мир', 'ru'); // Вернёт строку "Privet Mir"
// src/Controller/ExampleController.php class ExampleController extends ControllerBase { public function export() { $response = new Response(); $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); $response->headers->set('Content-Disposition', 'attachment; filename="example.txt"'); $response->setContent('Hello World!'); return $response; } }
// 404 throw new NotFoundHttpException(); // 403 throw new AccessDeniedHttpException();
// modulename.tokens.inc /** * Implements hook_token_info(). */ function MODULENAME_token_info() { $token_info['tokens']['node']['example-token'] = [ 'name' => 'Example node token', ]; return $token_info; } /** * Implements hook_tokens(). */ function MODULENAME_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $replacements = []; if ($type == 'node' && !empty($data['node'])) { $node = $data['node']; /** @var NodeInterface $node */ foreach ($tokens as $name => $original) { if ($name == 'example-token') { $replacements[$original] = 'It\'s example token for node ' . $node->id(); } } } return $replacements; }
Подробнее.
$string = \Drupal::token()->replace('String with [node:title] token', [ 'node' => \Drupal\node\Entity\Node::load(123), ]);
<script>
в head:/** * Implements hook_page_attachments(). */ function MODULENAME_page_attachments(array &$attachments) { $attachments['#attached']['html_head'][] = [[ '#tag' => 'script', '#value' => "document.documentElement.classList.add('js')", ], 'has_js']; }
Добавлять
#attached
можно добавлять в любой рендер-массив, необязательно в hook_page_attachments()
.
['#attached']['html_head']
. Пример добавления метатега description из своего контроллера:class ExampleController extends ControllerBase { public function exampleAction() { $meta_description = [ '#tag' => 'meta', '#attributes' => [ 'name' => 'description', 'content' => ['#plain_text' => 'Random text'], ], ]; return [ '#attached' => [ 'html_head' => [ [$meta_description, 'description'] ], ], ... ]; } }
$data = ['foo' => 'bar']; \Drupal::moduleHandler()->alter('my_data', $data);
После этого другие модули смогут альтерить с помощью хука
hook_my_data_alter
:function MODULENAME_my_data_alter(&$data) { $data['foo'] = 'baz'; }
Подробнее.
$formatted_text = check_markup('Hello world', 'full_html');
$comment_count = (int)$entity->get('field_comment')->comment_count;
/** * Implements hook_query_TAG_alter(): comment_filter. */ function hook_query_comment_filter_alter(QueryAlterableInterface $query) { if ($query instanceof PagerSelectExtender) { $order_by = &$query->getOrderBy(); unset($order_by['c.cid']); $query->orderBy('c.created', 'DESC'); } }
function _get_comment_page(int $entity_id, int $comment_cid, int $per_page) { $comment_index = \Drupal::database() ->select('comment_field_data') ->condition('entity_id', $entity_id) ->condition('status', 1) ->condition('cid', $comment_cid, '>=') ->countQuery() ->execute() ->fetchField(); return ceil($comment_index / $per_page) - 1; }
$entity_id
- id сущности с комментариями
$comment_cid
- id комментария
$per_page
- число комментариев на страницу
// src/EventSubscriber/ModulenameEventSubscriber.php class ModulenameEventSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events['example_event_name'][] = ['onExampleEventName', 0]; return $events; } /** * Event callback. */ public function onExampleEventName(Event $event) { ... } }
# modulename.services.yml services: modulename.event_subscriber: class: Drupal\modulename\EventSubscriber\ModulenameEventSubscriber tags: - { name: event_subscriber }
vendor/bin/drush eval "print_r(gd_info())"
hook_install()
:// modulename.install /** * Implements hook_install(). */ function modulename_install() { module_set_weight('modulename', 123); }
Если нужно изменить вес уже установленного модуля, то пользуемся hook_update_N()
:
// modulename.install /** * Set module weight. */ function modulename_update_8001() { module_set_weight('modulename', 123); }
Работа с сущностями и полями (Entity API, Field API)
// С помощью статического метода load() $node = \Drupal\node\Entity\Node::load(123); $term = \Drupal\taxonomy\Entity\Term::load(234); // С помощью сервиса entity_type.manager $node = \Drupal::entityTypeManager()->getStorage('node')->load(123); $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(234);
$field_value = $entity->get('field_name')->value;
У некоторых полей название свойства, в котором хранится значение, может отличаться, например у полей типа entity reference это
target_id
:$category_id = $entity->get('field_category')->target_id;
Бывают составные поля с несколькими свойствами:
$body_text = $node->get('body')->value; $body_format = $node->get('body')->format;
Бывают поля с вычисляемыми (computed) свойствами, которые не хранятся в базе:
$category = $entity->get('field_category')->entity;
$values = array_map(function (FieldItemInterface $item) { return $item->value; }, iterator_to_array($entity->get('field_name')));
foreach ($entity->get('field_name') as $item) { $item_value = $item->value; }
$node = \Drupal\node\Entity\Node::create([ 'type' => 'article', 'title' => 'My article', 'body' => 'Article body', ]); $node->save();
Подробнее
$category_field = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page')['field_category']; $category_field_storage = $field_definition->getFieldStorageDefinition();
Для настраиваемых полей можно так же пользоваться методом
\Drupal\field\Entity\FieldConfig::loadByName()
и \Drupal\field\Entity\FieldStorageConfig::loadByName()
.
if (!$entity->get('field_example')->isEmpty()) { // field_example not empty }
$label_key = $entity->getEntityType()->getKey('label'); // Вернёт "title" для ноды и "name" для термина
// Если есть доступ к объекту сущности $field_example_label = $entity->get('field_example')->getFieldDefinition()->getLabel(); // Иначе $field_example_label = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page')['field_example']->getLabel();
if ($entity->hasField('field_example')) { ... }
Важный момент — hasField()
проверяет наличие поля у бандла сущности, а не наличие значения в этом поле.
Если доступа к объекту сущности нет, то:
$entity_type = 'node'; $entity_bundle = 'page'; $field_name = 'field_example'; $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name); if ($field_storage && in_array($entity_bundle, $field_storage->getBundles())) { ... }
$date_start = $node->get('field_daterange')->start_date; /** @var \Drupal\Core\Datetime\DrupalDateTime $date_start */ $date_end = $node->get('field_daterange')->end_date; /** @var \Drupal\Core\Datetime\DrupalDateTime $date_end */ $days_between_dates = $date_end->diff($date_start)->format('%a');
if ($node->get('title')->getFieldDefinition() instanceof BaseFieldDefinition) { // Поле title является базовым } if ($node->get('field_example')->getFieldDefinition() instanceof FieldConfigInterface) { // Поле field_example является настраиваемым }
$field_example_items = $entity->get('field_example'); $allowed_values = options_allowed_values($field_example_items->getFieldDefinition()->getFieldStorageDefinition(), $entity); $value_label = $allowed_values[$field_example_items->value];
// Способ 1 $node_type = $node->get('type')->entity; // Способ 2 $node_type = NodeType::load($node->bundle());
$file = \Drupal\file\Entity\File::load(123); /** @var \Drupal\file\FileInterface $file */ $file_relative_url = $file->createFileUrl(); // /sites/default/files/example.jpg $file_absolute_url = $file->createFileUrl(FALSE); // http://example.com/sites/default/files/example.jpg $file_uri = $file->getFileUri(); // public://example.jpg
$file = $entity->get('field_file')->entity; /** @var \Drupal\file\FileInterface $file */ $file_url = $file->createFileUrl(); // /sites/default/files/example.jpg $file_uri = $file->getFileUri(); // public://example.jpg
$file_uri = 'public://example.jpg'; if ($files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $file_uri])) { $file = current($files); /** @var FileInterface $file */ }
$uri = 'public://example.jpg'; $file = \Drupal\file\Entity\File::create([ 'uri' => $uri, 'filename' => basename($uri), ]); $file->save();
$file_id = 123; $entity->get('field_file')->appendItem(['target_id' => $file_id]); $entity->save();
или так:
$file = \Drupal\file\Entity\File::load(123); $entity->get('field_file')->appendItem($file); $entity->save();
$file = system_retrieve_file('http://example.com/image.jpg', 'public://image.jpg', TRUE);
$node = \Drupal\node\Entity\Node::load(123); $node->delete();
Можно воспользоваться drush:
vendor/bin/drush entity-delete node 123
$entity_storage = \Drupal::entityTypeManager()->getStorage('node'); $entities = $entity_storage->loadMultiple([1, 2, 3]); $entity_storage->delete($entities);
Можно воспользоваться drush:
vendor/bin/drush entity-delete node 1,2,3
article
(способ подходит для удаления небольшого количества сущностей, до тысячи):$entity_storage = \Drupal::entityTypeManager()->getStorage('node'); $entities = $entity_storage->loadByProperties(['type' => 'article']); $entity_storage->delete($entities);
Можно с помощью drush:
vendor/bin/drush entity-delete node --bundle=article
\Drupal\Component\Utility\Environment::setTimeLimit(0); /** @var TermStorageInterface $term_storage */ $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); $terms = $term_storage->loadByProperties(['vid' => 'category']); foreach ($terms as $term) { $term->save(); }
\Drupal\Component\Utility\Environment::setTimeLimit(0); /** @var NodeStorageInterface $node_storage */ $node_storage = \Drupal::entityTypeManager()->getStorage('node'); $nodes = $node_storage->loadByProperties(['type' => 'article']); foreach ($nodes as $node) { $node->save(); }
/** @var EntityFieldManagerInterface $entity_field_manger */ $entity_field_manger = \Drupal::service('entity_field.manager'); $entity_reference_fields = $entity_field_manger->getFieldMapByFieldType('entity_reference');
/** @var EntityFieldManager $entity_field_manager */ $entity_field_manager = \Drupal::service('entity_field.manager'); $article_fields = $entity_field_manager->getFieldDefinitions('node', 'article');
$path = \Drupal::service('path.alias_manager')->getPathByAlias('/example-url-alias'); if (preg_match('/node\/(\d+)/', $path, $matches)) { $node = \Drupal\node\Entity\Node::load($matches[1]); }
/** * Implements hook_entity_update(). */ function hook_entity_update(EntityInterface $entity) { $field_example_items = $entity->get('field_example'); $original_field_example_items = $entity->original->get('field_example'); if ($field_example_items ->hasAffectingChanges($original_field_example_items, $field_example_items->getLangcode())) { // field_example changed } }
/** @var TermStorageInterface $term_storage */ $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); // Способ 1 - по id термина. Получает только непосредственных детей термина 123. // Результат не отсортирован по весу. $children_terms = $term_storage->loadChildren(123, 'category'); /** @var TermInterface[] $children_terms */ // Способ 2 - по объекту термина. Получает только непосредственных детей термина $term. // Результат не отсортирован по весу. $children_terms = $term_storage->getChildren($term); /** @var TermInterface[] $children_terms */ // Способ 3 - по id термина. Получает всех детей термина 123 независимо от вложенности. // Переданный термин в массив не добавляется. // Результат отсортирован по весу терминов. $children_terms = $term_storage->loadTree('category', 123, NULL, TRUE); /** @var TermInterface[] $children_terms */
/** @var TermStorageInterface $term_storage */ $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); // Получает всех родителей независимо от глубины. В массиве так же возвращается и исходный термин. $all_parents = $term_storage->loadAllParents(123); /** @var TermInterface[] $all_parents */ // Получает непосредственных родителей термина 123. В большинстве случаев это будет массив из одного элемента. $direct_parents = $term_storage->loadParents(123); /** @var TermInterface[] $direct_parents */ // Непосредственные родители так же доступны через поле термина "parent" $direct_parents_items = $term->get('parent'); /** @var EntityReferenceFieldItemListInterface $direct_parents_items */ // Самый верхний родитель $all_parents = $term_storage->loadAllParents(123); /** @var TermInterface[] $all_parents */ $root_parent = current($all_parents);
Следует помнить, что непосредственных родителей у термина может быть несколько. Так же следует помнить, что метод
$term->get('parent')->isEmpty()
всегда возвращает FALSE
, поэтому проверять есть ли у термина родители надо с помощью if ($term->get('parent')->entity)
.
$node = \Drupal\node\Entity\Node::load(123); $node->set('field_tags', NULL); // Remove all values $node->save();
$node = \Drupal\node\Entity\Node::load(123); // Три способа удалить значение по его индексу unset($node->get('field_tags')[2]); $node->get('field_tags')->offsetUnset(2); $node->get('field_tags')->removeItem(2); // Способ удалить значение если индекс неизвестен $node->get('field_tags')->filter(function ($item) { return $item->target_id != 123; // Удаляем значение с id термина 123 }); $node->save();
sort()
, который можно использовать в uasort()
. Пример сортировки сущностей модуля Domain Access:/** @var \Drupal\domain\DomainInterface[] $domains */ $domains = \Drupal\domain\Entity\Domain::loadMultiple(); uasort($domains, '\Drupal\domain\Entity\Domain::sort');
Темизация (Theming API, Render API)
1. Найти hook_theme, в котором объявлен шаблон.
2. Скопировать информацию о шаблоне в hook_theme своего модуля.
3. Скопировать twig шаблон в свой модуль.
4. Сбросить кэш.
Должно получиться как-то так:
modules/custom/MODULENAME/MODULENAME.module
/** * Implements hook_theme(). */ function MODULENAME_theme() { return [ 'node' => [ 'render element' => 'elements', ], ]; }
modules/custom/MODULENAME/templates/node.html.twig
{{ content }}
Важно помнить, что вес модуля должен быть больше того, в котором объявлен шаблон.
Пример переопределения шаблона node--article.html.twig
.
MODULENAME.module
:
/** * Implements hook_theme(). */ function improvements_test_theme() { return [ 'node__article' => [ 'base hook' => 'node', ], ]; }
templates/node--article.html.twig
:<article> {{ content }} </article>
У каждого блока есть машинное имя, которое прописывается в настройках блока при его добавлении в регион, запоминаем его. Дальше копируем файл core/modules/block/templates/block.html.twig
в папку templates
вашей темы. Переименовываем этот файл по шаблону block--machine-name.html.twig
(где machine-name
машинное имя нужного блока, нижние подчёркивания заменяются на тире). Сбрасываем кэш. Подробнее, раздел "Blocks".
core/modules/system/templates/field.html.twig
в папку templates
своей темы, переименовать файл по шаблону field--field-name.html.twig
, где field-name
это машинное имя поля, сбросить кэш. Подробнее, раздел "Fields".
1. Добавляем suggestion для шаблона page.html.twig:
function THEMENAME_theme_suggestions_page_alter(array &$suggestions, array $variables) { $route_match = \Drupal::routeMatch(); if ($route_match->getRouteName() == 'entity.node.canonical') { $suggestions[] = 'page__node__' . $route_match->getParameter('node')->bundle(); } }
2. Создаём шаблон page--node--NODETYPE.html.twig
(например page--node--article.html.twig
).
3. Сбрасываем кэш.
$current_theme = \Drupal::service('theme.manager')->getActiveTheme(); /** @var ActiveTheme $current_theme */ // Машинное имя темы $current_theme_machine_name = $current_theme->getName(); // Информация из файла themename.info.yml $current_theme_info = $current_theme->getExtension()->info;
$default_theme_name = \Drupal::config('system.theme')->get('default');
themename.info.yml
:libraries: - themename/libraryname
Замечание - библиотеки не будут подключаться на страницах сайта, отображаемых в другой теме, например административной.
# THEMENAME.libraries.yml my-library: js: js/my-library.js: {} header: true # <--
$node = \Drupal\node\Entity\Node::load(123); // Render-array поля, прошедшего через field.html.twig $field_example_build = $node->get('field_example')->view('full'); // Render-array первого значения поля, прошедшего через форматтер $field_example_build = $node->get('field_example')[0]->view('full');
123
:$node = \Drupal\node\Entity\Node::load(123); $node_view_builder = \Drupal::entityTypeManager()->getViewBuilder('node'); $node_build = $node_view_builder->view($node, 'teaser');
MODULENAME.module
или THEMENAME.theme
:/** * Implements hook_theme(). */ function MODULENAME_theme() { return [ 'my_template' => [ 'variables' => [ 'my_variable' => NULL, ], ], ]; }
my-template.html.twig
:<div class="my-template">{{ my_variable }}</div>
Подробнее.
hook_preprocess_TEMPLATE()
, где вместо TEMPLATE
подставить название шаблона. Пример добавления в шаблон views-view.html.twig
переменной с количеством результатов на текущей странице:// THEMENAME.theme function THEMENAME_preprocess_views_view(array &$variables) { $variables['result_count'] = count($vars['view']->result); }
Вместо
THEMENAME
пишем машинное имя темы или модуля, сбрасываем кэш. Подробнее.
htmlspecialchars()
. Чтобы этого не происходило переменная должна иметь тип MarkupInterface
:$variables['my_html_var'] = \Drupal\Core\Render\Markup::create('<b>Hello</b> <i>World</i>');
Пример темизации формы с идентификатором example_form
:
themename.theme
/** * Implements hook_theme(). */ function themename_theme() { return [ 'example_form' => [ 'render element' => 'form', ], ]; }
templates/example-form.html.twig
<form{{ attributes }}> <header> {{ form.element1 }} {{ form.element2 }} </header> {{ form.without('element1', 'element2') }} </form>
Подробнее.
$render_array = [ '#theme' => 'status_messages', '#message_list' => [ 'warning' => ['Warning!'], ], ]; $output = render($render_array);
$build = [ '#theme' => 'table', '#header' => ['ID', 'Title', 'Date'], '#rows' => [ [1, 'Title 1', '01.01.2019'], [2, 'Title 2', '02.01.2019'], [3, 'Title 3', '03.01.2019'], ], '#empty' => 'Empty...', ];
article
:/** * Implements hook_entity_extra_field_info(). */ function MODULENAME_entity_extra_field_info() { $extra_fields['node']['article']['display']['author'] = [ 'label' => t('Author name'), 'weight' => 0, ]; return $extra_fields; } /** * Implements hook_ENTITY_TYPE_view(): node. */ function MODULENAME_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) { if ($display->getComponent('author')) { $build['author'] = ['#markup' => $node->getOwner()->getDisplayName()]; } }
Подробнее.
main_menu
:function THEMENAME_preprocess_block__main_menu(&$vars) { $vars['attributes']['class'][] = 'my-block-class'; $vars['content_attributes']['class'][] = 'my-content-class'; }
item_list
может быть рендер массивом, поэтому просто вместо строки передаём такой же массив с '#theme' => 'item_list'
:$build = [ '#theme' => 'item_list', '#items' => [ 1 => 'Item 1', 2 => [ 'value' => ['#markup' => 'Item 2'], 'below' => [ '#theme' => 'item_list', '#items' => [ 1 => 'Item 2.1', 2 => 'Item 2.2', ], ], '#wrapper_attributes' => [ 'class' => ['open'], ], ], 3 => 'Item 3', ], ];
На выходе будет:
<ul> <li>Item 1</li> <li class="open"> Item 2 <ul> <li>Item 2.1</li> <li>Item 2.2</li> </ul> </li> <li>Item 3</li> </ul>
$build = [ '#theme' => 'item_list', '#items' => [ 0 => [ 'item' => ['#markup' => 'Item 1'], '#wrapper_attributes' => ['class' => ['item-1-class']], ], 1 => [ 'item' => ['#markup' => 'Item 2'], '#wrapper_attributes' => ['class' => ['item-2-class']], ], ], ];
На выходе будет:
<ul> <li class="item-1-class">Item 1</li> <li class="item-2-class">Item 2</li> </ul>
// THEMENAME.theme /** * Implements hook_comment_links_alter(). */ function THEMENAME_comment_links_alter(array &$links, CommentInterface $entity, array &$context) { if (isset($links['comment']['#links']['comment-reply'])) { unset($links['comment']['#links']['comment-reply']); } }
// src/ModulenameEventSubscriber.php /** * Event Subscriber MyEventSubscriber. */ class ModulenameEventSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events[KernelEvents::VIEW][] = ['onKernelView', 10]; return $events; } /** * KernelEvents::VIEW event callback. */ public function onKernelView(GetResponseForControllerResultEvent $event) { if (\Drupal::routeMatch()->getRouteName() == 'comment.reply') { $result = $event->getControllerResult(); $result['commented_entity']['#access'] = FALSE; $event->setControllerResult($result); } } }
# modulename.services.yml services: modulename.event_subscriber: class: Drupal\modulename\ModulenameEventSubscriber tags: - { name: event_subscriber }
@keyframes dialog-animation { from { opacity: 0; } to { opacity: 1; } } .ui-widget-overlay, .ui-dialog { animation-duration: 0.3s; animation-name: dialog-animation; }
$attributes_array = [ 'id' => 'cat', 'class' => ['cat', 'cat--white'], 'data-cat-name' => 'Kittie', ]; $attributes_object = new \Drupal\Core\Template\Attribute($attributes_array); echo '<div' . $attributes . '>Cat</div>';
Результат:
<div id="cat" class="cat cat--white" data-cat-name="Kittie">Cat</div>
// По имени роута $link = \Drupal\Core\Link::createFromRoute('My link', 'entity.node.canonical', ['node' => 123]); // По пути $url = \Drupal\Core\Url::fromUri('base:path/to/target'); $link = \Drupal\Core\Link::fromTextAndUrl('My link', $url); // С помощью рендер-массива $build = [ '#type' => 'link', '#title' => 'Link text', '#url' => Url::fromRoute('<current>'), ];
Подробнее.
$link = Link::fromTextAndUrl('Example', Url::fromUri('internal:#anchor'));
Создать ссылку с пустым анкором (
href="#"
) к сожалению нельзя.
Работа с Twig
{% for value in array %} {{ value }} {% endfor %} {% for key, value in array %} {{ key }}: {{ value }} {% endfor %} {% for key in array|keys %} {{ key }} {% endfor %}
Подробнее.
{{ 'now'|date('d.m.Y') }}
{{ 'English'|slice(0, 3) }}
Выведет
Eng
node.html.twig
:{% if not node.field_example.isEmpty() %} field_example is not empty {% endif %}
или так:
{% if content.field_example[0] %} field_example is not empty {% endif %}
node.html.twig
:{% if node.field_example|length > 1 %} Field items count more 1 {% endif %}
field.html.twig
:{{ content.field_example }}
Отформатированное первое значение поля без использования
field.html.twig
:{{ content.field_example[0] }}
Отформатированные значения поля без использования
field.html.twig
(нужен модуль Twig Tweak):{{ content.field_example|children }}
Сырое значение поля:
{{ node.field_example.value }} {# Или так, если в значении поля есть html код (небезопасно!) #} {{ node.field_example.value|raw }}
Подробнее.
{% if (example_array.foo is defined) %} {{ example_array.foo }} {% endif %} {% if (example_array.foo is not defined) %} Empty message... {% endif %}
{% for key, element in elements if key|first != '#' %} {{ element }} {% endfor %}
Или так, если стоит модуль Twig Tweak:
{% for element in elements|children %} {{ element }} {% endfor %}
element['#object']
. Пример вывода заголовка сущности:{{ element['#object'].title.value }}
{{ file_url(node.field_image.entity.uri.value) }}
{{ node.created.value|format_date('custom', 'd.m.Y') }}
{{ 'Home'|t }} {{ 'Order'|t({}, {'context': 'Commerce'}) }}
{% trans %} {{ count }} review {% plural count %} {{ count }} reviews {% endtrans %}
{% if logged_in %} Current user is authenticated {% else %} Current user is anonymouse {% endif %}
{{ drupal_view('my_view', 'my_display_name') }}
Иначе:
{{ {'#type': 'view', '#name': 'my_view', '#display_id': 'my_display_name'} }}
$build = [ '#type' => 'inline_template', '#template' => '<div>{{ message }}</div>', '#context' => [ 'message' => 'Hello world', ], ]; $output = \Drupal::service('renderer')->renderRoot($build);
В результате
$output
будет содержать:<div>Hello world</div>
Работа с формами (Form API, Ajax API)
user_register_form
:// MODULENAME.module use Drupal\Core\Form\FormStateInterface; /** * Implements hook_form_FORM_ID_alter(): user_register_form. */ function MODULENAME_form_user_register_form_alter(array &$form, FormStateInterface $form_state) { $form['terms_of_use'] = [ '#type' => 'checkbox', '#title' => t('I agree with terms and conditions.'), '#required' => TRUE, ]; }
Подробнее.
/** * Implements hook_form_FORM_ID_alter(): views_exposed_form. */ function MODULENAME_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) { $view = $form_state->get('view'); /** @var ViewExecutable $view */ if ($view->id() == 'example_views' && $view->current_display == 'example_display') { ... } }
src/Form/ExampleForm.php
:namespace Drupal\modulename\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; class ExampleForm extends FormBase { public function getFormId() { return 'example_form'; } public function buildForm(array $form, FormStateInterface $form_state) { $form['example_text'] = [ '#type' => 'textfield', '#title' => $this->t('Text'), ]; $form['submit'] = [ '#type' => 'submit', '#value' => $this->t('Submit'), ]; return $form; } public function submitForm(array &$form, FormStateInterface $form_state) { $text = $form_state->getValue('example_text'); } }
Или:
vendor/bin/drush generate form-simple
Подробнее.
public function buildForm(array $form, FormStateInterface $form_state) { ... $form['#method'] = 'get'; $form['#action'] = \Drupal::urlGenerator()->generateFromRoute('example.route'); ... }
public function buildForm(array $form, FormStateInterface $form_state) { ... $form['#cache']['max-age'] = 0; ... }
Стоит помнить, что отключение кэширование формы отключит кэширование всех вышестоящих элементов, т.е. блока (если форма выводится в блоке), ноды (если форма выводится в ноде) и в итоге всей страницы.
public function buildForm(array $form, FormStateInterface $form_state) { ... $form['#pre_render'][] = [$this, 'preRender']; return $form; } public function preRender(array $form) { unset($form['form_id']); unset($form['form_build_id']); unset($form['form_token']); unset($form['submit']['#name']); return $form; }
Подробнее.
MODULENAME.routing.yml
modulename.example_form: path: '/example/form' defaults: _form: 'Drupal\MODULENAME\Form\ExampleForm' _title: 'Example form' requirements: _permission: 'access content'
Т.е. это обычный роут, только вместо
routename.defaults._controller
указывается routename.defaults._form
.
modulename.routing.yml
modulename.example_form: path: '/node/{node}/example-form' defaults: _form: 'Drupal\modulename\Form\ExampleForm' _title: 'Example form' requirements: _permission: 'access content'
src/Form/ExampleForm.php
class ExampleForm extends FormBase { ... public function buildForm(array $form, FormStateInterface $form_state, NodeInterface $node = NULL) { ... } }
Важное замечание — название параметра в buildForm (
$node
) должно быть таким же, как в роуте ({node}
), иначе в него ничего не передастся.
/** * @Block( * id = "my_block_with_form", * admin_label = @Translation("My block with form"), * category = @Translation("Forms") * ) */ class MyBlockWithForm extends BlockBase { public function build() { return [ 'form' => \Drupal::formBuilder()->getForm('Drupal\modulename\Form\ExampleForm') ]; } }
— Drupal 8: Configuration Schema
— ConfigFormBase with Simple Configuration API
— Working with Configuration Forms
Или:
vendor/bin/drush generate form-config
foreach (\Drupal\Core\Render\Element::children($form) as $key) { $form[$key]['#attributes']['class'][] = 'form-item'; }
Подробнее.
class ExampleForm extends FormBase { public function buildForm(array $form, FormStateInterface $form_state, NodeInterface $node = NULL) { ... } public function submitForm(array &$form, FormStateInterface $form_state) { /** @var NodeInterface $node */ $node = $form_state->getBuildInfo()['args'][0]; } }
public function buildForm(array $form, FormStateInterface $form_state) { $form['my_element'] = [ '#type' => 'number', '#default_value' => 1, ]; $my_element_current_value = $form_state->getValue('my_element', $form['my_element']['#default_value']); }
public function submitForm(array &$form, FormStateInterface $form_state) { ... $form_state->set('example_data', 123); }
Доступ:
$data = $form_state->get('example_data');
$form['example_select'] = [ '#type' => 'select', '#options' => [ 1 => 'Show', 2 => 'Hide', ], ]; $form['dependent_select'] = [ '#type' => 'select', '#options' => [...], '#states' => [ 'visible' => [ ':input[name="example_select"]' => ['value' => 1], ], ], ];
Подробнее.
$form['second_element'] = [ ... '#states' => [ 'invisible' => [ ':input[name="first_element"]' => [ ['value' => 'xxx'], ], ], ], ];
$form['#attributes']['novalidate'] = 'novalidate';
class ExampleForm extends FormBase { /** * {@inheritDoc} */ public function buildForm(array $form, FormStateInterface $form_state) { ... $form['submit'] = [ '#type' => 'submit', '#value' => $this->t('Submit'), '#ajax' => [ 'callback' => '::ajaxSubmit', ], ]; $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; return $form; } /** * Ajax submit callback. */ public function ajaxSubmit(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); if ($form_state->hasAnyErrors()) { $response->addCommand(new OpenModalDialogCommand($this->t('Error'), ['#type' => 'status_messages'])); } else { ... } return $response; } }
$response->addCommand(new InvokeCommand('.my-element', 'addClass', ['my-new-class'])); $response->addCommand(new InvokeCommand('.my-element', 'removeClass', ['my-old-class']));
$response->addCommand(new InvokeCommand('.my-element', 'trigger', ['click']));
public function validateForm(array &$form, FormStateInterface $form_state) { if (!\Drupal::flood()->isAllowed('example_form', 1)) { $form_state->setErrorByName('', $this->t('You cannot send more.')); } } public function submitForm(array &$form, FormStateInterface $form_state) { ... \Drupal::flood()->register('example_form'); }
public function buildForm(array $form, FormStateInterface $form_state) { $form['foo'] = [ '#type' => 'textfield', ]; $form['bar'] = [ '#tree' => TRUE, ]; $form['bar']['baz'] = [ '#type' => 'textfield', ]; return $form; } public function submitForm(array &$form, FormStateInterface $form_state) { $foo_value = $form_state->getValue('foo'); $baz_value = $form_state->getValue(['bar', 'baz']); }
$form['table'] = [ '#type' => 'table', '#header' => ['Key', 'Value'], ]; foreach ([1, 2, 3] as $key) { $form['table'][$key]['key'] = [ '#markup' => 'Key #' . $key, ]; $form['table'][$key]['value'] = [ '#type' => 'textfield', '#title' => 'Value #' . $key, ]; }
Подробнее
class ExampleForm extends FormBase { /** * {@inheritDoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $rows = [ 1 => ['nid' => 1, 'title' => 'First node'], 2 => ['nid' => 2, 'title' => 'Second node'], 3 => ['nid' => 3, 'title' => 'Third node'], ]; $form['nodes'] = [ '#type' => 'tableselect', '#header' => [ 'nid' => 'Node ID', 'title' => 'Node title', ], '#options' => $rows, '#empty' => 'Empty...', ]; $form['delete'] = [ '#type' => 'submit', '#value' => 'Delete selected', ]; return $form; } /** * {@inheritDoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { foreach ($form_state->getValue('nodes') as $nid => $state) { if ($state) { Node::load($nid)->delete(); } } } }
Подробнее.
public function buildForm(array $form, FormStateInterface $form_state) { $form['example_checkboxes'] = [ '#type' => 'checkboxes', '#title' => 'Example checkboxes', '#options' => [ 'foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', ], ]; return $form; } public function submitForm(array &$form, FormStateInterface $form_state) { $checked_checkboxes = \Drupal\Core\Render\Element\Checkboxes::getCheckedCheckboxes($form_state->getValue('example_checkboxes')); // Если были отмечены Foo и Baz, то в $checked_checkboxes будет ['foo', 'baz'] }
$form['element'] = [ '#type' => 'textfield', '#title' => 'Element', '#theme_wrappers' => [], // <-- ];
Замечание - после удаления обёртки не будут работать некоторые js функции, например #states.
public function buildForm(array $form, FormStateInterface $form_state) { $form['example_file'] = [ '#type' => 'file', '#title' => t('File'), ]; ... } public function submitForm(array &$form, FormStateInterface $form_state) { /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); /** @var \Symfony\Component\HttpFoundation\File\UploadedFile[] $files */ $files = \Drupal::request()->files->get('files', []); if ($files['example_file']) { $file_source = $files['example_file']->getRealPath(); $file_destination = 'public://example-file.' . $files['example_file']->getExtension(); $file_system->move($file_source, $file_destination); } }
Работа с базой данных (Database API)
$query = \Drupal::database()->select(...); ... $or_conditions = $query->orConditionGroup(); $or_conditions->condition('status', 0); $or_conditions->condition('status', 1); $query->condition($or_conditions); $query->condition('type', 'page');
Получим условие:
WHERE (status = 0 OR status = 1) AND type = 'page'
Подробнее.
$query->isNull('field_name'); $query->isNotNull('field_name');
$count = \Drupal::database() ->select('node') ->condition('type', 'page') ->countQuery() // <-- ->execute() ->fetchField(); // Или так $query = \Drupal::database()->select('node'); $query->addExpression('COUNT(*)'); // <-- $query->condition('type', 'page'); $count = $query->execute()->fetchField();
Подробнее. Замечание —
countQuery()
не работает в подзапросах.
// Max $query = \Drupal::database()->select('node', 'n'); $query->addExpression('MAX(n.nid)'); $max_nid = $query->execute()->fetchField(); // Min $query = \Drupal::database()->select('node', 'n'); $query->addExpression('MIN(n.nid)'); $min_nid = $query->execute()->fetchField();
$subquery = \Drupal::database() ->select('taxonomy_index', 't') ->fields('t') ->where('t.nid = n.nid'); $query = \Drupal::database() ->select('node', 'n') ->fields('n') ->exists($subquery);
Код сгенерит запрос вида:
SELECT * FROM node n WHERE EXISTS ( SELECT * FROM taxonomy_index t WHERE t.nid = n.nid )
$query->condition('title', '%' . \Drupal::database()->escapeLike('world') . '%', 'LIKE');
$query = \Drupal::database() ->select('node', 'n') ->fields('n', ['nid', 'title']) ->condition('n.type', 'article'); $subquery = \Drupal::database() ->select('taxonomy_index', 'ti') ->addExpression('COUNT(*)'); ->where('ti.nid = n.nid'); $query->addExpression('(' . $subquery . ')', 'nodes_count'); $result = $query->execute()->fetchAll();
Получившийся SQL запрос:
SELECT n.nid, n.title, ( SELECT COUNT(*) FROM taxonomy_index ti WHERE ti.nid = n.nid ) AS nodes_count WHERE n.type = 'article'
->execute()
вызываем:$sql = $query->__toString();
Или так, если надо посмотреть запрос с уже подставленными параметрами:
$query->addTag('debug');
\Drupal::database()->truncate('flood')->execute();
if (\Drupal::database()->schema()->tableExists('flood')) { ... }
Entity Query API
$nids = \Drupal::entityQuery('node'); ->condition('type', 'article'); ->condition('status', NodeInterface::PUBLISHED); ->condition('field_foo', 'bar') ->execute();
$nids = \Drupal::entityQuery('node'); ->condition('type', 'article'); ->condition('status', NodeInterface::PUBLISHED); ->condition('field_foo', 'bar') ->range(0, 1) ->execute(); if ($nids) { $node = Node::load(current($nids)); }
$node_query = \Drupal::entityQuery('node'); $node_query->condition($node_query->andConditionGroup()->condition('field_example', 'foo')); $node_query->condition($node_query->andConditionGroup()->condition('field_example', 'bar')); $nids = $node_query->execute();
// Условие "поле field_foo не должно иметь значений" $entity_query->notExists('field_foo'); // Или $entity_query->condition('field_bar', NULL, 'IS NULL'); // Обратное условие "поле field_bar должно иметь хотя бы одно значение" $entity_query->exists('field_bar'); // Или $entity_query->condition('field_bar', NULL, 'IS NOT NULL');
$node_query->notExists('field_example.1.value');
В зависимости от типа поля, вместо
value
может быть что-то другое, например target_id
для поля типа entity reference.
$news_count = \Drupal::entityQuery('node') ->condition('type', 'news') ->count() ->execute();
debug
:$node_query = \Drupal::entityQuery('node'); ... $node_query->addTag('debug'); $result = $node_query->execute();
Работает только с включённым модулем Devel.
$result = \Drupal::entityQuery('node') ->sort('nid', 'DESC') ->range(0, 1) ->execute(); $last_nid = $result ? current($result) : NULL;
Работа с меню и адресами, навигация, роутинг
modulename.links.menu.yml
:modulename.settings: title: 'Modulename settings' description: 'Settings for modulename.' parent: system.admin_config_system route_name: modulename.settings
При этом роут
modulename.settings
уже должен существовать. Подробнее.
modulename.routing.yml
:entity.node.my_tab: path: '/node/{node}/my-tab' defaults: _controller: '\Drupal\modulename\Controller\MyController::myAction' _title: 'My tab' requirements: _permission: 'access content'
modulename.links.task.yml
:entity.node.my_tab: route_name: entity.node.my_tab base_route: entity.node.canonical title: My tab weight: 2
Подробнее.
// Системный адрес. Аналог current_path(). $current_system_path = \Drupal::service('path.current')->getPath(); // Синоним адреса. Аналог request_path(). $current_system_path = \Drupal::service('path.current')->getPath(); $current_path_alias = \Drupal::service('path.alias_manager')->getAliasByPath($current_system_path); // Относительный путь из строки браузера, с GET параметрами. Аналог request_uri(). $current_request_uri = \Drupal::request()->getRequestUri(); // Абсолютный путь из строки браузера, с GET параметрами. $current_uri = \Drupal::request()->getUri();
$base_path = base_path();
base_path()
вернёт /
./drupal/folder
, то base_path()
вернёт /drupal/folder/
.if (\Drupal::service('router.admin_context')->isAdminRoute()) { ... }
if (\Drupal::service('path.matcher')->isFrontPage()) { ... }
$current_route_name = \Drupal::routeMatch()->getRouteName();
if (\Drupal::routeMatch()->getRouteName() == 'entity.node.canonical') { ... }
node/123
$nid = \Drupal::routeMatch()->getRawParameter('node'); // Integer $node = \Drupal::routeMatch()->getParameter('node'); // Node object
*.links.menu.yml
добавляем параметр options.attributes.class
:modulename.example_route: title: 'Example' route_name: modulename.example_route menu_name: account options: attributes: class: - use-ajax
Если ссылка определена в чужом модуле, то альтерим в
hook_menu_links_discovered_alter()
:/** * Implements hook_menu_links_discovered_alter(). */ function modulename_menu_links_discovered_alter(&$links) { $links['user.logout']['options']['attributes']['class'][] = 'use-ajax'; }
Так же можно воспользоваться модулем Menus attribute и добавить класс из админки.
$file_url = file_create_url('public://example.jpg'); // Вернёт http://example.com/sites/default/files/example.jpg
$destination_array = \Drupal::destination()->getAsArray(); $url = \Drupal::urlGenerator()->generateFromRoute('example.route', [], ['query' => $destination_array]);
class ExampleController extends ControllerBase { public function exampleAction() { return $this->redirect('<front>'); } }
$path = \Drupal::service('path.alias_manager')->getPathByAlias('/example-url-alias');
$node_alias = $node->get('path')->alias; // "/example/node/path"
/catalog/category1/category2/category3
, нужно прописать терминам следующий шаблон:catalog/[term:parents:join-path]/[term:name]
$image_uri = 'public://image.jpg'; $image_style = ImageStyle::load('thumbnail'); /** @var ImageStyleInterface $image_style */ $absolute_url = $image_style->buildUrl($image_uri); // http://example.com/sites/default/files/styles/thumbnail/public/image.jpg?itok=BN-lOMvj $relative_url = file_url_transform_relative($absolute_url); // /sites/default/files/styles/thumbnail/public/image.jpg?itok=BN-lOMvj
Работа с Views
status
для пользователя с правом administer comments
:/** * Implements hook_views_pre_view(). */ function MODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) { if ($view->id() == 'my_view_name') { if (\Drupal::currentUser()->hasPermission('administer comments')) { $view->removeHandler($view->current_display, 'filter', 'status'); } } }
/** * Implements hook_views_pre_view(). */ function MODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) { if ($view->id() == 'my_view_name') { $view->removeHandler($view->current_display, 'header', 'my_header_id'); $view->removeHandler($view->current_display, 'footer', 'my_footer_id'); } }
/** * Implements hook_views_pre_view(). */ function MODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) { if ($view->id() == 'my_view_name') { $view->setItemsPerPage(10); } }
/** * Implements hook_views_pre_view(). */ function MODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) { if ($view->id() == 'my_view_name') { $view->display_handler->setOption('pager', ['type' => 'none']); } }
title
в представлении content_recent
function THEMENAME_preprocess_views_view_field__content_recent__title(&$vars) { $vars['output'] = 'new field output'; }
example_view
:/** * Preprocess function for views-view--example-view.html.twig. */ function THEMENAME_preprocess_views_view__example_view(&$vars) { $view = $vars['view']; /** @var ViewExecutable $view */ $vars['attributes']['class'][] = 'views--count-' . count($view->result); }
$views_build = views_embed_view('my_view_name', 'my_view_display_name', 'my_argument');
Способ 2:
$views = \Drupal\views\Views::getView('my_view_name'); /** @var \Drupal\views\ViewExecutable $views */ $views->setDisplay('my_view_display_name'); $views->setArguments(['my_argument']); // Optional $views->preExecute(); $views->execute(); $views_build = $views->render();
Способ 3 (из twig файла):
{{ drupal_view('my_view_name', 'my_view_display_name', 'my_argument') }}
{{ drupal_view('my_view', 'my_display_name') }}
Иначе:
{{ {'#type': 'view', '#name': 'my_view', '#display_id': 'my_display_name'} }}
Работа с javascript
// THEMENAME.theme /** * Preprocess function for html.html.twig. */ function THEMENAME_preprocess_html(&$vars) { $vars['#attached']['drupalSettings']['path']['currentThemePath'] = $vars['directory']; }
# THEMENAME.libraries.yml my-library-name: js: ... dependencies: - core/drupalSettings
После этого путь к папке темы будет находиться в js переменной drupalSettings.path.currentThemePath
$response->addCommand(new InvokeCommand('.my-element', 'trigger', ['click']));
# THEMENAME.libraries.yml my-library: js: js/my-library.js: {} header: true # <--
var $dialog = $('#drupal-modal'); if ($dialog.length == 0) { $dialog = $('<div id="drupal-modal" class="ui-front"/>').appendTo('body'); } $dialog.append('Hello World!'); Drupal.dialog($dialog, {}).showModal();
Библиотека
core/drupal.dialog
должна быть уже подключена.
Работа с многоязычностью
$default_language = \Drupal::languageManager()->getDefaultLanguage(); // Объект с \Drupal\Core\Language\LanguageInterface $default_langcode = $default_language->getId(); // Строка "ru" или "en" или другой код языка.
$current_language = \Drupal::languageManager()->getCurrentLanguage(); // Объект с \Drupal\Core\Language\LanguageInterface $current_langcode = $current_language->getId(); // Строка "ru" или "en" или другой код языка.
{{ 'Home'|t }} {{ 'Order'|t({}, {'context': 'Commerce'}) }} {% trans %}Product{% endtrans %}
admin/config/regional/language/detection/url
и удаляем префикс дефолтного языка.
Комментарии
Спасибо! Ценный материал, и в одном месте.
Насколько люблю 7-ку настолько же ненавижу 8-ку. Может я его неправильно курю? Простой пример, но вместо одной строки в info файле 7-ки делать отдельный файл с кучей строк(https://www.drupal.org/docs/8/api/menu-api/providing-module-defined-menu...). Зачем так усложнять?
Из .info файла ссылку конечно не создать, нужен hook_menu, но то что писать теперь нужно больше это да. Усложнено в угоду масштабируемости, хоть большинству она и не нужна. Тут просто надо свыкнуться с мыслью, что семёрка мертва и придётся учить много нового.
Если использовать генераторы кода (drush и drupal console), то в некоторых местах приходится писать даже меньше кода, чем раньше. Да и многие вещи стали гибче, удобнее и логичнее. Но сложности добавилось, это факт.
Огромное Вам спасибо!
Это должно быть в закладках у всякого начинающего адепта секты "Друпал - наше все" )))
Эххх, такой бы гайд еще по коммерц 2....
Замечательная статья, благодарю, много полезного для новичков и не только.
Это просто ахринительски. Спасибо.
Спасибо, очень крутая подборка, теперь я в познании друпал настолько преисполнен что как-будто сто миллиардов миллионов лет с друпалом работаю, мне уже этот мир абсолютно понятен... )))
Оставить комментарий