Repository: statamic/cms (PHP)

Issue:
Ternary statement in partial evaluates as true for null values
### Bug description

When using a ternary statement within a partial to check if a value is set, the ternary expression is evaluated as true even if the value is null.

### How to reproduce

1. Add a select field `select_field` with some random options.
2. Create an entry, but don't select a value from `select_field`
3. Create a partial `container.antlers.html` with the following content

```antlers
{{ select_field ? 'true' : 'false' }}
```

4. In your template add the partial like so:
```antlers
{{ partial:container :select_field="select_field" }}
```

This returns 'true' 🤯

When dumping `select_field` in the partial you'll see that this seems not correct:
```antlers
{{ select_field | dump }}
```

```
Statamic\Fields\LabeledValue {#3116 ▼ // vendor/statamic/cms/src/Modifiers/CoreModifiers.php:613
  #value: null
  #extra: array:1 [▶]
}
```



### Logs

_No response_

### Environment

```yaml
Environment
Application Name: Statamic
Laravel Version: 11.20.0
PHP Version: 8.3.9
Composer Version: 2.7.7
Environment: local
Debug Mode: ENABLED
URL: ternary.test
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: log
Queue: sync
Session: file

Statamic
Addons: 0
Sites: 1
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 5.19.0 Solo
```


### Installation

Fresh statamic/statamic site via CLI

### Additional details

_No response_


Discussion: I wish there was a magic `__toBool()` method like `__toString()`.
We can probably make this work in Antlers though. 👌 
> We can probably make this work in Antlers though. 👌

This is already magically working with null values, but not when the ternary statement is used in a partial for some reason 🤷‍♂️



Fix (patch):
diff --git a/src/Contracts/Support/Boolable.php b/src/Contracts/Support/Boolable.php
new file mode 100644
index 00000000000..3cda81ce653
--- /dev/null
+++ b/src/Contracts/Support/Boolable.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Statamic\Contracts\Support;
+
+interface Boolable
+{
+    public function toBool(): bool;
+}
diff --git a/src/Fields/ArrayableString.php b/src/Fields/ArrayableString.php
index 4a2554be17d..5997aa9284e 100644
--- a/src/Fields/ArrayableString.php
+++ b/src/Fields/ArrayableString.php
@@ -4,8 +4,9 @@
 
 use Illuminate\Contracts\Support\Arrayable;
 use JsonSerializable;
+use Statamic\Contracts\Support\Boolable;
 
-class ArrayableString implements Arrayable, JsonSerializable
+class ArrayableString implements Arrayable, Boolable, JsonSerializable
 {
     protected $value;
     protected $extra;
@@ -26,6 +27,11 @@ public function extra()
         return (array) $this->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 be8b289b5ee..050083e87ac 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 f39a8ed31fd..4784cf9a6f3 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()) {