Skip to content

Commit 7d3c406

Browse files
committed
Trigger events in Model::createOrFirst
1 parent 3f84373 commit 7d3c406

File tree

5 files changed

+132
-37
lines changed

5 files changed

+132
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
88
* Add `mongodb` driver for Batching by @GromNaN in [#2904](https://github.com/mongodb/laravel-mongodb/pull/2904)
99
* Rename queue option `table` to `collection`
1010
* Replace queue option `expire` with `retry_after`
11+
* Trigger events in `Model::createOrFirst()` @GromNaN in [#2980](https://github.com/mongodb/laravel-mongodb/pull/2980)
1112

1213
## [4.3.1]
1314

src/Eloquent/Builder.php

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@
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;
17+
use function assert;
2118
use function collect;
2219
use function is_array;
2320
use function iterator_to_array;
@@ -218,40 +215,9 @@ public function createOrFirst(array $attributes = [], array $values = []): Model
218215
// Apply casting and default values to the attributes
219216
// In case of duplicate key between the attributes and the values, the values have priority
220217
$instance = $this->newModelInstance($values + $attributes);
218+
assert($instance instanceof Model);
221219

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);
248-
}
249-
250-
$model = $this->model->newFromBuilder($document);
251-
$model->wasRecentlyCreated = $listener->created;
252-
253-
return $model;
254-
});
220+
return $instance->saveOrFirst($attributes);
255221
}
256222

257223
/**

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;
@@ -759,6 +761,71 @@ public function save(array $options = [])
759761
return $saved;
760762
}
761763

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

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' => 'john.doe@example.com']);
10561059

10571060
$this->assertSame('john.doe@example.com', $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' => 'john.doe@example.com'],
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' => 'jane.doe@example.com'],
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' => 'maria.doe@example.com'],
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' => 'john.doe@example.com'],
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();
@@ -1168,4 +1184,21 @@ public function testUpdateOrCreateWithNullId()
11681184
['email' => 'jane.doe@example.com'],
11691185
);
11701186
}
1187+
1188+
/** @param class-string<Model> $modelClass */
1189+
private static function registerModelEvents(string $modelClass, array &$events): void
1190+
{
1191+
$modelClass::creating(function () use (&$events) {
1192+
$events[] = 'creating';
1193+
});
1194+
$modelClass::created(function () use (&$events) {
1195+
$events[] = 'created';
1196+
});
1197+
$modelClass::saving(function () use (&$events) {
1198+
$events[] = 'saving';
1199+
});
1200+
$modelClass::saved(function () use (&$events) {
1201+
$events[] = 'saved';
1202+
});
1203+
}
11711204
}

0 commit comments

Comments
 (0)