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

@@ -276,6 +276,15 @@ site_structure/
│ └── pt-br.po # Portuguese (Brazil) translation
├── src/
│ ├── Attribute/
│ │ └── ParentEntityHandler.php # Plugin attribute definition
│ │
│ ├── ParentEntityHandler/
│ │ ├── ParentEntityHandlerInterface.php # Handler interface
│ │ ├── ParentEntityHandlerManagerInterface.php # Manager interface
│ │ ├── ParentEntityHandlerBase.php # Base implementation
│ │ └── ParentEntityHandlerManager.php # Plugin manager
│ │
│ ├── Breadcrumb/
│ │ └── SectionBreadcrumbBuilder.php
│ │
@@ -283,8 +292,19 @@ site_structure/
│ │ └── SiteStructureSettingsForm.php
│ │
│ └── Plugin/
── Block/
└── SiteStructureMenuBlock.php # Dynamic menu block
── Block/
└── SiteStructureMenuBlock.php # Dynamic menu block
│ │
│ └── ParentEntityHandler/ # Built-in handlers
│ ├── TaxonomyTermHandler.php # taxonomy_term handler
│ ├── UserHandler.php # user handler
│ └── NodeHandler.php # node handler
├── modules/
│ └── site_structure_group/ # Group integration submodule
│ ├── site_structure_group.info.yml
│ └── src/Plugin/ParentEntityHandler/
│ └── GroupHandler.php # group handler
└── docs/
└── DESIGN.md # This document
@@ -358,16 +378,81 @@ site_structure/
---
## Parent Entity Handler Plugin System
The module uses a plugin system to support different entity types as parents for content_page nodes. This allows for extensibility without modifying the core module.
### Architecture
The plugin system consists of:
1. **Attribute**: `Drupal\site_structure\Attribute\ParentEntityHandler` - PHP 8 attribute for defining handlers
2. **Interface**: `ParentEntityHandlerInterface` - Contract for all handlers
3. **Base class**: `ParentEntityHandlerBase` - Common implementation
4. **Manager**: `ParentEntityHandlerManager` - Plugin discovery and aggregation
### Built-in Handlers
| Handler | Entity Type | Clears Site Section | Sort Field | Bundle Restrictions |
|---------|-------------|---------------------|------------|---------------------|
| TaxonomyTermHandler | taxonomy_term | No | name | site_section |
| UserHandler | user | Yes | name | - |
| NodeHandler | node | No | title | content_page, section_page |
### Creating Custom Handlers
To add support for a new entity type, create a handler plugin:
```php
<?php
namespace Drupal\my_module\Plugin\ParentEntityHandler;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\site_structure\Attribute\ParentEntityHandler;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase;
#[ParentEntityHandler(
id: 'my_entity',
label: new TranslatableMarkup('My Entities'),
entity_type_id: 'my_entity',
clears_site_section: FALSE,
sort_field: 'title',
)]
class MyEntityHandler extends ParentEntityHandlerBase {
// Override methods as needed
}
```
### Handler Attributes
| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | string | Unique plugin ID |
| `label` | TranslatableMarkup | Human-readable label |
| `entity_type_id` | string | Entity type this handler manages |
| `provider_module` | string|null | Module required for availability |
| `clears_site_section` | bool | Whether to clear field_site_section |
| `sort_field` | string | Field for sorting entities |
| `route_parameter` | string|null | Route parameter name (defaults to entity_type_id) |
| `bundle_restrictions` | array | Specific bundles to handle |
| `weight` | int | Handler priority (lower = first) |
---
## Group Integration
The module supports the Group module for content organization:
Group support is provided via the `site_structure_group` submodule, which adds a handler for group entities. This submodule requires the Group module.
**Features**:
- Content pages can have groups as parent entities
- When a content_page has a group as parent, the context is the group itself
- Breadcrumbs show: Home > Group Name > Parent Nodes > Current Page
- `field_site_section` is cleared for group-parented content (context is the group)
- Per-section permissions can be implemented via Groups
**Installation**: Enable the `site_structure_group` submodule after installing the Group module.
**Configuration**: Enable group types in the settings form at `/admin/config/local-modules/site-structure`
---
@@ -394,3 +479,4 @@ The module supports the Group module for content organization:
| 1.1.0 | - | Added dynamic_entity_reference support for multi-type parents |
| 1.2.0 | - | Added user and group entity support as parent types |
| 1.3.0 | - | Added Site Structure Menu block for dynamic hierarchical navigation |
| 2.0.0 | - | Refactored to plugin system for parent entity handlers. Group support moved to submodule. |

View File

@@ -0,0 +1,8 @@
name: 'Site Structure Group Integration'
type: module
description: 'Provides Group entity support as parent type for Site Structure module.'
package: 'Site Structure'
core_version_requirement: ^10.3 || ^11
dependencies:
- site_structure:site_structure
- group:group

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure_group\Plugin\ParentEntityHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\group\Entity\GroupInterface;
use Drupal\site_structure\Attribute\ParentEntityHandler;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase;
/**
* Handler for group entities.
*
* This handler is provided by the site_structure_group submodule and is only
* available when the Group module is installed.
*/
#[ParentEntityHandler(
id: 'group',
label: new TranslatableMarkup('Groups (group)'),
entity_type_id: 'group',
provider_module: 'group',
clears_site_section: TRUE,
sort_field: 'label',
weight: 15,
)]
class GroupHandler extends ParentEntityHandlerBase {
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface {
$group = $route_match->getParameter('group');
if (!$group instanceof GroupInterface) {
return NULL;
}
return $group;
}
/**
* {@inheritdoc}
*/
public function handlesEntity(EntityInterface $entity): bool {
return $entity instanceof GroupInterface;
}
}

View File

@@ -51,9 +51,11 @@ function site_structure_entity_presave(EntityInterface $entity): void {
}
// Handle site section based on parent type.
// For user and group, we don't set field_site_section - the context is the entity itself.
if (in_array($parent_entity_type, ['user', 'group'])) {
// Clear field_site_section as the context is the user/group, not a site section.
// Some entity types (like user and group) act as context containers themselves.
/** @var \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface $handler_manager */
$handler_manager = \Drupal::service('plugin.manager.parent_entity_handler');
if ($handler_manager->clearsSiteSection($parent_entity_type)) {
// Clear field_site_section as the context is the parent entity, not a site section.
$entity->set('field_site_section', NULL);
return;
}

View File

@@ -1,6 +1,16 @@
services:
plugin.manager.parent_entity_handler:
class: Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManager
arguments:
- '@container.namespaces'
- '@cache.discovery'
- '@module_handler'
- '@entity_type.manager'
site_structure.breadcrumb.section:
class: Drupal\site_structure\Breadcrumb\SectionBreadcrumbBuilder
arguments: ['@entity_type.manager']
arguments:
- '@entity_type.manager'
- '@plugin.manager.parent_entity_handler'
tags:
- { name: breadcrumb_builder, priority: 100 }

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a ParentEntityHandler plugin attribute.
*
* Plugin namespace: Plugin\ParentEntityHandler.
*
* @see \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface
* @see \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase
* @see \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManager
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ParentEntityHandler extends Plugin {
/**
* Constructs a ParentEntityHandler attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $label
* The human-readable label of the handler.
* @param string $entity_type_id
* The entity type ID this handler manages.
* @param string|null $provider_module
* (optional) The module that must be installed for this handler to be
* available. If NULL, the handler is always available.
* @param bool $clears_site_section
* (optional) Whether this handler clears field_site_section when used as
* parent. Defaults to FALSE.
* @param string $sort_field
* (optional) The field to use for sorting entities of this type.
* Defaults to 'title'.
* @param string|null $route_parameter
* (optional) The route parameter name used to detect entities on routes.
* Defaults to the entity_type_id if not specified.
* @param array $bundle_restrictions
* (optional) An array of bundle IDs that this handler applies to. If empty,
* the handler applies to all bundles.
* @param int $weight
* (optional) The weight of the handler. Lower weights are processed first.
* Defaults to 0.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $label,
public readonly string $entity_type_id,
public readonly ?string $provider_module = NULL,
public readonly bool $clears_site_section = FALSE,
public readonly string $sort_field = 'title',
public readonly ?string $route_parameter = NULL,
public readonly array $bundle_restrictions = [],
public readonly int $weight = 0,
public readonly ?string $deriver = NULL,
) {}
}

View File

@@ -6,12 +6,12 @@ namespace Drupal\site_structure\Breadcrumb;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\NodeInterface;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface;
/**
* Provides a breadcrumb builder for section_page and content_page content types.
@@ -27,6 +27,13 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The parent entity handler manager.
*
* @var \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface
*/
protected ParentEntityHandlerManagerInterface $handlerManager;
/**
* Content types that this builder applies to.
*
@@ -39,9 +46,15 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface $handler_manager
* The parent entity handler manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
ParentEntityHandlerManagerInterface $handler_manager,
) {
$this->entityTypeManager = $entity_type_manager;
$this->handlerManager = $handler_manager;
}
/**
@@ -79,25 +92,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
$context = $this->getParentContext($node);
if ($context) {
switch ($context['type']) {
case 'user':
// User context: Home > User Name > ... > Current Page.
$this->addUserBreadcrumb($breadcrumb, $context['entity']);
break;
case 'group':
// Group context: Home > Group Name > ... > Current Page.
$this->addGroupBreadcrumb($breadcrumb, $context['entity']);
break;
default:
// Node or taxonomy context: use site section.
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
$term_id = $node->get('field_site_section')->target_id;
$this->addTaxonomyBreadcrumbs($breadcrumb, $term_id);
}
break;
}
// Use the handler manager to build breadcrumbs for the context entity.
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $context['entity']);
}
else {
// No parent context, check for site section directly.
@@ -193,32 +189,6 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
return NULL;
}
/**
* Adds user breadcrumb.
*
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
* The breadcrumb object.
* @param \Drupal\Core\Entity\EntityInterface $user
* The user entity.
*/
protected function addUserBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $user): void {
$breadcrumb->addCacheableDependency($user);
$breadcrumb->addLink($user->toLink());
}
/**
* Adds group breadcrumb.
*
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
* The breadcrumb object.
* @param \Drupal\Core\Entity\EntityInterface $group
* The group entity.
*/
protected function addGroupBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $group): void {
$breadcrumb->addCacheableDependency($group);
$breadcrumb->addLink($group->toLink());
}
/**
* Adds breadcrumbs based on taxonomy hierarchy.
*

View File

@@ -7,9 +7,9 @@ namespace Drupal\site_structure\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -32,23 +32,11 @@ class SiteStructureSettingsForm extends ConfigFormBase {
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The module handler.
* The parent entity handler manager.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
* @var \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* Entity types that can be used as parents.
*
* @var array
*/
protected array $supportedEntityTypes = [
'node' => 'Content Types (node)',
'taxonomy_term' => 'Taxonomy Vocabularies (taxonomy_term)',
'user' => 'Users (user)',
'group' => 'Groups (group)',
];
protected ParentEntityHandlerManagerInterface $handlerManager;
/**
* Constructs a SiteStructureSettingsForm object.
@@ -59,19 +47,19 @@ class SiteStructureSettingsForm extends ConfigFormBase {
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerManagerInterface $handler_manager
* The parent entity handler manager.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_type_bundle_info,
ModuleHandlerInterface $module_handler,
ParentEntityHandlerManagerInterface $handler_manager,
) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->moduleHandler = $module_handler;
$this->handlerManager = $handler_manager;
}
/**
@@ -82,7 +70,7 @@ class SiteStructureSettingsForm extends ConfigFormBase {
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('module_handler'),
$container->get('plugin.manager.parent_entity_handler'),
);
}
@@ -137,20 +125,12 @@ class SiteStructureSettingsForm extends ConfigFormBase {
'#tree' => TRUE,
];
foreach ($this->supportedEntityTypes as $entity_type => $label) {
// Check if entity type exists.
if (!$this->entityTypeManager->hasDefinition($entity_type)) {
continue;
}
// Special handling for 'group' - check if module is enabled.
if ($entity_type === 'group' && !$this->moduleHandler->moduleExists('group')) {
continue;
}
$supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes();
foreach ($supportedEntityTypes as $entity_type => $label) {
$form['allowed_parent_targets'][$entity_type] = [
'#type' => 'details',
'#title' => $this->t($label),
'#title' => $label,
'#open' => TRUE,
];
@@ -207,8 +187,9 @@ class SiteStructureSettingsForm extends ConfigFormBase {
public function submitForm(array &$form, FormStateInterface $form_state): void {
$targets = [];
$values = $form_state->getValue('allowed_parent_targets');
$supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes();
foreach ($this->supportedEntityTypes as $entity_type => $label) {
foreach ($supportedEntityTypes as $entity_type => $label) {
if (empty($values[$entity_type])) {
continue;
}
@@ -276,12 +257,7 @@ class SiteStructureSettingsForm extends ConfigFormBase {
}
if (!isset($entity_type_settings[$entity_type])) {
$sort_field = match($entity_type) {
'taxonomy_term' => 'name',
'user' => 'name',
'group' => 'label',
default => 'title',
};
$sort_field = $this->handlerManager->getSortField($entity_type);
$entity_type_settings[$entity_type] = [
'handler' => 'default:' . $entity_type,

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for parent entity handler plugins.
*
* Provides default implementations for common handler functionality.
*/
abstract class ParentEntityHandlerBase extends PluginBase implements ParentEntityHandlerInterface, ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* Constructs a ParentEntityHandlerBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
EntityTypeManagerInterface $entity_type_manager,
ModuleHandlerInterface $module_handler,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('module_handler'),
);
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId(): string {
return $this->pluginDefinition['entity_type_id'];
}
/**
* {@inheritdoc}
*/
public function getLabel(): string {
return (string) $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function isAvailable(): bool {
// Check if provider module is required and installed.
$provider = $this->pluginDefinition['provider_module'] ?? NULL;
if ($provider && !$this->moduleHandler->moduleExists($provider)) {
return FALSE;
}
// Check if entity type exists.
return $this->entityTypeManager->hasDefinition($this->getEntityTypeId());
}
/**
* {@inheritdoc}
*/
public function clearsSiteSection(): bool {
return $this->pluginDefinition['clears_site_section'] ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function getSortField(): string {
return $this->pluginDefinition['sort_field'] ?? 'title';
}
/**
* {@inheritdoc}
*/
public function getRouteParameter(): string {
return $this->pluginDefinition['route_parameter'] ?? $this->getEntityTypeId();
}
/**
* {@inheritdoc}
*/
public function getBundleRestrictions(): array {
return $this->pluginDefinition['bundle_restrictions'] ?? [];
}
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface {
$parameter = $this->getRouteParameter();
$entity = $route_match->getParameter($parameter);
if (!$entity instanceof EntityInterface) {
return NULL;
}
if (!$this->handlesEntity($entity)) {
return NULL;
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function handlesEntity(EntityInterface $entity): bool {
if ($entity->getEntityTypeId() !== $this->getEntityTypeId()) {
return FALSE;
}
$bundle_restrictions = $this->getBundleRestrictions();
if (!empty($bundle_restrictions)) {
return in_array($entity->bundle(), $bundle_restrictions, TRUE);
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function buildBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $entity): void {
$breadcrumb->addCacheableDependency($entity);
$breadcrumb->addLink($entity->toLink());
}
/**
* {@inheritdoc}
*/
public function getSiteSectionId(EntityInterface $entity): int|string|null {
// Default implementation returns NULL.
// Subclasses should override for entity types that can provide a site section.
return NULL;
}
/**
* Loads an entity by ID.
*
* @param int|string $entity_id
* The entity ID.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The loaded entity, or NULL if not found.
*/
protected function loadEntity(int|string $entity_id): ?EntityInterface {
return $this->entityTypeManager
->getStorage($this->getEntityTypeId())
->load($entity_id);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Interface for parent entity handler plugins.
*
* Parent entity handlers define how different entity types behave when used
* as parent entities for content_page nodes in the site structure hierarchy.
*/
interface ParentEntityHandlerInterface {
/**
* Gets the entity type ID this handler manages.
*
* @return string
* The entity type ID.
*/
public function getEntityTypeId(): string;
/**
* Gets the human-readable label of this handler.
*
* @return string
* The handler label.
*/
public function getLabel(): string;
/**
* Checks if this handler is available.
*
* A handler is available if its provider module is installed and the
* entity type exists.
*
* @return bool
* TRUE if the handler is available, FALSE otherwise.
*/
public function isAvailable(): bool;
/**
* Checks if this handler clears field_site_section when used as parent.
*
* Some entity types (like users and groups) act as context containers
* themselves, so content pages under them don't inherit a site section.
*
* @return bool
* TRUE if field_site_section should be cleared, FALSE otherwise.
*/
public function clearsSiteSection(): bool;
/**
* Gets the field to use for sorting entities of this type.
*
* @return string
* The sort field name.
*/
public function getSortField(): string;
/**
* Gets the route parameter name for detecting entities on routes.
*
* @return string
* The route parameter name.
*/
public function getRouteParameter(): string;
/**
* Gets the bundle restrictions for this handler.
*
* @return array
* An array of bundle IDs, or empty array for no restrictions.
*/
public function getBundleRestrictions(): array;
/**
* Gets an entity from the current route.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity from the route, or NULL if not found or not applicable.
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface;
/**
* Checks if this handler handles the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if this handler handles the entity, FALSE otherwise.
*/
public function handlesEntity(EntityInterface $entity): bool;
/**
* Builds breadcrumb entries for the given entity.
*
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
* The breadcrumb to add entries to.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to build breadcrumb for.
*/
public function buildBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $entity): void;
/**
* Gets the site section ID from an entity of this type.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return int|string|null
* The site section term ID, or NULL if not applicable.
*/
public function getSiteSectionId(EntityInterface $entity): int|string|null;
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\site_structure\Attribute\ParentEntityHandler;
/**
* Plugin manager for parent entity handlers.
*
* @see \Drupal\site_structure\Attribute\ParentEntityHandler
* @see \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface
* @see \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerBase
*/
class ParentEntityHandlerManager extends DefaultPluginManager implements ParentEntityHandlerManagerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* Static cache for handler instances.
*
* @var \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface[]|null
*/
protected ?array $handlerInstances = NULL;
/**
* Constructs a ParentEntityHandlerManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface $module_handler,
EntityTypeManagerInterface $entity_type_manager,
) {
parent::__construct(
'Plugin/ParentEntityHandler',
$namespaces,
$module_handler,
ParentEntityHandlerInterface::class,
ParentEntityHandler::class,
);
$this->alterInfo('parent_entity_handler_info');
$this->setCacheBackend($cache_backend, 'parent_entity_handler_plugins');
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function getAvailableHandlers(): array {
if ($this->handlerInstances !== NULL) {
return $this->handlerInstances;
}
$this->handlerInstances = [];
$definitions = $this->getDefinitions();
// Sort by weight.
uasort($definitions, function ($a, $b) {
return ($a['weight'] ?? 0) <=> ($b['weight'] ?? 0);
});
foreach ($definitions as $plugin_id => $definition) {
$handler = $this->createInstance($plugin_id);
if ($handler instanceof ParentEntityHandlerInterface && $handler->isAvailable()) {
$this->handlerInstances[$plugin_id] = $handler;
}
}
return $this->handlerInstances;
}
/**
* {@inheritdoc}
*/
public function getHandlerForEntityType(string $entity_type_id): ?ParentEntityHandlerInterface {
foreach ($this->getAvailableHandlers() as $handler) {
if ($handler->getEntityTypeId() === $entity_type_id) {
return $handler;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getHandlerForEntity(EntityInterface $entity): ?ParentEntityHandlerInterface {
foreach ($this->getAvailableHandlers() as $handler) {
if ($handler->handlesEntity($entity)) {
return $handler;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getSupportedEntityTypes(): array {
$types = [];
foreach ($this->getAvailableHandlers() as $handler) {
$types[$handler->getEntityTypeId()] = $handler->getLabel();
}
return $types;
}
/**
* {@inheritdoc}
*/
public function clearsSiteSection(string $entity_type_id): bool {
$handler = $this->getHandlerForEntityType($entity_type_id);
return $handler ? $handler->clearsSiteSection() : FALSE;
}
/**
* {@inheritdoc}
*/
public function getSortField(string $entity_type_id): string {
$handler = $this->getHandlerForEntityType($entity_type_id);
return $handler ? $handler->getSortField() : 'title';
}
/**
* {@inheritdoc}
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface {
foreach ($this->getAvailableHandlers() as $handler) {
$entity = $handler->getEntityFromRoute($route_match);
if ($entity) {
return $entity;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function buildBreadcrumbForEntity(Breadcrumb $breadcrumb, EntityInterface $entity): bool {
$handler = $this->getHandlerForEntity($entity);
if ($handler) {
$handler->buildBreadcrumb($breadcrumb, $entity);
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getSiteSectionFromParent(string $entity_type_id, int|string $entity_id): int|string|null {
$handler = $this->getHandlerForEntityType($entity_type_id);
if (!$handler) {
return NULL;
}
$entity = $this->entityTypeManager
->getStorage($entity_type_id)
->load($entity_id);
if (!$entity) {
return NULL;
}
return $handler->getSiteSectionId($entity);
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions(): void {
parent::clearCachedDefinitions();
$this->handlerInstances = NULL;
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\site_structure\ParentEntityHandler;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Interface for the parent entity handler plugin manager.
*
* This manager provides centralized access to all registered parent entity
* handlers and their aggregated functionality.
*/
interface ParentEntityHandlerManagerInterface {
/**
* Gets all available handlers.
*
* Handlers are available if their provider module is installed and the
* entity type exists.
*
* @return \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface[]
* An array of available handler instances, keyed by plugin ID.
*/
public function getAvailableHandlers(): array;
/**
* Gets a handler for the given entity type.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface|null
* The handler for the entity type, or NULL if none found.
*/
public function getHandlerForEntityType(string $entity_type_id): ?ParentEntityHandlerInterface;
/**
* Gets a handler that can handle the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\site_structure\ParentEntityHandler\ParentEntityHandlerInterface|null
* The handler for the entity, or NULL if none found.
*/
public function getHandlerForEntity(EntityInterface $entity): ?ParentEntityHandlerInterface;
/**
* Gets supported entity types for the settings form.
*
* @return array
* An associative array of entity_type_id => label for available handlers.
*/
public function getSupportedEntityTypes(): array;
/**
* Checks if the given entity type clears site section.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return bool
* TRUE if field_site_section should be cleared, FALSE otherwise.
*/
public function clearsSiteSection(string $entity_type_id): bool;
/**
* Gets the sort field for the given entity type.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return string
* The sort field name, or 'title' as default.
*/
public function getSortField(string $entity_type_id): string;
/**
* Gets an entity from the current route using available handlers.
*
* Iterates through available handlers to find an entity on the current route.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity from the route, or NULL if not found.
*/
public function getEntityFromRoute(RouteMatchInterface $route_match): ?EntityInterface;
/**
* Builds breadcrumb for the given entity using the appropriate handler.
*
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
* The breadcrumb to add entries to.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to build breadcrumb for.
*
* @return bool
* TRUE if a handler was found and built the breadcrumb, FALSE otherwise.
*/
public function buildBreadcrumbForEntity(Breadcrumb $breadcrumb, EntityInterface $entity): bool;
/**
* Gets the site section ID from a parent entity.
*
* @param string $entity_type_id
* The parent entity type ID.
* @param int|string $entity_id
* The parent entity ID.
*
* @return int|string|null
* The site section term ID, or NULL if not applicable.
*/
public function getSiteSectionFromParent(string $entity_type_id, int|string $entity_id): int|string|null;
}

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;
}
}

View File

@@ -336,3 +336,31 @@ msgstr "Expandir automaticamente os itens de menu na trilha ativa."
#: templates/site-structure-menu.html.twig
msgid "Site structure navigation"
msgstr "Navegação da estrutura do site"
#: src/Plugin/ParentEntityHandler/TaxonomyTermHandler.php
#: src/Form/SiteStructureSettingsForm.php
msgid "Taxonomy Vocabularies (taxonomy_term)"
msgstr "Vocabulários de Taxonomia (taxonomy_term)"
#: src/Plugin/ParentEntityHandler/UserHandler.php
#: src/Form/SiteStructureSettingsForm.php
msgid "Users (user)"
msgstr "Usuários (user)"
#: src/Plugin/ParentEntityHandler/NodeHandler.php
#: src/Form/SiteStructureSettingsForm.php
msgid "Content Types (node)"
msgstr "Tipos de Conteúdo (node)"
#: modules/site_structure_group/src/Plugin/ParentEntityHandler/GroupHandler.php
#: src/Form/SiteStructureSettingsForm.php
msgid "Groups (group)"
msgstr "Grupos (group)"
#: modules/site_structure_group/site_structure_group.info.yml
msgid "Site Structure Group Integration"
msgstr "Integração de Grupos do Site Structure"
#: modules/site_structure_group/site_structure_group.info.yml
msgid "Provides Group entity support as parent type for Site Structure module."
msgstr "Fornece suporte a entidades Grupo como tipo pai para o módulo Site Structure."