Skip to content

refactor: store article and category meta cache in separate files - #6675

Merged
gharlan merged 3 commits into
6.xfrom
structure-cache-split
Sep 18, 2026
Merged

gharlan merged 3 commits into
6.xfrom
structure-cache-split

Conversation

@gharlan

@gharlan gharlan commented Sep 18, 2026

Copy link
Copy Markdown
Member

Follow-up to the category split: the structure meta cache was still one flat row per article and language, holding the article and (for a start article) the category columns of that language.

That shared row was the last place mixing both entities, and it needed three workarounds to stay readable:

  • the art_/cat_ prefixes had to act as the separator between article and category meta fields, so both constructors stripped the other entity's fields with an unset loop
  • the colliding core columns were aliased explicitly (catname, catpriority, catcreatedate, …)
  • categoryMetaColumns() picked up the remaining category columns by excluding a hardcoded list of core column names

It also saved nothing: File::getCache() has no memoization, so Article::get() and Category::get() for the same id already read the file twice — and Category::get() deserialized every art_ meta field only to discard it, once per element in category-heavy loops such as navigations.

What changed

The cache now holds one file per entity and language:

structure/<id>.<lang>.article    rex_article + rex_article_translation + startarticle
structure/<id>.<lang>.category   rex_category + rex_category_translation  (start articles only)
  • ArticleCache::generateMeta() reads both tables through a small selectRows() helper instead of one joined query with aliases; categoryMetaColumns() and the aliases are gone, as are both unset loops
  • StructureElement::get() picks the file via the new cacheFileSuffix(), so Category::fromCache() no longer needs the startarticle check — a missing file means "not a category" — and fromCache() is non-nullable now
  • parent_id, path and status belong to the start article, not to rex_category; they are copied into the category file so a category can be built from its own file alone
  • ArticleCache::deleteMeta() removes both files

Nothing about the public API changes: getValue()/hasValue() including the catname/catpriority aliases and the prefix-less meta field access behave exactly as before.

Note

Category::get() on an id that is not a category finds no file and therefore regenerates the cache on every call, because InstancePoolTrait::getInstance() guards with isset() and never remembers a null. #6674 fixes that on 5.x; once it is merged up, this is covered. In practice the case is rare — anything coming from path, .clist or categoryId is always a real category.

Testing

  • a probe over a nested structure (root category, sub category, article, two languages) comparing name, status, priority, path, parentId, startArticle, categoryId, getParentTree(), getChildren(), isOnlineIncludingParents() against the previous implementation: identical output
  • backend: structure tree, breadcrumb across three levels, renaming a category (cache regeneration), content edit page with the metadata sidebar
  • frontend: 200, no new entries in the error log
  • composer check clean, no new baseline entries

@gharlan gharlan added this to the REDAXO 6.0 milestone Sep 18, 2026
Since categories live in their own tables, the shared flat cache row is the
last place that mixes both entities. It forced the `art_`/`cat_` prefixes to
act as a separator, explicit `catname`/`catpriority`/`catcreatedate` aliases
for the colliding core columns, and a hardcoded core-column blocklist in
`categoryMetaColumns()` to pick up the remaining category columns.

The cache now holds one file per entity and language, `<id>.<lang>.article`
and `<id>.<lang>.category`, the latter only for start articles. Both are read
through the new `cacheFileSuffix()`, so `Category::fromCache()` no longer needs
the `startarticle` check: a missing file means "not a category". `parent_id`,
`path` and `status` belong to the start article and are copied into the
category file so a category can be built from its own file alone.

This also stops `Category::get()` from deserializing the full article payload
(including every `art_` meta field) only to discard it, which happens once per
element in category-heavy loops such as navigations.
@gharlan
gharlan force-pushed the structure-cache-split branch from c6041dc to 5e272ce Compare September 18, 2026 00:26
@gharlan
gharlan force-pushed the structure-cache-split branch from a5dba70 to 22f3d15 Compare September 18, 2026 00:28
@gharlan
gharlan merged commit 36b1a89 into 6.x Sep 18, 2026
16 checks passed
@gharlan
gharlan deleted the structure-cache-split branch September 18, 2026 00:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant