Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHPORM-180 Keep createOrFirst in 2 commands to simplify implementation #2984

Merged
merged 4 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 10 additions & 53 deletions src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,22 @@

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use InvalidArgumentException;
use MongoDB\Driver\Cursor;
use MongoDB\Laravel\Collection;
use MongoDB\Driver\Exception\WriteException;
use MongoDB\Laravel\Helpers\QueriesRelationships;
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
use MongoDB\Laravel\Query\AggregationBuilder;
use MongoDB\Model\BSONDocument;
use MongoDB\Operation\FindOneAndUpdate;

use function array_intersect_key;
use function array_key_exists;
use function array_merge;
use function collect;
use function is_array;
use function iterator_to_array;
use function json_encode;

/** @method \MongoDB\Laravel\Query\Builder toBase() */
class Builder extends EloquentBuilder
{
private const DUPLICATE_KEY_ERROR = 11000;
use QueriesRelationships;

/**
Expand Down Expand Up @@ -202,56 +198,17 @@ 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
public function createOrFirst(array $attributes = [], array $values = []): Builder|Model
{
if ($attributes === [] || $attributes === ['_id' => null]) {
throw new InvalidArgumentException('You must provide attributes to check for duplicates. Got ' . json_encode($attributes));
}

// Apply casting and default values to the attributes
// In case of duplicate key between the attributes and the values, the values have priority
$instance = $this->newModelInstance($values + $attributes);

/* @see \Illuminate\Database\Eloquent\Model::performInsert */
if ($instance->usesTimestamps()) {
$instance->updateTimestamps();
}

$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,
// Before MongoDB 5.0, $setOnInsert requires a non-empty document.
// This should not be an issue as $values includes the query filter.
['$setOnInsert' => (object) $values],
[
'upsert' => true,
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
'typeMap' => ['root' => 'array', 'document' => 'array'],
],
);
} finally {
$collection->getManager()->removeSubscriber($listener);
try {
return $this->create(array_merge($attributes, $values));
} catch (WriteException $e) {
if ($e->getCode() === self::DUPLICATE_KEY_ERROR) {
jmikola marked this conversation as resolved.
Show resolved Hide resolved
return $this->where($attributes)->first() ?? throw $e;
}

$model = $this->model->newFromBuilder($document);
$model->wasRecentlyCreated = $listener->created;

return $model;
});
throw $e;
}
}

/**
Expand Down
34 changes: 0 additions & 34 deletions src/Internal/FindAndModifyCommandSubscriber.php

This file was deleted.

51 changes: 34 additions & 17 deletions tests/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use InvalidArgumentException;
use MongoDB\BSON\Binary;
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\UTCDateTime;
Expand Down Expand Up @@ -48,7 +48,7 @@ class ModelTest extends TestCase
public function tearDown(): void
{
Carbon::setTestNow();
User::truncate();
DB::connection('mongodb')->getCollection('users')->drop();
Soft::truncate();
Book::truncate();
Item::truncate();
Expand Down Expand Up @@ -1050,17 +1050,25 @@ public function testNumericFieldName(): void

public function testCreateOrFirst()
{
DB::connection('mongodb')
->getCollection('users')
->createIndex(['email' => 1], ['unique' => true]);

Carbon::setTestNow('2010-06-22');
$createdAt = Carbon::now()->getTimestamp();
$events = [];
self::registerModelEvents(User::class, $events);
$user1 = User::createOrFirst(['email' => '[email protected]']);

$this->assertSame('[email protected]', $user1->email);
$this->assertNull($user1->name);
$this->assertTrue($user1->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);

Carbon::setTestNow('2020-12-28');
$events = [];
jmikola marked this conversation as resolved.
Show resolved Hide resolved
$user2 = User::createOrFirst(
['email' => '[email protected]'],
['name' => 'John Doe', 'birthday' => new DateTime('1987-05-28')],
Expand All @@ -1073,7 +1081,9 @@ public function testCreateOrFirst()
$this->assertFalse($user2->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating'], $events);

$events = [];
$user3 = User::createOrFirst(
['email' => '[email protected]'],
['name' => 'Jane Doe', 'birthday' => new DateTime('1987-05-28')],
Expand All @@ -1086,21 +1096,17 @@ public function testCreateOrFirst()
$this->assertTrue($user3->wasRecentlyCreated);
$this->assertEquals($createdAt, $user1->created_at->getTimestamp());
$this->assertEquals($createdAt, $user1->updated_at->getTimestamp());
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);

$events = [];
$user4 = User::createOrFirst(
['name' => 'Robert Doe'],
['name' => 'Maria Doe', 'email' => '[email protected]'],
);

$this->assertSame('Maria Doe', $user4->name);
$this->assertTrue($user4->wasRecentlyCreated);
}

public function testCreateOrFirstRequiresFilter()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('You must provide attributes to check for duplicates');
User::createOrFirst([]);
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
jmikola marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

// Create
$user = User::updateOrCreate(
Expand All @@ -1127,11 +1135,13 @@ public function testUpdateOrCreate(array $criteria)
$this->assertEquals(new DateTime('1987-05-28'), $user->birthday);
$this->assertEquals($createdAt, $user->created_at->getTimestamp());
$this->assertEquals($createdAt, $user->updated_at->getTimestamp());

$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
$this->assertEquals(['saving', 'creating', 'created', 'saved'], $events);
Carbon::setTestNow('2010-02-01');
$updatedAt = Carbon::now()->getTimestamp();

// Update
$events = [];
$user = User::updateOrCreate(
$criteria,
['birthday' => new DateTime('1990-01-12'), 'foo' => 'bar'],
Expand Down Expand Up @@ -1159,13 +1169,20 @@ public function testCreateWithNullId()
$this->assertSame(1, User::count());
}

public function testUpdateOrCreateWithNullId()
/** @param class-string<Model> $modelClass */
private static function registerModelEvents(string $modelClass, array &$events): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('You must provide attributes to check for duplicates');
User::updateOrCreate(
['_id' => null],
['email' => '[email protected]'],
);
$modelClass::creating(function () use (&$events) {
$events[] = 'creating';
});
$modelClass::created(function () use (&$events) {
$events[] = 'created';
});
$modelClass::saving(function () use (&$events) {
$events[] = 'saving';
});
$modelClass::saved(function () use (&$events) {
$events[] = 'saved';
});
}
}
Loading