Skip to content

[6.x] Short-circuit a Value wrapping null before it reaches a modifier - #15540

Merged
jasonvarga merged 1 commit into
statamic:6.xfrom
joshdaugherty:fix/modifier-null-value-short-circuit
Sep 29, 2026
Merged

jasonvarga merged 1 commit into
statamic:6.xfrom
joshdaugherty:fix/modifier-null-value-short-circuit

Conversation

@joshdaugherty

Copy link
Copy Markdown
Contributor

Fixes #15539

ModifierManager::evaluate() checked $returnValue === null before unwrapping a Value, so a Value wrapping null passed the check and reached the modifier as a bare null. A bare null never gets that far, because evaluate() returns before the modifier loop. This swaps the two blocks so the unwrap happens first and a wrapped null stops the chain the same way.

Empty optional fields in replicator rows arrive as Value(null), because Replicator::performAugmentation() fills in every field in the set's blueprint, so this path is common. Measured on 6.x by rendering each registered modifier with no parameters. There are 240 handles, and 235 were swept, leaving out dd, ddd, dump, debug and ray. Before the change, a wrapped null:

  • emitted an E_DEPRECATED in 56 modifiers (for example sanitize, strip_tags, nl2br, md5);
  • emitted a warning or notice in 20;
  • threw in 43 (for example count, explode, limit, is_after). These are mostly TypeErrors, which Modify::modify() doesn't turn into a ModifierException, so they aren't logged away when debug is off;
  • rendered output in 32 where a bare null renders nothing.

After the change, every one of the 235 renders the same for Value(null) as for null.

Behavior change

Those 32 modifiers no longer produce output for a wrapped null. They now return what they already returned for a bare null:

bool_string cdata ceil console_log contains_all doesnt_overlap ends_with favicon floor format_number image is_alpha is_alphanumeric is_blank is_lowercase is_uppercase json length link mailto md5 neatify read_time round shrug starts_with sum table to_json type_of word_count years_ago

For example, {{ empty_field | md5 }} rendered d41d8cd98f00b204e9800998ecf8427e and {{ empty_field | is_blank }} rendered 1 when the field was a Value(null), but nothing when it was null. So this removes an inconsistency rather than adding one.

Tests

Adds test_a_value_wrapping_null_short_circuits_modifiers_like_a_bare_null to tests/Antlers/Runtime/CoreModifiersTest.php. It renders sanitize, count, md5, is_blank and a chain (upper | md5) on both a bare null and a Value(null) and expects empty output from each. On 6.x without the change it fails with count(): Argument #1 ($value) must be of type Countable|array, null given; with it, it passes. tests/Antlers (961 tests) and tests/Modifiers (571) pass. In the full suite on Windows, the only failures are ComposerLockBackupTest (2) and TemplateFolderTest (symlink(): Permission denied), and those fail the same way on 6.x without this change.

@jasonvarga
jasonvarga merged commit 6dd4001 into statamic:6.x Sep 29, 2026
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A Value wrapping null gets past the modifier chain's null check and reaches modifiers as a bare null

2 participants