Dieser Artikel kann konnte keine Schlagworte speichern oder alte Tags Anzeigen. Ursache vermutlich Bug in irgendeiner Migration ( 3 -> 4 -> 5 -> 6 )
Also: wie geht das in Joomla 6.0.3: Tag speichern und Anzeigen im FrontEnd ? Die Grafik zeigt die vollständige Kette – von HTTP POST bis zum Datenbankschreiben, und von HTTP GET bis zum com_tags JOIN. Leider scheint Joomla in dieser Hinsicht nicht dokumentiert zu sein - bis jetzt :)
ca. 300 kByte Quellcode analysiert: Endlich repariert - Für alle die es interessiert...
--
╔══════════════════════════════════════════════════════════════════════════════════╗ ║ JOOMLA 6 – TAG SPEICHER & ANZEIGE LOGIK (vollstaendig analysiert) ║ ║ Stand: Mai 2026 / Joomla 6.0.3 ║ ╚══════════════════════════════════════════════════════════════════════════════════╝ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ A) TAGS SPEICHERN (Backend: Artikel speichern) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HTTP POST (Artikel-Editor Formular) │ ▼ [1] ArticleController::save() │ ▼ [2] ArticleModel::save($data) │ ├─ Sonderfall: clearTagsHelper() wenn Tags nicht geändert werden sollen [3] │ └─ $table->newTags = $data['tags'] ← Tags als Array │ ▼ [4] AdminModel::save($data) [parent] │ └─ array_key_exists('tags',$data) → $table->newTags = $data['tags'] │ ▼ [5] Content::store($updateNulls) │ └─ Setzt modified, checked_out, etc. │ ▼ [6] Table::store($updateNulls) ├─ unset($this->typeAlias) ← temp. entfernt für DB-Write ├─ dispatch: onTableBeforeStore [→ B] ├─ $db->updateObject('#__content') ← eigentlicher DB-Write ├─ $this->typeAlias = $typeAlias ← wiederhergestellt └─ dispatch: onTableAfterStore [→ C] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ B) EVENT: onTableBeforeStore ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [6] Table::store() → dispatch → Application Dispatcher │ ▼ [7] Taggable::onTableBeforeStore(BeforeStoreEvent) │ ├─ Prüft: instanceof TaggableTableInterface ? │ ├─ Prüft: getTagsHelper() !== null ? │ └─ $tagsHelper->typeAlias = $table->getTypeAlias() │ ▼ [8] TagsHelper::preStoreProcess($table, $newTags) ├─ clone $table → reset() → load() ├─ Liest oldTags via getTagIds() [SQL-1] ├─ implode(',', $newTags) zum Vergleich └─ $this->tagsChanged = (oldTags leer && newTags nicht leer) || (oldTags != newTags als String) || (!$table->primaryKey) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ C) EVENT: onTableAfterStore ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [6] Table::store() → dispatch → Application Dispatcher │ ▼ [9] Taggable::onTableAfterStore(AfterStoreEvent) │ ├─ Prüft: $event['result'] === true ? │ ├─ Prüft: instanceof TaggableTableInterface ? │ └─ Prüft: getTagsHelper() !== null ? │ ▼ [10] TagsHelper::postStore($table, $newTags) │ ├─ tagsChanged=false && newTags leer → return (nichts zu tun) │ ├─ newTags leer && replace=true │ └─ deleteTagData() [SQL-3] │ └─ newTags vorhanden: │ ├─ getRowData($table) → Artikel-Felder als Array ├─ Liest field_mappings aus #__content_types [SQL-2a] ├─ Baut $ucmData['common'] aus field_mappings │ ├─ SELECT ucm_id FROM #__ucm_base [SQL-2b] │ WHERE ucm_item_id = :artikelId │ AND ucm_type_id = :typeId │ → $primaryId = ucm_id ODER null │ ├─ [11] CoreContent->load($primaryId) │ │ ┌─ PFAD 1: $primaryId = NULL (kein ucm_base Eintrag) │ │ └─ Table::load(null) │ │ keys = ['core_content_id' => null] │ │ empty = true → return TRUE ← ohne DB-Abfrage! │ │ core_content_id bleibt 0 │ │ │ └─ PFAD 2: $primaryId = 677 (ucm_base Eintrag vorhanden) │ └─ Table::load(677) │ SELECT * FROM #__ucm_content │ WHERE core_content_id = 677 │ ┌─ Zeile gefunden → return TRUE → weiter ↓ │ └─ Zeile NICHT gefunden → return FALSE │ bind() ÜBERSPRUNGEN ← BUG │ check() ÜBERSPRUNGEN │ store() ÜBERSPRUNGEN → Tags nie gespeichert! │ ├─ [12] CoreContent->bind($ucmData['common']) │ core_content_id = 0 (Pfad 1) oder geladen (Pfad 2) │ ├─ [13] CoreContent->check() │ Validiert core_title, core_alias etc. │ ├─ [14] CoreContent->store() │ ├─ core_content_id > 0 → UPDATE #__ucm_content │ └─ core_content_id = 0 → INSERT #__ucm_content │ AUTO_INCREMENT → neue core_content_id │ ucm_base wird NICHT geschrieben! │ (storeUcmBase() ist @deprecated, wird nie aufgerufen) │ ├─ $ucmId = $coreContentTable->core_content_id │ └─ [15] tagItem($ucmId, $table, $newTags) ├─ deleteTagData() [SQL-3] └─ INSERT #__contentitem_tag_map [SQL-4] core_content_id = $ucmId ← KRITISCH für com_tags JOIN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ D) TAGS ANZEIGEN (Frontend: Artikel-Detail) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HTTP GET (Artikel-URL) │ ▼ [16] Article/HtmlView::display() │ ├─ $item->tags = new TagsHelper() │ └─ getTagIds($item->id, 'com_content.article') [SQL-5] │ ▼ [17] article/default.php │ └─ LayoutHelper::render('joomla.content.tags', $item) │ ▼ [18] layouts/joomla/content/tags.php └─ Tags als Links → href zu com_tags Komponente ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ E) TAG SUCHE (Frontend: Klick auf Tag-Link) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HTTP GET (/tag/schlagwort) │ ▼ [19] Tag/HtmlView::display() │ ▼ [20] TagModel::getItems() → getListQuery() │ ▼ [21] TagsHelper::getTagItemsQuery($tagId) └─ KRITISCHER JOIN: [SQL-6] SELECT ... FROM #__contentitem_tag_map m INNER JOIN #__ucm_content c ON m.type_alias = c.core_type_alias AND m.core_content_id = c.core_content_id ← muss übereinstimmen! WHERE m.tag_id = :tagId AND c.core_state = 1 AND c.core_access IN (...) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DATENBANK-TABELLEN & ABHÄNGIGKEITEN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #__content Artikel-Daten │ ├──► #__ucm_base UCM-Basisregister │ ucm_id KEIN AUTO_INCREMENT – wird von CoreContent::store() │ ucm_item_id via storeUcmBase() gesetzt (deprecated, nie aufgerufen) │ ucm_type_id → wird in Praxis NICHT mehr befüllt! │ ucm_language_id │ ├──► #__ucm_content UCM-Inhaltsspiegel │ core_content_id AUTO_INCREMENT ✓ │ → JOIN-Schlüssel für com_tags (KRITISCH) │ → wird bei jedem Save neu eingefügt wenn │ load(null) → TRUE (kein ucm_base Eintrag) │ ├──► #__contentitem_tag_map Tag-Zuordnung │ content_item_id → Artikel-ID (für Anzeige unter Artikel) │ core_content_id → ucm_content (für com_tags Suche – KRITISCH) │ tag_id → #__tags.id │ type_alias → 'com_content.article' │ └──► #__tags Tag-Definitionen (Nested Set) lft, rgt, level → Nested Set Integrität wichtig bei Löschung! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BEKANNTE PROBLEME & FIXES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PROBLEM 1: Import-Artikel (Artikel direkt per SQL/CSV importiert) ───────────────────────────────────────────────────────────────── Ursache: Import erzeugt ucm_base Einträge aber kein ucm_content Effekt: load($primaryId) → FALSE → Tags können nicht gespeichert werden Fix: DELETE aus ucm_base wo kein ucm_content existiert → nächster Save: load(NULL) → TRUE → INSERT ucm_content ✓ DELETE FROM #__ucm_base WHERE ucm_type_id = (SELECT type_id FROM #__content_types WHERE type_alias = 'com_content.article') AND NOT EXISTS ( SELECT 1 FROM #__ucm_content uc WHERE uc.core_content_item_id = #__ucm_base.ucm_item_id AND uc.core_type_alias = 'com_content.article'); PROBLEM 2: ucm_content wächst unbegrenzt ───────────────────────────────────────── Ursache: ucm_base wird nie geschrieben (storeUcmBase deprecated) → bei jedem Save: load(NULL) → INSERT neu statt UPDATE Effekt: 1423 ucm_content Einträge für 1020 Artikel (Stand Analyse) Status: Funktioniert aber ist ineffizient – Joomla-Bug ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ FUSSNOTEN – VOLLSTÄNDIGE DATEIPFADE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [1] administrator/components/com_content/src/Controller/ArticleController.php [2] administrator/components/com_content/src/Model/ArticleModel.php [3] libraries/src/Tag/TaggableTableTrait.php → clearTagsHelper() / getTagsHelper() / setTagsHelper() [4] libraries/src/MVC/Model/AdminModel.php → save() [5] libraries/src/Table/Content.php → store() (erbt Table, implementiert TaggableTableInterface) [6] libraries/src/Table/Table.php → store() → load() ← SCHLÜSSELMETHODE: load(null) gibt TRUE zurück! → dispatch onTableBeforeStore / onTableAfterStore [7] plugins/behaviour/taggable/src/Extension/Taggable.php → onTableObjectCreate() initialisiert TagsHelper → onTableBeforeStore() Zeile 108 → onTableAfterStore() Zeile 146 → onTableBeforeDelete() löscht Tag-Zuordnungen → onTableSetNewTags() Batch-Operationen → onTableAfterReset() → onTableAfterLoad() → onBeforeBatch() [8] libraries/src/Helper/TagsHelper.php → preStoreProcess() liest oldTags, setzt tagsChanged [9] plugins/behaviour/taggable/src/Extension/Taggable.php → onTableAfterStore() [10] libraries/src/Helper/TagsHelper.php → postStore() Hauptlogik Tag-Speicherung [11] libraries/src/Table/CoreContent.php → load() PFAD 1: null → TRUE / PFAD 2: id → FALSE wenn fehlt → store() INSERT (AUTO_INCREMENT) oder UPDATE → storeUcmBase() @deprecated seit 5.4, nie aufgerufen → ucm_base leer [12] libraries/src/Table/CoreContent.php → bind() [13] libraries/src/Table/CoreContent.php → check() [14] libraries/src/Table/CoreContent.php → store() [15] libraries/src/Helper/TagsHelper.php → tagItem() INSERT #__contentitem_tag_map → deleteTagData() DELETE #__contentitem_tag_map [16] components/com_content/src/View/Article/HtmlView.php [17] components/com_content/tmpl/article/default.php [18] layouts/joomla/content/tags.php [19] components/com_tags/src/View/Tag/HtmlView.php [20] components/com_tags/src/Model/TagModel.php → getItems() / getListQuery() [21] libraries/src/Helper/TagsHelper.php → getTagItemsQuery() KRITISCHER JOIN auf ucm_content ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SQL-REFERENZEN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [SQL-1] SELECT t.id FROM #__tags t INNER JOIN #__contentitem_tag_map m ON m.tag_id = t.id WHERE m.type_alias = :alias AND m.content_item_id IN (:id) Quelle: TagsHelper::preStoreProcess() via getTagIds() [SQL-2a] SELECT ct.* FROM #__content_types ct WHERE ct.type_alias = :alias Quelle: TagsHelper::postStore() → field_mappings [SQL-2b] SELECT ucm_id FROM #__ucm_base WHERE ucm_item_id = :artikelId AND ucm_type_id = :typeId Quelle: TagsHelper::postStore() → NULL wenn kein Eintrag → load(NULL) → TRUE (PFAD 1) → ID wenn Eintrag → load(ID) → FALSE wenn ucm_content fehlt (PFAD 2) [SQL-3] DELETE FROM #__contentitem_tag_map WHERE type_alias = :alias AND content_item_id = :id Quelle: TagsHelper::deleteTagData() [SQL-4] INSERT INTO #__contentitem_tag_map (type_alias, core_content_id, content_item_id, tag_id, tag_date, type_id) Quelle: TagsHelper::tagItem() [SQL-5] SELECT t.id FROM #__tags t INNER JOIN #__contentitem_tag_map m ON m.tag_id = t.id WHERE m.type_alias = :alias AND m.content_item_id = :id Quelle: HtmlView::display() via TagsHelper::getTagIds() [SQL-6] SELECT ... FROM #__contentitem_tag_map m INNER JOIN #__ucm_content c ON m.type_alias = c.core_type_alias AND m.core_content_id = c.core_content_id WHERE m.tag_id = :tagId AND c.core_state = 1 AND c.core_access IN (...) Quelle: TagsHelper::getTagItemsQuery() aufgerufen von TagModel::getListQuery()
Lösung:
-- DELETE der kaputten Einträge
-- Löscht ucm_base Einträge für Artikel die kein ucm_content haben. -- Beim nächsten Speichern: load(NULL) → TRUE → INSERT ucm_content → funktioniert. DELETE FROM po9_ucm_base WHERE ucm_type_id = ( SELECT type_id FROM po9_content_types WHERE type_alias = 'com_content.article') AND NOT EXISTS ( SELECT 1 FROM po9_ucm_content uc WHERE uc.core_content_item_id = po9_ucm_base.ucm_item_id AND uc.core_type_alias = 'com_content.article' ); SELECT ROW_COUNT() AS 'Gelöschte ucm_base Einträge';
Lösung:
Der Schlüsselfund der alles erklärt – eine einzige Zeile in Table::load():
if ($empty) { return true; // ← ohne DB-Abfrage, ohne Fehler }
Denn...
Neuer Artikel (funktioniert): ucm_base → kein Eintrag → $primaryId = NULL load(NULL) → empty=true → return TRUE (ohne DB!) bind() → core_content_id = 0 (nicht in field_mappings) store() → INSERT mit AUTO_INCREMENT → neue ID z.B. 1486 tagItem() → contentitem_tag_map mit core_content_id = 1486 ✓
Import-Artikel (kaputt): ucm_base → NIE geschrieben (storeUcmBase() deprecated, nie aufgerufen) Import-Artikel (kaputt): ucm_base → Eintrag vorhanden → $primaryId = 677 load(677) → sucht ucm_content WHERE core_content_id = 677 → nicht vorhanden → return FALSE bind() → ÜBERSPRUNGEN store() → ÜBERSPRUNGEN tagItem() → ÜBERSPRUNGEN → Tags nie gespeichert ✗
Und warum 1423 ucm_content für 1020 Artikel: Jeder Artikel-Save → load(NULL) → INSERT neu → wächst unbegrenzt (Joomla = Bug ? Realität : Egal)
Kaputte Arikel finden:
SELECT c.id, c.title, u.ucm_id, c.state, c.language FROM po9_content c INNER JOIN po9_ucm_base u ON u.ucm_item_id = c.id AND u.ucm_type_id = ( SELECT type_id FROM po9_content_types WHERE type_alias = 'com_content.article') WHERE NOT EXISTS ( SELECT 1 FROM po9_ucm_content uc WHERE uc.core_content_item_id = c.id AND uc.core_type_alias = 'com_content.article') ORDER BY c.id;
Mehr als 0 –> das DELETE ausführen, betroffene Artikel einmal speichern, fertig.
#
## That's all Folks !
Kontext:

Kommentar hinzufügen