Skip to content

$templates->new() stores fieldgroups_id = 0, leaving the template without a fieldgroup #2299

Description

@szabeszg

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:1321foreach($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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions