diff --git a/CHANGELOG.md b/CHANGELOG.md index 2263ac29d..a8c2cefc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [unreleased] * Add support for Laravel 11 by @GromNaN in [#2735](https://github.com/mongodb/laravel-mongodb/pull/2735) +* Implement Model::createOrFirst() using findOneAndUpdate operation by @GromNaN in [#2742](https://github.com/mongodb/laravel-mongodb/pull/2742) ## [4.1.3] - 2024-03-05 diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index b9005c442..6ef960456 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -7,9 +7,12 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use MongoDB\Driver\Cursor; +use MongoDB\Laravel\Collection; use MongoDB\Laravel\Helpers\QueriesRelationships; +use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber; use MongoDB\Model\BSONDocument; +use function array_intersect_key; use function array_key_exists; use function array_merge; use function collect; @@ -183,6 +186,41 @@ public function raw($value = null) return $results; } + /** + * Attempt to create the record if it does not exist with the matching attributes. + * If the record exists, it will be returned. + * + * @param array $attributes The attributes to check for duplicate records + * @param array $values The attributes to insert if no matching record is found + */ + public function createOrFirst(array $attributes = [], array $values = []): Model + { + // Apply casting and default values to the attributes + $instance = $this->newModelInstance($values + $attributes); + $values = $instance->getAttributes(); + $attributes = array_intersect_key($attributes, $values); + + return $this->raw(function (Collection $collection) use ($attributes, $values) { + $listener = new FindAndModifyCommandSubscriber(); + $collection->getManager()->addSubscriber($listener); + + try { + $document = $collection->findOneAndUpdate( + $attributes, + ['$setOnInsert' => $values], + ['upsert' => true, 'new' => true, 'typeMap' => ['root' => 'array', 'document' => 'array']], + ); + } finally { + $collection->getManager()->removeSubscriber($listener); + } + + $model = $this->model->newFromBuilder($document); + $model->wasRecentlyCreated = $listener->created; + + return $model; + }); + } + /** * Add the "updated at" column to an array of values. * TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e diff --git a/src/Internal/FindAndModifyCommandSubscriber.php b/src/Internal/FindAndModifyCommandSubscriber.php new file mode 100644 index 000000000..55b13436b --- /dev/null +++ b/src/Internal/FindAndModifyCommandSubscriber.php @@ -0,0 +1,34 @@ +created = ! $event->getReply()->lastErrorObject->updatedExisting; + } +} diff --git a/tests/ModelTest.php b/tests/ModelTest.php index ec1579869..f4d459422 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -1044,4 +1044,43 @@ public function testNumericFieldName(): void $this->assertInstanceOf(User::class, $found); $this->assertEquals([3 => 'two.three'], $found[2]); } + + public function testCreateOrFirst() + { + $user1 = User::createOrFirst(['email' => 'taylorotwell@gmail.com']); + + $this->assertSame('taylorotwell@gmail.com', $user1->email); + $this->assertNull($user1->name); + $this->assertTrue($user1->wasRecentlyCreated); + + $user2 = User::createOrFirst( + ['email' => 'taylorotwell@gmail.com'], + ['name' => 'Taylor Otwell', 'birthday' => new DateTime('1987-05-28')], + ); + + $this->assertEquals($user1->id, $user2->id); + $this->assertSame('taylorotwell@gmail.com', $user2->email); + $this->assertNull($user2->name); + $this->assertNull($user2->birthday); + $this->assertFalse($user2->wasRecentlyCreated); + + $user3 = User::createOrFirst( + ['email' => 'abigailotwell@gmail.com'], + ['name' => 'Abigail Otwell', 'birthday' => new DateTime('1987-05-28')], + ); + + $this->assertNotEquals($user3->id, $user1->id); + $this->assertSame('abigailotwell@gmail.com', $user3->email); + $this->assertSame('Abigail Otwell', $user3->name); + $this->assertEquals(new DateTime('1987-05-28'), $user3->birthday); + $this->assertTrue($user3->wasRecentlyCreated); + + $user4 = User::createOrFirst( + ['name' => 'Dries Vints'], + ['name' => 'Nuno Maduro', 'email' => 'nuno@laravel.com'], + ); + + $this->assertSame('Nuno Maduro', $user4->name); + $this->assertTrue($user4->wasRecentlyCreated); + } }