Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: when() & whenNot() evaluate empty array to true
### PHP Version

8.3

### CodeIgniter4 Version

4.6.1

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Linux

### Which server did you use?

apache

### Database

_No response_

### What happened?

If condition for function when() or whenNot() in [ConditionalTrait](https://github.com/codeigniter4/CodeIgniter4/blob/9d032efb2586601679c4da6947615769ee268109/system/Traits/ConditionalTrait.php) is empty array the function evaluate it to true (as opposed to PHP which evaluates it to false).

### Steps to Reproduce

when([], $callback)

### Expected Output

Callback not called.

### Anything else?

Same thing is happening with "0", 0 and 0.0, if this behaviour is intentional I believe it should be mentioned in description and user guide. 


Discussion:
Yes, it seems like a bug since the original code in the trait contained a condition with `if ($condition)` for the `when()` method and `if (! $condition)` for the `notWhen()` method.
Yes, I think that's a bug. But come to think of it, I think it should not account for all possible truthy/falsey values. After all, it is a condition so at the very least I'm expecting the type to be a boolean or maybe a callable returning a boolean.
The workaround to this, or perhaps you may put it as a proper fix is to convert the condition to a proper boolean: like `$cond !== []`
Values like `[]`, `0`, `"0"`, `0.0`, and `null` are treated as falsy in native PHP conditionals. Changing this behavior introduces inconsistency and may break user expectations.

IMO, the refactor (done to satisfy a PHPStan rule) introduced a behavioral breaking change.

The user guide states that (https://codeigniter.com/user_guide/database/query_builder.html#builder-when):

> The first parameter is the condition, and it should evaluate to a boolean.

For me, the user guide implies PHP truthiness, so the current logic violates our documentation.

I would go with:

```php
if (! in_array($condition, [false, 0, 0.0, '', '0', [], null], true)) {
```
At risk of making fool of myself but why not just explicitly convert it to bool?
That’s not a bad suggestion, and I would be in favor - if only it wouldn’t bring us back to square one, which is PHPStan complaining.
I think @ECode16 is suggesting to cast to bool instead of the in_array check.
@paulbalandan would that be okay with PHPStan? Because, like I said (probably poorly) - I like the idea. I just had an impression that PHPStan would not like that. I'll be able to check it out in a few hours.
I think yes, what I remember is PHPStan complains when the expression in the if condition is not a proper boolean.
I've confirmed that casting to boolean doesn’t cause any issues with PHPStan. Thank you @ECode16 for the suggestion.
Happy to help.



Patch:
diff --git a/system/Traits/ConditionalTrait.php b/system/Traits/ConditionalTrait.php
index a778e9375a36..8f9d4a91f3a8 100644
--- a/system/Traits/ConditionalTrait.php
+++ b/system/Traits/ConditionalTrait.php
@@ -29,7 +29,7 @@ trait ConditionalTrait
      */
     public function when($condition, callable $callback, ?callable $defaultCallback = null): self
     {
-        if ($condition !== '' && $condition !== false && $condition !== null) {
+        if ((bool) $condition) {
             $callback($this, $condition);
         } elseif ($defaultCallback !== null) {
             $defaultCallback($this);
@@ -52,7 +52,7 @@ public function when($condition, callable $callback, ?callable $defaultCallback
      */
     public function whenNot($condition, callable $callback, ?callable $defaultCallback = null): self
     {
-        if ($condition === '' || $condition === null || $condition === false || $condition === '0') {
+        if (! (bool) $condition) {
             $callback($this, $condition);
         } elseif ($defaultCallback !== null) {
             $defaultCallback($this);
diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst
index 089fb01e1514..8be89ae72b32 100644
--- a/user_guide_src/source/changelogs/v4.6.2.rst
+++ b/user_guide_src/source/changelogs/v4.6.2.rst
@@ -35,6 +35,7 @@ Deprecations
 Bugs Fixed
 **********
 
+- **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness.
 - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests.
 
 See the repo's
diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst
index 31515768d530..e901869fb5f7 100644
--- a/user_guide_src/source/database/query_builder.rst
+++ b/user_guide_src/source/database/query_builder.rst
@@ -1272,9 +1272,10 @@ $builder->when()
 .. versionadded:: 4.3.0
 
 This allows modifying the query based on a condition without breaking out of the
-query builder chain. The first parameter is the condition, and it should evaluate
-to a boolean. The second parameter is a callable that will be ran
-when the condition is true.
+query builder chain. The first parameter is the condition, and it is evaluated
+using PHP's native boolean logic - meaning that values like ``false``, ``null``,
+``0``, ``'0'``, ``0.0``, empty string ``''`` and empty array ``[]`` will be considered false.
+The second parameter is a callable that will be ran when the condition is true.
 
 For example, you might only want to apply a given WHERE statement based on the
 value sent within an HTTP request:


Test patch:
diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php
index a7fede7879d3..87f9b3ff233d 100644
--- a/tests/system/Database/Builder/WhenTest.php
+++ b/tests/system/Database/Builder/WhenTest.php
@@ -15,7 +15,9 @@
 
 use CodeIgniter\Test\CIUnitTestCase;
 use CodeIgniter\Test\Mock\MockConnection;
+use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Group;
+use stdClass;
 
 /**
  * @internal
@@ -101,6 +103,23 @@ public function testWhenPassesParemeters(): void
         $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
     }
 
+    #[DataProvider('provideConditionValues')]
+    public function testWhenRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void
+    {
+        $builder = $this->db->table('jobs');
+
+        $builder = $builder->when($condition, static function ($query): void {
+            $query->select('id');
+        }, static function ($query): void {
+            $query->select('name');
+        });
+
+        $expected    = $expectDefault ? 'name' : 'id';
+        $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"';
+
+        $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
+    }
+
     public function testWhenNotFalse(): void
     {
         $builder = $this->db->table('jobs');
@@ -166,4 +185,43 @@ public function testWhenNotPassesParemeters(): void
         $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = \'0\'';
         $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
     }
+
+    #[DataProvider('provideConditionValues')]
+    public function testWhenNotRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void
+    {
+        $builder = $this->db->table('jobs');
+
+        $builder = $builder->whenNot($condition, static function ($query): void {
+            $query->select('id');
+        }, static function ($query): void {
+            $query->select('name');
+        });
+
+        $expected    = $expectDefault ? 'id' : 'name';
+        $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"';
+
+        $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
+    }
+
+    /**
+     * @return array<string, array{0: mixed, 1: bool}>
+     */
+    public static function provideConditionValues(): array
+    {
+        return [
+            'false'            => [false, true], // [condition, expectedDefaultCallbackRuns]
+            'int 0'            => [0, true],
+            'float 0.0'        => [0.0, true],
+            'empty string'     => ['', true],
+            'string 0'         => ['0', true],
+            'empty array'      => [[], true],
+            'null'             => [null, true],
+            'true'             => [true, false],
+            'int 1'            => [1, false],
+            'float 1.1'        => [1.1, false],
+            'non-empty string' => ['foo', false],
+            'non-empty array'  => [[1], false],
+            'object'           => [new stdClass(), false],
+        ];
+    }
 }