Adiciona token [node:content-page-ancestors] para Pathauto

Registra token do tipo array que resolve o caminho hierárquico completo
de um content_page: domínio (alias de user/term/etc.) + slugs dos nós
pai, do mais distante ao mais próximo. Permite padrão Pathauto como
"[node:content-page-ancestors:join-path]/[node:title]".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 09:55:13 -03:00
parent 724f1336d0
commit 83690db7e9

View File

@@ -158,6 +158,12 @@ function structural_pages_token_info(): array {
'description' => t('The hierarchical path of the site section taxonomy (e.g., undergraduate/courses).'),
];
$info['tokens']['node']['content-page-ancestors'] = [
'name' => t('Content Page Ancestors Path'),
'description' => t('Array of ancestor path segments for a content_page node (domain + parent pages). Use [node:content-page-ancestors:join-path] in Pathauto patterns to get a slash-separated path (e.g., user/joao/pesquisa).'),
'type' => 'array',
];
$info['tokens']['term']['hierarchy-path'] = [
'name' => t('Hierarchy Path'),
'description' => t('The hierarchical path of the term including ancestors (e.g., institutional/news).'),
@@ -178,6 +184,30 @@ function structural_pages_tokens(string $type, array $tokens, array $data, array
if ($name === 'site-section-path') {
$replacements[$original] = _structural_pages_get_section_path($node);
}
if ($name === 'content-page-ancestors') {
$path = _structural_pages_get_content_page_ancestors($node);
$replacements[$original] = !empty($path)
? array_values(array_filter(explode('/', $path)))
: '';
}
}
// Handle [node:content-page-ancestors:join-path] and other chained tokens.
// Pathauto's [array:join-path] cleans each segment individually and joins
// with '/', preserving slashes. The token ends in ':join-path]' which is
// in Pathauto's safe_tokens list, so the result is NOT re-cleaned.
if ($ancestors_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-page-ancestors')) {
$path = _structural_pages_get_content_page_ancestors($node);
if (!empty($path)) {
$segments = array_values(array_filter(explode('/', $path)));
$replacements += \Drupal::token()->generate(
'array',
$ancestors_tokens,
['array' => $segments],
$options,
$bubbleable_metadata
);
}
}
}
@@ -234,6 +264,109 @@ function _structural_pages_get_section_path(NodeInterface $node): string {
return implode('/', $path_parts);
}
/**
* Gets the full ancestor path for a content_page node.
*
* Traverses the field_parent_page chain to the root, then prepends the domain
* entity's URL alias (from field_site_section). Intended as a Pathauto token
* prefix: configure the pattern as "[node:content-page-ancestors]/[node:title]".
*
* Examples:
* - Root page under user "joao" → "user/joao"
* - Child of "Pesquisa" under user "joao" → "user/joao/pesquisa"
* - Root page under term "graduacao/disciplinas" → "graduacao/disciplinas"
*
* @param \Drupal\node\NodeInterface $node
* The content_page node.
*
* @return string
* The ancestor path without leading/trailing slashes, or empty string.
*/
function _structural_pages_get_content_page_ancestors(NodeInterface $node): string {
if ($node->bundle() !== 'content_page') {
return '';
}
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$alias_cleaner = \Drupal::service('pathauto.alias_cleaner');
$path_alias_manager = \Drupal::service('path_alias.manager');
// Walk up the field_parent_page chain collecting parent titles.
$parent_slugs = [];
$visited = [];
$current = $node;
$max = 50;
while ($max-- > 0) {
if (isset($visited[$current->id()])) {
break;
}
$visited[$current->id()] = TRUE;
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
// Reached the root content_page: resolve domain from field_site_section.
if (!$current->hasField('field_site_section') || $current->get('field_site_section')->isEmpty()) {
break;
}
$fss = $current->get('field_site_section')->first();
$domain_type = $fss->target_type ?? 'taxonomy_term';
$domain_id = $fss->target_id;
$domain = \Drupal::entityTypeManager()->getStorage($domain_type)->load($domain_id);
if (!$domain || !$domain->hasLinkTemplate('canonical')) {
break;
}
// Build the system path for the domain entity.
// Constructed directly for known entity types to avoid URL generator
// context issues that can occur inside hook_tokens().
$domain_system_path = match ($domain_type) {
'user' => '/user/' . $domain_id,
'taxonomy_term' => '/taxonomy/term/' . $domain_id,
default => (static function () use ($domain): string {
try {
return '/' . $domain->toUrl('canonical')->getInternalPath();
}
catch (\Exception $e) {
return '';
}
})(),
};
if (empty($domain_system_path)) {
break;
}
// Resolve to alias if one exists.
$domain_alias = $path_alias_manager->getAliasByPath($domain_system_path);
$domain_path = ltrim($domain_alias, '/');
// Build: domain/parent2/parent1 (parents were collected innermost-first).
$parts = array_merge([$domain_path], array_reverse($parent_slugs));
return implode('/', array_filter($parts));
}
// Collect this node's parent's title (we traverse before adding current).
$parent_field = $current->get('field_parent_page')->first();
$parent_id = $parent_field->target_id ?? NULL;
if (!$parent_id) {
break;
}
$parent = $node_storage->load($parent_id);
if (!$parent) {
break;
}
// Add the parent's slug to the list (innermost first, reversed later).
$parent_slugs[] = $alias_cleaner->cleanString($parent->getTitle());
$current = $parent;
}
return '';
}
/**
* Gets the hierarchical path of a taxonomy term.
*