Short description
$templates->new($name) returns a Template whose fieldgroup is correct in memory, but the templates row it writes has fieldgroups_id = 0. On the next request the template therefore has no fieldgroup, and any code touching it fatals.
The most visible consequence is that installing the core WireTests module (which uses $templates->new()) leaves the admin page tree throwing Call to a member function hasField() on null.
Steps to reproduce
Minimal — no modules involved:
$t = $templates->new('probetemplate');
echo $t->fieldgroup->id; // e.g. 129 — correct in memory
Then read the row back:
SELECT id, name, fieldgroups_id FROM templates WHERE name = 'probetemplate';
-- id | name | fieldgroups_id
-- 75 | probetemplate | 0
The fieldgroup itself is created and saved correctly — it simply is not referenced by the template. A second $t->save() immediately afterwards does not correct the stored value.
Expected: fieldgroups_id holds the id of the fieldgroup that new() created.
Actual: fieldgroups_id is 0.
Real-world trigger, and why it is easy to miss
WireTests::install() creates its template through $templates->new(), so:
$modules->install('WireTests');
SELECT fieldgroups_id FROM templates WHERE name = 'wire-test'; -- 0
Deterministic: 3 out of 3 clean uninstall/install cycles produced 0.
From then on, on any subsequent request:
wire/core/Pages/PagesLoader.php:1293 — $fields = $template->fieldgroup; → null
wire/core/Pages/PagesLoader.php:1321 — foreach($fields as $field) →
PHP Warning: foreach() argument must be of type array|object, null given
wire/core/Page/PageValues.php:847 — $result = $template->fieldgroup->hasField($field); →
Error: Call to a member function hasField() on null
ProcessPageListRender::getPageLabelIconMarkup() calls Page::getIcon() → hasField() for every row, so the admin page tree fatals and the admin becomes unusable. $modules->install() reports success and isInstalled() returns true throughout.
What makes this hard to spot is that WireTests repairs it by accident. getTestTemplate() has an explicit self-heal branch:
} else if(!$template->fieldgroup) {
$template->save();
}
so any later invocation of WireTests silently fixes the row. install() itself creates the template indirectly via getTestPage() and then only reads it back with getTestTemplate(false), which skips that branch. On one of our sites the next command we happened to run was a real test, which repaired it and left only a stray PHP warning in the log; on another site nothing invoked WireTests afterwards, and the admin stayed broken.
Note on the code path
Templates::___new() looks correct at a glance — it saves the fieldgroup before saving the
template, so the fieldgroup does have an id by then:
public function ___new($name, $settings = []) {
$template = $this->newTemplate($name, $settings);
$template->fieldgroup->save();
$this->save($template);
return $template;
}
Yet the stored fieldgroups_id is still 0, and the in-memory object is fine — which suggests the value written by Templates::save() comes from a fieldgroups_id property that was never updated when the fieldgroup was assigned, rather than from $template->fieldgroup. I have not chased it further than that, so please treat the last sentence as a hint rather than a diagnosis.
Workaround
$t = $templates->get($name);
$fg = $fieldgroups->get($name);
if ($fg) $t->setFieldgroup($fg);
$t->save();
A site-wide check for the damaged state:
foreach ($templates as $t) {
if (!$t->fieldgroup) echo "broken: $t->name\n";
}
Environment
|
|
| ProcessWire |
3.0.268 and 3.0.269 (both reproduce) |
| PHP |
8.4.10 |
| MySQL |
5.7.44 |
| OS |
macOS |
Short description
$templates->new($name)returns a Template whosefieldgroupis correct in memory, but thetemplatesrow it writes hasfieldgroups_id = 0. On the next request the template therefore has no fieldgroup, and any code touching it fatals.The most visible consequence is that installing the core WireTests module (which uses
$templates->new()) leaves the admin page tree throwingCall to a member function hasField() on null.Steps to reproduce
Minimal — no modules involved:
Then read the row back:
The fieldgroup itself is created and saved correctly — it simply is not referenced by the template. A second
$t->save()immediately afterwards does not correct the stored value.Expected:
fieldgroups_idholds the id of the fieldgroup thatnew()created.Actual:
fieldgroups_idis0.Real-world trigger, and why it is easy to miss
WireTests::install()creates its template through$templates->new(), so:Deterministic: 3 out of 3 clean uninstall/install cycles produced
0.From then on, on any subsequent request:
wire/core/Pages/PagesLoader.php:1293—$fields = $template->fieldgroup;→nullwire/core/Pages/PagesLoader.php:1321—foreach($fields as $field)→PHP Warning: foreach() argument must be of type array|object, null givenwire/core/Page/PageValues.php:847—$result = $template->fieldgroup->hasField($field);→Error: Call to a member function hasField() on nullProcessPageListRender::getPageLabelIconMarkup()callsPage::getIcon()→hasField()for every row, so the admin page tree fatals and the admin becomes unusable.$modules->install()reports success andisInstalled()returnstruethroughout.What makes this hard to spot is that WireTests repairs it by accident.
getTestTemplate()has an explicit self-heal branch:so any later invocation of WireTests silently fixes the row.
install()itself creates the template indirectly viagetTestPage()and then only reads it back withgetTestTemplate(false), which skips that branch. On one of our sites the next command we happened to run was a real test, which repaired it and left only a stray PHP warning in the log; on another site nothing invoked WireTests afterwards, and the admin stayed broken.Note on the code path
Templates::___new()looks correct at a glance — it saves the fieldgroup before saving thetemplate, so the fieldgroup does have an id by then:
Yet the stored
fieldgroups_idis still0, and the in-memory object is fine — which suggests the value written byTemplates::save()comes from afieldgroups_idproperty that was never updated when the fieldgroup was assigned, rather than from$template->fieldgroup. I have not chased it further than that, so please treat the last sentence as a hint rather than a diagnosis.Workaround
A site-wide check for the damaged state:
Environment