Skip to content

Commit 19fc801

Browse files
authored
PHPORM-139 Implement Model::createOrFirst() using findOneAndUpdate operation (#2742)
Use monitoring to track if model was created or found
1 parent 963d01f commit 19fc801

File tree

4 files changed

+112
-0
lines changed

4 files changed

+112
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44
## [unreleased]
55

66
* Add support for Laravel 11 by @GromNaN in [#2735](https://github.com/mongodb/laravel-mongodb/pull/2735)
7+
* Implement Model::createOrFirst() using findOneAndUpdate operation by @GromNaN in [#2742](https://github.com/mongodb/laravel-mongodb/pull/2742)
78

89
## [4.1.3] - 2024-03-05
910

src/Eloquent/Builder.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
use Illuminate\Database\ConnectionInterface;
88
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
99
use MongoDB\Driver\Cursor;
10+
use MongoDB\Laravel\Collection;
1011
use MongoDB\Laravel\Helpers\QueriesRelationships;
12+
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
1113
use MongoDB\Model\BSONDocument;
1214

15+
use function array_intersect_key;
1316
use function array_key_exists;
1417
use function array_merge;
1518
use function collect;
@@ -183,6 +186,41 @@ public function raw($value = null)
183186
return $results;
184187
}
185188

189+
/**
190+
* Attempt to create the record if it does not exist with the matching attributes.
191+
* If the record exists, it will be returned.
192+
*
193+
* @param array $attributes The attributes to check for duplicate records
194+
* @param array $values The attributes to insert if no matching record is found
195+
*/
196+
public function createOrFirst(array $attributes = [], array $values = []): Model
197+
{
198+
// Apply casting and default values to the attributes
199+
$instance = $this->newModelInstance($values + $attributes);
200+
$values = $instance->getAttributes();
201+
$attributes = array_intersect_key($attributes, $values);
202+
203+
return $this->raw(function (Collection $collection) use ($attributes, $values) {
204+
$listener = new FindAndModifyCommandSubscriber();
205+
$collection->getManager()->addSubscriber($listener);
206+
207+
try {
208+
$document = $collection->findOneAndUpdate(
209+
$attributes,
210+
['$setOnInsert' => $values],
211+
['upsert' => true, 'new' => true, 'typeMap' => ['root' => 'array', 'document' => 'array']],
212+
);
213+
} finally {
214+
$collection->getManager()->removeSubscriber($listener);
215+
}
216+
217+
$model = $this->model->newFromBuilder($document);
218+
$model->wasRecentlyCreated = $listener->created;
219+
220+
return $model;
221+
});
222+
}
223+
186224
/**
187225
* Add the "updated at" column to an array of values.
188226
* TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Internal;
6+
7+
use MongoDB\Driver\Monitoring\CommandFailedEvent;
8+
use MongoDB\Driver\Monitoring\CommandStartedEvent;
9+
use MongoDB\Driver\Monitoring\CommandSubscriber;
10+
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
11+
12+
/**
13+
* Track findAndModify command events to detect when a document is inserted or
14+
* updated.
15+
*
16+
* @internal
17+
*/
18+
final class FindAndModifyCommandSubscriber implements CommandSubscriber
19+
{
20+
public bool $created;
21+
22+
public function commandFailed(CommandFailedEvent $event)
23+
{
24+
}
25+
26+
public function commandStarted(CommandStartedEvent $event)
27+
{
28+
}
29+
30+
public function commandSucceeded(CommandSucceededEvent $event)
31+
{
32+
$this->created = ! $event->getReply()->lastErrorObject->updatedExisting;
33+
}
34+
}

tests/ModelTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,4 +1044,43 @@ public function testNumericFieldName(): void
10441044
$this->assertInstanceOf(User::class, $found);
10451045
$this->assertEquals([3 => 'two.three'], $found[2]);
10461046
}
1047+
1048+
public function testCreateOrFirst()
1049+
{
1050+
$user1 = User::createOrFirst(['email' => 'taylorotwell@gmail.com']);
1051+
1052+
$this->assertSame('taylorotwell@gmail.com', $user1->email);
1053+
$this->assertNull($user1->name);
1054+
$this->assertTrue($user1->wasRecentlyCreated);
1055+
1056+
$user2 = User::createOrFirst(
1057+
['email' => 'taylorotwell@gmail.com'],
1058+
['name' => 'Taylor Otwell', 'birthday' => new DateTime('1987-05-28')],
1059+
);
1060+
1061+
$this->assertEquals($user1->id, $user2->id);
1062+
$this->assertSame('taylorotwell@gmail.com', $user2->email);
1063+
$this->assertNull($user2->name);
1064+
$this->assertNull($user2->birthday);
1065+
$this->assertFalse($user2->wasRecentlyCreated);
1066+
1067+
$user3 = User::createOrFirst(
1068+
['email' => 'abigailotwell@gmail.com'],
1069+
['name' => 'Abigail Otwell', 'birthday' => new DateTime('1987-05-28')],
1070+
);
1071+
1072+
$this->assertNotEquals($user3->id, $user1->id);
1073+
$this->assertSame('abigailotwell@gmail.com', $user3->email);
1074+
$this->assertSame('Abigail Otwell', $user3->name);
1075+
$this->assertEquals(new DateTime('1987-05-28'), $user3->birthday);
1076+
$this->assertTrue($user3->wasRecentlyCreated);
1077+
1078+
$user4 = User::createOrFirst(
1079+
['name' => 'Dries Vints'],
1080+
['name' => 'Nuno Maduro', 'email' => 'nuno@laravel.com'],
1081+
);
1082+
1083+
$this->assertSame('Nuno Maduro', $user4->name);
1084+
$this->assertTrue($user4->wasRecentlyCreated);
1085+
}
10471086
}

0 commit comments

Comments
 (0)