Skip to content

Repeater item with multi-language title turns output formatting back on for its owner page (3.0.272+) #2352

Description

@csaggo

Short description

Since Languages::getPageTemplate() was added (@since 3.0.272), turning a multi-language value on a repeater item into a string can switch $page->of() back on for the owner page. That page's next save() then fails with:

Can’t save page (id=123): /path/: Call $page->of(false); before getting/setting values that will be modified and saved. [some_field]

The code did call $page->of(false) before the set() calls.

Steps to reproduce

Setup: LanguageSupport is installed. A template has a repeater field, and the repeater template contains title (FieldtypePageTitleLanguage). Run this in a template file, or bootstrapped after $pages->of(true):

$order = $pages->get('template=order, items.count>0'); // owner page, now in the page cache
$items = $order->items;          // repeater loaded while output formatting is on
$order->of(false);

$items->first()->title;          // string conversion of LanguagesPageFieldValue

var_dump($order->of());          // bool(true)  <-- expected false
$order->set('some_field', 1);
$order->save();                  // WireException: Call $page->of(false); ... [some_field]

Expected behavior

Reading a value from a repeater item does not change the output formatting state of the owner page.

Actual behavior

$order->of() is true again after the item's title is read, so save() throws.

Cause (call chain)

  1. LanguagesPageFieldValue::___getStringValue() calls $languages->getPageTemplate($this->page). This happens on every string conversion, also in the default language.
  2. Languages::getPageTemplate() calls RepeaterPage::getForPageRoot() for repeater items, which leads to getForRoot() and getForPage().
  3. RepeaterPage::getForPage() has no $forPage yet, because items loaded by FieldtypeRepeater::wakeupValue() are never given one. It therefore calls $this->wire()->pages->get($forID).
  4. That returns the same cached instance of the owner page through PagesLoaderCache::getCache(), which runs $page->of($this->pages->loader()->getOutputFormatting()). In template context that value is true, so the owner page's output formatting is switched back on.

Why it looks intermittent

It only happens while the owner page is still in the page cache. If something in between calls uncacheAll(), for example saving any other page, then getForPage() gets a fresh copy of the owner and the original $order object stays unchanged. In our case, orders that saved a linked user in between worked, and guest orders failed.

Real-world impact

Our export script creates an order in an external system (ERP) and then saves the external ID on the local page. The save fails, so the page is never marked as done. Every cron run then creates the same order in the ERP again.

Environment

  • ProcessWire 3.0.273 (dev); getPageTemplate() is marked @since 3.0.272
  • LanguageSupport, LanguageSupportFields, LanguageSupportPageNames installed
  • PHP 8.5 (local DDEV repro), also seen on production

Workaround

Save with setAndSave(), which sets of(false) right before saving:

$order->setAndSave(['plenty_id' => $id, 'plenty_status' => 'abgeschlossen']);

Possible fixes

  • FieldtypeRepeater::wakeupValue() could call setForPage($page) on the loaded items. Then getForPage() never needs to go through $pages->get().
  • Or RepeaterPage::getForPage() could look up the owner without the of() change that PagesLoaderCache::getCache() makes.
  • Or Languages::getPageTemplate() could find the owner's template without loading the owner page, for example from the for-page-123 parent name.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions