3 namespace Drupal\Core\Utility;
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Render\MarkupInterface;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\GeneratedLink;
10 use Drupal\Core\GeneratedNoLink;
12 use Drupal\Core\Render\RendererInterface;
13 use Drupal\Core\Routing\UrlGeneratorInterface;
14 use Drupal\Core\Template\Attribute;
18 * Provides a class which generates a link with route names and parameters.
20 class LinkGenerator implements LinkGeneratorInterface {
25 * @var \Drupal\Core\Routing\UrlGeneratorInterface
27 protected $urlGenerator;
30 * The module handler firing the route_link alter hook.
32 * @var \Drupal\Core\Extension\ModuleHandlerInterface
34 protected $moduleHandler;
37 * The renderer service.
39 * @var \Drupal\Core\Render\RendererInterface
44 * Constructs a LinkGenerator instance.
46 * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
48 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
50 * @param \Drupal\Core\Render\RendererInterface $renderer
51 * The renderer service.
53 public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, RendererInterface $renderer) {
54 $this->urlGenerator = $url_generator;
55 $this->moduleHandler = $module_handler;
56 $this->renderer = $renderer;
62 public function generateFromLink(Link $link) {
63 return $this->generate($link->getText(), $link->getUrl());
69 * For anonymous users, the "active" class will be calculated on the server,
70 * because most sites serve each anonymous user the same cached page anyway.
71 * For authenticated users, the "active" class will be calculated on the
72 * client (through JavaScript), only data- attributes are added to links to
73 * prevent breaking the render cache. The JavaScript is added in
74 * system_page_attachments().
76 * @see system_page_attachments()
78 public function generate($text, Url $url) {
79 // The link generator should not modify the original URL object, this
80 // ensures consistent rendering.
81 // @see https://www.drupal.org/node/2842399
84 // Performance: avoid Url::toString() needing to retrieve the URL generator
85 // service from the container.
86 $url->setUrlGenerator($this->urlGenerator);
88 if (is_array($text)) {
89 $text = $this->renderer->render($text);
92 // Start building a structured representation of our link to be altered later.
96 'options' => $url->getOptions(),
99 // Merge in default options.
100 $variables['options'] += [
104 'set_active_class' => FALSE,
108 // Add a hreflang attribute if we know the language of this link's url and
109 // hreflang has not already been set.
110 if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) {
111 $variables['options']['attributes']['hreflang'] = $variables['options']['language']->getId();
114 // Ensure that query values are strings.
115 array_walk($variables['options']['query'], function (&$value) {
116 if ($value instanceof MarkupInterface) {
117 $value = (string) $value;
121 // Set the "active" class if the 'set_active_class' option is not empty.
122 if (!empty($variables['options']['set_active_class']) && !$url->isExternal()) {
123 // Add a "data-drupal-link-query" attribute to let the
124 // drupal.active-link library know the query in a standardized manner.
125 if (!empty($variables['options']['query'])) {
126 $query = $variables['options']['query'];
128 $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query);
131 // Add a "data-drupal-link-system-path" attribute to let the
132 // drupal.active-link library know the path in a standardized manner.
133 if ($url->isRouted() && !isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
134 // @todo System path is deprecated - use the route name and parameters.
135 $system_path = $url->getInternalPath();
136 // Special case for the front page.
137 $variables['options']['attributes']['data-drupal-link-system-path'] = $system_path == '' ? '<front>' : $system_path;
141 // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags()
142 // only when a quick strpos() gives suspicion tags are present.
143 if (isset($variables['options']['attributes']['title']) && strpos($variables['options']['attributes']['title'], '<') !== FALSE) {
144 $variables['options']['attributes']['title'] = strip_tags($variables['options']['attributes']['title']);
147 // Allow other modules to modify the structure of the link.
148 $this->moduleHandler->alter('link', $variables);
149 $url = $variables['url'];
151 // Move attributes out of options since generateFromRoute() doesn't need
152 // them. Make sure the "href" comes first for testing purposes.
153 $attributes = ['href' => ''] + $variables['options']['attributes'];
154 unset($variables['options']['attributes']);
155 $url->setOptions($variables['options']);
157 // External URLs can not have cacheable metadata.
158 if ($url->isExternal()) {
159 $generated_link = new GeneratedLink();
160 $attributes['href'] = $url->toString(FALSE);
162 elseif ($url->isRouted() && $url->getRouteName() === '<nolink>') {
163 $generated_link = new GeneratedNoLink();
164 unset($attributes['href']);
167 $generated_url = $url->toString(TRUE);
168 $generated_link = GeneratedLink::createFromObject($generated_url);
169 // The result of the URL generator is a plain-text URL to use as the href
170 // attribute, and it is escaped by \Drupal\Core\Template\Attribute.
171 $attributes['href'] = $generated_url->getGeneratedUrl();
174 if (!($variables['text'] instanceof MarkupInterface)) {
175 $variables['text'] = Html::escape($variables['text']);
177 $attributes = new Attribute($attributes);
178 // This is safe because Attribute does escaping and $variables['text'] is
179 // either rendered or escaped.
180 return $generated_link->setGeneratedLink('<' . $generated_link::TAG . $attributes . '>' . $variables['text'] . '</' . $generated_link::TAG . '>');