From ff608f7833af3d1e4f4b81ac678797d129e80f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Sun, 22 Feb 2026 17:41:36 +0100 Subject: [PATCH] Fix organization listing numeric tag rendering and template mapping --- ...rganizationListingTemplateDataListener.php | 370 +++++++----------- 1 file changed, 147 insertions(+), 223 deletions(-) diff --git a/src/EventListener/OrganizationListingTemplateDataListener.php b/src/EventListener/OrganizationListingTemplateDataListener.php index 597dbba..370227b 100644 --- a/src/EventListener/OrganizationListingTemplateDataListener.php +++ b/src/EventListener/OrganizationListingTemplateDataListener.php @@ -34,11 +34,6 @@ class OrganizationListingTemplateDataListener } $rowToOrganizationIdMap = []; - $rowToTitleMap = []; - - $resolvedByRowIdCount = 0; - $resolvedByTitleToIdCount = 0; - $enrichedByTitleTagFallbackCount = 0; foreach ($tbody as $rowIndex => $row) { if (!\is_array($row)) { @@ -49,31 +44,6 @@ class OrganizationListingTemplateDataListener if ($organizationId > 0) { $rowToOrganizationIdMap[(int) $rowIndex] = $organizationId; - ++$resolvedByRowIdCount; - continue; - } - - $title = $this->extractRowFieldContent($row, 'title'); - - if ('' !== $title) { - $rowToTitleMap[(int) $rowIndex] = $this->normalizeTitle($title); - } - } - - if ([] !== $rowToTitleMap) { - $titleToOrganizationIdMap = $this->resolveOrganizationIdsByTitle(array_values(array_unique(array_filter($rowToTitleMap)))); - - foreach ($rowToTitleMap as $rowIndex => $title) { - if (isset($rowToOrganizationIdMap[$rowIndex])) { - continue; - } - - $organizationId = $titleToOrganizationIdMap[$title] ?? 0; - - if ($organizationId > 0) { - $rowToOrganizationIdMap[$rowIndex] = $organizationId; - ++$resolvedByTitleToIdCount; - } } } @@ -81,21 +51,53 @@ class OrganizationListingTemplateDataListener return; } - $organizationTagMap = $this->fetchOrganizationTagMap(array_values(array_unique(array_values($rowToOrganizationIdMap)))); - $organizationLogoUuidMap = $this->fetchOrganizationLogoUuidMap(array_values(array_unique(array_values($rowToOrganizationIdMap)))); - $organizationTagMapByTitle = $this->fetchOrganizationTagMapByTitle(array_values(array_unique(array_filter($rowToTitleMap)))); + $organizationIds = array_values(array_unique(array_values($rowToOrganizationIdMap))); + $organizationTagMap = $this->fetchOrganizationTagMap($organizationIds); + $organizationLogoUuidMap = $this->fetchOrganizationLogoUuidMap($organizationIds); + + $organizationTagIdsMap = []; + $organizationTagLabelMap = []; + $rowTagIdsMap = []; + $rowTagIdsList = []; + + foreach ($organizationTagMap as $organizationId => $tagData) { + if ([] !== ($tagData['ids'] ?? [])) { + $organizationTagIdsMap[(string) $organizationId] = implode(',', $tagData['ids']); + } + + foreach (($tagData['ids'] ?? []) as $index => $tagId) { + $label = trim((string) (($tagData['labels'][$index] ?? '') ?: '')); + + if ('' !== $tagId && '' !== $label && !isset($organizationTagLabelMap[(string) $tagId])) { + $organizationTagLabelMap[(string) $tagId] = $label; + } + } + } foreach ($rowToOrganizationIdMap as $rowIndex => $organizationId) { - $tagData = $organizationTagMap[$organizationId] ?? ['labels' => [], 'slugs' => []]; - $logoUuid = $organizationLogoUuidMap[$organizationId] ?? ''; - if (!isset($tbody[$rowIndex]) || !\is_array($tbody[$rowIndex])) { continue; } - $tbody[$rowIndex]['tag_labels']['content'] = implode(', ', $tagData['labels']); - $tbody[$rowIndex]['tag_slugs']['content'] = implode(',', $tagData['slugs']); - $tbody[$rowIndex]['tags']['content'] = implode(', ', $tagData['labels']); + $tagData = $organizationTagMap[$organizationId] ?? ['ids' => [], 'labels' => [], 'slugs' => []]; + $logoUuid = $organizationLogoUuidMap[$organizationId] ?? ''; + $tagIdsCsv = implode(',', $tagData['ids']); + $tagLabelsCsv = implode(', ', $tagData['labels']); + $tagSlugsCsv = implode(',', $tagData['slugs']); + + $tbody[$rowIndex]['tag_ids']['content'] = $tagIdsCsv; + $tbody[$rowIndex]['tag_labels']['content'] = $tagLabelsCsv; + $tbody[$rowIndex]['tag_slugs']['content'] = $tagSlugsCsv; + $tbody[$rowIndex]['tags']['content'] = $tagLabelsCsv; + + $tbody[$rowIndex]['tag_ids'] = $tagIdsCsv; + $tbody[$rowIndex]['tag_labels'] = $tagLabelsCsv; + $tbody[$rowIndex]['tag_slugs'] = $tagSlugsCsv; + $tbody[$rowIndex]['tags'] = $tagLabelsCsv; + + if ('' !== $tagIdsCsv) { + $rowTagIdsMap[(string) $rowIndex] = $tagIdsCsv; + } if ('' !== $logoUuid) { $tbody[$rowIndex]['logo_uuid']['content'] = $logoUuid; @@ -103,60 +105,40 @@ class OrganizationListingTemplateDataListener } foreach ($tbody as $rowIndex => $row) { - if (!\is_array($row) || isset($rowToOrganizationIdMap[$rowIndex])) { + if (!\is_array($row)) { + $rowTagIdsList[] = ''; continue; } - $title = $this->normalizeTitle($this->extractRowFieldContent($row, 'title')); - - if ('' === $title || !isset($organizationTagMapByTitle[$title])) { - continue; - } - - $tagData = $organizationTagMapByTitle[$title]; - - $tbody[$rowIndex]['tag_labels']['content'] = implode(', ', $tagData['labels']); - $tbody[$rowIndex]['tag_slugs']['content'] = implode(',', $tagData['slugs']); - $tbody[$rowIndex]['tags']['content'] = implode(', ', $tagData['labels']); - ++$enrichedByTitleTagFallbackCount; + $organizationId = $rowToOrganizationIdMap[(int) $rowIndex] ?? 0; + $rowTagIdsList[] = ($organizationId > 0 && isset($organizationTagMap[$organizationId])) + ? implode(',', $organizationTagMap[$organizationId]['ids'] ?? []) + : ''; } - $rowsWithoutTagSlugs = 0; - $sampleUnresolvedTitles = []; + if (null !== $this->logger) { + $rowsWithoutTagIds = 0; - foreach ($tbody as $row) { - if (!\is_array($row)) { - continue; - } - - $tagSlugs = $this->extractRowFieldContent($row, 'tag_slugs'); - - if ('' !== $tagSlugs) { - continue; - } - - ++$rowsWithoutTagSlugs; - - if (\count($sampleUnresolvedTitles) < 10) { - $title = $this->normalizeTitle($this->extractRowFieldContent($row, 'title')); - - if ('' !== $title) { - $sampleUnresolvedTitles[] = $title; + foreach ($rowToOrganizationIdMap as $rowIndex => $organizationId) { + if (!isset($organizationTagMap[$organizationId]) || [] === ($organizationTagMap[$organizationId]['ids'] ?? [])) { + ++$rowsWithoutTagIds; } } + + if ($rowsWithoutTagIds > 0) { + $this->logger->warning('Organization listing enrichment found rows without tag IDs.', [ + 'template' => (string) $template->getName(), + 'totalRows' => \count($tbody), + 'mappedRows' => \count($rowToOrganizationIdMap), + 'rowsWithoutTagIds' => $rowsWithoutTagIds, + ]); + } } - if (null !== $this->logger && $rowsWithoutTagSlugs > 0) { - $this->logger->warning('Organization listing tag enrichment left rows without tag slugs.', [ - 'template' => (string) $template->getName(), - 'totalRows' => \count($tbody), - 'resolvedByRowId' => $resolvedByRowIdCount, - 'resolvedByTitleToId' => $resolvedByTitleToIdCount, - 'enrichedByTitleTagFallback' => $enrichedByTitleTagFallbackCount, - 'rowsWithoutTagSlugs' => $rowsWithoutTagSlugs, - 'sampleUnresolvedTitles' => array_values(array_unique($sampleUnresolvedTitles)), - ]); - } + $template->organization_tag_ids_map = $organizationTagIdsMap; + $template->organization_tag_label_map = $organizationTagLabelMap; + $template->organization_row_tag_ids_map = $rowTagIdsMap; + $template->organization_row_tag_ids_list = $rowTagIdsList; $template->tbody = $tbody; } @@ -165,6 +147,10 @@ class OrganizationListingTemplateDataListener private function extractOrganizationId(array $row): int { foreach (['id', 'organization_id', 'org_id'] as $fieldName) { + if (isset($row[$fieldName]) && \is_scalar($row[$fieldName]) && ctype_digit((string) $row[$fieldName])) { + return (int) $row[$fieldName]; + } + $value = $this->extractRowFieldContent($row, $fieldName); if ('' !== $value && ctype_digit($value)) { @@ -172,27 +158,21 @@ class OrganizationListingTemplateDataListener } } - $urlCandidates = []; - foreach ($row as $column) { if (!\is_array($column)) { continue; } - if (isset($column['url']) && \is_scalar($column['url'])) { - $urlCandidates[] = (string) $column['url']; - } + foreach (['url', 'href'] as $urlField) { + if (!isset($column[$urlField]) || !\is_scalar($column[$urlField])) { + continue; + } - if (isset($column['href']) && \is_scalar($column['href'])) { - $urlCandidates[] = (string) $column['href']; - } - } + $organizationId = $this->extractIdFromUrl((string) $column[$urlField]); - foreach ($urlCandidates as $url) { - $organizationId = $this->extractIdFromUrl($url); - - if ($organizationId > 0) { - return $organizationId; + if ($organizationId > 0) { + return $organizationId; + } } } @@ -202,7 +182,15 @@ class OrganizationListingTemplateDataListener /** @param array $row */ private function extractRowFieldContent(array $row, string $fieldName): string { - if (!isset($row[$fieldName]) || !\is_array($row[$fieldName])) { + if (!isset($row[$fieldName])) { + return ''; + } + + if (\is_scalar($row[$fieldName])) { + return trim((string) $row[$fieldName]); + } + + if (!\is_array($row[$fieldName])) { return ''; } @@ -240,57 +228,8 @@ class OrganizationListingTemplateDataListener return 0; } - /** @param list $titles - * @return array - */ - private function resolveOrganizationIdsByTitle(array $titles): array - { - if ([] === $titles) { - return []; - } - - $rows = $this->connection->executeQuery( - 'SELECT o.id, o.title FROM tl_organization o WHERE o.title IN (?)', - [$titles], - [ArrayParameterType::STRING], - )->fetchAllAssociative(); - - $titleToIdsMap = []; - - foreach ($rows as $row) { - $organizationId = (int) ($row['id'] ?? 0); - $title = $this->normalizeTitle((string) ($row['title'] ?? '')); - - if ($organizationId <= 0 || '' === $title) { - continue; - } - - $titleToIdsMap[$title][] = $organizationId; - } - - $titleToOrganizationIdMap = []; - - foreach ($titleToIdsMap as $title => $organizationIds) { - $organizationIds = array_values(array_unique(array_map('intval', $organizationIds))); - - if (1 === \count($organizationIds)) { - $titleToOrganizationIdMap[$title] = $organizationIds[0]; - } - } - - return $titleToOrganizationIdMap; - } - - private function normalizeTitle(string $title): string - { - $title = html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8'); - $title = trim(strip_tags($title)); - - return preg_replace('/\s+/u', ' ', $title) ?? ''; - } - /** @param list $organizationIds - * @return array, slugs: list}> + * @return array, labels: list, slugs: list}> */ private function fetchOrganizationTagMap(array $organizationIds): array { @@ -298,33 +237,46 @@ class OrganizationListingTemplateDataListener return []; } + $scope = $this->resolveTagRelationScope($organizationIds); + + $conditions = ['r.pid IN (?)']; + $params = [$organizationIds]; + $types = [ArrayParameterType::INTEGER]; + + if ('' === $scope['ptable']) { + $conditions[] = "(r.ptable = '' OR r.ptable IS NULL)"; + } else { + $conditions[] = 'r.ptable = ?'; + $params[] = $scope['ptable']; + $types[] = ParameterType::STRING; + } + + if ('' === $scope['field']) { + $conditions[] = "(r.field = '' OR r.field IS NULL)"; + } else { + $conditions[] = 'r.field = ?'; + $params[] = $scope['field']; + $types[] = ParameterType::STRING; + } + $rows = $this->connection->executeQuery( - 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.ptable = ? AND r.field = ? AND r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', - ['tl_organization', 'tags', $organizationIds], - [ParameterType::STRING, ParameterType::STRING, ArrayParameterType::INTEGER], + 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE '.implode(' AND ', $conditions).' ORDER BY r.pid ASC, r.tag_id ASC', + $params, + $types, )->fetchAllAssociative(); - if ([] === $rows) { - $rows = $this->connection->executeQuery( - 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.ptable = ? AND r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', - ['tl_organization', $organizationIds], - [ParameterType::STRING, ArrayParameterType::INTEGER], - )->fetchAllAssociative(); - } + if ([] === $rows && '' !== $scope['field']) { + $fieldFreeConditions = array_values(array_filter($conditions, static fn (string $condition): bool => 'r.field = ?' !== $condition)); + $fieldFreeParams = $params; + $fieldFreeTypes = $types; - if ([] === $rows) { - $rows = $this->connection->executeQuery( - 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.field = ? AND r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', - ['tags', $organizationIds], - [ParameterType::STRING, ArrayParameterType::INTEGER], - )->fetchAllAssociative(); - } + array_pop($fieldFreeParams); + array_pop($fieldFreeTypes); - if ([] === $rows) { $rows = $this->connection->executeQuery( - 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', - [$organizationIds], - [ArrayParameterType::INTEGER], + 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE '.implode(' AND ', $fieldFreeConditions).' ORDER BY r.pid ASC, r.tag_id ASC', + $fieldFreeParams, + $fieldFreeTypes, )->fetchAllAssociative(); } @@ -336,15 +288,12 @@ class OrganizationListingTemplateDataListener $tagId = (int) ($row['tag_id'] ?? 0); $label = trim((string) ($row['label'] ?? '')); - if ($organizationId <= 0 || $tagId <= 0 || '' === $label) { - continue; - } - - if (isset($seen[$organizationId][$tagId])) { + if ($organizationId <= 0 || $tagId <= 0 || '' === $label || isset($seen[$organizationId][$tagId])) { continue; } $seen[$organizationId][$tagId] = true; + $map[$organizationId]['ids'][] = (string) $tagId; $map[$organizationId]['labels'][] = $label; $slug = $this->slugify($label); @@ -355,6 +304,7 @@ class OrganizationListingTemplateDataListener } foreach ($map as $organizationId => $tagData) { + $map[$organizationId]['ids'] = array_values(array_unique($tagData['ids'] ?? [])); $map[$organizationId]['labels'] = array_values(array_unique($tagData['labels'] ?? [])); $map[$organizationId]['slugs'] = array_values(array_unique($tagData['slugs'] ?? [])); } @@ -362,69 +312,43 @@ class OrganizationListingTemplateDataListener return $map; } - /** @param list $titles - * @return array, slugs: list}> + /** @param list $organizationIds + * @return array{ptable: string, field: string} */ - private function fetchOrganizationTagMapByTitle(array $titles): array + private function resolveTagRelationScope(array $organizationIds): array { - if ([] === $titles) { - return []; - } - $rows = $this->connection->executeQuery( - 'SELECT o.title, r.tag_id, t.tag AS label FROM tl_organization o INNER JOIN tl_tags_rel r ON r.pid = o.id AND r.ptable = ? INNER JOIN tl_tags t ON t.id = r.tag_id WHERE o.title IN (?) ORDER BY o.title ASC, r.tag_id ASC', - ['tl_organization', $titles], - [ParameterType::STRING, ArrayParameterType::STRING], + "SELECT COALESCE(NULLIF(TRIM(r.ptable), ''), '') AS ptable_scope, COALESCE(NULLIF(TRIM(r.field), ''), '') AS field_scope, COUNT(DISTINCT r.pid) AS pid_count, COUNT(*) AS rel_count FROM tl_tags_rel r WHERE r.pid IN (?) GROUP BY COALESCE(NULLIF(TRIM(r.ptable), ''), ''), COALESCE(NULLIF(TRIM(r.field), ''), '') ORDER BY pid_count DESC, rel_count DESC", + [$organizationIds], + [ArrayParameterType::INTEGER], )->fetchAllAssociative(); if ([] === $rows) { - $rows = $this->connection->executeQuery( - 'SELECT o.title, r.tag_id, t.tag AS label FROM tl_organization o INNER JOIN tl_tags_rel r ON r.pid = o.id AND r.field = ? INNER JOIN tl_tags t ON t.id = r.tag_id WHERE o.title IN (?) ORDER BY o.title ASC, r.tag_id ASC', - ['tags', $titles], - [ParameterType::STRING, ArrayParameterType::STRING], - )->fetchAllAssociative(); + return ['ptable' => 'tl_organization', 'field' => 'tags']; } - if ([] === $rows) { - $rows = $this->connection->executeQuery( - 'SELECT o.title, r.tag_id, t.tag AS label FROM tl_organization o INNER JOIN tl_tags_rel r ON r.pid = o.id INNER JOIN tl_tags t ON t.id = r.tag_id WHERE o.title IN (?) ORDER BY o.title ASC, r.tag_id ASC', - [$titles], - [ArrayParameterType::STRING], - )->fetchAllAssociative(); - } - - $map = []; - $seen = []; - foreach ($rows as $row) { - $title = $this->normalizeTitle((string) ($row['title'] ?? '')); - $tagId = (int) ($row['tag_id'] ?? 0); - $label = trim((string) ($row['label'] ?? '')); - - if ('' === $title || $tagId <= 0 || '' === $label) { - continue; - } - - if (isset($seen[$title][$tagId])) { - continue; - } - - $seen[$title][$tagId] = true; - $map[$title]['labels'][] = $label; - - $slug = $this->slugify($label); - - if ('' !== $slug) { - $map[$title]['slugs'][] = $slug; + if ('tl_organization' === (string) ($row['ptable_scope'] ?? '') && 'tags' === (string) ($row['field_scope'] ?? '')) { + return ['ptable' => 'tl_organization', 'field' => 'tags']; } } - foreach ($map as $title => $tagData) { - $map[$title]['labels'] = array_values(array_unique($tagData['labels'] ?? [])); - $map[$title]['slugs'] = array_values(array_unique($tagData['slugs'] ?? [])); + foreach ($rows as $row) { + if ('tl_organization' === (string) ($row['ptable_scope'] ?? '')) { + return ['ptable' => 'tl_organization', 'field' => (string) ($row['field_scope'] ?? '')]; + } } - return $map; + foreach ($rows as $row) { + if ('tags' === (string) ($row['field_scope'] ?? '')) { + return ['ptable' => (string) ($row['ptable_scope'] ?? ''), 'field' => 'tags']; + } + } + + return [ + 'ptable' => (string) ($rows[0]['ptable_scope'] ?? 'tl_organization'), + 'field' => (string) ($rows[0]['field_scope'] ?? 'tags'), + ]; } /** @param list $organizationIds