Skip to content

Commit d99053b

Browse files
committed
fix(model): prevent _id from being marked dirty in created event
1 parent 0d7bc9d commit d99053b

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

src/Eloquent/Model.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace MongoDB\Laravel\Eloquent;
66

7+
use Illuminate\Database\Eloquent\Builder;
78
use Illuminate\Database\Eloquent\Model as BaseModel;
89
use MongoDB\Laravel\Auth\User;
910

@@ -57,4 +58,62 @@ final public static function isDocumentModel(string|object $class): bool
5758
// Document models must use the DocumentModel trait.
5859
return self::$documentModelClasses[$class] = array_key_exists(DocumentModel::class, class_uses_recursive($class));
5960
}
61+
62+
/**
63+
* Override of Laravel's performInsert() to handle MongoDB _id immutability.
64+
*
65+
* Ensures syncOriginal() is called immediately after insert, so that
66+
* subsequent save() calls (e.g., inside the "created" model event)
67+
* don't treat the primary key (_id) as dirty.
68+
*
69+
* @param Builder $query
70+
*
71+
* @return bool
72+
*/
73+
protected function performInsert(Builder $query)
74+
{
75+
if ($this->usesUniqueIds()) {
76+
$this->setUniqueIds();
77+
}
78+
79+
if ($this->fireModelEvent('creating') === false) {
80+
return false;
81+
}
82+
83+
// First we'll need to create a fresh query instance and touch the creation and
84+
// update timestamps on this model, which are maintained by us for developer
85+
// convenience. After, we will just continue saving these model instances.
86+
if ($this->usesTimestamps()) {
87+
$this->updateTimestamps();
88+
}
89+
90+
// If the model has an incrementing key, we can use the "insertGetId" method on
91+
// the query builder, which will give us back the final inserted ID for this
92+
// table from the database. Not all tables have to be incrementing though.
93+
$attributes = $this->getAttributesForInsert();
94+
95+
if ($this->getIncrementing()) {
96+
$this->insertAndSetId($query, $attributes);
97+
} else {
98+
if (empty($attributes)) {
99+
return true;
100+
}
101+
102+
$query->insert($attributes);
103+
}
104+
105+
// MongoDB-specific fix: prevent _id from being considered dirty after insert
106+
$this->syncOriginal();
107+
108+
// We will go ahead and set the exists property to true, so that it is set when
109+
// the created event is fired, just in case the developer tries to update it
110+
// during the event. This will allow them to do so and run an update here.
111+
$this->exists = true;
112+
113+
$this->wasRecentlyCreated = true;
114+
115+
$this->fireModelEvent('created', false);
116+
117+
return true;
118+
}
60119
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace MongoDB\Laravel\Tests\Eloquent\Fixtures;
4+
5+
use MongoDB\Laravel\Eloquent\Model;
6+
7+
class CreatedEventTestModel extends Model
8+
{
9+
protected $connection = 'mongodb';
10+
protected string $collection = 'test_created_event';
11+
protected $guarded = [];
12+
13+
protected static function booted(): void
14+
{
15+
static::created(function ($model) {
16+
$model->extra = 'written-in-created';
17+
$model->saveQuietly();
18+
});
19+
}
20+
}

tests/Eloquent/ModelTest.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
use MongoDB\Laravel\Auth\User;
88
use MongoDB\Laravel\Eloquent\DocumentModel;
99
use MongoDB\Laravel\Eloquent\Model;
10+
use MongoDB\Laravel\MongoDBServiceProvider;
11+
use MongoDB\Laravel\Tests\Eloquent\Fixtures\CreatedEventTestModel;
1012
use MongoDB\Laravel\Tests\Models\Book;
1113
use MongoDB\Laravel\Tests\Models\Casting;
1214
use MongoDB\Laravel\Tests\Models\SqlBook;
15+
use Orchestra\Testbench\TestCase;
1316
use PHPUnit\Framework\Attributes\DataProvider;
14-
use PHPUnit\Framework\TestCase;
17+
18+
use function env;
1519

1620
class ModelTest extends TestCase
1721
{
@@ -21,6 +25,18 @@ public function testIsDocumentModel(bool $expected, string|object $classOrObject
2125
$this->assertSame($expected, Model::isDocumentModel($classOrObject));
2226
}
2327

28+
public function testCreatedEventCanSafelyCallSave(): void
29+
{
30+
$model = new CreatedEventTestModel();
31+
$model->foo = 'bar';
32+
$model->save();
33+
34+
$fresh = $model->fresh();
35+
36+
$this->assertEquals('bar', $fresh->foo);
37+
$this->assertEquals('written-in-created', $fresh->extra);
38+
}
39+
2440
public static function provideDocumentModelClasses(): Generator
2541
{
2642
// Test classes
@@ -59,4 +75,26 @@ public static function provideDocumentModelClasses(): Generator
5975
},
6076
];
6177
}
78+
79+
protected function getEnvironmentSetUp($app): void
80+
{
81+
$app['config']->set('database.default', 'mongodb');
82+
83+
$app['config']->set('database.connections.mongodb', [
84+
'driver' => 'mongodb',
85+
'host' => env('DB_HOST', 'localhost'),
86+
'port' => env('DB_PORT', 27017),
87+
'database' => env('DB_DATABASE', 'testing'),
88+
'username' => env('DB_USERNAME', null),
89+
'password' => env('DB_PASSWORD', null),
90+
'options' => [],
91+
]);
92+
}
93+
94+
protected function getPackageProviders($app): array
95+
{
96+
return [
97+
MongoDBServiceProvider::class,
98+
];
99+
}
62100
}

0 commit comments

Comments
 (0)