Обновляемый список коротких вопросов и ответов по Drupal 8+.
Все консольные команды рассчитаны на выполнение из корня друпала. Пользователям Windows перед выполнением команд в cmd.exe нужно заменить разделитель директорий с /
на \
, т.е. вместо vendor/bin/drush
писать vendor\bin\drush
, а лучше пользоваться башем из Cygwin. Так же пользователям Windows надо брать в двойные кавычки аргументы с символом ^
, например composer require "foo/bar:^1.0"
.
Содержание
- Работа с Composer
- Общие вопросы
- Общие вопросы по работе с кодом (Drupal API)
- Работа с сущностями (Entity API)
- Работа с полями сущностей (Field API)
- Работа с таксономией (Taxonomy 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
1. Изменить "minimum-stability": "stable"
на "minimum-stability": "dev"
2. В секцию config
добавить "optimize-autoloader": true
3. Из require
удалить пакеты drupal/core-composer-scaffold
и drupal/core-project-message
4. Из секции extra
удалить drupal-scaffold
и drupal-core-project-message
composer update drupal/core* --with-all-dependencies
vendor/bin/drush updb
Для версий меньше 8.8.0 сначала обновляемся до 8.8.0 по статье никлана, а потом до актуальной версии способом выше.
Перед обновлением обязательно делаем бэкап файлов и базы!
composer require drush/drush
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 версия.
vendor/bin/drush pm-uninstall devel
2. Потом удаляем из файловой системы:
composer remove drupal/devel
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
После обновления с помощью composer не забываем выполнять
vendor/bin/drush updb
prohibits
(синоним why-not
), которая выдаст причины:
composer prohibits drupal/core 8.9.2
Бывает при попытке обновиться composer выплёвывает кучу ошибок и совершенно непонятно что делать. Я обычно поступаю кардинально:
0. Делаю бэкап файлов и базы
1. Удаляю папки /core
, /vendor
, /modules/contrib
и файл /composer.lock
2. Обновляю composer — composer self-update
(composer self-update --2
если нужно обновиться с первой до второй версии)
3. Проверяю, что composer работает нормально — composer diagnose
4. Изменяю composer.json
(исправляю версии, добавляю/удаляю пакеты и т.д.)
5. Выполняю composer install
6. Если всё хорошо, то vendor/bin/drush updb
composer outdated
Только drupal модули:
composer outdated drupal/*
composer show drupal/core
symfony/process
composer why symfony/process
composer show --all drupal/devel
Общие вопросы
composer create-project drupal/recommended-project .
composer require drush/drush
vendor/bin/drush site-install standard --db-url=mysql://root:root@localhost/drupal --account-pass=admin
Способ 1 - на странице admin/config/development/performance
нажать кнопку "Clear all caches".
Способ 2 - выполнить в консоли:
vendor/bin/drush cache-rebuild
Способ 3 - выполнить php функцию:
drupal_flush_all_caches();
admin
:
vendor/bin/drush user-password admin qwerty
vendor/bin/drush config-import --partial --source=modules/modulename/config/install
/**
* Implements hook_query_TAG_alter(): comment_filter.
*/
function hook_query_comment_filter_alter(QueryAlterableInterface $query): void {
if ($query instanceof PagerSelectExtender) {
$order_by = &$query->getOrderBy();
unset($order_by['c.cid']);
$query->orderBy('c.created', 'DESC');
}
}
vendor/bin/drush eval "print_r(gd_info())"
/admin/config/media/file-system
включаем опцию "Транслитерировать".
Общие вопросы по работе с кодом (Drupal API)
vendor/bin/drush eval "drupal_flush_all_caches();"
Или из админки с помощью модуля Devel PHP.
vendor/bin/drush generate module
// src/Controller/ModulenameController.php
namespace Drupal\modulename\Controller;
use Drupal\Core\Controller\ControllerBase;
class ModulenameController extends ControllerBase {
public function helloWorld(): array {
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 {
/**
* {@inheritDoc}
*/
public function build(): array {
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(): array {
...
}
}
Способ 2:
/**
* @Block(...)
*/
class MyBlock extends BlockBase {
public function build(): array {
...
}
public function getCacheMaxAge() {
return 0; // <---
}
}
class MyBlock extends BlockBase {
...
/**
* {@inheritdoc}
*/
public function blockAccess(AccountInterface $account): AccessResultInterface {
$result = TRUE;
if (...) {
$result = FALSE;
}
return AccessResult::allowedIf($result);
}
}
system_powered_by_block
в регионах отличных от footer
:
// MODULENAME.module
use Drupal\block\Entity\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Access\AccessResult;
/**
* Implements hook_block_access().
*/
function MODULENAME_block_access(Drupal\block\Entity\Block $block, string $operation, AccountInterface $account): AccessResultInterface {
if ($operation == 'view' && $block->getPluginId() == 'system_powered_by_block') {
return AccessResult::forbiddenIf($block->getRegion() != 'footer')->addCacheableDependency($block);
}
return AccessResult::neutral();
}
Работает только в файлах модуля, в файле .theme
этот хук не вызывается.
$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');
$cache_backend = \Drupal::cache('default');
if ($cache = $cache_backend->get('my_data')) {
$my_data = $cache->data;
}
else {
$my_data = ...;
$cache_backend->set('my_data', $my_data, strtotime('+1 hour'));
}
\Drupal::cache('default')->set('my_data', $my_data, strtotime('tomorrow'));
\Drupal::messenger()->addMessage('Hello World!');
use Drupal\Component\Utility\Timer;
Timer::start('test');
sleep(1);
debug(Timer::read('test') . ' ms'); // Выведет "1000 ms"
\Drupal::logger('modulename')->info('Hello World!');
\Drupal::logger('modulename')->error('It\'s error');
/** @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"
}
$admin_role_name = current(
\Drupal::entityQuery('user_role')
->condition('is_admin', TRUE)
->range(0, 1)
->execute()
); // Вернёт "administrator"
$admin_role_name = current(
\Drupal::entityQuery('user_role')
->condition('is_admin', TRUE)
->range(0, 1)
->execute()
);
$admin_user_id = current(
\Drupal::entityQuery('user')
->condition('status', 1)
->condition('roles', $admin_role_name)
->range(0, 1)
->execute()
);
$admin_user = \Drupal\user\Entity\User::load($admin_user_id);
// $_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();
Замечание — в Drupal 10 с помощью метода ->get('param-name')
нельзя получать массивы, надо делать либо ->all('param-name')
, либо ->all()['param-name']
.
$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>'),
]);
$string = \Drupal::translation()->formatPlural(123, '@count day', '@count days');
$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']
$current_timestamp = \Drupal::time()->getCurrentTime(); // Вернёт число, например 1598281486
/** @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');
$date1 = new \Drupal\Core\Datetime\DrupalDateTime('-1 day');
$date2 = new \Drupal\Core\Datetime\DrupalDateTime('+1 day');
dvm($date1 > $date2); // false
dvm($date1 < $date2); // true
Но это не совсем надёжно, так как сравниваются по сути строки и могут быть проблемы при разных таймзонах. Надёжнее сравнивать таймстампы:
$date1 = new \Drupal\Core\Datetime\DrupalDateTime('-1 day');
$date2 = new \Drupal\Core\Datetime\DrupalDateTime('+1 day');
dvm($date1->getTimestamp() > $date2->getTimestamp()); // false
dvm($date1->getTimestamp() < $date2->getTimestamp()); // true
$timestamp_from = strtotime('-10 second');
$timestamp_to = strtotime('-3 second');
echo \Drupal::service('date.formatter')->formatDiff($timestamp_from, $timestamp_to);
// Выведет "7 секунд"
$timestamp_from = strtotime('-10 hours');
$timestamp_to = strtotime('-30 minutes');
echo \Drupal::service('date.formatter')->formatDiff($timestamp_from, $timestamp_to);
// Выведет "9 часов 30 минут"
\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): void {
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): void {
if ($message['module'] == 'commerce' && $message['key'] == 'order_receipt') {
$message['subject'] = 'New subject';
$message['body'] = ['New body'];
}
}
$ip = \Drupal::request()->getClientIp();
Если на сервере используется reverse proxy, например nginx поверх apache, то надо добавить в settings.php
:
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = ['ip адрес вашего сервера'];
$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/plain; 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(): array {
$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): array {
$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): void {
$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(): array {
$meta_description = [
'#tag' => 'meta',
'#attributes' => [
'name' => 'description',
'content' => ['#plain_text' => 'Random text'],
],
];
return [
'#attached' => [
'html_head' => [
[$meta_description, 'description']
],
],
...
];
}
}
\Drupal::moduleHandler()->invokeAll('my_custom_hook', ['foo', 'bar']);
После этого другие модули смогут реализовывать хук hook_my_custom_hook()
:
function MODULENAME_my_custom_hook($arg1, $arg2): void {
...
}
$data = ['foo' => 'bar'];
\Drupal::moduleHandler()->alter('my_data', $data);
После этого другие модули смогут альтерить данные с помощью хука hook_my_data_alter
:
function MODULENAME_my_data_alter(&$data): void {
$data['foo'] = 'baz';
}
$formatted_text = check_markup('Hello world', 'full_html');
$comment_count = (int)$entity->get('field_comment')->comment_count;
function _get_comment_page(int $entity_id, int $comment_cid, int $per_page): int {
$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(): array {
$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 }
hook_install()
:
// modulename.install
/**
* Implements hook_install().
*/
function modulename_install(): void {
module_set_weight('modulename', 123);
}
Если нужно изменить вес уже установленного модуля, то пользуемся hook_update_N()
:
// modulename.install
/**
* Set module weight.
*/
function modulename_update_8001(): void {
module_set_weight('modulename', 123);
}
if (\Drupal::service('module_handler')->moduleExists('views')) {
// Views module enabled
}
$file = system_retrieve_file('http://example.com/image.jpg', 'public://image.jpg', TRUE);
// src/Plugin/Field/FieldFormatter/EmailLinkFormatter.php
namespace Drupal\random\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
/**
* @FieldFormatter(
* id = "email_link",
* label = @Translation("E-mail link"),
* field_types = {
* "email",
* },
* )
*/
class EmailLinkFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$element = [];
foreach ($items as $delta => $item) {
$element[$delta] = [
'#markup' => '<a href="mailto:' . $item->value . '">' . $item->value . '</a>',
];
}
return $element;
}
}
// MODULENAME.module
/**
* Implements hook_cron().
*/
function MODULENAME_cron(): void {
$current_timestamp = \Drupal::time()->getRequestTime();
$cron_last_run_timestamp = \Drupal::state()->get('system.cron_last');
if (date('Ymd', $current_timestamp) != date('Ymd', $cron_last_run_timestamp)) {
// code
}
}
\Kint\Kint::$depth_limit = 10;
dsm($data);
$cart_page_result = \Drupal::service('controller_resolver')
->getControllerFromDefinition(\Drupal\commerce_cart\Controller\CartController::class)
->cartPage();
// src/ExampleController.php
class ExampleController extends ControllerBase {
public function exampleText() {
return new Response('Hello World!');
}
}
# example.routing.yml
example.text:
path: '/example/text'
defaults:
_controller: '\Drupal\example\ExampleController::exampleText'
requirements:
_access: 'TRUE'
\Drupal\Component\Utility\Environment::setTimeLimit(0);
Работа с сущностями (Entity 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);
$node = \Drupal\node\Entity\Node::create([
'type' => 'article',
'title' => 'My article',
'body' => 'Article body',
]);
$node->save();
$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]);
}
// Способ 1
$node_type = $node->get('type')->entity;
// Способ 2
$node_type = NodeType::load($node->bundle());
$node_type_label = $node->get('type')->entity->label(); // Возвратит например "Статья" или "Страница"
$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_uri = 'public://example.jpg';
if ($files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $file_uri])) {
$file = current($files); /** @var FileInterface $file */
}
$file = \Drupal\file\Entity\File::create(['uri' => 'public://example.jpg']);
$file->save();
$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
(способ подходит для удаления небольшого количества сущностей, до тысячи):
\Drupal\Component\Utility\Environment::setTimeLimit(0);
$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 NodeStorageInterface $node_storage */
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$nodes = $node_storage->loadByProperties(['type' => 'article']);
foreach ($nodes as $node) {
$node->save();
}
Или с помощью drush 11+:
vendor/bin/drush entity-save node --bundle=article
sort()
, который можно использовать в uasort()
. Пример сортировки сущностей модуля Domain Access:
/** @var \Drupal\domain\DomainInterface[] $domains */
$domains = \Drupal\domain\Entity\Domain::loadMultiple();
uasort($domains, '\Drupal\domain\Entity\Domain::sort');
Работа с полями сущностей (Field API)
$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;
}
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())) {
...
}
$field_definition = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page')['field_category'];
$field_storage_definition = $field_definition->getFieldStorageDefinition();
Для настраиваемых полей можно так же пользоваться методом \Drupal\field\Entity\FieldConfig::loadByName()
и \Drupal\field\Entity\FieldStorageConfig::loadByName()
.
/** @var EntityFieldManagerInterface $entity_field_manger */
$entity_field_manger = \Drupal::service('entity_field.manager');
$entity_reference_fields = $entity_field_manger->getFieldMapByFieldType('entity_reference');
Результат:
[node] => Array
(
[uid] => Array
(
[type] => entity_reference
[bundles] => Array
(
[page] => page
[article] => article
)
)
)
...
/** @var EntityFieldManager $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
$article_fields = $entity_field_manager->getFieldDefinitions('node', 'article');
/** @var \Drupal\Core\Datetime\DrupalDateTime $date_start */
$date_start = $node->get('field_daterange')->start_date;
/** @var \Drupal\Core\Datetime\DrupalDateTime $date_end */
$date_end = $node->get('field_daterange')->end_date;
$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_items = $entity->get('field_example');
$allowed_values = options_allowed_values($field_items->getFieldDefinition()->getFieldStorageDefinition(), $entity);
Иначе:
$field_name = 'field_example';
$entity_type = 'node';
$entity_bundle = 'article';
$field_manager = \Drupal::service('entity_field.manager'); /** @var EntityFieldManagerInterface $field_manager */
$field_definition = $field_manager->getFieldDefinitions($entity_type, $entity_bundle)[$field_name];
$field_storage_definition = $field_definition->getFieldStorageDefinition();
$allowed_values = options_allowed_values($field_storage_definition);
$field_items = $entity->get('field_example');
$allowed_values = options_allowed_values($field_items->getFieldDefinition()->getFieldStorageDefinition(), $entity);
$value_label = $allowed_values[$items->value] ?? NULL;
$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
$url = $entity->get('field_link')[0]->getUrl(); // "/node/123"
$uri = $entity->get('field_link')->uri; // "internal:/node/123"
$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();
/**
* Implements hook_entity_update().
*/
function hook_entity_update(EntityInterface $entity): void {
$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
}
}
$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]); // Способ 1
$node->get('field_tags')->offsetUnset(2); // Способ 2
$node->get('field_tags')->removeItem(2); // Способ 3
// Способ удалить значение если индекс неизвестен
$node->get('field_tags')->filter(function ($item) {
return $item->target_id != 123; // Оставляем все значения кроме термина с id=123
});
$node->save();
1. Поставить модуль Svg Image.
2. В настройках поля к допустимым расширениям добавить svg
Работа с таксономией (Taxonomy API)
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadByProperties(['vid' => 'category']);
// Способ 1
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadByProperties(['vid' => 'category']);
$names = array_map(function (TermInterface $term) {
return $term->label();
}, $terms);
// Способ 2
$names = \Drupal::database()
->select('taxonomy_term_field_data', 't')
->fields('t', ['tid', 'name'])
->condition('t.vid', 'category')
->orderBy('t.weight')
->execute()
->fetchAllKeyed();
\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();
}
Или с помощью drush 11+:
vendor/bin/drush entity-save taxonomy_term --bundle=category
/** @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".
// Если термин не имеет родителей, то в "parent" будет "0", но referencedEntities() вернёт пустой массив.
$direct_parents_items = $term->get('parent'); /** @var EntityReferenceFieldItemListInterface $direct_parents_items */
$direct_parent_id = $direct_parents_items->target_id;
$direct_parents = $direct_parents_items->referencedEntities(); /** @var TermInterface[] $direct_parents */
$direct_parent = $direct_parents_items->entity; /** @var ?TermInterface $direct_parent */
// Самый верхний родитель.
$all_parents = $term_storage->loadAllParents(123); /** @var TermInterface[] $all_parents */
$root_parent = current($all_parents); // Если термин не имеет родителей, то в $root_parent будет текущий термин.
Следует помнить, что непосредственных родителей у термина может быть несколько. Так же следует помнить, что метод $term->get('parent')->isEmpty()
всегда возвращает FALSE
, потому что там как минимум есть 0
, поэтому проверять есть ли у термина родители надо с помощью if ($term->get('parent')->entity)
.
Темизация, рендеринг (Theming API, Render API)
1. Найти hook_theme, в котором объявлен шаблон.
2. Скопировать информацию о шаблоне в hook_theme своего модуля.
3. Скопировать twig шаблон в свой модуль.
4. Сбросить кэш.
Должно получиться как-то так:
modules/custom/MODULENAME/MODULENAME.module
/**
* Implements hook_theme().
*/
function MODULENAME_theme(): array {
return [
'node' => [
'render element' => 'elements',
],
];
}
modules/custom/MODULENAME/templates/node.html.twig
{{ content }}
Важно помнить, что вес модуля должен быть больше того, в котором объявлен шаблон.
Пример переопределения шаблона node--article.html.twig
.
MODULENAME.module
:
/**
* Implements hook_theme().
*/
function MODULENAME_theme(): array {
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
машинное имя нужного блока, нижние подчёркивания заменяются на тире). Сбрасываем кэш. Подробнее.
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): void {
$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
Замечание - библиотеки не будут подключаться на страницах сайта, отображаемых в другой теме, например административной.
$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');
element['#object']
. Пример вывода заголовка сущности:
{{ element['#object'].title.value }}
{% for delta, item in items %}
{% set real_item = element['#items'][delta] %}
Raw item value: {{ real_item.value }}<br />
{% endfor %}
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(): array {
return [
'my_template' => [
'variables' => [
'my_variable' => NULL,
],
],
];
}
templates/my-template.html.twig
:
<div class="my-template">{{ my_variable }}</div>
hook_preprocess_TEMPLATE()
, где вместо TEMPLATE
подставить название шаблона. Пример добавления в шаблон views-view.html.twig
переменной с количеством результатов на текущей странице:
// THEMENAME.theme
/**
* Preprocess function for views-view.html.twig.
*/
function THEMENAME_preprocess_views_view(array &$variables): void {
$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
.
1. В THEMENAME.theme
добавляем:
/**
* Implements hook_theme().
*/
function themename_theme(): array {
return [
'example_form' => [
'render element' => 'form',
],
];
}
2. Создаём файл templates/example-form.html.twig
с нужной разметкой (в имени файла подчёркивания заменяются на тире):
<div class="container">
<div class="row">
<div class="col-sm">{{ form.element1 }}</div>
<div class="col-sm">{{ form.element2 }}</div>
</div>
</div>
{{ form|without('element1', 'element2') }}
Последняя строчка, с рендерингом служебной информации о форме, обязательна!
3. Сбрасываем кэш.
$build = [
'#theme' => 'status_messages',
'#message_list' => [
'warning' => ['Warning!'],
],
];
$html = \Drupal::service('renderer')->render($build);
// Или
$html = \Drupal::service('renderer')->renderRoot($build);
// Или
$html = \Drupal::service('renderer')->renderPlain($build);
\Drupal::service('cache_tags.invalidator')->invalidateTags(['node:123']);
$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
:
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\node\NodeInterface;
/**
* Implements hook_entity_extra_field_info().
*/
function MODULENAME_entity_extra_field_info(): array {
$extra_fields['node']['article']['display']['author'] = [
'label' => t('Author name'),
'weight' => 0,
'visible' => FALSE,
];
return $extra_fields;
}
/**
* Implements hook_ENTITY_TYPE_view(): node.
*/
function MODULENAME_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, string $view_mode): void {
if ($display->getComponent('author')) {
$build['author'] = ['#markup' => $node->getOwner()->getDisplayName()];
}
}
main_menu
:
function THEMENAME_preprocess_block__main_menu(array &$vars): void {
$vars['attributes']['class'][] = 'my-block-class';
$vars['content_attributes']['class'][] = 'my-content-class';
}
function THEMENAME_preprocess_html(array &$vars): void {
$vars['attributes']['class'][] = 'my-body-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): void {
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(): array {
$events[KernelEvents::VIEW][] = ['onKernelView', 10];
return $events;
}
/**
* KernelEvents::VIEW event callback.
*/
public function onKernelView(ViewEvent $event): void {
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="#"
) к сожалению нельзя.
$build = [
'foo' => [
'#weight' => 2,
],
'bar' => [
'#weight' => 1,
],
];
uasort($build, '\Drupal\Component\Utility\SortArray::sortByWeightProperty');
В результате в $build
будет:
[
'bar' => [
'#weight' => 1,
],
'foo' => [
'#weight' => 2,
],
]
Работа с Twig
{% for value in array %}
{{ value }}
{% endfor %}
{% for key, value in array %}
{{ key }}: {{ value }}
{% endfor %}
{% for key in array|keys %}
{{ key }}
{% endfor %}
{% if (example_array.foo is defined) %}
{{ example_array.foo }}
{% endif %}
{% if (example_array.foo is not defined) %}
Empty message...
{% endif %}
{% set arr = ['foo', 'bar'] %}
Добавляем в массив arr значение 'baz':
{% set arr = arr|merge(['baz']) %}
В arr
будет ['foo', 'bar', 'baz']
{% set arr = {'foo': 'bar', 'baz': 'bat'} %}
Меняем значение arr.baz на 'new':
{% set arr = arr|merge({'baz': 'new'}) %}
{% if 'Hello' in str %}
...
{% endif %}
Замечание — если в str
будет объект, условие будет всегда возвращать false
, потому что метод __toString()
не вызывается.
{% if str|length > 100 %}
...
{% endif %}
{{ 'now'|date('d.m.Y') }}
{{ 'English'|slice(0, 3) }}
{{ 'English'|truncate(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|without('#theme') }}
Отформатированное первое значение поля без использования field.html.twig
:
{{ content.field_example[0] }}
Сырое значение поля:
{{ node.field_example.value }}
{# Или так, если в значении поля есть html код (небезопасно!) #}
{{ node.field_example.value|raw }}
Node ID: {{ node.id }}
Node title: {{ node.label }}
String field: {{ node.field_string.value }}
Reference entity label: {{ node.field_reference.entity.label }}
{% if not node.field_image.isEmpty() %}
<img src="{{ file_url(node.field_image.entity.getFileUri()) }}" />
{% endif %}
Или:
{% if not node.field_image.isEmpty() %}
<img src="{{ file_url(node.field_image.entity.uri.value) }}" />
{% endif %}
{% if not node.field_image.isEmpty() %}
<img src="{{ node.field_image.entity.getFileUri()|image_style('thumbnail') }}" />
{% endif %}
Или так:
{{ drupal_image(node.field_image.entity.getFileUri(), 'thumbnail') }}
Нужен модуль Twig Tweak.
{{ node.created.value|format_date('custom', 'd.m.Y') }}
{% for key, element in elements if key|first != '#' %}
{{ element }}
{% endfor %}
Или так, если стоит модуль Twig Tweak:
{% for element in elements|children %}
{{ element }}
{% endfor %}
{{ path('entity.node.canonical', {'node': 123}) }}
{{ 'Home'|t }}
{{ 'Order'|t({}, {'context': 'Commerce'}) }}
{% set count = 123 %}
{% trans %}
{{ count }} review
{% plural count %}
{{ count }} reviews
{% endtrans %}
logged_in
:
{% if logged_in %}
Current user is authenticated
{% else %}
Current user is anonymouse
{% endif %}
user
типа AccountProxyInterface
:
{% if user.hasPermission('administer comments') %}
Current user has permission "administer comments"
{% endif %}
{{ drupal_view('my_view', 'my_display_name') }}
Иначе:
{{ {'#type': 'view', '#name': 'my_view', '#display_id': 'my_display_name'} }}
{% set foo = 'bar' %}
{{ foo }}
Выведет bar
$build = [
'#type' => 'inline_template',
'#template' => '<div>{{ message }}</div>',
'#context' => [
'message' => 'Hello world',
],
];
$output = \Drupal::service('renderer')->renderRoot($build);
В результате $output
будет содержать:
<div>Hello world</div>
Пример выноса шапки из page.html.twig
.
1. /themes/mytheme/templates/page.html.twig
:
<div class="layout__header">
{% include '@mytheme/page-header.html.twig' %}
</div>
<div class="layout__content">
{{ page.content }}
</div>
<div class="layout__footer">
{{ page.footer }}
</div>
2. /themes/mytheme/templates/page-header.html.twig
:
<a class="site-logo" href="/"></a>
{{ page.header }}
В подключаемом файле будут доступны все переменные из основного файла. Важно, что в {% include '@mytheme/...' %}
надо указывать путь к файлу относительно папки templates
, т.е. если файл лежит в /themes/custom/mytheme/templates/includes/template.html.twig
, то подключать надо так: {% include '@mytheme/includes/template.html.twig' %}
.
Если есть объект сущности: {{ entity.field_name|view('teaser') }}
Если есть только id сущности: {{ drupal_field('field_name', 'node', 123, 'teaser') }}
Подробнее про фильтр view. Подробнее про функцию drupal_field()
{{ drupal_token('site:name') }}
Необходим модуль twig_tweak.
// THEMENAME.theme
/**
* Implements hook_twig_tweak_functions_alter().
*/
function THEMENAME_twig_tweak_functions_alter(array &$functions): void {
$functions[] = new TwigFunction('registration_link', 'THEMENAME_registration_link');
}
/**
* Return registration link build.
*/
function THEMENAME_registration_link(string $text = 'Registration', string $class = 'form-button'): array {
return [
'#type' => 'link',
'#title' => $text,
'#url' => Url::fromRoute('user.register'),
'#attributes' => [
'class' => $class,
],
];
}
Использование:
{{ registration_link() }}
Работа с формами (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): void {
$form['terms_of_use'] = [
'#type' => 'checkbox',
'#title' => t('I agree with terms and conditions.'),
'#required' => TRUE,
];
}
// MODULENAME.module
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewExecutable;
/**
* Implements hook_form_FORM_ID_alter(): views_exposed_form.
*/
function MODULENAME_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state): void {
$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 {
/**
* {@inheritDoc}
*/
public function getFormId(): string {
return 'example_form';
}
/**
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['example_text'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$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): array {
...
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
/** @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): void {
...
$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' => [...],
// Показываем селект dependent_select только когда в example_select выбрано "Show" (т.е. 1)
'#states' => [
'visible' => [
':input[name="example_select"]' => ['value' => 1],
],
],
];
Пример с чекбоксом:
$form['make_link'] = array(
'#type' => 'checkbox',
'#title' => 'Сделать ссылкой',
);
$form['in_new_window'] = array(
'#type' => 'checkbox',
'#title' => 'Открывать в новом окне',
// Показывать чекбокс in_new_window только когда отмечен чекбокс make_link
'#states' => [
'visible' => [
':input[name="make_link"]' => [
'checked' => TRUE,
],
],
],
);
$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',
],
];
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',
'#attached' => [
'library' => ['core/drupal.dialog.ajax'],
],
]));
}
else {
...
}
return $response;
}
}
$response->addCommand(new InvokeCommand('.my-element', 'addClass', ['my-new-class']));
$response->addCommand(new InvokeCommand('.my-element', 'removeClass', ['my-old-class']));
.my-element
:
$response->addCommand(new InvokeCommand('.my-element', 'trigger', ['click']));
public function validateForm(array &$form, FormStateInterface $form_state): void {
if (!\Drupal::flood()->isAllowed('example_form', 1)) {
$form_state->setErrorByName('', $this->t('You cannot send more.'));
}
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
...
\Drupal::flood()->register('example_form');
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['foo'] = [
'#type' => 'textfield',
];
$form['bar'] = [
'#tree' => TRUE,
];
$form['bar']['baz'] = [
'#type' => 'textfield',
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$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): array {
$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): void {
foreach ($form_state->getValue('nodes') as $nid => $checked) {
if ($checked) {
Node::load($nid)->delete();
}
}
}
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$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): void {
$checked_checkboxes = \Drupal\Core\Render\Element\Checkboxes::getCheckedCheckboxes($form_state->getValue('example_checkboxes'));
// Если были отмечены Foo и Baz, то в $checked_checkboxes будет ['foo', 'baz']
}
$form['example_checkboxes'] = [
'#type' => 'checkboxes',
'#title' => 'Example checkboxes',
'#options' => [
'foo' => 'Foo',
'bar' => 'Bar',
],
'foo' => [
'#attributes' => [
'data-dummy' => 123,
],
],
];
На выходе будет:
<label><input type="checkbox" value="example_checkboxes[foo]" data-dummy="123" /> Foo</label>
<label><input type="checkbox" value="example_checkboxes[bar]" /> Bar</label>
$form['example_checkboxes'] = [
'#type' => 'checkboxes',
'#title' => 'Example checkboxes',
'#options' => [
'foo' => 'Foo',
'bar' => 'Bar',
],
'foo' => [
'#disabled' => TRUE,
],
];
На выходе будет:
<label><input type="checkbox" value="example_checkboxes[foo]" disabled /> Foo</label>
<label><input type="checkbox" value="example_checkboxes[bar]" /> Bar</label>
$form['element'] = [
'#type' => 'textfield',
'#title' => 'Element',
'#theme_wrappers' => [], // <--
];
Замечание - после удаления обёртки не будут работать некоторые js функции, например #states.
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['example_file'] = [
'#type' => 'file',
'#title' => t('File'),
];
...
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
/** @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);
}
}
public function buildForm(array $form, FormStateInterface $form_state) {
if (...) {
return $this->redirect('<front>');
}
}
public function submitForm(array $form, FormStateInterface $form_state) {
$form_state->setRedirect('<front>');
// Или
$redirect_url = Url::fromRoute('<front>');
$form_state->setRedirectUrl($redirect_url);
}
Работа с базой данных (Database API)
$result = \Drupal::database()
->select('node', 'n')
->fields('n', ['nid', 'title'])
->condition('n.type', 'article')
->orderBy('n.nid')
->execute();
$result = \Drupal::database()->select('node')->execute();
foreach ($result as $row) {
$nid = $row->nid;
}
// Или так
$rows = \Drupal::database()->select('node')->execute()->fetchAll();
foreach ($rows as $row) {
$nid = $row->nid;
}
$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'
$result = \Drupal::database()->select('node', 'n')
->fields('n')
->condition('n.type', ['page', 'article'], 'IN') // <--
->execute();
$query->isNull('field_name');
$query->isNotNull('field_name');
// или
$query->condition('field_name', NULL, 'IS NULL');
$query->condition('field_name', NULL, 'IS NOT NULL');
$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'
Замечание — в $subquery
нельзя пользоваться аргументами, т.е. вместо $subquery->condition('field', $value)
надо писать $subquery->where("field = '$value'")
// Пример 1, когда вы сами придумываете синоним присоединяемой таблицы
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid']);
$query->fields('u', ['name']);
$query->innerJoin('users_field_data', 'u', 'u.uid = n.uid');
$result = $query->execute();
// Пример 2, когда синоним присоединяемой таблицы генерируется автоматически
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid']);
$users_table_alias = $query->innerJoin('users_field_data', NULL, '%alias.uid = n.uid');
$query->fields($users_table_alias, ['name']);
$result = $query->execute();
Важно помнить, что методы innerJoin()
и leftJoin()
возвращают синоним присоединённой таблицы, а не объект SelectInterface
, поэтому их нельзя использовать в цепочках вызовов.
$query = \Drupal::database()->select('node', 'n');
$query->fields('n', ['nid']);
$query->leftJoin('node__field_tags', 'ft', 'ft.entity_id = n.nid');
$query->addExpression("GROUP_CONCAT(ft.field_tags_target_id)", 'tags');
$query->groupBy('n.nid');
$result = $query->execute()->fetchAll();
Результат:
0 => [
'nid' => 1,
'tags' => '2,3',
],
1 => [
'nid' => 2,
'tags' => '3,4,7',
],
2 => [
'nid' => 3,
'tags' => '1',
]
$query = \Drupal::database()->select('node', 'n')
->fields('n', ['nid']);
$subquery = \Drupal::database()->select('node__field_tags', 'ft');
$subquery->addExpression("GROUP_CONCAT(ft.field_tags_value)");
$subquery->where('ft.entity_id = n.nid');
$query->addExpression('(' . $subquery . ')', 'tags');
$result = $query->execute()->fetchAll();
Результат будет содержать:
0 => [
'nid' => 1,
'tags' => 'foo,bar',
],
1 => [
'nid' => 2,
'tags' => 'baz',
],
2 => [
'nid' => 3,
'tags' => 'foo,baz',
]
->execute()
вызываем:
$sql = $query->__toString();
Или так, если надо посмотреть запрос с уже подставленными параметрами:
$query->addTag('debug');
\Drupal::database()->truncate('flood')->execute();
if (\Drupal::database()->schema()->tableExists('flood')) {
...
}
database.sql
в корне друпала:
vendor/bin/drush sql-dump --result-file=database.sql --structure-tables-list=batch,cache_*,cachetags,flood,queue,sessions,watchdog
В опции --structure-tables-list
перечисляются таблицы, данные которых экспортировать не надо
database.sql
, находящегося в корне друпала:
vendor/bin/drush sql-drop
vendor/bin/drush sql-cli < database.sql
Если не работает, то так:
vendor/bin/drush sql-drop
cat database.sql | vendor/bin/drush sql-cli
Или даже так:
vendor/bin/drush sql-drop
mysql --user=USERNAME --password=PASSWORD DATABASENAME < database.sql
// Способ 1
$node_data_table = \Drupal::entityTypeManager()->getStorage('node')->getDataTable();
// Способ 2
$node_data_table = \Drupal::entityTypeManager()->getDefinition('node')->getDataTable();
// Способ 3
/** @var NodeStorage $node_storage */
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
/** @var DefaultTableMapping $node_table_mapping */
$node_table_mapping = $node_storage->getTableMapping();
$node_data_table = $node_table_mapping->getDataTable();
/** @var NodeStorage $node_storage */
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
/** @var DefaultTableMapping $node_table_mapping */
$node_table_mapping = $node_storage->getTableMapping();
$field_example_table = $node_table_mapping->getFieldTableName('field_example');
settings.php
информацию о второй БД:
$databases['wordpress']['default'] = array (
'database' => 'wordpress',
'username' => 'root',
'password' => 'root',
'prefix' => '',
'host' => 'localhost',
'port' => '',
'isolation_level' => 'READ COMMITTED',
'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql',
'driver' => 'mysql',
'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
);
2. Делаем запрос:
$wp_database = \Drupal\Core\Database\Database::getConnection('default', 'wordpress');
$query = $wp_database->select('bl_posts', 'p');
$query->addField('p', 'ID', 'id');
$query->addField('p', 'post_date', 'date');
$query->addField('p', 'post_title', 'title');
$query->addField('p', 'post_content', 'content');
$query->condition('p.post_type', 'post');
$query->orderBy('p.ID');
dsm($query->execute()->fetchAll());
Entity Query API
article
со значением поля field_foo
равным bar
:
$nids = \Drupal::entityQuery('node');
->condition('type', 'article');
->condition('status', \Drupal\node\NodeInterface::PUBLISHED);
->condition('field_foo', 'bar')
->accessCheck(FALSE)
->execute();
$nodes = \Drupal\node\Entity\Node::loadMultiple($nids);
$nids = \Drupal::entityQuery('node');
->condition('type', 'article');
->condition('status', \Drupal\node\NodeInterface::PUBLISHED);
->condition('field_example1', 'foo')
->condition('field_example2', 'bar')
->range(0, 1) // <--
->accessCheck(FALSE)
->execute();
if ($nids) {
$node = \Drupal\node\Entity\Node::load(current($nids));
}
$query = \Drupal::entityQuery('node');
// "node.uid=1 OR node.sticky=1"
$or_group = $query->orConditionGroup()
->condition('uid', 1)
->condition('sticky', 1);
$query->condition($or_group);
$nids = \Drupal::entityQuery('node')
->condition('field_example', ['foo', 'bar'], 'IN')
->accessCheck(FALSE)
->execute();
field_example
содержащим как минимум значения foo
и bar
, т.е. значений у поля ноды может быть больше, но обязательно наличие этих двух:
$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->accessCheck(FALSE)->execute();
SQL запрос будет выглядеть как-то так:
SELECT vid, nid
FROM node
INNER JOIN node__field_example AS field_example_1 ON field_example_1.entity_id = node.nid
INNER JOIN node__field_example AS field_example_2 ON field_example_2.entity_id = node.nid
WHERE
field_example_1.field_example_value = 'foo' AND
field_example_2.field_example_value = 'bar'
Если надо получить ноды у которых field_example
равно foo
И bar
и не содержит других значений:
$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'));
$node_query->notExists('field_example.2.value');
$nids = $node_query->accessCheck(FALSE)->execute();
В field_example.2.value
вместо 2
подставляем число значений, а вместо value
название свойства в котором хранится значение (main property name). Запрос получится:
SELECT vid, nid
FROM node
INNER JOIN node__field_example AS field_example_1 ON field_example_1.entity_id = node.nid
INNER JOIN node__field_example AS field_example_2 ON field_example_2.entity_id = node.nid
LEFT JOIN node__field_example AS field_example_3 ON field_example_3.entity_id = node.nid AND field_example_3.delta = '2'
WHERE
field_example_1.field_example_value = 'foo' AND
field_example_2.field_example_value = 'bar' AND
field_example_3.field_example_value IS NULL
// Условие "поле field_example не должно иметь значений"
$entity_query->notExists('field_example');
// Или
$entity_query->condition('field_example', NULL, 'IS NULL');
// Обратное условие "поле field_example должно иметь хотя бы одно значение"
$entity_query->exists('field_example');
// Или
$entity_query->condition('field_example', NULL, 'IS NOT NULL');
$node_query->notExists('field_example.1.value');
В зависимости от типа поля, вместо value
может быть что-то другое, например target_id
для поля типа entity reference.
// Если в поле хранится только дата
$current_date = new \Drupal\Core\Datetime\DrupalDateTime();
$query->condition('field_date', $current_date->format(\Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT), '<');
// Если в поле хранится дата и время
$current_datetime = new \Drupal\Core\Datetime\DrupalDateTime();
$query->condition('field_date', $current_date->format(\Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATETIME_STORAGE_FORMAT), '<');
Сложность в том, что в базе значения поля типа date хранятся в виде строк, поэтому сравнивать надо даты в одном формате — если это только дата, то Y-m-d
, если дата с временем, то Y-m-d\TH:i:s
.
field_category
имеет значение поля status
равным 1
:
$query->condition('field_category.entity.status', 1);
$news_count = \Drupal::entityQuery('node')
->condition('type', 'news')
->count() // <--
->accessCheck(FALSE)
->execute();
Вариант 1:
Перед выполнением execute()
добавить к объекту запроса тэг debug
:
$node_query = \Drupal::entityQuery('node');
...
$node_query->addTag('debug'); // <---
$result = $node_query->execute();
Работает только с включённым модулем Devel.
Вариант 2:
$node_query = \Drupal::entityQuery('node')->condition('type', 'article');
debug($node_query->__toString());
$result = \Drupal::entityQuery('node')
->sort('nid', 'DESC')
->sort('title', 'DESC')
->accessCheck(FALSE)
->execute();
$result = \Drupal::entityQuery('node')
->range(0, 10) // <--
->accessCheck(FALSE)
->execute();
$result = \Drupal::entityQuery('node')
->sort('nid', 'DESC')
->range(0, 1)
->accessCheck(FALSE)
->execute();
$last_nid = $result ? current($result) : NULL;
Работа с меню и адресами, навигация, роутинг
modulename.links.menu.yml
:
modulename.settings:
title: 'Modulename settings'
description: 'Settings for modulename.'
route_name: modulename.settings
parent: system.admin_config_system
При этом роут 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
// Системный адрес без $_GET параметров. Аналог current_path().
$current_system_path = \Drupal::service('path.current')->getPath(); // Вернёт например "/node/123"
// Синоним адреса без $_GET параметров. Аналог request_path().
$current_system_path = \Drupal::service('path.current')->getPath();
$current_path_alias = \Drupal::service('path_alias.manager')->getAliasByPath($current_system_path); // Вернёт например "/article/my-example-article"
// Относительный путь из строки браузера, с GET параметрами. Аналог request_uri().
$current_request_uri = \Drupal::request()->getRequestUri(); // Вернёт например "/article/my-example-article?foo=bar"
// Абсолютный путь из строки браузера, с GET параметрами.
$current_uri = \Drupal::request()->getUri(); // Вернёт например "http://example.com/article/my-example-article?foo=bar"
$base_path = base_path();
base_path()
вернёт /
./drupal/folder
, то base_path()
вернёт /drupal/folder/
.$path = \Drupal::service('extension.path.resolver')->getPath('module', 'views_ui'); // core/modules/views_ui
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(array &$links): void {
$links['user.logout']['options']['attributes']['class'][] = 'use-ajax';
}
Так же можно воспользоваться модулем Menus attribute и добавить класс из админки.
$file_local_url = Drupal::service('file_url_generator')->generateString('public://example.jpg'); // Вернёт /sites/default/files/example.jpg
$file_absolute_url = Drupal::service('file_url_generator')->generateAbsoluteString('public://example.jpg'); // Вернёт http://example.com/sites/default/files/example.jpg
$realpath = \Drupal::service('file_system')->realpath('public://image.jpg'); // Вернёт что-нибудь типа "/var/www/public_html/sites/default/files/image.jpg"
$destination_query = \Drupal::destination()->getAsArray();
$url = \Drupal::urlGenerator()->generateFromRoute('example.route', [], ['query' => $destination_query]);
// /example/route?destination=/current/path
class ExampleController extends ControllerBase {
public function exampleAction() {
return $this->redirect('<front>');
}
}
$path = \Drupal::service('path_alias.manager')->getPathByAlias('/example-url-alias');
$alias = \Drupal::service('path_alias.manager')->getAliasByPath('/node/123');
$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 = \Drupal::service('file_url_generator')->transformRelative($absolute_url); // /sites/default/files/styles/thumbnail/public/image.jpg?itok=BN-lOMvj
$domain = \Drupal::request()->getHost(); // "www.example.com"
Работа с Views
status
для пользователя с правом administer comments
:
/**
* Implements hook_views_pre_view().
*/
function MODULENAME_views_pre_view(ViewExecutable $view, string $display_id, array &$args): void {
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, string $display_id, array &$args): void {
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, string $display_id, array &$args): void {
if ($view->id() == 'my_view_name') {
$view->setItemsPerPage(10);
}
}
/**
* Implements hook_views_pre_view().
*/
function MODULENAME_views_pre_view(ViewExecutable $view, string $display_id, array &$args): void {
if ($view->id() == 'my_view_name') {
$view->display_handler->setOption('pager', ['type' => 'none']);
}
}
title
в представлении content_recent
/**
* Preprocess function for views-view-field--content-recent--title.html.twig.
*/
function THEMENAME_preprocess_views_view_field__content_recent__title(array &$vars): void {
$vars['output'] = 'new field output';
$vars['field']->last_render = $vars['output'];
}
myview
и дисплея page
:
// MODULENAME.module
use Drupal\views\ViewExecutable;
/**
* Implements hook_views_post_execute().
*/
function MODULENAME_views_post_execute(ViewExecutable $view): void {
if ($view->id() == 'myview' && $view->current_display == 'page') {
unset($view->result[2]);
unset($view->result[5]);
}
}
myview
поля edit_node
для пользователей, которые не могут управлять материалами:
// MODULENAME.module
use Drupal\views\ViewExecutable;
/**
* Implements hook_views_pre_render().
*/
function MODULENAME_views_pre_render(ViewExecutable $view): void {
if ($view->id() == 'myview') {
if (!\Drupal::currentUser()->hasPermission('administer nodes')) {
$view->field['edit_node']->options['exclude'] = TRUE;
}
}
}
example_view
:
/**
* Preprocess function for views-view--example-view.html.twig.
*/
function THEMENAME_preprocess_views_view__example_view(array &$vars): void {
$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_build = [
'#type' => 'view',
'#name' => 'my_view_name',
'#display_id' => 'my_view_display_name',
'#arguments' => ['arg1', 'arg2'],
];
Способ 3 (с рендер-кешем):
$views_build = \Drupal\views\Plugin\views\display\Page::buildBasicRenderable('my_view_name', 'my_view_display_name', ['arg1', 'arg2'], \Drupal::routeMatch()->getRouteObject());
Способ 4 (без рендер-кеша):
$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();
Способ 5 (из twig файла) (без рендер-кеша):
{{ drupal_view('my_view_name', 'my_view_display_name', 'my_argument') }}
Способ 6 (из twig файла) (без рендер-кеша):
{{ {
'#type': 'view',
'#name': 'my_view_name',
'#display_id': 'my_view_display_name',
'#arguments': ['arg1', 'arg2'],
} }}
{{ drupal_view('my_view', 'my_display_name') }}
Иначе:
{{ {'#type': 'view', '#name': 'my_view', '#display_id': 'my_display_name'} }}
view.VIEWS_NAME.VIEWS_DISPLAY_NAME
Например для страницы управления материалами это будет:
view.content.page
$('.views-selector').trigger('RefreshView');
При этом в настройках Views должен быть включён ajax.
Работа с javascript
1. В папке модуля создаём файл my-script.js
2. В MODULENAME.libraries.yml
описываем library:
my-library:
js:
my-script.js: {}
3. В MODULENAME.module
подключаем library:
/**
* Implements hook_page_attachments().
*/
function MODULENAME_page_attachments(array &$page): void {
$page['#attached']['library'][] = 'MODULENAME/my-library';
}
# THEMENAME.libraries.yml
my-library:
js:
js/my-library.js: {}
header: true # <--
// THEMENAME.theme
/**
* Preprocess function for html.html.twig.
*/
function THEMENAME_preprocess_html(array &$vars): void {
$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']));
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
должна быть уже подключена.
<a href="/contact/feedback" class="use-ajax" data-dialog-type="modal" data-dialog-options='{"classes":{"ui-dialog":"ui-dialog--full-width"}}'>Открыть диалог</a>
(function (Drupal) {
Drupal.behaviors.myModule = {
attach: function (context, settings) {
once('menu-toggle', '.main-menu-block__title', context).forEach(element => {
element.addEventListener('click', event => {
element.closest('.main-menu-block').classList.toggle('main-menu-block--open');
});
});
}
};
})(Drupal);
Не забываем прописать зависимость либы от core/once
:
my-library:
js:
...
dependencies:
- core/once
Подробнее на английском, на русском.
Работа с многоязычностью
/** @var \Drupal\Core\Language\LanguageInterface $default_language */
$default_language = \Drupal::languageManager()->getDefaultLanguage();
$default_langcode = $default_language->getId(); // Строка "ru", "en" или другой код языка.
/** @var \Drupal\Core\Language\LanguageInterface $current_language */
$current_language = \Drupal::languageManager()->getCurrentLanguage();
$current_langcode = $current_language->getId(); // Строка "ru", "en" или другой код языка.
$node = \Drupal\node\Entity\Node::load(123);
$node_en = $node->hasTranslation('en') ? $node->getTranslation('en') : $node;
$node_en_foo_value = $node_en->get('field_foo')->value;
{{ 'Home'|t }}
{{ 'Order'|t({}, {'context': 'Commerce'}) }}
{% trans %}Product{% endtrans %}
{% set node_en = node.hasTranslation('en') ? node.getTranslation('en') : node %}
{{ node_en.field_foo.value }}
admin/config/regional/language/detection/url
и удаляем префикс дефолтного языка.
services.yml
расскоментируем и изменяем значение cookie_domain
:
parameters:
session.storage.options:
...
cookie_domain: '.example.com'
Сбрасываем кэш.
Комментарии
Спасибо! Ценный материал, и в одном месте.
Насколько люблю 7-ку настолько же ненавижу 8-ку. Может я его неправильно курю? Простой пример, но вместо одной строки в info файле 7-ки делать отдельный файл с кучей строк(https://www.drupal.org/docs/8/api/menu-api/providing-module-defined-men…). Зачем так усложнять?
Из .info файла ссылку конечно не создать, нужен hook_menu, но то что писать теперь нужно больше это да. Усложнено в угоду масштабируемости, хоть большинству она и не нужна. Тут просто надо свыкнуться с мыслью, что семёрка мертва и придётся учить много нового.
Если использовать генераторы кода (drush и drupal console), то в некоторых местах приходится писать даже меньше кода, чем раньше. Да и многие вещи стали гибче, удобнее и логичнее. Но сложности добавилось, это факт.
Огромное Вам спасибо!
Это должно быть в закладках у всякого начинающего адепта секты "Друпал - наше все" )))
Эххх, такой бы гайд еще по коммерц 2....
Замечательная статья, благодарю, много полезного для новичков и не только.
Это просто ахринительски. Спасибо.
Спасибо, очень крутая подборка, теперь я в познании друпал настолько преисполнен что как-будто сто миллиардов миллионов лет с друпалом работаю, мне уже этот мир абсолютно понятен... )))
Спасибо! Закрепить бы где-нибудь, что бы не потерялось.
Здравствуйте. Добавьте в шапке, что в Windows надо прописывать 4 циркумфлекса при указании к версии иначе пакеты могут не ставиться. Например, composer require "foo/bar:^^^^1.0"
Привет. Я тут решил потренироваться по твоим заметкам.
Нюанс... http://xandeadx.ru/blog/drupal/946#faq-load-vocabulary-terms-names
Способ 2 через запрос в базу не покажет все термины словаря, а покажет все термины словаря, которые задействованы в энтити.
Другими словами покажет ТОЛЬКО те термины, которые уже используются (т.е. есть в базе data данных).
И спасибо за FAQ, очень полезно, черпнул для себя всякого.
@Dishvola таблица
taxonomy_term_field_data
содержит все термины, а используемые находятся в таблицеtaxonomy_index
, поэтому запрос покажет все термины словаря, независимо, используются они или нетКруто!
А библиотеки composer устанавливает? Я предпологал, что ставит модуль, все зависимости включая библиотеки. Нужно ручками?
и . я ещё не понимаю что такое twg2, а уже говорят нужно обновлять до 3. Может уже смысл 10 ку сразу разворачивать?
Я понял, у меня была опечатка vid вместо tid, поэтому выдавало один термин, а не оба для моего кейса.
Добавить комментарий