src/EventSubscriber/EntityEventSubscriber.php
<?php
namespace Drupal\stanford_profile_helper\EventSubscriber;
use Drupal\config_pages\ConfigPagesInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\core_event_dispatcher\EntityHookEvents;
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
use Drupal\core_event_dispatcher\Event\Entity\EntityDeleteEvent;
use Drupal\core_event_dispatcher\Event\Entity\EntityInsertEvent;
use Drupal\core_event_dispatcher\Event\Entity\EntityPresaveEvent;
use Drupal\core_event_dispatcher\Event\Entity\EntityUpdateEvent;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\NodeInterface;
use Drupal\stanford_profile_helper\StanfordDefaultContentInterface;
use Drupal\stanford_profile_helper\StanfordProfileHelper;
use Drupal\user\RoleInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Entity event subscriber service.
*/
class EntityEventSubscriber implements EventSubscriberInterface {
use MessengerTrait;
use StringTranslationTrait;
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents(): array {
return [
EntityHookEvents::ENTITY_PRE_SAVE => 'onEntityPresave',
EntityHookEvents::ENTITY_INSERT => 'onEntityInsert',
EntityHookEvents::ENTITY_UPDATE => 'onEntityUpdate',
EntityHookEvents::ENTITY_DELETE => 'onEntityDelete',
LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY => 'prepareLayoutBuilderComponent',
];
}
/**
* Event subscriber constructor.
*
* @param \Drupal\stanford_profile_helper\StanfordDefaultContentInterface $defaultContent
* Default content importer service.
* @param \Drupal\Core\State\StateInterface $state
* Core state service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Core entity type manager service.
*/
public function __construct(protected StanfordDefaultContentInterface $defaultContent, protected StateInterface $state, protected EntityTypeManagerInterface $entityTypeManager) {}
/**
* Call individual methods for each entity type for the events.
*
* @param \Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent $event
* The event.
* @param string $action
* Entity event action: preSave, update, insert, delete.
* @param mixed $args
* Other arguments to pass to the method.
*/
protected function callIndividualEntityMethods(AbstractEntityEvent $event, string $action, ...$args): void {
$entity = $event->getEntity();
$entity_type = $entity->getEntityTypeId();
$method_name = $action . str_replace(' ', '', ucwords(str_replace('_', ' ', $entity_type)));
// Call individual methods for each entity type if one is available.
if (method_exists($this, $method_name)) {
call_user_func([$this, $method_name], $event->getEntity(), ...$args);
}
}
/**
* Before saving a new node, if it's the first one, create a list page.
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityPresaveEvent $event
* Triggered Event.
*/
public function onEntityPresave(EntityPresaveEvent $event): void {
$this->callIndividualEntityMethods($event, 'preSave');
self::fixEntityUuid($event->getEntity());
}
/**
* On entity insert event listener.
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityInsertEvent $event
* Triggered Event.
*/
public function onEntityInsert(EntityInsertEvent $event): void {
$this->callIndividualEntityMethods($event, 'insert');
}
/**
* On entity update event listener.
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityUpdateEvent $event
* Triggered Event.
*/
public function onEntityUpdate(EntityUpdateEvent $event): void {
$this->callIndividualEntityMethods($event, 'update', $event->getOriginalEntity());
}
/**
* On entity delete event listener.
*
* @param \Drupal\core_event_dispatcher\Event\Entity\EntityDeleteEvent $event
* Triggered Event.
*/
public function onEntityDelete(EntityDeleteEvent $event): void {
$this->callIndividualEntityMethods($event, 'delete');
}
/**
* Modify the component build for layout builder.
*
* @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
* Triggered event.
*/
public function prepareLayoutBuilderComponent(SectionComponentBuildRenderArrayEvent $event) {
$menus = self::getTaxonomyMenuIds();
$component_config = $event->getComponent()->get('configuration');
if (in_array($component_config['id'], $menus)) {
// Always display the label for taxonomy menus due to the twig template.
$build = $event->getBuild();
$build['#configuration']['label_display'] = 'visible';
$event->setBuild($build);
}
}
/**
* Get the list of all taxonomy menus.
*
* @return string[]
* Menu id strings.
*/
protected function getTaxonomyMenuIds(): array {
$menu_ids = &drupal_static(self::class . __METHOD__, []);
if ($menu_ids) {
return $menu_ids;
}
$tax_menus = $this->entityTypeManager->getStorage('taxonomy_menu')
->loadMultiple();
foreach ($tax_menus as $menu) {
$menu_ids[] = 'system_menu_block:' . $menu->getMenu();
}
return $menu_ids;
}
/**
* On node insert event listener.
*
* @param \Drupal\node\NodeInterface $node
* The node.
*/
protected static function insertNode(NodeInterface $node): void {
// Clear menu links cache if the node has a menu link data.
if (
$node->hasField('field_menulink') &&
!$node->get('field_menulink')->isEmpty()
) {
StanfordProfileHelper::clearMenuCacheTag();
}
}
/**
* On node update event listener.
*
* @param \Drupal\node\NodeInterface $node
* The node.
* @param \Drupal\node\NodeInterface $original_node
* The original node.
*/
protected static function updateNode(NodeInterface $node, NodeInterface $original_node): void {
// Compare the original menu link with the new menu link data. If any
// important parts changed, clear the menu links cache.
if (
$node->hasField('field_menulink') && (
!$node->get('field_menulink')->isEmpty() ||
!$original_node->get('field_menulink')->isEmpty()
)
) {
if ($original_node->isPublished() != $node->isPublished()) {
StanfordProfileHelper::clearMenuCacheTag();
return;
}
$keys = ['title', 'description', 'weight', 'expanded', 'parent'];
$changes = $node->get('field_menulink')->getValue();
$original = $original_node->get('field_menulink')->getValue();
foreach ($keys as $key) {
$change_value = $changes[0][$key] ?? NULL;
$original_value = $original[0][$key] ?? NULL;
if ($change_value != $original_value) {
StanfordProfileHelper::clearMenuCacheTag();
return;
}
}
}
}
/**
* Force the menu link to clear when a node is deleted.
*
* @param \Drupal\node\NodeInterface $node
* Node being deleted.
*/
protected static function deleteNode(NodeInterface $node): void {
// If a node has menu link data, delete the menu link.
if (
$node->hasField('field_menulink') &&
!$node->get('field_menulink')->isEmpty()
) {
\Drupal::database()->delete('menu_tree')
->condition('id', 'menu_link_field:%', 'LIKE')
->condition('route_param_key', 'node=' . $node->id())
->execute();
\Drupal::service('router.builder')->rebuildIfNeeded();
StanfordProfileHelper::clearMenuCacheTag();
}
}
/**
* For configuration entities, make sure the uuid matches the config file.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to fix.
*/
protected static function fixEntityUuid(EntityInterface $entity) {
if ($entity instanceof ConfigEntityInterface && $entity->isNew()) {
/** @var \Drupal\Core\Config\StorageInterface $config_storage */
$config_storage = \Drupal::service('config.storage.sync');
// The entity exists in the config sync directory, lets check if it's uuid
// matches.
if (in_array($entity->getConfigDependencyName(), $config_storage->listAll())) {
$staged_config = $config_storage->read($entity->getConfigDependencyName());
// The uuid of the entity doesn't match that of the config in the sync
// directory. Make sure they match so that we don't get config sync
// issues.
if (isset($staged_config['uuid']) && $staged_config['uuid'] != $entity->uuid()) {
$entity->set('uuid', $staged_config['uuid']);
}
}
}
}
/**
* Before saving a configuration page, set some state and clear caches.
*
* @param \Drupal\config_pages\ConfigPagesInterface $config_page
* The configuration page being saved.
*/
protected static function preSaveConfigPages(ConfigPagesInterface $config_page) {
if (InstallerKernel::installationAttempted()) {
// Rebuild the routes so that the config pages will save from the default
// content import at site installation.
\Drupal::service('router.builder')->rebuildIfNeeded();
}
$state = \Drupal::state();
if ($config_page->hasField('su_site_nobots')) {
$enable_nobots = (bool) $config_page->get('su_site_nobots')->getString();
$enable_nobots ? $state->set('nobots', TRUE) : $state->delete('nobots');
}
if (
$config_page->hasField('su_site_url') &&
$config_page->get('su_site_url')->count()
) {
// Set the xml sitemap module state to the new domain.
$state->set('xmlsitemap_base_url', $config_page->get('su_site_url')
->get(0)
->get('uri')
->getString());
}
// Invalidate cache tags on config pages save. This is a blanket cache clear
// since config pages mostly affect the entire site.
Cache::invalidateTags([
'config:system.site',
'system.site',
'block_view',
'node_view',
]);
}
/**
* Before saving a field storage, adjust the third party settings.
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* Field storage being saved.
*/
protected static function preSaveFieldStorageConfig(FieldStorageConfigInterface $field_storage) {
// If a field is saved and the field permissions are public, lets just
// remove those third party settings before save so that it keeps the
// config clean.
if ($field_storage->getThirdPartySetting('field_permissions', 'permission_type') === 'public') {
$field_storage->unsetThirdPartySetting('field_permissions', 'permission_type');
$field_storage->calculateDependencies();
}
}
/**
* Before saving a menu item, clear caches.
*
* @param \Drupal\menu_link_content\MenuLinkContentInterface $entity
* Menu item being saved.
*/
protected function insertMenuLinkContent(MenuLinkContentInterface $entity) {
StanfordProfileHelper::clearMenuCacheTag();
}
/**
* When deleting a menu item, clear caches.
*
* @param \Drupal\menu_link_content\MenuLinkContentInterface $entity
* Menu item being deleted.
*/
protected function deleteMenuLinkContent(MenuLinkContentInterface $entity) {
StanfordProfileHelper::clearMenuCacheTag();
}
/**
* When updating a menu item, clear caches if necessary.
*
* @param \Drupal\menu_link_content\MenuLinkContentInterface $entity
* Modified menu item.
* @param \Drupal\menu_link_content\MenuLinkContentInterface $original_entity
* Original unmodified menu item.
*/
protected function updateMenuLinkContent(MenuLinkContentInterface $entity, MenuLinkContentInterface $original_entity) {
$compare_fields = ['title', 'link', 'parent', 'weight', 'expanded'];
$original = $updated = [];
foreach ($compare_fields as $field_name) {
$original[] = $original_entity->get($field_name)->getValue();
$updated[] = $entity->get($field_name)->getValue();
}
if (md5(json_encode($original)) != md5(json_encode($updated))) {
StanfordProfileHelper::clearMenuCacheTag();
}
}
/**
* Before saving a menu item, adjust the path if an internal path exists.
*
* @param \Drupal\menu_link_content\MenuLinkContentInterface $entity
* The menu link being saved.
*/
protected static function preSaveMenuLinkContent(MenuLinkContentInterface $entity): void {
$destination = $entity->get('link')->getString();
if ($internal_path = self::lookupInternalPath($destination)) {
$entity->set('link', $internal_path);
}
// For new menu link items created on a node form (normally), set the
// expanded attribute so all menu items are expanded by default.
$expanded = $entity->isNew() ?: $entity->isExpanded();
$entity->set('expanded', $expanded);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $link_manager */
$link_manager = \Drupal::service('plugin.manager.menu.link');
$parent_ids = $link_manager->getParentIds($entity->getPluginId()) ?: [];
$cache_tags = [];
// When a menu item is added as a child of another menu item clear the
// parent pages cache so that the block shows up as it doesn't get
// invalidated just by the menu cache tags.
foreach ($parent_ids as $parent_id) {
$link = $link_manager->getDefinition($parent_id);
if (isset($link['route_parameters']['node'])) {
$cache_tags[] = 'node:' . $link['route_parameters']['node'];
}
}
Cache::invalidateTags($cache_tags);
}
/**
* Before saving a redirect, adjust the path if an internal path exists.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Redirect to be saved.
*/
protected function preSaveRedirect(ContentEntityInterface $entity): void {
$destination = $entity->get('redirect_redirect')->getString();
if ($internal_path = self::lookupInternalPath($destination)) {
$entity->set('redirect_redirect', $internal_path);
}
}
/**
* Before saving a node, if a default content list page exists, create it.
*
* @param \Drupal\node\NodeInterface $entity
* The node being saved.
*/
protected function preSaveNode(NodeInterface $entity): void {
// Invalidate any search result cached so the updated/new content will be
// displayed for previously searched terms.
Cache::invalidateTags(['config:views.view.search']);
if (
InstallerKernel::installationAttempted() ||
!$entity->isNew()
) {
return;
}
$pages = [
'stanford_news' => '0b83d1e9-688a-4475-9673-a4c385f26247',
'stanford_event' => '8ba98fcf-d390-4014-92de-c77a59b30f3b',
'stanford_person' => '673a8fb8-39ac-49df-94c2-ed8d04db16a7',
'stanford_course' => '14768832-f763-4d27-8df6-7cd784886d57',
];
$bundle = $entity->bundle();
$state_key = 'stanford_profile_helper.default_content.' . $bundle;
if (
array_key_exists($bundle, $pages) &&
!$this->state->get($state_key)
) {
$this->state->set($state_key, TRUE);
$count = $this->entityTypeManager->getStorage('node')
->getQuery()
->accessCheck(FALSE)
->condition('type', $bundle)
->count()
->execute();
if ((int) $count == 0) {
$new_entity = $this->defaultContent->createDefaultContent($pages[$bundle]);
if ($new_entity) {
$this->messenger()
->addMessage($this->t('A new page was created automatically for you. View the @link page to make changes.', [
'@link' => Link::fromTextAndUrl($new_entity->label(), $new_entity->toUrl())
->toString(),
]));
}
}
}
}
/**
* Before saving a user role, prepend it with `custm_`.
*
* @param \Drupal\user\RoleInterface $role
* The role being saved.
*/
protected static function preSaveUserRole(RoleInterface $role) {
/** @var \Drupal\Core\Config\StorageInterface $config_storage */
$config_storage = \Drupal::service('config.storage.sync');
// Only modify new roles if they are created through the UI and don't exist
// in the config management - Prefix them with "custm_" so they can be
// easily identifiable.
if (
PHP_SAPI != 'cli' &&
$role->isNew() &&
!in_array($role->getConfigDependencyName(), $config_storage->listAll())
) {
$role->set('id', 'custm_' . $role->id());
}
}
/**
* Lookup an internal path.
*
* @param string $uri
* The destination path.
*
* @return string|null
* The internal path, or NULL if not found.
*/
protected static function lookupInternalPath(string $uri): ?string {
// If a redirect is added to go to the aliased path of a node (often from
// importing redirect), change the destination to target the node instead.
// This works if the destination is `/about` or `/node/9`.
if (preg_match('/^internal:(\/.*)/', $uri, $matches)) {
// Find the internal path from the alias.
$path = \Drupal::service('path_alias.manager')
->getPathByAlias($matches[1]);
// Grab the node id from the internal path and use that as destination.
if (preg_match('/node\/(\d+)/', $path, $matches)) {
return 'entity:node/' . $matches[1];
}
}
return NULL;
}
}