You add a field to an AdminCP form, load the page, and get this:
[[Template core/global/forms/checkboxset is throwing an error. This theme may be out of date. Run the support tool in the AdminCP to restore the default theme.]]
The theme is fine. Restoring it will not help. The message is what Invision Community prints when a template throws, and the template threw because of the options you passed to the field — but the message names the template, so it sends you to the support tool and the theme system instead of to the six lines you just wrote.
Why it points at the wrong place
Field construction does almost no validation. new CheckboxSet( ... ) stores your options and returns happily; the options are not read until the template renders them. So the stack you would want — "bad option in your controller" — does not exist by the time anything goes wrong. What is left is a template that could not cope with what it was handed.
Two consequences worth internalising: the error appears on page load rather than at the line you wrote, and it is worded as an infrastructure problem rather than a code one.
The usual cause: `unlimited` not matching the value
For a multi-select field with an "everything" option, three things have to agree. This is the shape core uses (see applications/core/modules/admin/applications/applications.php, the app_disabled_groups field):
$form->add( new CheckboxSet( 'my_groups', $allowed === '*' ? '*' : explode( ',', $allowed ), FALSE, array(
'options' => array_combine(
array_keys( \IPS\Member\Group::groups() ),
array_map( function ( $group ) { return (string) $group; }, \IPS\Member\Group::groups() )
),
'multiple' => TRUE,
'unlimited' => '*',
'unlimitedLang' => 'my_groups_all',
'impliedUnlimited' => TRUE,
) ) );
🚨 unlimited must be a scalar sentinel, and the field's value must be that same sentinel when everything is selected. Passing 'unlimited' => array() and a value of array() looks reasonable and produces the template error above.
🚨 options must be id => label. array_map() over Group::groups() looks like it builds that, but you lose the ids the moment the callback returns a string, so the checkboxes have nothing to submit. Hence the array_combine( array_keys( ... ), array_map( ... ) ) dance in core's own code — it is not superstition.
Then handle the sentinel on the way back out
With impliedUnlimited, the field returns the literal sentinel, not an array:
if ( $values['my_groups'] === '*' )
{
$save = '*';
}
else
{
$ids = array_filter( array_map( 'intval', (array) $values['my_groups'] ) );
$save = $ids ? implode( ',', $ids ) : '*';
}
🚨 Casting first is a real bug: (array) '*' is array('*'), which intval()s to 0 and gets filtered away, turning "everyone is allowed" into "nobody is allowed" — silently, and in the direction that hides things rather than exposing them.
Test it by rendering, not by constructing
Because construction does not validate, a test that builds the field proves nothing. Call html() and look for the marker:
$html = (string) $field->html(); $broken = str_contains( $html, 'is throwing an error' ) or str_contains( $html, 'This theme may be out of date' );
Two things make this awkward from the command line, both avoidable:
- 🚨 Do not boot
Dispatcher\Admin::i()to "set up the ACP context". From CLI it tries to redirect to the login screen and dies on a missingREQUEST_METHOD. You do not need it:Theme::getTemplate()only consults the dispatcher when the location argument is omitted, and the form templates pass one. - Some fields —
Node, and anything reachingRequest->url()— read$_SERVERdirectly. Stub the handful they want rather than building a request:$_SERVER['REQUEST_METHOD'] ??= 'GET'; $_SERVER['SERVER_NAME'] ??= 'localhost'; $_SERVER['SERVER_PORT'] ??= '80'; $_SERVER['HTTP_HOST'] ??= 'localhost'; $_SERVER['REQUEST_URI'] ??= '/admin/'; $_SERVER['SCRIPT_NAME'] ??= '/index.php'; $_SERVER['QUERY_STRING'] ??= ''; $_SERVER['HTTPS'] ??= '';
And assert that the broken version really is broken. A render check that passes whatever you feed it is worse than none, because it reads as coverage. Construct the field the wrong way on purpose, render it, and require that one to fail.
Verified against
Invision Community 5.0.19, against system/Helpers/Form/CheckboxSet.php, system/Theme/Dev/Theme.php and core's own app_disabled_groups and menu_manager_access fields.
Recommended Comments