Skip to content

Commit 990612e

Browse files
committed
PHPORM-180 Keep createOrFirst in 2 commands to simplify implementation
1 parent 3f84373 commit 990612e

File tree

3 files changed

+44
-104
lines changed

3 files changed

+44
-104
lines changed

src/Eloquent/Builder.php

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,22 @@
66

77
use Illuminate\Database\ConnectionInterface;
88
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
9-
use InvalidArgumentException;
109
use MongoDB\Driver\Cursor;
11-
use MongoDB\Laravel\Collection;
10+
use MongoDB\Driver\Exception\WriteException;
1211
use MongoDB\Laravel\Helpers\QueriesRelationships;
13-
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
1412
use MongoDB\Laravel\Query\AggregationBuilder;
1513
use MongoDB\Model\BSONDocument;
16-
use MongoDB\Operation\FindOneAndUpdate;
1714

18-
use function array_intersect_key;
1915
use function array_key_exists;
2016
use function array_merge;
2117
use function collect;
2218
use function is_array;
2319
use function iterator_to_array;
24-
use function json_encode;
2520

2621
/** @method \MongoDB\Laravel\Query\Builder toBase() */
2722
class Builder extends EloquentBuilder
2823
{
24+
private const DUPLICATE_KEY_ERROR = 11000;
2925
use QueriesRelationships;
3026

3127
/**
@@ -202,56 +198,17 @@ public function raw($value = null)
202198
return $results;
203199
}
204200

205-
/**
206-
* Attempt to create the record if it does not exist with the matching attributes.
207-
* If the record exists, it will be returned.
208-
*
209-
* @param array $attributes The attributes to check for duplicate records
210-
* @param array $values The attributes to insert if no matching record is found
211-
*/
212-
public function createOrFirst(array $attributes = [], array $values = []): Model
201+
public function createOrFirst(array $attributes = [], array $values = []): Builder|Model
213202
{
214-
if ($attributes === [] || $attributes === ['_id' => null]) {
215-
throw new InvalidArgumentException('You must provide attributes to check for duplicates. Got ' . json_encode($attributes));
216-
}
217-
218-
// Apply casting and default values to the attributes
219-
// In case of duplicate key between the attributes and the values, the values have priority
220-
$instance = $this->newModelInstance($values + $attributes);
221-
222-
/* @see \Illuminate\Database\Eloquent\Model::performInsert */
223-
if ($instance->usesTimestamps()) {
224-
$instance->updateTimestamps();
225-
}
226-
227-
$values = $instance->getAttributes();
228-
$attributes = array_intersect_key($attributes, $values);
229-
230-
return $this->raw(function (Collection $collection) use ($attributes, $values) {
231-
$listener = new FindAndModifyCommandSubscriber();
232-
$collection->getManager()->addSubscriber($listener);
233-
234-
try {
235-
$document = $collection->findOneAndUpdate(
236-
$attributes,
237-
// Before MongoDB 5.0, $setOnInsert requires a non-empty document.
238-
// This should not be an issue as $values includes the query filter.
239-
['$setOnInsert' => (object) $values],
240-
[
241-
'upsert' => true,
242-
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
243-
'typeMap' => ['root' => 'array', 'document' => 'array'],
244-
],
245-
);
246-
} finally {
247-
$collection->getManager()->removeSubscriber($listener);
203+
try {
204+
return $this->create(array_merge($attributes, $values));
205+
} catch (WriteException $e) {
206+
if ($e->getCode() === self::DUPLICATE_KEY_ERROR) {
207+
return $this->where($attributes)->first() ?? throw $e;
248208
}
249209

250-
$model = $this->model->newFromBuilder($document);
251-
$model->wasRecentlyCreated = $listener->created;
252-
253-
return $model;
254-
});
210+
throw $e;
211+
}
255212
}
256213

257214
/**

src/Internal/FindAndModifyCommandSubscriber.php

Lines changed: 0 additions & 34 deletions
This file was deleted.

tests/ModelTest.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
1111
use Illuminate\Database\Eloquent\ModelNotFoundException;
1212
use Illuminate\Support\Facades\Date;
13+
use Illuminate\Support\Facades\DB;
1314
use Illuminate\Support\Str;
14-
use InvalidArgumentException;
1515
use MongoDB\BSON\Binary;
1616
use MongoDB\BSON\ObjectID;
1717
use MongoDB\BSON\UTCDateTime;
@@ -48,7 +48,7 @@ class ModelTest extends TestCase
4848
public function tearDown(): void
4949
{
5050
Carbon::setTestNow();
51-
User::truncate();
51+
DB::connection('mongodb')->getCollection('users')->drop();
5252
Soft::truncate();
5353
Book::truncate();
5454
Item::truncate();
@@ -1050,17 +1050,25 @@ public function testNumericFieldName(): void
10501050

10511051
public function testCreateOrFirst()
10521052
{
1053+
DB::connection('mongodb')
1054+
->getCollection('users')
1055+
->createIndex(['email' => 1], ['unique' => true]);
1056+
10531057
Carbon::setTestNow('2010-06-22');
10541058
$createdAt = Carbon::now()->getTimestamp();
1059+
$events = [];
1060+
self::registerModelEvents(User::class, $events);
10551061
$user1 = User::createOrFirst(['email' => 'john.doe@example.com']);
10561062

10571063
$this->assertSame('john.doe@example.com', $user1->email);
10581064
$this->assertNull($user1->name);
10591065
$this->assertTrue($user1->wasRecentlyCreated);
10601066
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10611067
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1068+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
10621069

10631070
Carbon::setTestNow('2020-12-28');
1071+
$events = [];
10641072
$user2 = User::createOrFirst(
10651073
['email' => 'john.doe@example.com'],
10661074
['name' => 'John Doe', 'birthday' => new DateTime('1987-05-28')],
@@ -1073,7 +1081,9 @@ public function testCreateOrFirst()
10731081
$this->assertFalse($user2->wasRecentlyCreated);
10741082
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10751083
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1084+
$this->assertEquals(['saving', 'creating'], $events);
10761085

1086+
$events = [];
10771087
$user3 = User::createOrFirst(
10781088
['email' => 'jane.doe@example.com'],
10791089
['name' => 'Jane Doe', 'birthday' => new DateTime('1987-05-28')],
@@ -1086,21 +1096,17 @@ public function testCreateOrFirst()
10861096
$this->assertTrue($user3->wasRecentlyCreated);
10871097
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10881098
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1099+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
10891100

1101+
$events = [];
10901102
$user4 = User::createOrFirst(
10911103
['name' => 'Robert Doe'],
10921104
['name' => 'Maria Doe', 'email' => 'maria.doe@example.com'],
10931105
);
10941106

10951107
$this->assertSame('Maria Doe', $user4->name);
10961108
$this->assertTrue($user4->wasRecentlyCreated);
1097-
}
1098-
1099-
public function testCreateOrFirstRequiresFilter()
1100-
{
1101-
$this->expectException(InvalidArgumentException::class);
1102-
$this->expectExceptionMessage('You must provide attributes to check for duplicates');
1103-
User::createOrFirst([]);
1109+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
11041110
}
11051111

11061112
#[TestWith([['_id' => new ObjectID()]])]
@@ -1116,6 +1122,8 @@ public function testUpdateOrCreate(array $criteria)
11161122

11171123
Carbon::setTestNow('2010-01-01');
11181124
$createdAt = Carbon::now()->getTimestamp();
1125+
$events = [];
1126+
self::registerModelEvents(User::class, $events);
11191127

11201128
// Create
11211129
$user = User::updateOrCreate(
@@ -1127,11 +1135,13 @@ public function testUpdateOrCreate(array $criteria)
11271135
$this->assertEquals(new DateTime('1987-05-28'), $user->birthday);
11281136
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
11291137
$this->assertEquals($createdAt, $user->updated_at->getTimestamp());
1130-
1138+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
1139+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
11311140
Carbon::setTestNow('2010-02-01');
11321141
$updatedAt = Carbon::now()->getTimestamp();
11331142

11341143
// Update
1144+
$events = [];
11351145
$user = User::updateOrCreate(
11361146
$criteria,
11371147
['birthday' => new DateTime('1990-01-12'), 'foo' => 'bar'],
@@ -1159,13 +1169,20 @@ public function testCreateWithNullId()
11591169
$this->assertSame(1, User::count());
11601170
}
11611171

1162-
public function testUpdateOrCreateWithNullId()
1172+
/** @param class-string<Model> $modelClass */
1173+
private static function registerModelEvents(string $modelClass, array &$events): void
11631174
{
1164-
$this->expectException(InvalidArgumentException::class);
1165-
$this->expectExceptionMessage('You must provide attributes to check for duplicates');
1166-
User::updateOrCreate(
1167-
['_id' => null],
1168-
['email' => 'jane.doe@example.com'],
1169-
);
1175+
$modelClass::creating(function () use (&$events) {
1176+
$events[] = 'creating';
1177+
});
1178+
$modelClass::created(function () use (&$events) {
1179+
$events[] = 'created';
1180+
});
1181+
$modelClass::saving(function () use (&$events) {
1182+
$events[] = 'saving';
1183+
});
1184+
$modelClass::saved(function () use (&$events) {
1185+
$events[] = 'saved';
1186+
});
11701187
}
11711188
}

0 commit comments

Comments
 (0)