From 83ca5a8e7bfedb2b17039f2828cb8c15b5ddd71f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= <jerome@tamarelle.net>
Date: Wed, 21 Aug 2024 12:04:53 +0200
Subject: [PATCH 1/2] PHPORM-230 Convert DateTimeInterface to UTCDateTime in
 queries

---
 src/Query/Builder.php                  | 58 +++++++++-----------------
 tests/Query/AggregationBuilderTest.php |  5 +--
 tests/Query/BuilderTest.php            |  6 +++
 tests/QueryBuilderTest.php             | 12 ++++--
 4 files changed, 36 insertions(+), 45 deletions(-)

diff --git a/src/Query/Builder.php b/src/Query/Builder.php
index 6168159df..9bd59c503 100644
--- a/src/Query/Builder.php
+++ b/src/Query/Builder.php
@@ -32,7 +32,6 @@
 use function array_map;
 use function array_merge;
 use function array_values;
-use function array_walk_recursive;
 use function assert;
 use function blank;
 use function call_user_func;
@@ -689,17 +688,7 @@ public function insert(array $values)
             $values = [$values];
         }
 
-        // Compatibility with Eloquent queries that uses "id" instead of MongoDB's _id
-        foreach ($values as &$document) {
-            if (isset($document['id'])) {
-                if (isset($document['_id']) && $document['_id'] !== $document['id']) {
-                    throw new InvalidArgumentException('Cannot insert document with different "id" and "_id" values');
-                }
-
-                $document['_id'] = $document['id'];
-                unset($document['id']);
-            }
-        }
+        $values = $this->aliasIdForQuery($values);
 
         $options = $this->inheritConnectionOptions();
 
@@ -876,6 +865,7 @@ public function delete($id = null)
         }
 
         $wheres  = $this->compileWheres();
+        $wheres  = $this->aliasIdForQuery($wheres);
         $options = $this->inheritConnectionOptions();
 
         if (is_int($this->limit)) {
@@ -1080,6 +1070,7 @@ protected function performUpdate(array $update, array $options = [])
         $options = $this->inheritConnectionOptions($options);
 
         $wheres = $this->compileWheres();
+        $wheres = $this->aliasIdForQuery($wheres);
         $result = $this->collection->updateMany($wheres, $update, $options);
         if ($result->isAcknowledged()) {
             return $result->getModifiedCount() ? $result->getModifiedCount() : $result->getUpsertedCount();
@@ -1191,32 +1182,12 @@ protected function compileWheres(): array
                 }
             }
 
-            // Convert DateTime values to UTCDateTime.
-            if (isset($where['value'])) {
-                if (is_array($where['value'])) {
-                    array_walk_recursive($where['value'], function (&$item, $key) {
-                        if ($item instanceof DateTimeInterface) {
-                            $item = new UTCDateTime($item);
-                        }
-                    });
-                } else {
-                    if ($where['value'] instanceof DateTimeInterface) {
-                        $where['value'] = new UTCDateTime($where['value']);
-                    }
-                }
-            } elseif (isset($where['values'])) {
-                if (is_array($where['values'])) {
-                    array_walk_recursive($where['values'], function (&$item, $key) {
-                        if ($item instanceof DateTimeInterface) {
-                            $item = new UTCDateTime($item);
-                        }
-                    });
-                } elseif ($where['values'] instanceof CarbonPeriod) {
-                    $where['values'] = [
-                        new UTCDateTime($where['values']->getStartDate()),
-                        new UTCDateTime($where['values']->getEndDate()),
-                    ];
-                }
+            // Convert CarbonPeriod to DateTime interval.
+            if (isset($where['values']) && $where['values'] instanceof CarbonPeriod) {
+                $where['values'] = [
+                    $where['values']->getStartDate(),
+                    $where['values']->getEndDate(),
+                ];
             }
 
             // In a sequence of "where" clauses, the logical operator of the
@@ -1631,12 +1602,21 @@ public function orWhereIntegerNotInRaw($column, $values, $boolean = 'and')
     private function aliasIdForQuery(array $values): array
     {
         if (array_key_exists('id', $values)) {
+            if (array_key_exists('_id', $values)) {
+                throw new InvalidArgumentException('Cannot have both "id" and "_id" fields.');
+            }
+
             $values['_id'] = $values['id'];
             unset($values['id']);
         }
 
         foreach ($values as $key => $value) {
             if (is_string($key) && str_ends_with($key, '.id')) {
+                $newkey = substr($key, 0, -3) . '._id';
+                if (array_key_exists($newkey, $values)) {
+                    throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey));
+                }
+
                 $values[substr($key, 0, -3) . '._id'] = $value;
                 unset($values[$key]);
             }
@@ -1645,6 +1625,8 @@ private function aliasIdForQuery(array $values): array
         foreach ($values as &$value) {
             if (is_array($value)) {
                 $value = $this->aliasIdForQuery($value);
+            } elseif ($value instanceof DateTimeInterface) {
+                $value = new UTCDateTime($value);
             }
         }
 
diff --git a/tests/Query/AggregationBuilderTest.php b/tests/Query/AggregationBuilderTest.php
index b3828597d..a355db439 100644
--- a/tests/Query/AggregationBuilderTest.php
+++ b/tests/Query/AggregationBuilderTest.php
@@ -11,7 +11,6 @@
 use InvalidArgumentException;
 use MongoDB\BSON\Document;
 use MongoDB\BSON\ObjectId;
-use MongoDB\BSON\UTCDateTime;
 use MongoDB\Builder\BuilderEncoder;
 use MongoDB\Builder\Expression;
 use MongoDB\Builder\Pipeline;
@@ -33,8 +32,8 @@ public function tearDown(): void
     public function testCreateAggregationBuilder(): void
     {
         User::insert([
-            ['name' => 'John Doe', 'birthday' => new UTCDateTime(new DateTimeImmutable('1989-01-01'))],
-            ['name' => 'Jane Doe', 'birthday' => new UTCDateTime(new DateTimeImmutable('1990-01-01'))],
+            ['name' => 'John Doe', 'birthday' => new DateTimeImmutable('1989-01-01')],
+            ['name' => 'Jane Doe', 'birthday' => new DateTimeImmutable('1990-01-01')],
         ]);
 
         // Create the aggregation pipeline from the query builder
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index b081a0557..666747a46 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -566,6 +566,12 @@ function (Builder $builder) {
             fn (Builder $builder) => $builder->whereBetween('id', [[1], [2, 3]]),
         ];
 
+        $date = new DateTimeImmutable('2018-09-30 15:00:00 +02:00');
+        yield 'where $lt DateTimeInterface' => [
+            ['find' => [['created_at' => ['$lt' => new UTCDateTime($date)]], []]],
+            fn (Builder $builder) => $builder->where('created_at', '<', $date),
+        ];
+
         $period = now()->toPeriod(now()->addMonth());
         yield 'whereBetween CarbonPeriod' => [
             [
diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php
index 6b08a15b7..0495f38aa 100644
--- a/tests/QueryBuilderTest.php
+++ b/tests/QueryBuilderTest.php
@@ -1053,16 +1053,20 @@ public function testIncrementEach()
     #[TestWith(['id', 'id'])]
     #[TestWith(['id', '_id'])]
     #[TestWith(['_id', 'id'])]
+    #[TestWith(['_id', '_id'])]
     public function testIdAlias($insertId, $queryId): void
     {
-        DB::collection('items')->insert([$insertId => 'abc', 'name' => 'Karting']);
-        $item = DB::collection('items')->where($queryId, '=', 'abc')->first();
+        DB::table('items')->insert([$insertId => 'abc', 'name' => 'Karting']);
+        $item = DB::table('items')->where($queryId, '=', 'abc')->first();
         $this->assertNotNull($item);
         $this->assertSame('abc', $item['id']);
         $this->assertSame('Karting', $item['name']);
 
-        DB::collection('items')->where($insertId, '=', 'abc')->update(['name' => 'Bike']);
-        $item = DB::collection('items')->where($queryId, '=', 'abc')->first();
+        DB::table('items')->where($insertId, '=', 'abc')->update(['name' => 'Bike']);
+        $item = DB::table('items')->where($queryId, '=', 'abc')->first();
         $this->assertSame('Bike', $item['name']);
+
+        $result = DB::table('items')->where($queryId, '=', 'abc')->delete();
+        $this->assertSame(1, $result);
     }
 }

From e5a2d330faef0592d173ae88449dddf35e983269 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= <jerome@tamarelle.net>
Date: Wed, 21 Aug 2024 13:48:09 +0200
Subject: [PATCH 2/2] Alias id to _id in subdocuments

---
 src/Query/Builder.php | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/Query/Builder.php b/src/Query/Builder.php
index 9bd59c503..fef4eb45c 100644
--- a/src/Query/Builder.php
+++ b/src/Query/Builder.php
@@ -1060,12 +1060,7 @@ protected function performUpdate(array $update, array $options = [])
             $options['multiple'] = true;
         }
 
-        // Since "id" is an alias for "_id", we prevent updating it
-        foreach ($update as $operator => $fields) {
-            if (array_key_exists('id', $fields)) {
-                throw new InvalidArgumentException('Cannot update "id" field.');
-            }
-        }
+        $update = $this->aliasIdForQuery($update);
 
         $options = $this->inheritConnectionOptions($options);