3 namespace Drupal\metatag;
5 use Drupal\Component\Render\PlainTextOutput;
6 use Drupal\Core\Entity\ContentEntityInterface;
7 use Drupal\Core\Entity\EntityTypeManagerInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
10 use Drupal\views\ViewEntityInterface;
13 * Class MetatagManager.
15 * @package Drupal\metatag
17 class MetatagManager implements MetatagManagerInterface {
20 * The group plugin manager.
22 * @var \Drupal\metatag\MetatagGroupPluginManager
24 protected $groupPluginManager;
27 * The tag plugin manager.
29 * @var \Drupal\metatag\MetatagTagPluginManager
31 protected $tagPluginManager;
34 * The Metatag defaults.
38 protected $metatagDefaults;
43 * @var \Drupal\metatag\MetatagToken
45 protected $tokenService;
48 * The Metatag logging channel.
50 * @var \Drupal\Core\Logger\LoggerChannelInterface
55 * Constructor for MetatagManager.
57 * @param \Drupal\metatag\MetatagGroupPluginManager $groupPluginManager
58 * The MetatagGroupPluginManager object.
59 * @param \Drupal\metatag\MetatagTagPluginManager $tagPluginManager
60 * The MetatagTagPluginMÏ€anager object.
61 * @param \Drupal\metatag\MetatagToken $token
62 * The MetatagToken object.
63 * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory
64 * The LoggerChannelFactoryInterface object.
65 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
66 * The EntityTypeManagerInterface object.
68 public function __construct(MetatagGroupPluginManager $groupPluginManager,
69 MetatagTagPluginManager $tagPluginManager,
71 LoggerChannelFactoryInterface $channelFactory,
72 EntityTypeManagerInterface $entityTypeManager) {
73 $this->groupPluginManager = $groupPluginManager;
74 $this->tagPluginManager = $tagPluginManager;
75 $this->tokenService = $token;
76 $this->logger = $channelFactory->get('metatag');
77 $this->metatagDefaults = $entityTypeManager->getStorage('metatag_defaults');
81 * Returns the list of protected defaults.
84 * Th protected defaults.
86 public static function protectedDefaults() {
101 public function tagsFromEntity(ContentEntityInterface $entity) {
104 $fields = $this->getFields($entity);
106 /* @var \Drupal\field\Entity\FieldConfig $field_info */
107 foreach ($fields as $field_name => $field_info) {
108 // Get the tags from this field.
109 $tags = $this->getFieldTags($entity, $field_name);
118 public function tagsFromEntityWithDefaults(ContentEntityInterface $entity) {
119 return $this->tagsFromEntity($entity) + $this->defaultTagsFromEntity($entity);
125 public function defaultTagsFromEntity(ContentEntityInterface $entity) {
126 /** @var \Drupal\metatag\Entity\MetatagDefaults $metatags */
127 $metatags = $this->metatagDefaults->load('global');
131 // Add/overwrite with tags set on the entity type.
132 $entity_type_tags = $this->metatagDefaults->load($entity->getEntityTypeId());
133 if (!is_null($entity_type_tags)) {
134 $metatags->overwriteTags($entity_type_tags->get('tags'));
136 // Add/overwrite with tags set on the entity bundle.
137 $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
138 if (!is_null($bundle_metatags)) {
139 $metatags->overwriteTags($bundle_metatags->get('tags'));
141 return $metatags->get('tags');
145 * Gets the group plugin definitions.
150 protected function groupDefinitions() {
151 return $this->groupPluginManager->getDefinitions();
155 * Gets the tag plugin definitions.
160 protected function tagDefinitions() {
161 return $this->tagPluginManager->getDefinitions();
167 public function sortedGroups() {
168 $metatag_groups = $this->groupDefinitions();
170 // Pull the data from the definitions into a new array.
172 foreach ($metatag_groups as $group_name => $group_info) {
173 $groups[$group_name]['id'] = $group_info['id'];
174 $groups[$group_name]['label'] = $group_info['label']->render();
175 $groups[$group_name]['description'] = $group_info['description'];
176 $groups[$group_name]['weight'] = $group_info['weight'];
179 // Create the 'sort by' array.
181 foreach ($groups as $group) {
182 $sort_by[] = $group['weight'];
185 // Sort the groups by weight.
186 array_multisort($sort_by, SORT_ASC, $groups);
194 public function sortedTags() {
195 $metatag_tags = $this->tagDefinitions();
197 // Pull the data from the definitions into a new array.
199 foreach ($metatag_tags as $tag_name => $tag_info) {
200 $tags[$tag_name]['id'] = $tag_info['id'];
201 $tags[$tag_name]['label'] = $tag_info['label']->render();
202 $tags[$tag_name]['group'] = $tag_info['group'];
203 $tags[$tag_name]['weight'] = $tag_info['weight'];
206 // Create the 'sort by' array.
208 foreach ($tags as $key => $tag) {
209 $sort_by['group'][$key] = $tag['group'];
210 $sort_by['weight'][$key] = $tag['weight'];
213 // Sort the tags by weight.
214 array_multisort($sort_by['group'], SORT_ASC, $sort_by['weight'], SORT_ASC, $tags);
222 public function sortedGroupsWithTags() {
223 $groups = $this->sortedGroups();
224 $tags = $this->sortedTags();
226 foreach ($tags as $tag_name => $tag) {
227 $tag_group = $tag['group'];
229 if (!isset($groups[$tag_group])) {
230 // If the tag is claiming a group that has no matching plugin, log an
231 // error and force it to the basic group.
232 $this->logger->error("Undefined group '%group' on tag '%tag'", ['%group' => $tag_group, '%tag' => $tag_name]);
233 $tag['group'] = 'basic';
234 $tag_group = 'basic';
237 $groups[$tag_group]['tags'][$tag_name] = $tag;
246 public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) {
247 // Add the outer fieldset.
249 '#type' => 'details',
252 $element += $this->tokenService->tokenBrowser($token_types);
254 $groups_and_tags = $this->sortedGroupsWithTags();
256 foreach ($groups_and_tags as $group_name => $group) {
257 // Only act on groups that have tags and are in the list of included
258 // groups (unless that list is null).
259 if (isset($group['tags']) && (is_null($included_groups) || in_array($group_name, $included_groups) || in_array($group['id'], $included_groups))) {
260 // Create the fieldset.
261 $element[$group_name]['#type'] = 'details';
262 $element[$group_name]['#title'] = $group['label'];
263 $element[$group_name]['#description'] = $group['description'];
264 $element[$group_name]['#open'] = FALSE;
266 foreach ($group['tags'] as $tag_name => $tag) {
267 // Only act on tags in the included tags list, unless that is null.
268 if (is_null($included_tags) || in_array($tag_name, $included_tags) || in_array($tag['id'], $included_tags)) {
269 // Make an instance of the tag.
270 $tag = $this->tagPluginManager->createInstance($tag_name);
272 // Set the value to the stored value, if any.
273 $tag_value = isset($values[$tag_name]) ? $values[$tag_name] : NULL;
274 $tag->setValue($tag_value);
276 // Open any groups that have non-empty values.
277 if (!empty($tag_value)) {
278 $element[$group_name]['#open'] = TRUE;
281 // Create the bit of form for this tag.
282 $element[$group_name][$tag_name] = $tag->form($element);
292 * Returns a list of the Metatag fields on an entity.
294 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
295 * The entity to examine.
298 * The fields from the entity which are Metatag fields.
300 protected function getFields(ContentEntityInterface $entity) {
303 if ($entity instanceof ContentEntityInterface) {
304 // Get a list of the metatag field types.
305 $field_types = $this->fieldTypes();
307 // Get a list of the field definitions on this entity.
308 $definitions = $entity->getFieldDefinitions();
310 // Iterate through all the fields looking for ones in our list.
311 foreach ($definitions as $field_name => $definition) {
312 // Get the field type, ie: metatag.
313 $field_type = $definition->getType();
315 // Check the field type against our list of fields.
316 if (isset($field_type) && in_array($field_type, $field_types)) {
317 $field_list[$field_name] = $definition;
326 * Returns a list of the meta tags with values from a field.
328 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
329 * The ContentEntityInterface object.
330 * @param string $field_name
331 * The name of the field to work on.
334 * Array of field tags.
336 protected function getFieldTags(ContentEntityInterface $entity, $field_name) {
338 foreach ($entity->{$field_name} as $item) {
339 // Get serialized value and break it into an array of tags with values.
340 $serialized_value = $item->get('value')->getValue();
341 if (!empty($serialized_value)) {
342 $tags += unserialize($serialized_value);
350 * Returns default meta tags for an entity.
352 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
353 * The entity to work on.
356 * The default meta tags appropriate for this entity.
358 public function getDefaultMetatags(ContentEntityInterface $entity = NULL) {
359 // Get general global metatags.
360 $metatags = $this->getGlobalMetatags();
361 // If that is empty something went wrong.
366 // Check if this is a special page.
367 $special_metatags = $this->getSpecialMetatags();
369 // Merge with all globals defaults.
370 if ($special_metatags) {
371 $metatags->set('tags', array_merge($metatags->get('tags'), $special_metatags->get('tags')));
374 // Next check if there is this page is an entity that has meta tags.
375 // @todo Think about using other defaults, e.g. views. Maybe use plugins?
377 if (is_null($entity)) {
378 $entity = metatag_get_route_entity();
381 if (!empty($entity)) {
382 // Get default meta tags for a given entity.
383 $entity_defaults = $this->getEntityDefaultMetatags($entity);
384 if ($entity_defaults != NULL) {
385 $metatags->set('tags', array_merge($metatags->get('tags'), $entity_defaults));
390 return $metatags->get('tags');
394 * Returns global meta tags.
397 * The global meta tags.
399 public function getGlobalMetatags() {
400 return $this->metatagDefaults->load('global');
404 * Returns special meta tags.
407 * The defaults for this page, if it's a special page.
409 public function getSpecialMetatags() {
412 if (\Drupal::service('path.matcher')->isFrontPage()) {
413 $metatags = $this->metatagDefaults->load('front');
415 elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') {
416 $metatags = $this->metatagDefaults->load('403');
418 elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') {
419 $metatags = $this->metatagDefaults->load('404');
426 * Returns default meta tags for an entity.
428 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
429 * The entity to work with.
432 * The appropriate default meta tags.
434 public function getEntityDefaultMetatags(ContentEntityInterface $entity) {
435 $entity_metatags = $this->metatagDefaults->load($entity->getEntityTypeId());
437 if ($entity_metatags != NULL) {
438 // Merge with global defaults.
439 $metatags = array_merge($metatags, $entity_metatags->get('tags'));
442 // Finally, check if we should apply bundle overrides.
443 $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
444 if ($bundle_metatags != NULL) {
445 // Merge with existing defaults.
446 $metatags = array_merge($metatags, $bundle_metatags->get('tags'));
453 * Generate the elements that go in the hook_page_attachments attached array.
456 * The array of tags as plugin_id => value.
457 * @param object $entity
458 * Optional entity object to use for token replacements.
461 * Render array with tag elements.
463 public function generateElements(array $tags, $entity = NULL) {
465 $tags = $this->generateRawElements($tags, $entity);
467 foreach ($tags as $name => $tag) {
469 $elements['#attached']['html_head'][] = [
480 * Generate the actual meta tag values.
483 * The array of tags as plugin_id => value.
484 * @param object $entity
485 * Optional entity object to use for token replacements.
488 * Render array with tag elements.
490 public function generateRawElements(array $tags, $entity = NULL) {
491 // Ignore the update.php path.
492 $request = \Drupal::request();
493 if ($request->getBaseUrl() == '/update.php') {
499 $metatag_tags = $this->tagPluginManager->getDefinitions();
501 // Order the elements by weight first, as some systems like Facebook care.
502 uksort($tags, function ($tag_name_a, $tag_name_b) use ($metatag_tags) {
503 $weight_a = isset($metatag_tags[$tag_name_a]['weight']) ? $metatag_tags[$tag_name_a]['weight'] : 0;
504 $weight_b = isset($metatag_tags[$tag_name_b]['weight']) ? $metatag_tags[$tag_name_b]['weight'] : 0;
506 return ($weight_a < $weight_b) ? -1 : 1;
509 // Each element of the $values array is a tag with the tag plugin name as
511 foreach ($tags as $tag_name => $value) {
512 // Check to ensure there is a matching plugin.
513 if (isset($metatag_tags[$tag_name])) {
514 // Get an instance of the plugin.
515 $tag = $this->tagPluginManager->createInstance($tag_name);
517 // Render any tokens in the value.
518 $token_replacements = [];
520 // @todo This needs a better way of discovering the context.
521 if ($entity instanceof ViewEntityInterface) {
522 // Views tokens require the ViewExecutable, not the config entity.
523 // @todo Can we move this into metatag_views somehow?
524 $token_replacements = ['view' => $entity->getExecutable()];
526 elseif ($entity instanceof ContentEntityInterface) {
527 $token_replacements = [$entity->getEntityTypeId() => $entity];
531 // Set the value as sometimes the data needs massaging, such as when
532 // field defaults are used for the Robots field, which come as an array
533 // that needs to be filtered and converted to a string.
534 // @see Robots::setValue()
535 $tag->setValue($value);
536 $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
538 $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode])));
540 // Now store the value with processed tokens back into the plugin.
541 $tag->setValue($processed_value);
543 // Have the tag generate the output based on the value we gave it.
544 $output = $tag->output();
546 if (!empty($output)) {
547 $output = $tag->multiple() ? $output : [$output];
549 // Backwards compatibility for modules which don't support this logic.
550 if (isset($output['#tag'])) {
554 foreach ($output as $index => $element) {
555 // Add index to tag name as suffix to avoid having same key.
556 $index_tag_name = $tag->multiple() ? $tag_name . '_' . $index : $tag_name;
557 $rawTags[$index_tag_name] = $element;
567 * Returns a list of fields handled by Metatag.
570 * A list of supported field types.
572 protected function fieldTypes() {
573 // @todo Either get this dynamically from field plugins or forget it and
574 // just hardcode metatag where this is called.