diff --git a/src/Contracts/Support/Boolable.php b/src/Contracts/Support/Boolable.php new file mode 100644 index 0000000000..3cda81ce65 --- /dev/null +++ b/src/Contracts/Support/Boolable.php @@ -0,0 +1,8 @@ +extra; } + public function toBool(): bool + { + return (bool) $this->value; + } + public function __toString() { return (string) $this->value ?? ''; diff --git a/src/View/Antlers/Language/Runtime/PathDataManager.php b/src/View/Antlers/Language/Runtime/PathDataManager.php index be8b289b5e..050083e87a 100644 --- a/src/View/Antlers/Language/Runtime/PathDataManager.php +++ b/src/View/Antlers/Language/Runtime/PathDataManager.php @@ -10,6 +10,7 @@ use Illuminate\Support\Str; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Query\Builder; +use Statamic\Contracts\Support\Boolable; use Statamic\Contracts\View\Antlers\Parser; use Statamic\Fields\ArrayableString; use Statamic\Fields\Value; @@ -89,6 +90,8 @@ class PathDataManager */ private $reduceFinal = true; + private $isReturningForConditions = false; + /** * @var Parser|null */ @@ -165,6 +168,17 @@ private function unlockData() } } + /** + * Reset state for things that should not persist + * across repeated calls to getData and friends. + * + * @return void + */ + private function resetInternalState() + { + $this->isReturningForConditions = false; + } + /** * Sets the internal environment reference. * @@ -300,6 +314,13 @@ public function setReduceFinal($reduceFinal) return $this; } + public function setIsReturningForConditions($isCondition) + { + $this->isReturningForConditions = $isCondition; + + return $this; + } + public function getReduceFinal() { return $this->reduceFinal; @@ -523,6 +544,8 @@ public function getData(VariableReference $path, $data, $isForArrayIndex = false $path = $tempPathParser->parse($dynamicPath); if (! $this->guardRuntimeAccess($path->normalizedReference)) { + $this->resetInternalState(); + return null; } } @@ -555,6 +578,8 @@ public function getData(VariableReference $path, $data, $isForArrayIndex = false } if ($pathItem->name == 'void' && count($path->pathParts) == 1) { + $this->resetInternalState(); + return 'void::'.GlobalRuntimeState::$environmentId; } @@ -750,6 +775,7 @@ public function getData(VariableReference $path, $data, $isForArrayIndex = false } $this->namedSlotsInScope = false; + $this->resetInternalState(); return $this->reducedVar; } @@ -889,6 +915,12 @@ private function compact($isFinal) return; } + if ($this->isReturningForConditions && $isFinal && $this->reducedVar instanceof Boolable) { + $this->reducedVar = $this->reducedVar->toBool(); + + return; + } + if ($this->antlersParser == null) { $this->reducedVar = self::reduce($this->reducedVar, $this->isPair, $this->shouldDoValueIntercept); } else { diff --git a/src/View/Antlers/Language/Runtime/Sandbox/Environment.php b/src/View/Antlers/Language/Runtime/Sandbox/Environment.php index f39a8ed31f..4784cf9a6f 100644 --- a/src/View/Antlers/Language/Runtime/Sandbox/Environment.php +++ b/src/View/Antlers/Language/Runtime/Sandbox/Environment.php @@ -9,6 +9,7 @@ use Illuminate\Support\MessageBag; use Illuminate\Support\ViewErrorBag; use Statamic\Contracts\Query\Builder; +use Statamic\Contracts\Support\Boolable; use Statamic\Contracts\View\Antlers\Parser; use Statamic\Fields\ArrayableString; use Statamic\Fields\Value; @@ -353,8 +354,8 @@ public function evaluateBool($nodes) $this->isEvaluatingTruthValue = false; if (is_object($result)) { - if ($result instanceof ArrayableString) { - $value = $this->getTruthValue($result->value()); + if ($result instanceof Boolable) { + $value = $this->getTruthValue($result->toBool()); $this->unlock(); return $value; @@ -1255,6 +1256,8 @@ private function scopeValue($name, $originalNode = null) if ($name instanceof VariableReference) { if (! $this->isEvaluatingTruthValue) { $this->dataRetriever->setReduceFinal(false); + } else { + $this->dataRetriever->setIsReturningForConditions(true); } if ($originalNode != null && $originalNode->hasModifiers()) { diff --git a/tests/Antlers/Runtime/TemplateTest.php b/tests/Antlers/Runtime/TemplateTest.php index 928d2b3725..5d0b2db1f1 100644 --- a/tests/Antlers/Runtime/TemplateTest.php +++ b/tests/Antlers/Runtime/TemplateTest.php @@ -12,12 +12,14 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Query\Builder; +use Statamic\Contracts\Support\Boolable; use Statamic\Data\HasAugmentedData; use Statamic\Facades\Entry; use Statamic\Fields\ArrayableString; use Statamic\Fields\Blueprint; use Statamic\Fields\Field; use Statamic\Fields\Fieldtype; +use Statamic\Fields\LabeledValue; use Statamic\Fields\Value; use Statamic\Fields\Values; use Statamic\Tags\Tags; @@ -464,6 +466,55 @@ public function ternary_condition_inside_parameter() )); } + #[Test] + #[DataProvider('boolablesInTernaryProvider')] + public function ternary_condition_with_boolables_supplied_to_tags_resolve_correctly($value, $expected) + { + $this->withFakeViews(); + + $this->viewShouldReturnRaw('test', "{{ the_field ? 'true' : 'false' }}"); + + $template = <<<'EOT' +view: {{ the_field ? 'true' : 'false' }}, partial: {{ partial:test :the_field="the_field" }} +EOT; + + $this->assertSame($expected, $this->renderString($template, ['the_field' => $value], true)); + } + + public static function boolablesInTernaryProvider() + { + return [ + 'truthy generic boolable' => [ + new class implements Boolable + { + public function toBool(): bool + { + return true; + } + }, + 'view: true, partial: true', + ], + 'falsey generic boolable' => [ + new class implements Boolable + { + public function toBool(): bool + { + return false; + } + }, + 'view: false, partial: false', + ], + 'truthy LabeledValue' => [ + new LabeledValue('foo', 'Foo'), + 'view: true, partial: true', + ], + 'falsey LabeledValue' => [ + new LabeledValue(null, null), + 'view: false, partial: false', + ], + ]; + } + #[Test] public function null_coalescence() { diff --git a/tests/Fields/ArrayableStringTest.php b/tests/Fields/ArrayableStringTest.php index 4fdbcadeed..2207a7eb92 100644 --- a/tests/Fields/ArrayableStringTest.php +++ b/tests/Fields/ArrayableStringTest.php @@ -46,4 +46,13 @@ public function it_converts_to_json() $this->assertSame(json_encode($val->toArray()), json_encode($val)); } + + #[Test] + public function it_converts_to_bool() + { + $this->assertTrue((new ArrayableString('world'))->toBool()); + $this->assertFalse((new ArrayableString(null))->toBool()); + $this->assertTrue((new ArrayableString(4))->toBool()); + $this->assertFalse((new ArrayableString(''))->toBool()); + } }