158 подписчиков

Рендеринг страницы в Drupal

 Недавняя проблема, с которой я столкнулся в проекте, заключалась в создании HTML полной страницы Drupal из запроса Drupal.

Недавняя проблема, с которой я столкнулся в проекте, заключалась в создании HTML полной страницы Drupal из запроса Drupal. Проект требовал, чтобы шаблон отображался в структуре темы Drupal, а также включал стили, связанные с этой темой.

Поначалу это не кажется большой проблемой, но как только я начал разбираться, все стало намного сложнее, чем я ожидал.

Проблема в том, что вы не можете просто отобразить страницу, используя шаблоны «html» и «page», поскольку эти шаблоны окружены большим количеством контекста. Без этого контекста Drupal создает страницу разметки, которая не содержит стилей или блоков. Контекст — это то, как Drupal узнает, какую тему загрузить, какие библиотеки добавить на страницу, какие хуки предварительной обработки вызвать, какие элементы меню сгенерировать и так далее.

Я нашел несколько различных решений этой проблемы, которые работают довольно хорошо.

В обеих этих ситуациях я создам визуализацию страницы, а затем отправлю ее обратно пользователю, используя новый объект Response из контроллера. По сути, это заменяет ответ от контроллера Drupal нашей пользовательской разметкой.

Рендеринг страницы с использованием запроса на получение

Поскольку мы пытаемся отобразить страницу, имеет смысл попросить Drupal отобразить для нас эту страницу целиком с помощью подзапроса. Это означает выполнение вторичного запроса с использованием HTTP-клиента Guzzle к странице, которую мы хотим отобразить, а затем возврат результата этого запроса в объект Response.

Это, пожалуй, самый простой подход, так как нам просто нужно определить страницу, на которую мы хотим перейти, а затем сделать запрос к этой странице.

Вот метод действия для обычного контроллера Drupal, где мы отображаем узел 1 в полном контексте ответа страницы. Я не упомянул здесь немного стандартного кода, но вы захотите внедрить службу ' http_client ' в свой контроллер и сохранить ее в свойстве httpClient объектов.

public function returnGetPage() {

// Создайте объект Url для страницы, которую мы хотим получить.
$url = new Url('entity.node.canonical', ['node' => 1]);

// Сделайте звонок на страницу.
$response = $this->httpClient->request('GET', $url->setAbsolute()->toString());

// Извлеките содержимое из ответа.
$page = $response->getBody()->getContents();

// Верните содержимое.
return new Response($page);
}

Примечание: если вы хотите сделать запрос, а также игнорировать любые ошибки сертификата SSL, вы можете передать такой массив параметров.

$options = [
\GuzzleHttp\RequestOptions::VERIFY => FALSE,
];
$response = $this->httpClient->request('GET', $url->setAbsolute()->toString(), $options);

Этот массив опций полезен, если вы пишете этот код локально и хотите иметь возможность его тестировать. У вас может не быть действительного сертификата, поэтому он понадобится вам для выполнения запроса.

У такого подхода к использованию подзапроса для доступа к странице Drupal есть несколько больших ограничений. Если страница, к которой вы пытаетесь получить доступ, находится за какой-либо аутентификацией, вам нужно будет внедрить эту аутентификацию в запрос, иначе вы просто получите ответ 403 HTTP. Вы можете сделать это в том же примере массива параметров, который я добавил выше.

Кроме того, поскольку здесь мы, по сути, создаем еще один веб-запрос, существует опасность, что мы удвоим время запроса страницы и, возможно, нагрузку на сервер. Поскольку запрос к сайту Drupal затем сделает другой запрос к Drupal, время ответа на этот запрос страницы может быть медленным.

Если вы пойдете по этому пути, я настоятельно рекомендую вам добавить кэширование, чтобы предотвратить значительное замедление работы сайта.

В конечном счете, ответ, который мы получаем от действия контроллера, представляет собой полностью отображаемую страницу контента. Поскольку мы не передали текущий сеанс подзапросу и, по сути, вернули ответ раньше, отображаемая страница выглядит так, как будто мы получили к ней доступ через анонимный сеанс.

Рендеринг страницы с использованием конвейера рендеринга Drupal

После того, как я создал код для первого решения, я начал искать другие способы сделать это, поскольку этот подход имеет ряд ограничений. Это привело меня к тому, что я нашел пару сервисов Drupal, которыми я раньше не пользовался, но я смог создать полную страницу контента, используя сервисы и шаблоны Drupal.

При таком подходе происходит довольно много всего, поэтому я разделю код на более мелкие фрагменты и пройдусь по ним по частям.

Первое, что нам нужно сделать, это отобразить содержимое страницы. Поскольку я хотел отобразить узел 1, это означало загрузку этого узла и передачу его через механизм рендеринга для создания содержимого страницы. Вы могли видеть этот код в другом месте, так как это довольно распространенный вопрос.

// Рендеринг узла
$nid = 1;
$entityType = 'node';
$viewMode = 'default';
$storage = \Drupal::entityTypeManager()->getStorage($entityType);
$node = $storage->load($nid);
$viewBuilder = \Drupal::entityTypeManager()->getViewBuilder($entityType); $build = $viewBuilder->view($node, $viewMode);

После запуска этого кода переменная $build теперь содержит массив рендеринга для узла.

Поскольку мы хотим отображать страницу на основе контекста загруженного узла, следующим шагом будет сообщить Drupal, что мы хотим это сделать. Это включает в себя загрузку текущего объекта запроса и установку узла в качестве атрибута запроса. Это будет использоваться вверх по течению для извлечения блоков и других вещей, которые составляют страницу Drupal.

//Получите текущий объект запроса.
$request = \Drupal::request();
// Введите объект узла в запрос.
$request->attributes->set('node', $node);

Обратите внимание, что вам может потребоваться удалить атрибуты из запроса, если ответ ваших страниц содержит вещи, не связанные с узлами. Поскольку мы находимся в простом контроллере и используем жестко запрограммированный идентификатор узла, в объекте ответа не будет многого.

Аналогично настройке правильного запроса нам также необходимо сгенерировать объект RouteMatch, который содержит информацию о маршруте, к которому мы обращаемся. Это используется Drupal для информирования процесса рендеринга о текущем контексте страницы. RouteMatch генерируется путем получения объекта Url из объекта Node, который мы загрузили выше, и использования его для создания нового объекта RouteMatch.

// Извлеките URL-адрес из объекта узла.
$url = $node->toUrl();
// Создайте сопоставление маршрутов .
/** @var \Drupal\Core\Routing\RouteProvider $route */
$route = \Drupal::service('router.route_provider')->getRouteByName($url->getRouteName());
$routeMatch = new RouteMatch($url->getRouteName(), $route, $url->getRouteParameters());

Имея все это в руках, мы можем перейти к полному рендерингу страницы.

Это включает в себя извлечение экземпляра службы main_content_renderer.html и использование метода renderResponse() для рендеринга сгенерированного ранее массива рендеринга. Массив рендеринга $build, объект Request и объект RouteMatch, которые мы только что создали, также передаются в этот метод. Ответом этого метода является объект Response, который содержит всю необходимую нам информацию о разметке, а также некоторые другие фрагменты.

// Визуализируйте страницу.
/** @var \Drupal\Core\Render\MainContent\HtmlRenderer $renderer */
$renderer = \Drupal::service('main_content_renderer.html');
$response = $renderer->renderResponse($build, $request, $routeMatch);

Однако это еще не конец истории. В ответе, который мы получаем от этого метода, отсутствуют все стили и сценарии, которые обычно внедряются на страницу Drupal.

Сгенерированная разметка на этом этапе будет содержать заполнители для этого контента, полученного из шаблона html.html.twig. Эти заполнители из этого шаблона выглядят так и еще не были заменены.

<head>
<head-placeholder token="{{ placeholder_token }}">
<title>{{ head_title|safe_join(' | ') }}</title>
<css-placeholder token="{{ placeholder_token }}">
<js-placeholder token="{{ placeholder_token }}">
</head>

Чтобы внедрить необходимые библиотеки для страницы, нам нужно использовать службу ' html_response.attachments_processor '. Эта служба содержит метод под названием processAttachments(), который принимает объект ответа и преобразует заполнители в правильные теги стиля и скрипта.

// Завершите рендеринг.
/** @var \Drupal\Core\Render\HtmlResponseAttachmentsProcessor $processor */
$processor = \Drupal::service('html_response.attachments_processor');
$response = $processor->processAttachments($response);

Возвращаемый здесь объект ответа теперь содержит всю разметку страницы, как если бы он был сгенерирован с помощью обычного веб-запроса. Поскольку этот объект ответа содержит метаданные кеша, из-за которых Drupal выдает ошибку, нам необходимо получить содержимое ответа и создать собственный объект ответа. Это последние строки метода, и возвращаемый ответ будет содержать всю отображаемую страницу из Drupal.

// Извлеките содержимое из ответа.
$content = $response->getContent();
// Верните содержимое.
return new Response($content);

Преимущество этого метода заключается в том, что его можно проделать со страницами, которые в противном случае были бы скрыты от просмотра. Это дает нам изящный механизм показа пользователям страницы контента, фактически не предоставляя им прямого доступа к этому контенту. Единственное, что нам нужно сделать, это создать массив рендеринга из объекта или шаблона, чтобы мы могли передать эту информацию в службу main_content_renderer.html.

Собрав все это вместе, у нас есть контроллер, который выглядит примерно так, как показано ниже. Обратите внимание, что я заменил все статически вызываемые службы службами с внедрением зависимостей, что является лучшим подходом в Drupal.

<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Routing\RouteMatch;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
class MyModuleController extends ControllerBase {
/**
* Менеджер типов сущностей.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;

/**
* Интерфейс поставщика маршрутов.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;

/**
* Объект запроса.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;

/**
* Основной сервис рендеринга контента.
*
* @var \Drupal\Core\Render\MainContent\MainContentRendererInterface */
protected $mainContentRenderer;

/**
* Служба обработки вложений.
*
* @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
*/
protected $attachmentsProcessor;
/**
* Конструктор контроллера аудита.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Менеджер типов сущностей.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* Интерфейс поставщика маршрутов.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* Текущий объект запроса.
* @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $main_content_renderer
* Основной сервис рендеринга контента.
* @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $attachments_processor
* Служба обработки вложений.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteProviderInterface $route_provider, RequestStack $request_stack, MainContentRendererInterface $main_content_renderer, AttachmentsResponseProcessorInterface $attachments_processor) {
$this->entityTypeManager = $entity_type_manager;
$this->routeProvider = $route_provider;
$this->request = $request_stack->getCurrentRequest();
$this->mainContentRenderer = $main_content_renderer;
$this->attachmentsProcessor = $attachments_processor;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('router.route_provider'),
$container->get('request_stack'),
$container->get('main_content_renderer.html'),
$container->get('html_response.attachments_processor')
);
}
public function returnRenderedPage() {
// Визуализируйте узел.
$nid = 1;
$entityType = 'node';
$viewMode = 'default';
$storage = $this->entityTypeManager->getStorage($entityType);
$node = $storage->load($nid);
$viewBuilder = $this->entityTypeManager->getViewBuilder($entityType);
$build = $viewBuilder->view($node, $viewMode);
//Введите объект узла в запрос.
$this->request->attributes->set('node', $node);
// Извлеките URL-адрес из объекта узла.
$url = $node->toUrl();
// Создайте объект RouteMatch.
$route = $this->routeProvider->getRouteByName($url->getRouteName());
$routeMatch = new RouteMatch($url->getRouteName(), $route, $url->getRouteParameters());
// Визуализируйте страницу.
$response = $this->mainContentRenderer->renderResponse($build, $this->request, $routeMatch);
// Завершите рендеринг.
$response = $this->attachmentsProcessor->processAttachments($response);
// Извлеките содержимое из ответа.
$content = $response->getContent();
// Верните содержимое.
return new Response($content);
}
}

Тот факт, что мы используем конвейер рендеринга страниц Drupal для генерации HTML, полезен, поскольку это означает, что нам не нужно беспокоиться о размещении блоков на странице. Drupal сделает это за нас на основе контекста, который мы создадим.

Возможно, самая сложная часть этого заключается в том, чтобы выяснить, как визуализировать то, что вы хотите визуализировать, и убедиться, что объект Request содержит эту информацию. Как только вы это сделаете, все остальное будет происходить таким же образом.

Эти методы имеют ряд применений помимо простого рендеринга страницы контента и отправки ее пользователю. Например, вместо того, чтобы отправлять содержимое обратно пользователю, вы можете вместо этого сохранить содержимое в файл или, возможно, отправить ответ обратному прокси-системе для создания кеша.

Я надеюсь, что эта статья оказалась для вас полезной. Если вы столкнулись с похожими проблемами, свяжитесь с нами (поддержка сайта друпал) и узнайте, сможем ли мы помочь вам их решить.