Skip to content
Snippets Groups Projects
Commit 8ae87a91 authored by Thierry Beeckmans's avatar Thierry Beeckmans Committed by Oleksandr Kuzava
Browse files

Issue #3362725: Update / re-instate the path update logic so it works with the path_alias entity

parent f79a013a
Branches
Tags
1 merge request!69Issue #3362725: Update / re-instate the path update logic so it works with the path_alias entity
......@@ -13,11 +13,12 @@ use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Form\FormStateInterface;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\redirect\Entity\Redirect;
use Drupal\path_alias\PathAliasInterface;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\block\Entity\Block;
use Drupal\Component\Utility\Random;
use Drupal\rocketship_core\Event\PathAliasUpdateEvent;
use Drupal\user\Entity\Role;
use Symfony\Component\Yaml\Yaml;
use Drupal\Core\Url;
......@@ -109,7 +110,7 @@ function rocketship_core_tokens($type, $tokens, array $data, array $options, Bub
switch ($name) {
case 'parent-alias':
if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
$alias_manager = \Drupal::service('path.alias_manager');
$alias_manager = \Drupal::service('path_alias.manager');
$url = $parent->getUrlObject();
if (!$url->isExternal()) {
$path = '/' . $url->getInternalPath();
......@@ -157,6 +158,17 @@ function rocketship_core_tokens($type, $tokens, array $data, array $options, Bub
return $replacements;
}
/**
* Implements hook_ENTITY_TYPE_update() for 'path_alias'.
*/
function rocketship_core_path_alias_update(PathAliasInterface $path_alias) {
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */
$event_dispatcher = \Drupal::service('event_dispatcher');
// Dispatch the path alias update event.
$event = new PathAliasUpdateEvent($path_alias);
$event_dispatcher->dispatch(PathAliasUpdateEvent::PATH_ALIAS_UPDATE, $event);
}
/**
* Implements hook_BASE_FORM_ID_alter().
......
......@@ -4,3 +4,9 @@ services:
decorates: config.installer
decoration_priority: 10
arguments: ['@rocketship_core.config.installer.inner', '@extension.list.module']
rocketship_core.path_alias.subscriber:
class: Drupal\rocketship_core\EventSubscriber\PathAliasUpdateSubscriber
arguments: ['@entity_type.manager', '@config.factory', '@redirect.repository']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\rocketship_core\Event;
use Drupal\path_alias\PathAliasInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Class PathAliasUpdateEvent.
*
* @package Drupal\rocketship_core\Event
*/
class PathAliasUpdateEvent extends Event {
/**
* Name of the event fired on updating a path alias entity.
*/
const PATH_ALIAS_UPDATE = 'rocketship_core.path_alias.update';
/**
* The path alias entity.
*
* @var \Drupal\path_alias\PathAliasInterface
*/
protected $entity;
/**
* PathAliasUpdateEvent constructor.
*
* @param \Drupal\path_alias\PathAliasInterface $entity
* The updated path alias entity.
*/
public function __construct(PathAliasInterface $entity) {
$this->entity = $entity;
}
/**
* Get the updated path alias entity.
*
* @return \Drupal\path_alias\PathAliasInterface
* The path alias entity.
*/
public function getEntity() {
return $this->entity;
}
}
<?php
namespace Drupal\rocketship_core\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\redirect\RedirectRepository;
use Drupal\rocketship_core\Event\PathAliasUpdateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class PathAliasUpdateSubscriber.
*
* @package Drupal\rocketship_core\EventSubscriber
*/
class PathAliasUpdateSubscriber implements EventSubscriberInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The settings of the redirect module.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $redirectConfig;
/**
* The redirect repository.
*
* @var \Drupal\redirect\RedirectRepository
*/
protected $redirectRepository;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PathAliasUpdateEvent::PATH_ALIAS_UPDATE => ['onPathAliasUpdate'],
];
}
/**
* PathAliasUpdateSubscriber constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\redirect\RedirectRepository $redirect_repository
* The redirect repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, RedirectRepository $redirect_repository) {
$this->entityTypeManager = $entity_type_manager;
$this->redirectConfig = $config_factory->get('redirect.settings');
$this->redirectRepository = $redirect_repository;
}
/**
* Defines event listener.
*
* Handle path aliases update functionality that is useful for sites with a
* drill-down structure.
*
* @param \Drupal\rocketship_core\Event\PathAliasUpdateEvent $event
* The path alias update event.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function onPathAliasUpdate(PathAliasUpdateEvent $event) {
$path_alias = $event->getEntity();
$original_alias = $path_alias->original->getAlias();
$updated_alias = $path_alias->getAlias();
if ($updated_alias != $original_alias) {
$path_alias_storage = $this->entityTypeManager->getStorage('path_alias');
// Look for any aliases with the original alias as a part of it.
// Let's take into account the original language because we don't want to
// update aliases in other languages.
$results = $path_alias_storage->getQuery()
->condition('path', $path_alias->getPath(), '<>')
->condition('langcode', $path_alias->language()->getId())
->condition('alias', $original_alias . '/%', 'LIKE')
// Order from the newest to the oldest.
->sort('id', 'DESC')
->execute();
// Nothing to do when no matches found.
if (!$results) {
return;
}
// Check if auto_redirect option is enabled.
$auto_redirect = $this->redirectConfig->get('auto_redirect');
$status_code = $this->redirectConfig->get('default_status_code');
$redirect_storage = $this->entityTypeManager->getStorage('redirect');
/** @var \Drupal\path_alias\PathAliasInterface[] $entities */
$entities = $path_alias_storage->loadMultiple($results);
foreach ($entities as $alias_to_update) {
// Build and save the new alias.
$new_alias = str_replace($original_alias, $updated_alias, $alias_to_update->getAlias());
$alias_to_update->setAlias($new_alias);
$alias_to_update->save();
if (!$auto_redirect) {
continue;
}
// Delete all redirects having the same source as this alias.
redirect_delete_by_path($new_alias, $alias_to_update->language()->getId(), FALSE);
if ($alias_to_update->getAlias() != $new_alias) {
if (!$this->redirectRepository->findMatchingRedirect($alias_to_update->getAlias(), [], $alias_to_update->language()->getId())) {
$redirect = $redirect_storage->create();
$redirect->setSource($alias_to_update->getAlias());
$redirect->setRedirect($alias_to_update->getPath());
$redirect->setLanguage($alias_to_update->language()->getId());
$redirect->setStatusCode($status_code);
$redirect->save();
}
}
}
}
}
}
<?php
namespace Drupal\Tests\rocketship_core\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\NodeInterface;
use Drupal\Tests\pathauto\Functional\PathautoTestHelperTrait;
/**
* Class PathAliasUpdateTest.
*
* Covers multilingual and non-multilingual cases. Revisionable cases are not
* covered due path_alias entity does not create a new revision.
*
* @group rocketship_core.path_alias
*/
class PathAliasUpdateTest extends KernelTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'system',
'field',
'user',
'node',
'link',
'path',
'token',
'menu_ui',
'pathauto',
'language',
'redirect',
'rocketship_core',
'menu_link_content',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The default language service.
*
* @var \Drupal\Core\Language\LanguageDefault
*/
protected $languageDefault;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->entityTypeManager = $this->container->get('entity_type.manager');
$this->languageManager = $this->container->get('language_manager');
$this->languageDefault = $this->container->get('language.default');
// Install needed schema and configs.
$this->installSchema('system', 'sequences');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('redirect');
$this->installEntitySchema('path_alias');
$this->installEntitySchema('menu_link_content');
$this->installSchema('node', ['node_access']);
// Main menu will be created on installing system configs.
$this->installConfig(['pathauto', 'system', 'language']);
// Create page content type.
$this->createContentType(['type' => 'page']);
// Create url pattern for all node types.
$this->createPattern('node', '/[node:menu-link:parent-alias]/[node:title]');
// Make sure the custom token is safe.
$safe_tokens = $this->config('pathauto.settings')->get('safe_tokens') ?? [];
$safe_tokens[] = 'node:menu-link:parent-alias';
$this->config('pathauto.settings')
->set('safe_tokens', $safe_tokens)
->save();
$this->container->get('router.builder')->rebuild();
}
/**
* Create content type without fields.
*
* @param array $values
* List of values.
*/
protected function createContentType(array $values = []) {
$this->entityTypeManager->getStorage('node_type')
->create($values)
->save();
}
/**
* Create node without fields.
*
* @param array $values
* List of values.
* @param \Drupal\menu_link_content\MenuLinkContentInterface|null $parent_link
* The parent menu link.
*
* @return \Drupal\Core\Entity\EntityInterface|\Drupal\node\NodeInterface
* Create node.
*/
protected function createNodeWithMenuLink(array $values = [], MenuLinkContentInterface $parent_link = NULL) {
$values += [
'type' => 'page',
'uid' => 0,
'title' => $this->randomMachineName(),
'status' => NodeInterface::PUBLISHED,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
$node_storage = $this->entityTypeManager->getStorage('node');
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->create($values);
$node->save();
$menu_link_storage = $this->entityTypeManager->getStorage('menu_link_content');
$menu_link = $menu_link_storage->create([
'title' => $node->getTitle(),
'menu_name' => 'main',
'link' => ['uri' => 'entity:node/' . $node->id()],
'parent' => !is_null($parent_link) ? $parent_link->getPluginId() : NULL,
]);
$menu_link->save();
// Re-save node to apply new path alias after creation menu.
$node->setNewRevision(FALSE);
$node->save();
return $node;
}
/**
* Get related menu link of the node.
*
* @param \Drupal\node\NodeInterface $node
* The node entity.
*
* @return \Drupal\menu_link_content\MenuLinkContentInterface
* The related menu link content.
*/
protected function getMenuLink(NodeInterface $node) {
$storage = $this->entityTypeManager->getStorage('menu_link_content');
$links = $storage->loadByProperties([
'link.uri' => 'entity:node/' . $node->id(),
]);
$this->assertCount(1, $links);
return reset($links);
}
/**
* Create test content with menu structure.
*
* @param string $langcode
* The if of the language.
*
* @return \Drupal\node\NodeInterface[]
* List of three created node.
*/
protected function createNodes($langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
// Create first level content.
$level1 = $this->createNodeWithMenuLink(['title' => 'Level 1', 'langcode' => $langcode]);
$this->assertEntityAlias($level1, '/level-1');
// Create second level content.
$level1_menu_link = $this->getMenuLink($level1);
$level2 = $this->createNodeWithMenuLink(['title' => 'Level 2', 'langcode' => $langcode], $level1_menu_link);
$this->assertEntityAlias($level2, '/level-1/level-2');
// Create third level content.
$level2_menu_link = $this->getMenuLink($level2);
$level3 = $this->createNodeWithMenuLink(['title' => 'Level 3', 'langcode' => $langcode], $level2_menu_link);
$this->assertEntityAlias($level3, '/level-1/level-2/level-3');
return [$level1, $level2, $level3];
}
/**
* Change default and set current language.
*
* @param string $langcode
* The id of the language.
*/
protected function switchCurrentLanguage($langcode) {
$language = ConfigurableLanguage::load($langcode);
// Change default language.
$this->config('system.site')
->set('langcode', $language->getId())
->set('default_langcode', $language->getId())
->save();
$this->languageDefault->set($language);
// Make sure current language has been changed.
$current_language = $this->languageManager->getCurrentLanguage()->getId();
$this->assertEqual($current_language, $language->getId());
}
/**
* Test path aliases for non translatable content.
*/
public function testPathAliasNonMultilingualUpdate() {
[$level1, $level2, $level3] = $this->createNodes();
// Update title for second node.
$level2->setTitle('Level 2 new');
$level2->save();
// Nothing changed for first node. Make sure that second level of the alias
// has been updated for second and third levels of content.
$this->assertEntityAlias($level1, '/level-1');
$this->assertEntityAlias($level2, '/level-1/level-2-new');
$this->assertEntityAlias($level3, '/level-1/level-2-new/level-3');
// Update first item.
$level1->setTitle('Level 1 new');
$level1->save();
// Make sure that first level of the alias has been updated for all content.
$this->assertEntityAlias($level1, '/level-1-new');
$this->assertEntityAlias($level2, '/level-1-new/level-2-new');
$this->assertEntityAlias($level3, '/level-1-new/level-2-new/level-3');
}
/**
* Test path aliases update for translatable content.
*/
public function testPathAliasMultilingualUpdate() {
$language = ConfigurableLanguage::createFromLangcode('nl');
$language->save();
[$level1_en, $level2_en] = $this->createNodes('en');
// Switch current language to NL.
$this->switchCurrentLanguage('nl');
// Add NL translation.
$level1_nl = $level1_en->addTranslation('nl', [
'title' => 'Level 1 NL',
]);
$level1_nl->save();
// Make sure that EN alias has not been changed.
$this->assertEntityAlias($level1_en, '/level-1');
// Make sure that translated content has a new proper alias.
$this->assertEntityAlias($level1_nl, '/level-1-nl');
// Add translation for second node.
$level2_nl = $level2_en->addTranslation('nl', [
'title' => 'Level 2 NL',
]);
$level2_nl->save();
// Make sure that EN alias has not been changed.
$this->assertEntityAlias($level2_en, '/level-1/level-2');
// Make sure that translated content has new proper alias.
$this->assertEntityAlias($level2_nl, '/level-1-nl/level-2-nl');
// Update title for the first level.
$level1_nl->setTitle('Level 1 NL new');
$level1_nl->setNewRevision(FALSE);
$level1_nl->save();
// Make sure that alias of the translated nodes has been updated.
$this->assertEntityAlias($level1_nl, '/level-1-nl-new');
$this->assertEntityAlias($level2_nl, '/level-1-nl-new/level-2-nl');
// Make sure that EN translation has not been updated.
$this->assertEntityAlias($level1_en, '/level-1');
$this->assertEntityAlias($level2_en, '/level-1/level-2');
}
}
<?php
namespace Drupal\Tests\rocketship_core\Unit;
use Drupal\Tests\UnitTestCase;
/**
* Class PathAliasUpdateCalculationTest.
*
* @group rocketship_core.path_alias
*/
class PathAliasUpdateCalculationTest extends UnitTestCase {
/**
* Get a list of test aliases.
*
* @return array
* List of aliases.
*/
protected function getListOfPathAliases() {
return [
[
'langcode' => 'en',
'path' => '/node/1',
'alias' => '/level-1',
],
[
'langcode' => 'en',
'path' => '/node/2',
'alias' => '/level-1/level-2',
],
[
'langcode' => 'nl',
'path' => '/node/1',
'alias' => '/level-1-nl',
],
[
'langcode' => 'nl',
'path' => '/node/2',
'alias' => '/level-1-nl/level-2-nl',
],
];
}
/**
* Look for any aliases with the original alias as a part of it.
*
* @param array $path_alias_updated
* Path alias updated.
*
* @return array
* List of alias items.
*/
protected function findAliasMatches(array $path_alias_updated) {
$matches = array_filter($this->getListOfPathAliases(), function ($path_alias) use ($path_alias_updated) {
// Skip original source path.
if ($path_alias_updated['path'] == $path_alias['path']) {
return FALSE;
}
// Make sure we filter results by language.
if ($path_alias_updated['langcode'] != $path_alias['langcode']) {
return FALSE;
}
return strpos($path_alias['alias'], $path_alias_updated['alias']) === 0;
});
return array_values($matches);
}
/**
* Test a case when we updated existing alias by providing new alias.
*/
public function testPathAliasCalculation() {
// Fetch test path aliases.
$path_aliases = $this->getListOfPathAliases();
// Let's assume we updated first alias from the list.
$path_alias_updated = ['new_alias' => '/level-1-new'] + $path_aliases[0];
// Look for any aliases with the original alias as a part of it.
$matches = $this->findAliasMatches($path_alias_updated);
// Make sure we have one match.
$this->assertCount(1, $matches);
// Make sure we found second alias from the list.
$this->assertArrayEquals($path_aliases[1], $matches[0]);
// Generate new alias for one match.
$new_alias = str_replace($path_alias_updated['alias'], $path_alias_updated['new_alias'], $matches[0]['alias']);
// Make sure we generated the proper alias.
$this->assertEquals('/level-1-new/level-2', $new_alias);
}
/**
* Test a case when no matches found due a filter by language.
*/
public function testPathAliasMultilingualNoMatches() {
// Fetch test path aliases.
$path_aliases = $this->getListOfPathAliases();
// Let's assume we updated first alias from the list but used FR language.
$path_alias_updated = [
'new_alias' => '/level-1-new',
'langcode' => 'fr',
] + $path_aliases[0];
// Look for any aliases with the original alias as a part of it.
$matches = $this->findAliasMatches($path_alias_updated);
// No matches found because we don't have FR alias in the list.
$this->assertCount(0, $matches);
}
/**
* Test a case when no matches found due a filter by part of existing aliases.
*/
public function testPathAliasNoMatches() {
$path_alias_updated = [
'path' => '/node/3',
'alias' => '/level-root',
'new_alias' => '/level-root-new',
'langcode' => 'en',
];
// Look for any aliases with the original alias as a part of it.
$matches = $this->findAliasMatches($path_alias_updated);
// No matches found due the original alias is not a part of any existing.
$this->assertCount(0, $matches);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment