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)
LanguagesPageFieldValue::___getStringValue() calls $languages->getPageTemplate($this->page). This happens on every string conversion, also in the default language.
Languages::getPageTemplate() calls RepeaterPage::getForPageRoot() for repeater items, which leads to getForRoot() and getForPage().
RepeaterPage::getForPage() has no $forPage yet, because items loaded by FieldtypeRepeater::wakeupValue() are never given one. It therefore calls $this->wire()->pages->get($forID).
- 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.
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 nextsave()then fails with:The code did call
$page->of(false)before theset()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):Expected behavior
Reading a value from a repeater item does not change the output formatting state of the owner page.
Actual behavior
$order->of()istrueagain after the item's title is read, sosave()throws.Cause (call chain)
LanguagesPageFieldValue::___getStringValue()calls$languages->getPageTemplate($this->page). This happens on every string conversion, also in the default language.Languages::getPageTemplate()callsRepeaterPage::getForPageRoot()for repeater items, which leads togetForRoot()andgetForPage().RepeaterPage::getForPage()has no$forPageyet, because items loaded byFieldtypeRepeater::wakeupValue()are never given one. It therefore calls$this->wire()->pages->get($forID).PagesLoaderCache::getCache(), which runs$page->of($this->pages->loader()->getOutputFormatting()). In template context that value istrue, 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, thengetForPage()gets a fresh copy of the owner and the original$orderobject 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
getPageTemplate()is marked@since 3.0.272Workaround
Save with
setAndSave(), which setsof(false)right before saving:Possible fixes
FieldtypeRepeater::wakeupValue()could callsetForPage($page)on the loaded items. ThengetForPage()never needs to go through$pages->get().RepeaterPage::getForPage()could look up the owner without theof()change thatPagesLoaderCache::getCache()makes.Languages::getPageTemplate()could find the owner's template without loading the owner page, for example from thefor-page-123parent name.