Skip to content

Commit 43e5023

Browse files
committed
Trigger events in Model::createOrFirst
1 parent 7e72c2c commit 43e5023

File tree

4 files changed

+129
-37
lines changed

4 files changed

+129
-37
lines changed

src/Eloquent/Builder.php

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
99
use InvalidArgumentException;
1010
use MongoDB\Driver\Cursor;
11-
use MongoDB\Laravel\Collection;
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;
@@ -218,39 +214,7 @@ public function createOrFirst(array $attributes = [], array $values = []): Model
218214
// In case of duplicate key between the attributes and the values, the values have priority
219215
$instance = $this->newModelInstance($values + $attributes);
220216

221-
/* @see \Illuminate\Database\Eloquent\Model::performInsert */
222-
if ($instance->usesTimestamps()) {
223-
$instance->updateTimestamps();
224-
}
225-
226-
$values = $instance->getAttributes();
227-
$attributes = array_intersect_key($attributes, $values);
228-
229-
return $this->raw(function (Collection $collection) use ($attributes, $values) {
230-
$listener = new FindAndModifyCommandSubscriber();
231-
$collection->getManager()->addSubscriber($listener);
232-
233-
try {
234-
$document = $collection->findOneAndUpdate(
235-
$attributes,
236-
// Before MongoDB 5.0, $setOnInsert requires a non-empty document.
237-
// This should not be an issue as $values includes the query filter.
238-
['$setOnInsert' => (object) $values],
239-
[
240-
'upsert' => true,
241-
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
242-
'typeMap' => ['root' => 'array', 'document' => 'array'],
243-
],
244-
);
245-
} finally {
246-
$collection->getManager()->removeSubscriber($listener);
247-
}
248-
249-
$model = $this->model->newFromBuilder($document);
250-
$model->wasRecentlyCreated = $listener->created;
251-
252-
return $model;
253-
});
217+
return $instance->saveOrFirst($attributes);
254218
}
255219

256220
/**

src/Eloquent/Model.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MongoDB\Laravel\Eloquent;
66

77
use BackedEnum;
8+
use BadMethodCallException;
89
use Carbon\CarbonInterface;
910
use DateTimeInterface;
1011
use DateTimeZone;
@@ -31,6 +32,7 @@
3132
use function array_merge;
3233
use function array_unique;
3334
use function array_values;
35+
use function assert;
3436
use function class_basename;
3537
use function count;
3638
use function date_default_timezone_get;
@@ -754,6 +756,71 @@ public function save(array $options = [])
754756
return $saved;
755757
}
756758

759+
/** @internal Not part of Laravel Eloquent API. Use raw findOneAndModify if necessary */
760+
public function saveOrFirst(array $criteria): ?static
761+
{
762+
$this->mergeAttributesFromCachedCasts();
763+
764+
$query = $this->newModelQuery();
765+
assert($query instanceof Builder);
766+
767+
// If the "saving" event returns false we'll bail out of the save and return
768+
// false, indicating that the save failed. This provides a chance for any
769+
// listeners to cancel save operations if validations fail or whatever.
770+
if ($this->fireModelEvent('saving') === false) {
771+
return null;
772+
}
773+
774+
if ($this->exists) {
775+
throw new BadMethodCallException(sprintf('%s can be used on new model instances only', __FUNCTION__));
776+
}
777+
778+
if ($this->usesUniqueIds()) {
779+
$this->setUniqueIds();
780+
}
781+
782+
if ($this->fireModelEvent('creating') === false) {
783+
return null;
784+
}
785+
786+
// First we'll need to create a fresh query instance and touch the creation and
787+
// update timestamps on this model, which are maintained by us for developer
788+
// convenience. After, we will just continue saving these model instances.
789+
if ($this->usesTimestamps()) {
790+
$this->updateTimestamps();
791+
}
792+
793+
// If the model has an incrementing key, we can use the "insertGetId" method on
794+
// the query builder, which will give us back the final inserted ID for this
795+
// table from the database. Not all tables have to be incrementing though.
796+
$attributes = $this->getAttributesForInsert();
797+
798+
$document = $query->getQuery()
799+
->where($criteria)
800+
->insertOrFirst($attributes);
801+
802+
$connection = $query->getConnection();
803+
if (! $this->getConnectionName() && $connection) {
804+
$this->setConnection($connection->getName());
805+
}
806+
807+
// If a document matching the criteria was found, it is returned. Nothing was saved.
808+
if (! $document['wasRecentlyCreated']) {
809+
return $this->newInstance($document);
810+
}
811+
812+
// If the model is successfully saved, we need to do a few more things once
813+
// that is done. We will call the "saved" method here to run any actions
814+
// we need to happen after a model gets successfully saved right here.
815+
$this->exists = true;
816+
$this->wasRecentlyCreated = true;
817+
$this->setAttribute($this->getKeyName(), $document[$this->getKeyName()]);
818+
$this->fireModelEvent('created', false);
819+
$this->finishSave([]);
820+
821+
return $this;
822+
}
823+
757824
/**
758825
* {@inheritDoc}
759826
*/

src/Query/Builder.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
use MongoDB\BSON\UTCDateTime;
2424
use MongoDB\Builder\Stage\FluentFactoryTrait;
2525
use MongoDB\Driver\Cursor;
26+
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
27+
use MongoDB\Operation\FindOneAndUpdate;
2628
use Override;
2729
use RuntimeException;
2830

@@ -725,6 +727,32 @@ public function update(array $values, array $options = [])
725727
return $this->performUpdate($values, $options);
726728
}
727729

730+
public function insertOrFirst(array $document): array
731+
{
732+
$wheres = $this->compileWheres();
733+
$listener = new FindAndModifyCommandSubscriber();
734+
735+
try {
736+
$this->collection->getManager()->addSubscriber($listener);
737+
$document = $this->collection->findOneAndUpdate(
738+
$wheres,
739+
// Before MongoDB 5.0, $setOnInsert requires a non-empty document.
740+
['$setOnInsert' => (object) $document],
741+
[
742+
'upsert' => true,
743+
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
744+
'typeMap' => ['root' => 'array', 'document' => 'array'],
745+
],
746+
);
747+
} finally {
748+
$this->collection->getManager()->removeSubscriber($listener);
749+
}
750+
751+
$document['wasRecentlyCreated'] = $listener->created;
752+
753+
return $document;
754+
}
755+
728756
/** @inheritdoc */
729757
public function increment($column, $amount = 1, array $extra = [], array $options = [])
730758
{

tests/ModelTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,15 +1052,20 @@ public function testCreateOrFirst()
10521052
{
10531053
Carbon::setTestNow('2010-06-22');
10541054
$createdAt = Carbon::now()->getTimestamp();
1055+
$events = [];
1056+
self::registerModelEvents(User::class, $events);
1057+
10551058
$user1 = User::createOrFirst(['email' => '[email protected]']);
10561059

10571060
$this->assertSame('[email protected]', $user1->email);
10581061
$this->assertNull($user1->name);
10591062
$this->assertTrue($user1->wasRecentlyCreated);
10601063
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10611064
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1065+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
10621066

10631067
Carbon::setTestNow('2020-12-28');
1068+
$events = [];
10641069
$user2 = User::createOrFirst(
10651070
['email' => '[email protected]'],
10661071
['name' => 'John Doe', 'birthday' => new DateTime('1987-05-28')],
@@ -1073,7 +1078,9 @@ public function testCreateOrFirst()
10731078
$this->assertFalse($user2->wasRecentlyCreated);
10741079
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10751080
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1081+
$this->assertEquals(['saving', 'creating'], $events);
10761082

1083+
$events = [];
10771084
$user3 = User::createOrFirst(
10781085
['email' => '[email protected]'],
10791086
['name' => 'Jane Doe', 'birthday' => new DateTime('1987-05-28')],
@@ -1086,14 +1093,17 @@ public function testCreateOrFirst()
10861093
$this->assertTrue($user3->wasRecentlyCreated);
10871094
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
10881095
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
1096+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
10891097

1098+
$events = [];
10901099
$user4 = User::createOrFirst(
10911100
['name' => 'Robert Doe'],
10921101
['name' => 'Maria Doe', 'email' => '[email protected]'],
10931102
);
10941103

10951104
$this->assertSame('Maria Doe', $user4->name);
10961105
$this->assertTrue($user4->wasRecentlyCreated);
1106+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
10971107
}
10981108

10991109
public function testCreateOrFirstRequiresFilter()
@@ -1114,6 +1124,9 @@ public function testUpdateOrCreate(array $criteria)
11141124
['email' => '[email protected]'],
11151125
]);
11161126

1127+
$events = [];
1128+
self::registerModelEvents(User::class, $events);
1129+
11171130
Carbon::setTestNow('2010-01-01');
11181131
$createdAt = Carbon::now()->getTimestamp();
11191132

@@ -1127,6 +1140,8 @@ public function testUpdateOrCreate(array $criteria)
11271140
$this->assertEquals(new DateTime('1987-05-28'), $user->birthday);
11281141
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
11291142
$this->assertEquals($createdAt, $user->updated_at->getTimestamp());
1143+
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
1144+
$events = [];
11301145

11311146
Carbon::setTestNow('2010-02-01');
11321147
$updatedAt = Carbon::now()->getTimestamp();
@@ -1142,6 +1157,7 @@ public function testUpdateOrCreate(array $criteria)
11421157
$this->assertEquals(new DateTime('1990-01-12'), $user->birthday);
11431158
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
11441159
$this->assertEquals($updatedAt, $user->updated_at->getTimestamp());
1160+
$this->assertEquals(['saving', 'saved'], $events);
11451161

11461162
// Stored data
11471163
$checkUser = User::where($criteria)->first();
@@ -1151,4 +1167,21 @@ public function testUpdateOrCreate(array $criteria)
11511167
$this->assertEquals($createdAt, $checkUser->created_at->getTimestamp());
11521168
$this->assertEquals($updatedAt, $checkUser->updated_at->getTimestamp());
11531169
}
1170+
1171+
/** @param class-string<Model> $modelClass */
1172+
private static function registerModelEvents(string $modelClass, array &$events): void
1173+
{
1174+
$modelClass::creating(function () use (&$events) {
1175+
$events[] = 'creating';
1176+
});
1177+
$modelClass::created(function () use (&$events) {
1178+
$events[] = 'created';
1179+
});
1180+
$modelClass::saving(function () use (&$events) {
1181+
$events[] = 'saving';
1182+
});
1183+
$modelClass::saved(function () use (&$events) {
1184+
$events[] = 'saved';
1185+
});
1186+
}
11541187
}

0 commit comments

Comments
 (0)