3 namespace Drupal\pathauto;
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Entity\ContentEntityInterface;
7 use Drupal\Core\Entity\EntityInterface;
8 use Drupal\Core\Entity\RevisionableInterface;
9 use Drupal\Core\Extension\ModuleHandlerInterface;
10 use Drupal\Core\Language\LanguageInterface;
11 use Drupal\Core\Render\BubbleableMetadata;
12 use Drupal\Core\StringTranslation\StringTranslationTrait;
13 use Drupal\Core\StringTranslation\TranslationInterface;
14 use Drupal\Core\Utility\Token;
15 use Drupal\token\TokenEntityMapperInterface;
16 use Drupal\Core\Entity\EntityTypeManagerInterface;
19 * Provides methods for generating path aliases.
21 class PathautoGenerator implements PathautoGeneratorInterface {
23 use StringTranslationTrait;
28 * @var \Drupal\Core\Config\ConfigFactoryInterface
30 protected $configFactory;
35 * @var \Drupal\Core\Extension\ModuleHandlerInterface
37 protected $moduleHandler;
42 * @var \Drupal\Core\Utility\Token
47 * Calculated pattern for a specific entity.
51 protected $patterns = array();
54 * Available patterns per entity type ID.
58 protected $patternsByEntityType = array();
63 * @var \Drupal\pathauto\AliasCleanerInterface
65 protected $aliasCleaner;
68 * The alias storage helper.
70 * @var \Drupal\pathauto\AliasStorageHelperInterface
72 protected $aliasStorageHelper;
75 * The alias uniquifier.
77 * @var \Drupal\pathauto\AliasUniquifierInterface
79 protected $aliasUniquifier;
82 * The messenger service.
84 * @var \Drupal\pathauto\MessengerInterface
89 * The token entity mapper.
91 * @var \Drupal\token\TokenEntityMapperInterface
93 protected $tokenEntityMapper;
96 * The entity type manager.
98 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
100 protected $entityTypeManager;
103 * Creates a new Pathauto manager.
105 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
106 * The config factory.
107 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
108 * The module handler.
109 * @param \Drupal\Core\Utility\Token $token
111 * @param \Drupal\pathauto\AliasCleanerInterface $alias_cleaner
113 * @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
114 * The alias storage helper.
115 * @param AliasUniquifierInterface $alias_uniquifier
116 * The alias uniquifier.
117 * @param MessengerInterface $messenger
118 * The messenger service.
119 * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
120 * The string translation service.
121 * @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper
122 * The token entity mapper.
123 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
124 * The entity type manager.
126 public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper, EntityTypeManagerInterface $entity_type_manager) {
127 $this->configFactory = $config_factory;
128 $this->moduleHandler = $module_handler;
129 $this->token = $token;
130 $this->aliasCleaner = $alias_cleaner;
131 $this->aliasStorageHelper = $alias_storage_helper;
132 $this->aliasUniquifier = $alias_uniquifier;
133 $this->messenger = $messenger;
134 $this->stringTranslation = $string_translation;
135 $this->tokenEntityMapper = $token_entity_mapper;
136 $this->entityTypeManager = $entity_type_manager;
142 public function createEntityAlias(EntityInterface $entity, $op) {
143 // Retrieve and apply the pattern for this content type.
144 $pattern = $this->getPatternByEntity($entity);
145 if (empty($pattern)) {
146 // No pattern? Do nothing (otherwise we may blow away existing aliases...)
150 $source = '/' . $entity->toUrl()->getInternalPath();
151 $config = $this->configFactory->get('pathauto.settings');
152 $langcode = $entity->language()->getId();
154 // Core does not handle aliases with language Not Applicable.
155 if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) {
156 $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
161 $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity,
164 // Allow other modules to alter the pattern.
166 'module' => $entity->getEntityType()->getProvider(),
170 'bundle' => $entity->bundle(),
171 'language' => &$langcode,
173 $pattern_original = $pattern->getPattern();
174 $this->moduleHandler->alter('pathauto_pattern', $pattern, $context);
175 $pattern_altered = $pattern->getPattern();
177 // Special handling when updating an item which is already aliased.
178 $existing_alias = NULL;
179 if ($op == 'update' || $op == 'bulkupdate') {
180 if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) {
181 switch ($config->get('update_action')) {
182 case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
183 // If an alias already exists,
184 // and the update action is set to do nothing,
185 // then gosh-darn it, do nothing.
191 // Replace any tokens in the pattern.
192 // Uses callback option to clean replacements. No sanitization.
193 // Pass empty BubbleableMetadata object to explicitly ignore cacheablity,
194 // as the result is never rendered.
195 $alias = $this->token->replace($pattern->getPattern(), $data, array(
197 'callback' => array($this->aliasCleaner, 'cleanTokenValues'),
198 'langcode' => $langcode,
200 ), new BubbleableMetadata());
202 // Check if the token replacement has not actually replaced any values. If
203 // that is the case, then stop because we should not generate an alias.
205 $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern());
206 if ($alias === $pattern_tokens_removed) {
210 $alias = $this->aliasCleaner->cleanAlias($alias);
212 // Allow other modules to alter the alias.
213 $context['source'] = &$source;
214 $context['pattern'] = $pattern;
215 $this->moduleHandler->alter('pathauto_alias', $alias, $context);
217 // If we have arrived at an empty string, discontinue.
218 if (!mb_strlen($alias)) {
222 // If the alias already exists, generate a new, hopefully unique, variant.
223 $original_alias = $alias;
224 $this->aliasUniquifier->uniquify($alias, $source, $langcode);
225 if ($original_alias != $alias) {
226 // Alert the user why this happened.
227 $this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array(
228 '%original_alias' => $original_alias,
233 // Return the generated alias if requested.
234 if ($op == 'return') {
238 // Build the new path alias array and send it off to be created.
242 'language' => $langcode,
245 $return = $this->aliasStorageHelper->save($path, $existing_alias, $op);
247 // Because there is no way to set an altered pattern to not be cached,
248 // change it back to the original value.
249 if ($pattern_altered !== $pattern_original) {
250 $pattern->setPattern($pattern_original);
257 * Loads pathauto patterns for a given entity type ID.
259 * @param string $entity_type_id
262 * @return \Drupal\pathauto\PathautoPatternInterface[]
263 * A list of patterns, sorted by weight.
265 protected function getPatternByEntityType($entity_type_id) {
266 if (!isset($this->patternsByEntityType[$entity_type_id])) {
267 $ids = \Drupal::entityQuery('pathauto_pattern')
268 ->condition('type', array_keys(\Drupal::service('plugin.manager.alias_type')
269 ->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id))))
270 ->condition('status', 1)
274 $this->patternsByEntityType[$entity_type_id] = \Drupal::entityTypeManager()
275 ->getStorage('pathauto_pattern')
276 ->loadMultiple($ids);
279 return $this->patternsByEntityType[$entity_type_id];
285 public function getPatternByEntity(EntityInterface $entity) {
286 $langcode = $entity->language()->getId();
287 if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
288 foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) {
289 if ($pattern->applies($entity)) {
290 $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern;
295 if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
296 $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL;
299 return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode];
305 public function resetCaches() {
306 $this->patterns = [];
307 $this->patternsByEntityType = [];
308 $this->aliasCleaner->resetCaches();
314 public function updateEntityAlias(EntityInterface $entity, $op, array $options = array()) {
315 // Skip if the entity does not have the path field.
316 if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) {
320 // Skip if pathauto processing is disabled.
321 if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) {
325 // Only act if this is the default revision.
326 if ($entity instanceof RevisionableInterface && !$entity->isDefaultRevision()) {
330 $options += array('language' => $entity->language()->getId());
331 $type = $entity->getEntityTypeId();
333 // Skip processing if the entity has no pattern.
334 if (!$this->getPatternByEntity($entity)) {
338 // Deal with taxonomy specific logic.
339 // @todo Update and test forum related code.
340 if ($type == 'taxonomy_term') {
342 $config_forum = $this->configFactory->get('forum.settings');
343 if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) {
349 $result = $this->createEntityAlias($entity, $op);
351 catch (\InvalidArgumentException $e) {
352 $this->messenger->addError($e->getMessage());
356 // @todo Move this to a method on the pattern plugin.
357 if ($type == 'taxonomy_term') {
358 foreach ($this->loadTermChildren($entity->id()) as $subterm) {
359 $this->updateEntityAlias($subterm, $op, $options);
367 * Finds all children of a term ID.
370 * Term ID to retrieve parents for.
372 * @return \Drupal\taxonomy\TermInterface[]
373 * An array of term objects that are the children of the term $tid.
375 protected function loadTermChildren($tid) {
376 return $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($tid);