Refatora arquitetura de domínio: field_site_section DER, field_parent_page node-only

- field_site_section migrado de entity_reference para dynamic_entity_reference,
  permitindo referenciar taxonomy_term, user ou event como domínio raiz da página
- field_parent_page restrito a content_page nodes apenas (removidos taxonomy_term/user)
- Update hooks 10011–10013: migração de schema/config, limpeza de referências
  inválidas e correção do tipo do storage field_parent_page para DER
- SectionBreadcrumbBuilder reescrito para usar field_site_section como raiz
- StructuralPagesMenuBlock: queries de domínio via DB direto (evita bug do
  entity query com especificador target_type em campos DER)
- Formulário content_page: pre-fill de field_site_section via query params
  ?domain_type/domain_id ou ?parent_section_type/parent_id; atualiza widget
  #default_value diretamente para refletir o valor na renderização inicial
- Parâmetro de URL parent_type renomeado para parent_section_type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 09:41:17 -03:00
parent 6ab880045a
commit d2ef439227
9 changed files with 894 additions and 370 deletions

View File

@@ -91,17 +91,15 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
// Add "Home" as first item.
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
// Determine the context type for content_page.
$context = $this->getParentContext($node);
// Resolve domain root from field_site_section of this node or its ancestors.
$domain = $this->resolveDomain($node);
if ($context) {
// 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.
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);
if ($domain) {
if ($domain['type'] === 'taxonomy_term') {
$this->addTaxonomyBreadcrumbs($breadcrumb, $domain['entity']->id());
}
else {
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $domain['entity']);
}
}
@@ -120,85 +118,88 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
}
/**
* Gets the root parent context for a content_page.
* Resolves the domain root for the given node.
*
* Traverses up the parent chain to find the root context (user, group, or taxonomy).
* Walks up the field_parent_page chain to the topmost content_page, then
* reads field_site_section (DER) to get the domain entity (taxonomy_term,
* user, event, …).
*
* @param \Drupal\node\NodeInterface $node
* The content_page node.
* Any node (section_page or content_page).
*
* @return array|null
* An array with 'type' and 'entity' keys, or NULL if no context.
* @return array{type: string, entity: \Drupal\Core\Entity\EntityInterface}|null
* Array with 'type' and 'entity', or NULL if domain cannot be resolved.
*/
protected function getParentContext(NodeInterface $node): ?array
protected function resolveDomain(NodeInterface $node): ?array
{
if ($node->bundle() !== 'content_page') {
return NULL;
}
// For section_page or any node with field_site_section, read it directly.
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
$root = $node;
$visited = [];
$current = $node;
while (
$current instanceof NodeInterface &&
$current->hasField('field_parent_page') &&
!$current->get('field_parent_page')->isEmpty()
) {
$parent_field = $current->get('field_parent_page')->first();
if (!$parent_field) {
break;
// For content_page, walk up to the topmost node to get the canonical
// field_site_section (which should be the same on all nodes in the chain).
if ($node->bundle() === 'content_page') {
$root = $this->getRootContentPage($node);
}
$parent_entity_type = $parent_field->target_type ?? NULL;
$parent_id = $parent_field->target_id ?? NULL;
if ($root->hasField('field_site_section') && !$root->get('field_site_section')->isEmpty()) {
$fss = $root->get('field_site_section')->first();
$domain_type = $fss->target_type ?? 'taxonomy_term';
$domain_id = $fss->target_id;
if (!$parent_entity_type || !$parent_id) {
break;
}
// Avoid infinite loops.
$visit_key = $parent_entity_type . ':' . $parent_id;
if (isset($visited[$visit_key])) {
break;
}
$visited[$visit_key] = TRUE;
$parent = $this->entityTypeManager
->getStorage($parent_entity_type)
->load($parent_id);
if (!$parent) {
break;
}
// If parent is user or group, that's our context.
if (in_array($parent_entity_type, ['user', 'group'])) {
return [
'type' => $parent_entity_type,
'entity' => $parent,
];
}
// If parent is taxonomy_term, that's our context (handled via configured vocabulary).
if ($parent_entity_type === 'taxonomy_term') {
return [
'type' => 'taxonomy_term',
'entity' => $parent,
];
}
// If parent is a node, continue traversing.
if ($parent instanceof NodeInterface) {
$current = $parent;
} else {
break;
$domain_entity = $this->entityTypeManager->getStorage($domain_type)->load($domain_id);
if ($domain_entity) {
$breadcrumb_arg = &$this; // used only to add cache dependency below.
return ['type' => $domain_type, 'entity' => $domain_entity];
}
}
}
return NULL;
}
/**
* Walks up field_parent_page to find the topmost content_page.
*
* @param \Drupal\node\NodeInterface $node
* The starting node.
*
* @return \Drupal\node\NodeInterface
* The root content_page (or $node itself if no parent chain found).
*/
protected function getRootContentPage(NodeInterface $node): NodeInterface
{
$visited = [];
$current = $node;
while (TRUE) {
if (isset($visited[$current->id()])) {
break;
}
$visited[$current->id()] = TRUE;
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
return $current;
}
$pf = $current->get('field_parent_page')->first();
$parent_id = $pf->target_id ?? NULL;
if (!$parent_id) {
return $current;
}
$parent = $this->entityTypeManager->getStorage('node')->load($parent_id);
if (!$parent instanceof NodeInterface) {
return $current;
}
$current = $parent;
}
return $node;
}
/**
* Adds breadcrumbs based on taxonomy hierarchy.
*

View File

@@ -8,6 +8,7 @@ use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
@@ -252,28 +253,24 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
$visited[$current->id()] = TRUE;
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
// Node without parent - check if it has a direct section.
// Reached the root content_page: domain is in field_site_section (DER).
if ($current->hasField('field_site_section') && !$current->get('field_site_section')->isEmpty()) {
return $current->get('field_site_section')->entity;
$fss = $current->get('field_site_section')->first();
$domain_type = $fss->target_type ?? 'taxonomy_term';
$domain_id = $fss->target_id;
return $this->entityTypeManager->getStorage($domain_type)->load($domain_id);
}
return NULL;
}
$parent_field = $current->get('field_parent_page')->first();
$parent_type = $parent_field->target_type ?? NULL;
$parent_id = $parent_field->target_id ?? NULL;
$parent_id = $parent_field->target_id ?? NULL;
if (!$parent_type || !$parent_id) {
if (!$parent_id) {
break;
}
// If parent is not a node, it's our ancestor.
if ($parent_type !== 'node') {
$storage = $this->entityTypeManager->getStorage($parent_type);
return $storage->load($parent_id);
}
// Parent is a node, continue traversing.
// field_parent_page always points to a node; traverse upward.
$current = $this->entityTypeManager->getStorage('node')->load($parent_id);
}
@@ -368,6 +365,43 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
return $branch;
}
/**
* Applies domain conditions to a content_page entity query.
*
* Handles both entity_reference (legacy) and dynamic_entity_reference (DER)
* storage for field_site_section, so queries work before and after the
* migration update hook (10011) is applied.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $query
* The entity query to modify.
* @param string $domain_type
* The domain entity type (e.g. 'taxonomy_term', 'user').
* @param int|string $domain_id
* The domain entity ID.
*/
protected function applyDomainCondition(QueryInterface $query, string $domain_type, int|string $domain_id): void {
$database = \Drupal::database();
$schema = $database->schema();
if ($schema->fieldExists('node__field_site_section', 'field_site_section_target_type')) {
// DER storage: use a direct DB query to avoid DER entity query issues
// with the 'target_type' specifier in Drupal\Core\Entity\Query\Sql\Tables.
$nids = $database->select('node__field_site_section', 'fss')
->fields('fss', ['entity_id'])
->condition('fss.field_site_section_target_type', $domain_type)
->condition('fss.field_site_section_target_id', $domain_id)
->condition('fss.deleted', 0)
->execute()
->fetchCol();
$query->condition('nid', empty($nids) ? [0] : $nids, 'IN');
}
else {
// Legacy entity_reference (before update hook 10011).
$query->condition('field_site_section', $domain_id);
}
}
/**
* Gets child content pages for a given parent entity.
*
@@ -387,18 +421,15 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
->condition('status', 1)
->sort('title', 'ASC');
if ($parent_type === 'taxonomy_term') {
// Root pages could have an empty parent page OR explicitly point to the taxonomy term.
$or_group = $query->orConditionGroup()
->notExists('field_parent_page')
->condition('field_parent_page.target_type', 'taxonomy_term');
$query->condition($or_group);
$query->condition('field_site_section.target_id', $parent_id);
} else {
// Child pages belong to a specific parent entity (node).
$query->condition('field_parent_page.target_type', $parent_type);
if ($parent_type === 'node') {
// Child pages: field_parent_page points to this node.
$query->condition('field_parent_page.target_id', $parent_id);
}
else {
// Root pages: no parent page, but field_site_section matches the domain.
$query->notExists('field_parent_page');
$this->applyDomainCondition($query, $parent_type, $parent_id);
}
$nids = $query->execute();