Files
structural_pages/structural_pages.module
Quintino A. G. Souza 88b9605408 Rename module site_structure → structural_pages and vocabulary site_section → site_sections
Renames the module from site_structure to structural_pages and pluralizes
the taxonomy vocabulary machine name from site_section to site_sections,
updating all config, PHP, translations, and documentation references
while preserving field_site_section, clears_site_section, and $site_section_id.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 08:10:32 -03:00

324 lines
9.2 KiB
Plaintext

<?php
/**
* @file
* Primary module hooks for Structural Pages module.
*/
declare(strict_types=1);
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\NodeInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_entity_presave().
*
* Inherits field_site_section from parent to content_page.
* Validates circular reference in field_parent_page.
*/
function structural_pages_entity_presave(EntityInterface $entity): void {
if (!$entity instanceof NodeInterface || $entity->bundle() !== 'content_page') {
return;
}
// Check if has parent page.
if (!$entity->hasField('field_parent_page') || $entity->get('field_parent_page')->isEmpty()) {
return;
}
$parent_field = $entity->get('field_parent_page')->first();
if (!$parent_field) {
return;
}
$parent_entity_type = $parent_field->target_type ?? NULL;
$parent_id = $parent_field->target_id ?? NULL;
if (!$parent_entity_type || !$parent_id) {
return;
}
// Circular reference validation (only for node parents).
if ($parent_entity_type === 'node' && !$entity->isNew() && $parent_id) {
if (_structural_pages_creates_circular_reference($entity->id(), $parent_id)) {
\Drupal::messenger()->addError(t('Circular reference detected. A page cannot be a parent of itself or its ancestors.'));
// Remove invalid reference.
$entity->set('field_parent_page', NULL);
return;
}
}
// Handle site section based on parent type.
// Some entity types (like user and group) act as context containers themselves.
/** @var \Drupal\structural_pages\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;
}
// Inherit field_site_section based on parent type (node or taxonomy_term).
$site_section_id = _structural_pages_get_section_from_parent($parent_entity_type, $parent_id);
if ($site_section_id) {
$entity->set('field_site_section', $site_section_id);
}
}
/**
* Gets the site section ID from a parent entity.
*
* @param string $parent_entity_type
* The parent entity type (node or taxonomy_term).
* @param int|string $parent_id
* The parent entity ID.
*
* @return int|string|null
* The site section term ID, or NULL if not found.
*/
function _structural_pages_get_section_from_parent(string $parent_entity_type, int|string $parent_id): int|string|null {
$entity_type_manager = \Drupal::entityTypeManager();
if ($parent_entity_type === 'taxonomy_term') {
// If parent is a taxonomy term, verify it's from site_sections vocabulary.
$term = $entity_type_manager->getStorage('taxonomy_term')->load($parent_id);
if ($term instanceof TermInterface && $term->bundle() === 'site_sections') {
return $term->id();
}
return NULL;
}
if ($parent_entity_type === 'node') {
$parent_node = $entity_type_manager->getStorage('node')->load($parent_id);
if (!$parent_node instanceof NodeInterface) {
return NULL;
}
// If parent has field_site_section, use it.
if ($parent_node->hasField('field_site_section') && !$parent_node->get('field_site_section')->isEmpty()) {
return $parent_node->get('field_site_section')->target_id;
}
}
return NULL;
}
/**
* Gets the parent context information for a content_page.
*
* @param \Drupal\node\NodeInterface $node
* The content_page node.
*
* @return array|null
* An array with 'type' and 'entity' keys, or NULL if no parent.
*/
function _structural_pages_get_parent_context(NodeInterface $node): ?array {
if (!$node->hasField('field_parent_page') || $node->get('field_parent_page')->isEmpty()) {
return NULL;
}
$parent_field = $node->get('field_parent_page')->first();
if (!$parent_field) {
return NULL;
}
$parent_entity_type = $parent_field->target_type ?? NULL;
$parent_id = $parent_field->target_id ?? NULL;
if (!$parent_entity_type || !$parent_id) {
return NULL;
}
$parent = \Drupal::entityTypeManager()
->getStorage($parent_entity_type)
->load($parent_id);
if (!$parent) {
return NULL;
}
return [
'type' => $parent_entity_type,
'entity' => $parent,
];
}
/**
* Checks if setting parent_id as parent of node_id would create circular reference.
*
* @param int|string $node_id
* The ID of the node being edited.
* @param int|string $parent_id
* The ID of the potential parent.
*
* @return bool
* TRUE if it would create circular reference, FALSE otherwise.
*/
function _structural_pages_creates_circular_reference(int|string $node_id, int|string $parent_id): bool {
$visited = [];
$current_id = $parent_id;
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
while ($current_id) {
// If we find the original node in the parent chain, it's circular.
if ($current_id == $node_id) {
return TRUE;
}
// Avoid infinite loops in case of corrupted data.
if (isset($visited[$current_id])) {
return TRUE;
}
$visited[$current_id] = TRUE;
// Load the current node and get its parent.
$current_node = $node_storage->load($current_id);
if (!$current_node instanceof NodeInterface ||
!$current_node->hasField('field_parent_page') ||
$current_node->get('field_parent_page')->isEmpty()) {
break;
}
$parent_field = $current_node->get('field_parent_page')->first();
// Only continue checking if parent is also a node.
if (!$parent_field || ($parent_field->target_type ?? NULL) !== 'node') {
break;
}
$current_id = $parent_field->target_id;
}
return FALSE;
}
/**
* Implements hook_theme().
*/
function structural_pages_theme(): array {
return [
'structural_pages_menu' => [
'variables' => [
'ancestor' => NULL,
'tree' => [],
'active_trail' => [],
'show_ancestor_title' => TRUE,
],
],
'structural_pages_menu_tree' => [
'variables' => [
'items' => [],
'active_trail' => [],
'depth' => 0,
],
],
];
}
/**
* Implements hook_token_info().
*/
function structural_pages_token_info(): array {
$info = [];
$info['tokens']['node']['site-section-path'] = [
'name' => t('Site Section Path'),
'description' => t('The hierarchical path of the site_sections taxonomy (e.g., undergraduate/courses).'),
];
$info['tokens']['term']['hierarchy-path'] = [
'name' => t('Hierarchy Path'),
'description' => t('The hierarchical path of the term including ancestors (e.g., institutional/news).'),
];
return $info;
}
/**
* Implements hook_tokens().
*/
function structural_pages_tokens(string $type, array $tokens, array $data, array $options, $bubbleable_metadata): array {
$replacements = [];
if ($type === 'node' && !empty($data['node'])) {
$node = $data['node'];
foreach ($tokens as $name => $original) {
if ($name === 'site-section-path') {
$replacements[$original] = _structural_pages_get_section_path($node);
}
}
}
if ($type === 'term' && !empty($data['term'])) {
$term = $data['term'];
foreach ($tokens as $name => $original) {
if ($name === 'hierarchy-path') {
$replacements[$original] = _structural_pages_get_term_hierarchy_path($term);
}
}
}
return $replacements;
}
/**
* Gets the hierarchical path of the site section for a node.
*
* @param \Drupal\node\NodeInterface $node
* The node.
*
* @return string
* The section path (e.g., "undergraduate/courses") or empty string.
*/
function _structural_pages_get_section_path(NodeInterface $node): string {
if (!$node->hasField('field_site_section') || $node->get('field_site_section')->isEmpty()) {
return '';
}
$term_id = $node->get('field_site_section')->target_id;
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$term = $term_storage->load($term_id);
if (!$term) {
return '';
}
// Get all ancestors of the term.
$ancestors = $term_storage->loadAllParents($term_id);
$ancestors = array_reverse($ancestors);
// Build the path using the term names converted to URL.
$path_parts = [];
foreach ($ancestors as $ancestor) {
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
}
return implode('/', $path_parts);
}
/**
* Gets the hierarchical path of a taxonomy term.
*
* @param \Drupal\taxonomy\TermInterface $term
* The taxonomy term.
*
* @return string
* The hierarchical path (e.g., "institutional/news") or empty string.
*/
function _structural_pages_get_term_hierarchy_path(TermInterface $term): string {
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
// Get all ancestors of the term (including itself).
$ancestors = $term_storage->loadAllParents($term->id());
$ancestors = array_reverse($ancestors);
// Build the path using the term names converted to URL.
$path_parts = [];
foreach ($ancestors as $ancestor) {
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
}
return implode('/', $path_parts);
}