Implement ParentEntityHandler plugin system for extensible entity support

Replace hardcoded entity type checks with a plugin-based architecture using
PHP 8 attributes. This allows adding new parent entity types without modifying
core module files.

Changes:
- Add ParentEntityHandler attribute, interface, base class, and manager
- Create built-in handlers for taxonomy_term, user, and node entities
- Move Group support to site_structure_group submodule (fixes class not found
  error when Group module is not installed)
- Refactor SiteStructureSettingsForm to use handler manager
- Refactor SiteStructureMenuBlock to use handler manager
- Refactor SectionBreadcrumbBuilder to use handler manager
- Update site_structure.module to use handler manager for clearsSiteSection
- Update documentation and translations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 08:35:04 -03:00
parent 43fa9208a9
commit 0c8f0fc778
17 changed files with 1153 additions and 117 deletions

View File

@@ -8,13 +8,11 @@ use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\group\Entity\GroupInterface;
use Drupal\node\NodeInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\user\UserInterface;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -50,6 +48,13 @@ class SiteStructureMenuBlock extends BlockBase implements ContainerFactoryPlugin
*/
protected RouteMatchInterface $routeMatch;
/**
* The parent entity handler manager.
*
* @var \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface
*/
protected ParentEntityHandlerManagerInterface $handlerManager;
/**
* Cache tags collected during tree building.
*
@@ -66,10 +71,12 @@ class SiteStructureMenuBlock extends BlockBase implements ContainerFactoryPlugin
$plugin_definition,
EntityTypeManagerInterface $entity_type_manager,
RouteMatchInterface $route_match,
ParentEntityHandlerManagerInterface $handler_manager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->routeMatch = $route_match;
$this->handlerManager = $handler_manager;
}
/**
@@ -82,6 +89,7 @@ class SiteStructureMenuBlock extends BlockBase implements ContainerFactoryPlugin
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('current_route_match'),
$container->get('plugin.manager.parent_entity_handler'),
);
}
@@ -265,31 +273,13 @@ class SiteStructureMenuBlock extends BlockBase implements ContainerFactoryPlugin
}
/**
* Gets ancestor entity from current route (term, user, or group page).
* Gets ancestor entity from current route using available handlers.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The ancestor entity or NULL.
*/
protected function getAncestorFromRoute(): ?EntityInterface {
// Check taxonomy term.
$term = $this->routeMatch->getParameter('taxonomy_term');
if ($term instanceof TermInterface && $term->bundle() === 'site_section') {
return $term;
}
// Check user.
$user = $this->routeMatch->getParameter('user');
if ($user instanceof UserInterface) {
return $user;
}
// Check group (if module exists).
$group = $this->routeMatch->getParameter('group');
if ($group instanceof GroupInterface) {
return $group;
}
return NULL;
return $this->handlerManager->getEntityFromRoute($this->routeMatch);
}
/**

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\Plugin\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\node\NodeInterface;
use Drupal\site_structure\Attribute\ParentEntityHandler;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase;
/**
* Handler for node entities.
*/
#[ParentEntityHandler(
id: 'node',
label: new TranslatableMarkup('Content Types (node)'),
entity_type_id: 'node',
clears_site_section: FALSE,
sort_field: 'title',
bundle_restrictions: ['content_page', 'section_page'],
weight: 5,
)]
class NodeHandler extends ParentEntityHandlerBase {
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface {
$node = $route_match->getParameter('node');
if (!$node instanceof NodeInterface) {
return NULL;
}
if (!$this->handlesEntity($node)) {
return NULL;
}
return $node;
}
/**
* {@inheritdoc}
*/
public function buildBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $entity): void {
// Node breadcrumbs are handled separately through the parent chain.
// This method is not typically called for nodes.
$breadcrumb->addCacheableDependency($entity);
$breadcrumb->addLink($entity->toLink());
}
/**
* {@inheritdoc}
*/
public function getSiteSectionId(EntityInterface $entity): int|string|null {
if (!$entity instanceof NodeInterface) {
return NULL;
}
// Get site section from the node if it has one.
if ($entity->hasField('field_site_section') && !$entity->get('field_site_section')->isEmpty()) {
return $entity->get('field_site_section')->target_id;
}
return NULL;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\Plugin\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\site_structure\Attribute\ParentEntityHandler;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase;
use Drupal\taxonomy\TermInterface;
/**
* Handler for taxonomy term entities.
*/
#[ParentEntityHandler(
id: 'taxonomy_term',
label: new TranslatableMarkup('Taxonomy Vocabularies (taxonomy_term)'),
entity_type_id: 'taxonomy_term',
clears_site_section: FALSE,
sort_field: 'name',
bundle_restrictions: ['site_section'],
weight: 0,
)]
class TaxonomyTermHandler extends ParentEntityHandlerBase {
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(\Drupal\Core\Routing\RouteMatchInterface $route_match): ?EntityInterface {
$term = $route_match->getParameter('taxonomy_term');
if (!$term instanceof TermInterface) {
return NULL;
}
if (!$this->handlesEntity($term)) {
return NULL;
}
return $term;
}
/**
* {@inheritdoc}
*/
public function buildBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $entity): void {
if (!$entity instanceof TermInterface) {
return;
}
// Add all ancestors in the taxonomy hierarchy.
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$ancestors = $term_storage->loadAllParents($entity->id());
$ancestors = array_reverse($ancestors);
foreach ($ancestors as $ancestor) {
$breadcrumb->addCacheableDependency($ancestor);
$breadcrumb->addLink($ancestor->toLink());
}
}
/**
* {@inheritdoc}
*/
public function getSiteSectionId(EntityInterface $entity): int|string|null {
if (!$entity instanceof TermInterface) {
return NULL;
}
// Only site_section terms can be site sections.
if ($entity->bundle() !== 'site_section') {
return NULL;
}
return $entity->id();
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\Plugin\ParentEntityHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\site_structure\Attribute\ParentEntityHandler;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase;
use Drupal\user\UserInterface;
/**
* Handler for user entities.
*/
#[ParentEntityHandler(
id: 'user',
label: new TranslatableMarkup('Users (user)'),
entity_type_id: 'user',
clears_site_section: TRUE,
sort_field: 'name',
weight: 10,
)]
class UserHandler extends ParentEntityHandlerBase {
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface {
$user = $route_match->getParameter('user');
if (!$user instanceof UserInterface) {
return NULL;
}
return $user;
}
/**
* {@inheritdoc}
*/
public function handlesEntity(EntityInterface $entity): bool {
return $entity instanceof UserInterface;
}
}