Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Modifiers/AcceptsNull.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Statamic\Modifiers;

use Attribute;

/**
* Runs the modifier for null values, which Antlers skips by default.
*/
#[Attribute(Attribute::TARGET_METHOD)]
class AcceptsNull
{
}
3 changes: 3 additions & 0 deletions src/Modifiers/CoreModifiers.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ public function bardHtml($value)
return (new Augmentor(new Bard()))->augment($items);
}

#[AcceptsNull]
public function boolString($value)
{
if ($value == true) {
Expand Down Expand Up @@ -1296,6 +1297,7 @@ public function isBetween($value, $params, $context)
*
* @return bool
*/
#[AcceptsNull]
public function isBlank($value)
{
return Stringy::isBlank($value);
Expand Down Expand Up @@ -2802,6 +2804,7 @@ public function toBool($value, $params)
*
* @return string
*/
#[AcceptsNull]
public function toJson($value, $params)
{
$options = 0;
Expand Down
30 changes: 26 additions & 4 deletions src/Modifiers/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Statamic\Modifiers;

use ReflectionMethod;
use Statamic\Support\Str;
use Statamic\View\State\StateManager;

class Loader
{
protected static $acceptsNull = [];

/**
* Loads a modifier.
*
Expand All @@ -20,12 +23,31 @@ public function load($name)
throw new ModifierNotFoundException($name);
}

if (Str::contains($class = $modifiers->get($key), 'CoreModifiers@')) {
[$class, $method] = explode('@', $class);
}
[$class, $method] = $this->resolve($modifiers->get($key));

StateManager::track($class);

return [app($class), $method ?? 'index'];
return [app($class), $method];
}

public function acceptsNull($name)
{
if (! $binding = app('statamic.modifiers')->get(Str::snake($name))) {
return false;
}

[$class, $method] = $this->resolve($binding);

return static::$acceptsNull[$binding] ??= method_exists($class, $method)
&& (new ReflectionMethod($class, $method))->getAttributes(AcceptsNull::class);
}

protected function resolve($binding)
{
if (Str::contains($binding, 'CoreModifiers@')) {
return explode('@', $binding);
}

return [$binding, 'index'];
}
}
14 changes: 3 additions & 11 deletions src/View/Antlers/Language/Runtime/ModifierManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Facades\Log;
use Statamic\Fields\Value;
use Statamic\Modifiers\Loader;
use Statamic\Modifiers\ModifierException;
use Statamic\Modifiers\Modify;
use Statamic\Support\Str;
Expand All @@ -20,15 +21,6 @@ class ModifierManager
public static $lastModifierName = null;
const METHOD_MODIFIER = '{__method_args}';

/**
* Modifiers that should still run when the value is null.
*
* @var string[]
*/
protected static $nullAwareModifiers = [
'bool_string',
];

public static function isModifier(ParameterNode $node)
{
if (self::$statamicModifiers == null) {
Expand Down Expand Up @@ -102,9 +94,9 @@ public static function guardRuntimeModifier($modifierName)
return true;
}

protected static function acceptsNull($modifierName)
public static function acceptsNull($modifierName)
{
return in_array($modifierName, self::$nullAwareModifiers, true);
return app(Loader::class)->acceptsNull($modifierName);
}

public static function evaluate($value, Environment $env, ModifierChainNode $modifierChain, $context)
Expand Down
4 changes: 4 additions & 0 deletions src/View/Antlers/Language/Runtime/NodeProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2821,6 +2821,10 @@ protected function runModifier($modifier, $parameters, $data, $context = [])
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
}

if ($value === null && ! ModifierManager::acceptsNull($modifier)) {
return null;
}

try {
return Modify::value($value)->context($context)->$modifier($parameters)->fetch();
} catch (ModifierException $e) {
Expand Down
32 changes: 31 additions & 1 deletion tests/Antlers/Runtime/CoreModifiersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Statamic\Fields\Fieldtype;
use Statamic\Fields\Value;
use Statamic\Fieldtypes\Text;
use Statamic\Modifiers\AcceptsNull;
use Statamic\Modifiers\Modifier;
use Tests\Antlers\ParserTestCase;

class CoreModifiersTest extends ParserTestCase
Expand Down Expand Up @@ -599,7 +601,7 @@ public function test_a_value_wrapping_null_short_circuits_modifiers_like_a_bare_
{
$data = ['wrapped' => new Value(null), 'bare' => null];

foreach (['sanitize', 'count', 'md5', 'is_blank', 'upper | md5'] as $modifiers) {
foreach (['sanitize', 'count', 'md5', 'is_alpha', 'upper | md5'] as $modifiers) {
$this->assertSame('', $this->renderString("{{ bare | {$modifiers} }}", $data, true), $modifiers);
$this->assertSame('', $this->renderString("{{ wrapped | {$modifiers} }}", $data, true), $modifiers);
}
Expand All @@ -623,6 +625,34 @@ public function test_bool_string_outputs_false_for_null()
$this->assertSame('FALSE', $this->renderString('{{ empty_field | bool_string | upper }}', $data, true));
$this->assertSame('true', $this->renderString('{{ filled_field | bool_string }}', $data, true));
}

public function test_core_modifiers_that_accept_null_run_for_empty_fields()
{
$data = ['empty_field' => new Value(null, 'empty_field', new Text)];

$this->assertSame('null', $this->renderString('{{ empty_field | to_json }}', $data, true));
$this->assertSame('null', $this->renderString('{{ empty_field | json }}', $data, true));
$this->assertSame('1', $this->renderString('{{ empty_field | is_blank }}', $data, true));
}

public function test_modifiers_marked_with_accepts_null_run_for_null()
{
(new class extends Modifier
{
protected static $handle = 'accepts_null';

#[AcceptsNull]
public function index($value)
{
return var_export($value, true);
}
})::register();

$data = ['bare' => null, 'wrapped' => new Value(null)];

$this->assertSame('NULL', $this->renderString('{{ bare | accepts_null }}', $data, true));
$this->assertSame('NULL', $this->renderString('{{ wrapped | accepts_null }}', $data, true));
}
}

class SimpleEntryObject implements Arrayable
Expand Down
27 changes: 27 additions & 0 deletions tests/Antlers/Runtime/ParameterStyleModifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

use Facades\Tests\Factories\EntryFactory;
use Statamic\Entries\Collection;
use Statamic\Fields\Value;
use Statamic\Fieldtypes\Text;
use Statamic\View\Antlers\Language\Exceptions\RuntimeException;
use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState;
use Statamic\View\Antlers\Language\Utilities\StringUtilities;
use Tests\Antlers\Fixtures\Addon\Modifiers\VarTestModifier;
use Tests\Antlers\ParserTestCase;
Expand Down Expand Up @@ -69,4 +73,27 @@ public function test_modifier_style_parameters_applies_brace_escape_sequences()

$this->assertSame(['test{}', 'param_two{}'], VarTestModifier::$params);
}

public function test_parameter_style_modifiers_skip_null_like_chained_modifiers()
{
$data = ['empty_field' => new Value(null, 'empty_field', new Text)];

$this->assertSame('', $this->renderString('{{ empty_field count="true" }}', $data, true));
$this->assertSame('null', $this->renderString('{{ empty_field to_json="true" }}', $data, true));
}

public function test_parameter_style_modifiers_check_access_before_skipping_null()
{
$banned = GlobalRuntimeState::$bannedModifierPaths;
GlobalRuntimeState::$bannedModifierPaths = ['upper'];
GlobalRuntimeState::$throwErrorOnAccessViolation = true;

$this->expectException(RuntimeException::class);

try {
$this->renderString('{{ empty_field upper="true" }}', ['empty_field' => new Value(null, 'empty_field', new Text)], true);
} finally {
GlobalRuntimeState::$bannedModifierPaths = $banned;
}
}
}
Loading