3 namespace Drupal\metatag;
5 use Drupal\Component\Render\PlainTextOutput;
6 use Drupal\Core\Entity\ContentEntityInterface;
7 use Drupal\Core\Entity\EntityTypeManager;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
10 use Drupal\field\Entity\FieldConfig;
11 use Drupal\metatag\Entity\MetatagDefaults;
12 use Drupal\views\ViewEntityInterface;
15 * Class MetatagManager.
17 * @package Drupal\metatag
19 class MetatagManager implements MetatagManagerInterface {
21 protected $groupPluginManager;
22 protected $tagPluginManager;
23 protected $metatagDefaults;
24 protected $tokenService;
27 * Metatag logging channel.
29 * @var \Drupal\Core\Logger\LoggerChannelInterface
34 * Constructor for MetatagManager.
36 * @param MetatagGroupPluginManager $groupPluginManager
37 * @param MetatagTagPluginManager $tagPluginManager
38 * @param MetatagToken $token
39 * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory
40 * @param EntityTypeManager $entityTypeManager
42 public function __construct(MetatagGroupPluginManager $groupPluginManager,
43 MetatagTagPluginManager $tagPluginManager,
45 LoggerChannelFactoryInterface $channelFactory,
46 EntityTypeManager $entityTypeManager) {
47 $this->groupPluginManager = $groupPluginManager;
48 $this->tagPluginManager = $tagPluginManager;
49 $this->tokenService = $token;
50 $this->logger = $channelFactory->get('metatag');
51 $this->metatagDefaults = $entityTypeManager->getStorage('metatag_defaults');
57 public function tagsFromEntity(ContentEntityInterface $entity) {
60 $fields = $this->getFields($entity);
62 /* @var FieldConfig $field_info */
63 foreach ($fields as $field_name => $field_info) {
64 // Get the tags from this field.
65 $tags = $this->getFieldTags($entity, $field_name);
74 public function tagsFromEntityWithDefaults(ContentEntityInterface $entity) {
75 return $this->tagsFromEntity($entity) + $this->defaultTagsFromEntity($entity);
81 public function defaultTagsFromEntity(ContentEntityInterface $entity) {
82 /** @var MetatagDefaults $metatags */
83 $metatags = $this->metatagDefaults->load('global');
87 // Add/overwrite with tags set on the entity type.
88 $entity_type_tags = $this->metatagDefaults->load($entity->getEntityTypeId());
89 if (!is_null($entity_type_tags)) {
90 $metatags->overwriteTags($entity_type_tags->get('tags'));
92 // Add/overwrite with tags set on the entity bundle.
93 $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
94 if (!is_null($bundle_metatags)) {
95 $metatags->overwriteTags($bundle_metatags->get('tags'));
97 return $metatags->get('tags');
101 * Gets the group plugin definitions.
106 protected function groupDefinitions() {
107 return $this->groupPluginManager->getDefinitions();
111 * Gets the tag plugin definitions.
116 protected function tagDefinitions() {
117 return $this->tagPluginManager->getDefinitions();
123 public function sortedGroups() {
124 $metatag_groups = $this->groupDefinitions();
126 // Pull the data from the definitions into a new array.
128 foreach ($metatag_groups as $group_name => $group_info) {
129 $groups[$group_name]['id'] = $group_info['id'];
130 $groups[$group_name]['label'] = $group_info['label']->render();
131 $groups[$group_name]['description'] = $group_info['description'];
132 $groups[$group_name]['weight'] = $group_info['weight'];
135 // Create the 'sort by' array.
137 foreach ($groups as $group) {
138 $sort_by[] = $group['weight'];
141 // Sort the groups by weight.
142 array_multisort($sort_by, SORT_ASC, $groups);
150 public function sortedTags() {
151 $metatag_tags = $this->tagDefinitions();
153 // Pull the data from the definitions into a new array.
155 foreach ($metatag_tags as $tag_name => $tag_info) {
156 $tags[$tag_name]['id'] = $tag_info['id'];
157 $tags[$tag_name]['label'] = $tag_info['label']->render();
158 $tags[$tag_name]['group'] = $tag_info['group'];
159 $tags[$tag_name]['weight'] = $tag_info['weight'];
162 // Create the 'sort by' array.
164 foreach ($tags as $key => $tag) {
165 $sort_by['group'][$key] = $tag['group'];
166 $sort_by['weight'][$key] = $tag['weight'];
169 // Sort the tags by weight.
170 array_multisort($sort_by['group'], SORT_ASC, $sort_by['weight'], SORT_ASC, $tags);
178 public function sortedGroupsWithTags() {
179 $groups = $this->sortedGroups();
180 $tags = $this->sortedTags();
182 foreach ($tags as $tag_name => $tag) {
183 $tag_group = $tag['group'];
185 if (!isset($groups[$tag_group])) {
186 // If the tag is claiming a group that has no matching plugin, log an
187 // error and force it to the basic group.
188 $this->logger->error("Undefined group '%group' on tag '%tag'", ['%group' => $tag_group, '%tag' => $tag_id]);
189 $tag['group'] = 'basic';
190 $tag_group = 'basic';
193 $groups[$tag_group]['tags'][$tag_name] = $tag;
202 public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) {
203 // Add the outer fieldset.
205 '#type' => 'details',
208 $element += $this->tokenService->tokenBrowser($token_types);
210 $groups_and_tags = $this->sortedGroupsWithTags();
213 foreach ($groups_and_tags as $group_name => $group) {
214 // Only act on groups that have tags and are in the list of included
215 // groups (unless that list is null).
216 if (isset($group['tags']) && (is_null($included_groups) || in_array($group_name, $included_groups) || in_array($group['id'], $included_groups))) {
217 // Create the fieldset.
218 $element[$group_name]['#type'] = 'details';
219 $element[$group_name]['#title'] = $group['label'];
220 $element[$group_name]['#description'] = $group['description'];
221 $element[$group_name]['#open'] = $first;
224 foreach ($group['tags'] as $tag_name => $tag) {
225 // Only act on tags in the included tags list, unless that is null.
226 if (is_null($included_tags) || in_array($tag_name, $included_tags) || in_array($tag['id'], $included_tags)) {
227 // Make an instance of the tag.
228 $tag = $this->tagPluginManager->createInstance($tag_name);
230 // Set the value to the stored value, if any.
231 $tag_value = isset($values[$tag_name]) ? $values[$tag_name] : NULL;
232 $tag->setValue($tag_value);
234 // Create the bit of form for this tag.
235 $element[$group_name][$tag_name] = $tag->form($element);
245 * Returns a list of the metatag fields on an entity.
247 protected function getFields(ContentEntityInterface $entity) {
250 if ($entity instanceof ContentEntityInterface) {
251 // Get a list of the metatag field types.
252 $field_types = $this->fieldTypes();
254 // Get a list of the field definitions on this entity.
255 $definitions = $entity->getFieldDefinitions();
257 // Iterate through all the fields looking for ones in our list.
258 foreach ($definitions as $field_name => $definition) {
259 // Get the field type, ie: metatag.
260 $field_type = $definition->getType();
262 // Check the field type against our list of fields.
263 if (isset($field_type) && in_array($field_type, $field_types)) {
264 $field_list[$field_name] = $definition;
273 * Returns a list of the metatags with values from a field.
275 * @param ContentEntityInterface $entity
276 * @param string $field_name
278 protected function getFieldTags(ContentEntityInterface $entity, $field_name) {
280 foreach ($entity->{$field_name} as $item) {
281 // Get serialized value and break it into an array of tags with values.
282 $serialized_value = $item->get('value')->getValue();
283 if (!empty($serialized_value)) {
284 $tags += unserialize($serialized_value);
294 * @param ContentEntityInterface $entity
296 public function getDefaultMetatags(ContentEntityInterface $entity = NULL) {
297 // Get general global metatags
298 $metatags = $this->getGlobalMetatags();
299 // If that is empty something went wrong.
304 // Check if this is a special page.
305 $special_metatags = $this->getSpecialMetatags();
307 // Merge with all globals defaults.
308 if ($special_metatags) {
309 $metatags->set('tags', array_merge($metatags->get('tags'), $special_metatags->get('tags')));
312 // Next check if there is this page is an entity that has meta tags.
313 // @TODO: Think about using other defaults, e.g. views. Maybe use plugins?
315 if (is_null($entity)) {
316 $entity = metatag_get_route_entity();
319 if (!empty($entity)) {
320 // Get default metatags for a given entity.
321 $entity_defaults = $this->getEntityDefaultMetatags($entity);
322 if ($entity_defaults != NULL) {
323 $metatags->set('tags', array_merge($metatags->get('tags'), $entity_defaults));
328 return $metatags->get('tags');
334 public function getGlobalMetatags() {
335 return $this->metatagDefaults->load('global');
341 public function getSpecialMetatags() {
344 if (\Drupal::service('path.matcher')->isFrontPage()) {
345 $metatags = $this->metatagDefaults->load('front');
347 elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') {
348 $metatags = $this->metatagDefaults->load('403');
350 elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') {
351 $metatags = $this->metatagDefaults->load('404');
360 public function getEntityDefaultMetatags(ContentEntityInterface $entity) {
361 $entity_metatags = $this->metatagDefaults->load($entity->getEntityTypeId());
363 if ($entity_metatags != NULL) {
364 // Merge with global defaults.
365 $metatags = array_merge($metatags, $entity_metatags->get('tags'));
368 // Finally, check if we should apply bundle overrides.
369 $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
370 if ($bundle_metatags != NULL) {
371 // Merge with existing defaults.
372 $metatags = array_merge($metatags, $bundle_metatags->get('tags'));
379 * Generate the elements that go in the attached array in
380 * hook_page_attachments.
383 * The array of tags as plugin_id => value.
384 * @param object $entity
385 * Optional entity object to use for token replacements.
388 * Render array with tag elements.
390 public function generateElements($tags, $entity = NULL) {
392 $tags = $this->generateRawElements($tags, $entity);
394 foreach ($tags as $name => $tag) {
396 $elements['#attached']['html_head'][] = [
407 * Generate the actual meta tag values.
410 * The array of tags as plugin_id => value.
411 * @param object $entity
412 * Optional entity object to use for token replacements.
415 * Render array with tag elements.
417 public function generateRawElements($tags, $entity = NULL) {
420 $metatag_tags = $this->tagPluginManager->getDefinitions();
422 // Order the elements by weight first, as some systems like Facebook care.
423 uksort($tags, function ($tag_name_a, $tag_name_b) use ($metatag_tags) {
424 $weight_a = isset($metatag_tags[$tag_name_a]['weight']) ? $metatag_tags[$tag_name_a]['weight'] : 0;
425 $weight_b = isset($metatag_tags[$tag_name_b]['weight']) ? $metatag_tags[$tag_name_b]['weight'] : 0;
427 return ($weight_a < $weight_b) ? -1 : 1;
430 // Each element of the $values array is a tag with the tag plugin name
432 foreach ($tags as $tag_name => $value) {
433 // Check to ensure there is a matching plugin.
434 if (isset($metatag_tags[$tag_name])) {
435 // Get an instance of the plugin.
436 $tag = $this->tagPluginManager->createInstance($tag_name);
438 // Render any tokens in the value.
439 $token_replacements = [];
441 // @TODO: This needs a better way of discovering the context.
442 if ($entity instanceof ViewEntityInterface) {
443 // Views tokens require the ViewExecutable, not the config entity.
444 // @todo Can we move this into metatag_views somehow?
445 $token_replacements = ['view' => $entity->getExecutable()];
448 $token_replacements = [$entity->getEntityTypeId() => $entity];
452 // Set the value as sometimes the data needs massaging, such as when
453 // field defaults are used for the Robots field, which come as an array
454 // that needs to be filtered and converted to a string.
455 // @see @Robots::setValue().
456 $tag->setValue($value);
457 $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
459 if ($tag->type() === 'image') {
460 $processed_value = $this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]);
463 $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode])));
466 // Now store the value with processed tokens back into the plugin.
467 $tag->setValue($processed_value);
469 // Have the tag generate the output based on the value we gave it.
470 $output = $tag->output();
472 if (!empty($output)) {
473 $rawTags[$tag_name] = $output;
482 * Returns a list of fields handled by Metatag.
486 protected function fieldTypes() {
487 //@TODO: Either get this dynamically from field plugins or forget it and just hardcode metatag where this is called.