Commit 20f1c993 authored by catch's avatar catch

Issue #2351015 by plach, effulgentsia, Wim Leers, dawehner, martin107,...

Issue #2351015 by plach, effulgentsia, Wim Leers, dawehner, martin107, damiankloip, cilefen, Fabianx, catch, pwolanin, Damien Tournoud, znerol, YesCT, larowlan: URL generation does not bubble cache contexts
parent ae32aaae
......@@ -694,9 +694,15 @@ services:
arguments: ['@route_filter.lazy_collector']
tags:
- { name: event_subscriber }
url_generator:
url_generator.non_bubbling:
class: Drupal\Core\Routing\UrlGenerator
arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@request_stack']
public: false
calls:
- [setContext, ['@?router.request_context']]
url_generator:
class: Drupal\Core\Render\MetadataBubblingUrlGenerator
arguments: ['@url_generator.non_bubbling', '@renderer']
calls:
- [setContext, ['@?router.request_context']]
redirect.destination:
......@@ -1425,7 +1431,7 @@ services:
arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager']
renderer:
class: Drupal\Core\Render\Renderer
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%']
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '@request_stack', '%renderer.config%']
early_rendering_controller_wrapper_subscriber:
class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber
arguments: ['@controller_resolver', '@renderer']
......
......@@ -165,7 +165,7 @@ function _batch_progress_page() {
$query_options['op'] = $new_op;
$batch['url']->setOption('query', $query_options);
$url = $batch['url']->toString();
$url = $batch['url']->toString(TRUE)->getGeneratedUrl();
$build = array(
'#theme' => 'progress_bar',
......
......@@ -810,7 +810,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
$query_options['op'] = 'finished';
$error_url->setOption('query', $query_options);
$batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString()));
$batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString(TRUE)->getGeneratedUrl()));
// Clear the way for the redirection to the batch processing page, by
// saving and unsetting the 'destination', if there is any.
......@@ -840,7 +840,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
$function($batch_url->toString(), ['query' => $query_options]);
}
else {
return new RedirectResponse($batch_url->setAbsolute()->toString());
return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
}
}
else {
......
......@@ -176,6 +176,7 @@ function template_preprocess_pager(&$variables) {
$element = $variables['pager']['#element'];
$parameters = $variables['pager']['#parameters'];
$quantity = $variables['pager']['#quantity'];
$route_name = $variables['pager']['#route_name'];
global $pager_page_array, $pager_total;
// Nothing to do if there is only one page.
......@@ -218,7 +219,7 @@ function template_preprocess_pager(&$variables) {
$options = array(
'query' => pager_query_add_page($parameters, $element, 0),
);
$items['first']['href'] = \Drupal::url('<current>', [], $options);
$items['first']['href'] = \Drupal::url($route_name, [], $options);
if (isset($tags[0])) {
$items['first']['text'] = $tags[0];
}
......@@ -227,7 +228,7 @@ function template_preprocess_pager(&$variables) {
$options = array(
'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
);
$items['previous']['href'] = \Drupal::url('<current>', [], $options);
$items['previous']['href'] = \Drupal::url($route_name, [], $options);
if (isset($tags[1])) {
$items['previous']['text'] = $tags[1];
}
......@@ -243,7 +244,7 @@ function template_preprocess_pager(&$variables) {
$options = array(
'query' => pager_query_add_page($parameters, $element, $i - 1),
);
$items['pages'][$i]['href'] = \Drupal::url('<current>', [], $options);
$items['pages'][$i]['href'] = \Drupal::url($route_name, [], $options);
if ($i == $pager_current) {
$variables['current'] = $i;
}
......@@ -260,7 +261,7 @@ function template_preprocess_pager(&$variables) {
$options = array(
'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
);
$items['next']['href'] = \Drupal::url('<current>', [], $options);
$items['next']['href'] = \Drupal::url($route_name, [], $options);
if (isset($tags[3])) {
$items['next']['text'] = $tags[3];
}
......@@ -269,13 +270,18 @@ function template_preprocess_pager(&$variables) {
$options = array(
'query' => pager_query_add_page($parameters, $element, $pager_max - 1),
);
$items['last']['href'] = \Drupal::url('<current>', [], $options);
$items['last']['href'] = \Drupal::url($route_name, [], $options);
if (isset($tags[4])) {
$items['last']['text'] = $tags[4];
}
}
$variables['items'] = $items;
// The rendered link needs to play well with any other query parameter
// used on the page, like exposed filters, so for the cacheability all query
// parameters matter.
$variables['#cache']['contexts'][] = 'url.query_args';
}
/**
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
......@@ -14,6 +15,7 @@
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
......@@ -44,10 +46,12 @@
* metadata is then merged onto the render array.
*
* In other words: this just exists to ease the transition to Drupal 8: it
* allows controllers that return render arrays (the majority) to still do early
* rendering. But controllers that return responses are already expected to do
* the right thing: if early rendering is detected in such a case, an exception
* is thrown.
* allows controllers that return render arrays (the majority) and
* \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
* often involve a fair amount of rendering) to still do early rendering. But
* controllers that return any other kind of response are already expected to
* do the right thing, so if early rendering is detected in such a case, an
* exception is thrown.
*
* @see \Drupal\Core\Render\RendererInterface
* @see \Drupal\Core\Render\Renderer
......@@ -129,15 +133,26 @@ protected function wrapControllerExecutionInRenderContext($controller, array $ar
// drupal_render() outside of a render context, then the bubbleable metadata
// for that is stored in the current render context.
if (!$context->isEmpty()) {
// If a render array is returned by the controller, merge the "lost"
// bubbleable metadata.
/** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
$early_rendering_bubbleable_metadata = $context->pop();
// If a render array or AjaxResponse is returned by the controller, merge
// the "lost" bubbleable metadata.
if (is_array($response)) {
$early_rendering_bubbleable_metadata = $context->pop();
BubbleableMetadata::createFromRenderArray($response)
->merge($early_rendering_bubbleable_metadata)
->applyTo($response);
}
// If a Response or domain object is returned, and it cares about
elseif ($response instanceof AjaxResponse) {
$response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
// @todo Make AjaxResponse cacheable in
// https://www.drupal.org/node/956186. Meanwhile, allow contrib
// subclasses to be.
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($early_rendering_bubbleable_metadata);
}
}
// If a non-Ajax Response or domain object is returned and it cares about
// attachments or cacheability, then throw an exception: early rendering
// is not permitted in that case. It is the developer's responsibility
// to not use early rendering.
......
......@@ -8,6 +8,8 @@
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Url;
/**
* Provides a base class for form element plugins.
......@@ -111,18 +113,29 @@ public static function validatePattern(&$element, FormStateInterface $form_state
* The form element.
*/
public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
$url = NULL;
$access = FALSE;
if (!empty($element['#autocomplete_route_name'])) {
$parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
$path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
$access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
$url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
/** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
$access_manager = \Drupal::service('access_manager');
$access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);
}
if ($access) {
$element['#attributes']['class'][] = 'form-autocomplete';
$element['#attached']['library'][] = 'core/drupal.autocomplete';
// Provide a data attribute for the JavaScript behavior to bind to.
$element['#attributes']['data-autocomplete-path'] = $path;
$metadata = BubbleableMetadata::createFromRenderArray($element);
if ($access->isAllowed()) {
$element['#attributes']['class'][] = 'form-autocomplete';
$element['#attached']['library'][] = 'core/drupal.autocomplete';
// Provide a data attribute for the JavaScript behavior to bind to.
$element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl();
$metadata->merge($url);
}
$metadata
->merge(BubbleableMetadata::createFromObject($access))
->applyTo($element);
}
return $element;
......
......@@ -34,18 +34,29 @@ public function getInfo() {
'#quantity' => 9,
// An array of labels for the controls in the pager.
'#tags' => [],
// The name of the route to be used to build pager links. By default no
// path is provided, which will make links relative to the current URL.
// This makes the page more effectively cacheable.
'#route_name' => '<none>',
];
}
/**
* #pre_render callback to associate the appropriate cache context.
*
*
* @param array $pager
* A renderable array of #type => pager.
*
* @return array
*/
public static function preRenderPager(array $pager) {
// Note: the default pager theme process function
// template_preprocess_pager() also calls pager_query_add_page(), which
// maintains the existing query string. Therefore
// template_preprocess_pager() adds the 'url.query_args' cache context,
// which causes the more specific cache context below to be optimized away.
// In other themes, however, that may not be the case.
$pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element'];
return $pager;
}
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
......@@ -253,7 +254,11 @@ public static function preRenderAjaxForm($element) {
// Convert \Drupal\Core\Url object to string.
if (isset($settings['url']) && $settings['url'] instanceof Url) {
$settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
$url = $settings['url']->setOptions($settings['options'])->toString(TRUE);
BubbleableMetadata::createFromRenderArray($element)
->merge($url)
->applyTo($element);
$settings['url'] = $url->getGeneratedUrl();
}
else {
$settings['url'] = NULL;
......
......@@ -36,6 +36,7 @@ public function setContent($content) {
// A render array can automatically be converted to a string and set the
// necessary metadata.
if (is_array($content) && (isset($content['#markup']))) {
$content += ['#attached' => ['html_response_placeholders' => []]];
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
$this->setAttachments($content['#attached']);
$content = $content['#markup'];
......
<?php
/**
* @file
* Contains \Drupal\Core\Render\MetadataBubblingUrlGenerator.
*/
namespace Drupal\Core\Render;
use Drupal\Core\GeneratedUrl;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
/**
* Decorator for the URL generator, which bubbles bubbleable URL metadata.
*
* Implements a decorator for the URL generator that allows to automatically
* collect and bubble up bubbleable metadata associated with URLs due to
* outbound path and route processing. This approach helps keeping the render
* and the routing subsystems decoupled.
*
* @see \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface
* @see \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
* @see \Drupal\Core\Render\BubbleableMetadata
*/
class MetadataBubblingUrlGenerator implements UrlGeneratorInterface {
/**
* The non-bubbling URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new bubbling URL generator service.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The non-bubbling URL generator.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(UrlGeneratorInterface $url_generator, RendererInterface $renderer) {
$this->urlGenerator = $url_generator;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public function setContext(SymfonyRequestContext $context) {
$this->urlGenerator->setContext($context);
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->urlGenerator->getContext();
}
/**
* {@inheritdoc}
*/
public function getPathFromRoute($name, $parameters = array()) {
return $this->urlGenerator->getPathFromRoute($name, $parameters);
}
/**
* Bubbles the bubbleable metadata to the current render context.
*
* @param \Drupal\Core\GeneratedUrl $generated_url
* The generated URL whose bubbleable metadata to bubble.
* @param array $options
* (optional) The URL options. Defaults to none.
*/
protected function bubble(GeneratedUrl $generated_url, array $options = []) {
// Bubbling metadata makes sense only if the code is executed inside a
// render context. All code running outside controllers has no render
// context by default, so URLs used there are not supposed to affect the
// response cacheability.
if ($this->renderer->hasRenderContext()) {
$build = [];
$generated_url->applyTo($build);
$this->renderer->render($build);
}
}
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = array(), $absolute = FALSE) {
$options['absolute'] = $absolute;
$generated_url = $this->generateFromRoute($name, $parameters, $options, TRUE);
$this->bubble($generated_url);
return $generated_url->getGeneratedUrl();
}
/**
* {@inheritdoc}
*/
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
$generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
if (!$collect_bubbleable_metadata) {
$this->bubble($generated_url, $options);
}
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
}
/**
* {@inheritdoc}
*/
public function generateFromPath($path = NULL, $options = array(), $collect_bubbleable_metadata = FALSE) {
$generated_url = $this->urlGenerator->generateFromPath($path, $options, TRUE);
if (!$collect_bubbleable_metadata) {
$this->bubble($generated_url, $options);
}
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
}
/**
* {@inheritdoc}
*/
public function supports($name) {
return $this->urlGenerator->supports($name);
}
/**
* {@inheritdoc}
*/
public function getRouteDebugMessage($name, array $parameters = array()) {
return $this->urlGenerator->getRouteDebugMessage($name, $parameters);
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Render;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResultInterface;
......@@ -16,6 +15,7 @@
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Turns a render array into a HTML string.
......@@ -58,7 +58,25 @@ class Renderer implements RendererInterface {
protected $rendererConfig;
/**
* The render context.
* Whether we're currently in a ::renderRoot() call.
*
* @var bool
*/
protected $isRenderingRoot = FALSE;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The render context collection.
*
* An individual global render context is tied to the current request. We then
* need to maintain a different context for each request to correctly handle
* rendering in subrequests.
*
* This must be static as long as some controllers rebuild the container
* during a request. This causes multiple renderer instances to co-exist
......@@ -66,16 +84,9 @@ class Renderer implements RendererInterface {
* fail to render correctly. As soon as it is guaranteed that during a request
* the same container is used, it no longer needs to be static.
*
* @var \Drupal\Core\Render\RenderContext|null
* @var \Drupal\Core\Render\RenderContext[]
*/
protected static $context;
/**
* Whether we're currently in a ::renderRoot() call.
*
* @var bool
*/
protected $isRenderingRoot = FALSE;
protected static $contextCollection;
/**
* Constructs a new Renderer.
......@@ -88,15 +99,23 @@ class Renderer implements RendererInterface {
* The element info.
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
* The render cache service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param array $renderer_config
* The renderer configuration array.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
$this->controllerResolver = $controller_resolver;
$this->theme = $theme;
$this->elementInfo = $element_info;
$this->renderCache = $render_cache;
$this->rendererConfig = $renderer_config;
$this->requestStack = $request_stack;
// Initialize the context collection if needed.
if (!isset(static::$contextCollection)) {
static::$contextCollection = new \SplObjectStorage();
}
}
/**
......@@ -225,10 +244,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
return '';
}
if (!isset(static::$context)) {
$context = $this->getCurrentRenderContext();
if (!isset($context)) {
throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
}
static::$context->push(new BubbleableMetadata());
$context->push(new BubbleableMetadata());
// Set the bubbleable rendering metadata that has configurable defaults, if:
// - this is the root call, to ensure that the final render array definitely
......@@ -269,10 +289,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
}
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
static::$context->update($elements);
$context->update($elements);
// Render cache hit, so rendering is finished, all necessary info
// collected!
static::$context->bubble();
$context->bubble();
return $elements['#markup'];
}
}
......@@ -370,9 +390,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
if (!empty($elements['#printed'])) {
// The #printed element contains all the bubbleable rendering metadata for
// the subtree.
static::$context->update($elements);
$context->update($elements);
// #printed, so rendering is finished, all necessary info collected!
static::$context->bubble();
$context->bubble();
return '';
}
......@@ -499,7 +519,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// We've rendered this element (and its subtree!), now update the context.
static::$context->update($elements);
$context->update($elements);
// Cache the processed element if both $pre_bubbling_elements and $elements
// have the metadata necessary to generate a cache ID.
......@@ -522,40 +542,73 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
if ($is_root_call) {
$this->replacePlaceholders($elements);
// @todo remove as part of https://www.drupal.org/node/2511330.
if (static::$context->count() !== 1) {
if ($context->count() !== 1) {
throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
}
}
// Rendering is finished, all necessary info collected!
static::$context->bubble();
$context->bubble();
$elements['#printed'] = TRUE;
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
return $elements['#markup'];
}
/**
* {@inheritdoc}
*/
public function hasRenderContext() {
return (bool) $this->getCurrentRenderContext();
}
/**
* {@inheritdoc}
*/
public function executeInRenderContext(RenderContext $context, callable $callable) {
// Store the current render context.
$current_context = static::$context;
$previous_context = $this->getCurrentRenderContext();
// Set the provided context and call the callable, it will use that context.
static::$context = $context;
$this->setCurrentRenderContext($context);
$result = $callable();
// @todo Convert to an assertion in https://www.drupal.org/node/2408013
if (static::$context->count() > 1) {
if ($context->count() > 1) {
throw new \LogicException('Bubbling failed.');
}
// Restore the original render context.
static::$context = $current_context;
$this->setCurrentRenderContext($previous_context);
return $result;
}
/**
* Returns the current render context.
*
* @return \Drupal\Core\Render\RenderContext
* The current render context.
*/
protected function getCurrentRenderContext() {
$request = $this->requestStack->getCurrentRequest();
return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
}
/**
* Sets the current render context.
*
* @param \Drupal\Core\Render\RenderContext|null $context
* The render context. This can be NULL for instance when restoring the
* original render context, which is in fact NULL.
*
* @return $this
*/
protected function setCurrentRenderContext(RenderContext $context = NULL) {
$request = $this->requestStack->getCurrentRequest();
static::$contextCollection[$request] = $context;
return $this;
}
/**
* Replaces placeholders.
*
......
......@@ -321,6 +321,18 @@ public function renderPlain(&$elements);
*/
public function render(&$elements, $is_root_call = FALSE);
/**
* Checks whether a render context is active.
*
* This is useful only in very specific situations to determine whether the
* system is already capable of collecting bubbleable metadata. Normally it
* should not be necessary to be concerned about this.
*
* @return bool
* TRUE if the renderer has a render context active, FALSE otherwise.
*/
public function hasRenderContext();
/**
* Executes a callable within a render context.
*
......
......@@ -278,12 +278,9 @@ public function generate($name, $parameters = array(), $absolute = FALSE) {
* {@inheritdoc}
*/
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
$generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
$options += array('prefix' => '');
$route = $this->getRoute($name);
$name = $this->getRouteDebugMessage($name);
$this->processRoute($name, $route, $parameters, $generated_url);
$generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
$query_params = [];
// Symfony adds any parameters that are not path slugs as query strings.
......@@ -291,6 +288,23 @@ public function generateFromRoute($name, $parameters = array(), $options = array
$query_params = $options['query'];
}
$fragment = '';
if (isset($options['fragment'])) {
if (($fragment = trim($options['fragment'])) != '') {
$fragment = '#' . $fragment;
}
}
// Generate a relative URL having no path, just query string and fragment.
if ($route->getOption('_no_path')) {
$query = $query_params ? '?' . http_build_query($query_params, '', '&') : '';
$url = $query . $fragment;
return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
}
$options += array('prefix' => '');
$name = $this->getRouteDebugMessage($name);
$this->processRoute($name, $route, $parameters, $generated_url);
$path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
$path = $this->processPath($path, $options, $generated_url);
......@@ -300,13 +314,6 @@ public function generateFromRoute($name, $parameters = array(), $options = array