mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-03-08 01:27:42 -03:00
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>
324 lines
9.2 KiB
Plaintext
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);
|
|
}
|