Обновляемый список коротких вопросов и ответов по 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/develcomposer create-project drupal/recommended-projectСпособ 2 (папка vendor будет в web root):
composer create-project drupal/legacy-project1. Изменить "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/drushcomposer require drupal/develRC версия:
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 devel2. Потом удаляем из файловой системы:
composer remove drupal/develcomposer 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 updbprohibits (синоним 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/coresymfony/process
composer why symfony/processcomposer 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 qwertyivan, паролем qwerty и ролью administrator:
vendor/bin/drush user:create ivan --mail="ivan@example.com" --password=qwerty
vendor/bin/drush user:role:add administrator ivanvendor/bin/drush config-import --partial --source=modules/modulename/config/install/**
* Implements hook_query_TAG_alter(): comment_filter.
*/
function MODULENAME_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/HelloWorldController.php
namespace Drupal\modulename\Controller;
use Drupal\Core\Controller\ControllerBase;
class HelloWorldController extends ControllerBase {
public function __invoke(): array {
return [
'#markup' => 'Hello, World!',
];
}
}# modulename.routing.yml
modulename.hello_world:
path: '/hello-world'
defaults:
_controller: 'Drupal\modulename\Controller\ModulenameController'
_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 blockuse 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!');\Drupal\Component\Utility\Timer::start('test');
sleep(1);
debug(\Drupal\Component\Utility\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);vendor/bin/drush eval '\Drupal::service("update.update_hook_registry")->setInstalledVersion("MODULENAME", 123);'
vendor/bin/drush updbВместо 123 указываем версию N, которая была ДО нужного хука.
Работа с сущностями (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_path = \Drupal::service('file_system')->realpath($file->getFileUri()); // /home/username/sitename/web/sites/default/files/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,3article (способ подходит для удаления небольшого количества сущностей, до тысячи):
\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=articlesort(), который можно использовать в 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...',
];$build = [
'#theme' => 'table',
'#header' => ['ID', 'Title'],
'#rows' => [
[
1,
[
'data' => 'Title 1',
'class' => ['cell-class'],
'data-my-attr' => 'my cell attribute',
],
],
[2, 'Title 2'],
],
];Результат:
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td class="cell-class" data-my-attr="my cell attribute">Title 1</td>
</tr>
<tr>
<td>2</td>
<td>Title 2</td>
</tr>
</tbody>
</table>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,
];
}// src/Hook/FormViewsExposedFormAlter.php
/**
* Implements hook_form_FORM_ID_alter(): views_exposed_form.
*/
#[Hook('form_views_exposed_form_alter')]
class FormViewsExposedFormAlter {
public function __invoke(array &$form, FormStateInterface $form_state): void {
$view = $form_state->get('view'); /** @var ViewExecutable $view */
if ($view->id() == 'example_views' && $view->current_display == 'example_display') {
...
}
}
}Drupal 10-:
// 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-simplepublic 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:configforeach (\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();Имена таблиц и колонок могут конфликтовать с зарезервированными словами базы данных или содержать недопустимые символы, зависящие от движка БД. Например если колонка таблицы называется from, то запрос select from from table_name выдаст ошибку.
Чтобы этого не происходило названия таблиц и колонок принято заключать в кавычки — select `from` from `table_name`. Тип кавычек зависит от движка БД.
В Drupal вместо кавычек используются круглые скобки для имён таблиц и квадратные для названия колонок — select [from] from {table_name}.
$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_uiif (\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/pathclass 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
(Drupal => {
const userLoginUrl = Drupal.url('user/login');
} (Drupal));Если сайт установлен в /, то url будет /user/login, а если в /subpath, то /subpath/user/login
$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'Сбрасываем кэш.
- Видео уроки от Lullabot.com
- Third Party Settings — интерфейс хранения дополнительных настроек конфигурационных сущностей и плагинов
- Отличие методов BaseFieldDefinition::setDefaultValue() и BaseFieldDefinition::setInitialValue()
- Навесить на элемент managed_file свой ajax callback (Как обновить всю форму при загрузки файла в managed_file)
- Добавить своё действие над сущностью в contextual links
Комментарии
Спасибо! Ценный материал, и в одном месте.
Насколько люблю 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, поэтому выдавало один термин, а не оба для моего кейса.
Добавить комментарий