Skip to content

Commit 92efdda

Browse files
committed
Merge branch '4.2' into merge-4.1-into-4.2-1710254470287
* 4.2: PHPORM-139 Implement `Model::createOrFirst()` using `findOneAndUpdate` operation (#2742) Test Laravel 10 and 11 (#2746) PHPORM-150 Run CI on Laravel 11 (#2735) PHPORM-152 Fix tests for Carbon 3 (#2733)
2 parents 74899f9 + 19fc801 commit 92efdda

File tree

9 files changed

+157
-18
lines changed

9 files changed

+157
-18
lines changed

.github/workflows/build-ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ jobs:
2424
- "8.2"
2525
- "8.3"
2626
laravel:
27-
- "10.*"
27+
- "10.*"
28+
- "11.*"
2829
include:
2930
- php: "8.1"
3031
laravel: "10.*"
3132
mongodb: "5.0"
3233
mode: "low-deps"
34+
exclude:
35+
- php: "8.1"
36+
laravel: "11.*"
3337

3438
steps:
3539
- uses: "actions/checkout@v4"
@@ -76,8 +80,7 @@ jobs:
7680
restore-keys: "${{ matrix.os }}-composer-"
7781

7882
- name: "Install dependencies"
79-
run: composer update --no-interaction $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest --prefer-stable')
80-
83+
run: composer update --no-interaction $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest')
8184
- name: "Run tests"
8285
run: "./vendor/bin/phpunit --coverage-clover coverage.xml"
8386
env:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [unreleased]
5+
6+
* Add support for Laravel 11 by @GromNaN in [#2735](https://github.com/mongodb/laravel-mongodb/pull/2735)
7+
* Implement Model::createOrFirst() using findOneAndUpdate operation by @GromNaN in [#2742](https://github.com/mongodb/laravel-mongodb/pull/2742)
8+
49
## [4.1.3] - 2024-03-05
510

611
* Fix the timezone of `datetime` fields when they are read from the database. By @GromNaN in [#2739](https://github.com/mongodb/laravel-mongodb/pull/2739)

composer.json

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@
2424
"require": {
2525
"php": "^8.1",
2626
"ext-mongodb": "^1.15",
27-
"illuminate/support": "^10.0",
28-
"illuminate/container": "^10.0",
29-
"illuminate/database": "^10.30",
30-
"illuminate/events": "^10.0",
27+
"illuminate/support": "^10.0|^11",
28+
"illuminate/container": "^10.0|^11",
29+
"illuminate/database": "^10.30|^11",
30+
"illuminate/events": "^10.0|^11",
3131
"mongodb/mongodb": "^1.15"
3232
},
3333
"require-dev": {
3434
"phpunit/phpunit": "^10.3",
35-
"orchestra/testbench": "^8.0",
35+
"orchestra/testbench": "^8.0|^9.0",
3636
"mockery/mockery": "^1.4.4",
3737
"doctrine/coding-standard": "12.0.x-dev",
3838
"spatie/laravel-query-builder": "^5.6",
3939
"phpstan/phpstan": "^1.10"
4040
},
41+
"minimum-stability": "dev",
4142
"replace": {
4243
"jenssegers/mongodb": "self.version"
4344
},
@@ -66,9 +67,6 @@
6667
"cs:fix": "phpcbf"
6768
},
6869
"config": {
69-
"platform": {
70-
"php": "8.1"
71-
},
7270
"allow-plugins": {
7371
"dealerdirect/phpcodesniffer-composer-installer": true
7472
}

src/Eloquent/Builder.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
use Illuminate\Database\ConnectionInterface;
88
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
99
use MongoDB\Driver\Cursor;
10+
use MongoDB\Laravel\Collection;
1011
use MongoDB\Laravel\Helpers\QueriesRelationships;
12+
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
1113
use MongoDB\Model\BSONDocument;
1214

15+
use function array_intersect_key;
1316
use function array_key_exists;
1417
use function array_merge;
1518
use function collect;
@@ -183,6 +186,41 @@ public function raw($value = null)
183186
return $results;
184187
}
185188

189+
/**
190+
* Attempt to create the record if it does not exist with the matching attributes.
191+
* If the record exists, it will be returned.
192+
*
193+
* @param array $attributes The attributes to check for duplicate records
194+
* @param array $values The attributes to insert if no matching record is found
195+
*/
196+
public function createOrFirst(array $attributes = [], array $values = []): Model
197+
{
198+
// Apply casting and default values to the attributes
199+
$instance = $this->newModelInstance($values + $attributes);
200+
$values = $instance->getAttributes();
201+
$attributes = array_intersect_key($attributes, $values);
202+
203+
return $this->raw(function (Collection $collection) use ($attributes, $values) {
204+
$listener = new FindAndModifyCommandSubscriber();
205+
$collection->getManager()->addSubscriber($listener);
206+
207+
try {
208+
$document = $collection->findOneAndUpdate(
209+
$attributes,
210+
['$setOnInsert' => $values],
211+
['upsert' => true, 'new' => true, 'typeMap' => ['root' => 'array', 'document' => 'array']],
212+
);
213+
} finally {
214+
$collection->getManager()->removeSubscriber($listener);
215+
}
216+
217+
$model = $this->model->newFromBuilder($document);
218+
$model->wasRecentlyCreated = $listener->created;
219+
220+
return $model;
221+
});
222+
}
223+
186224
/**
187225
* Add the "updated at" column to an array of values.
188226
* TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e

src/Eloquent/Model.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace MongoDB\Laravel\Eloquent;
66

7+
use BackedEnum;
78
use Carbon\CarbonInterface;
89
use DateTimeInterface;
910
use DateTimeZone;
@@ -23,6 +24,7 @@
2324
use MongoDB\BSON\UTCDateTime;
2425
use MongoDB\Laravel\Query\Builder as QueryBuilder;
2526
use Stringable;
27+
use ValueError;
2628

2729
use function array_key_exists;
2830
use function array_keys;
@@ -40,10 +42,12 @@
4042
use function is_string;
4143
use function ltrim;
4244
use function method_exists;
45+
use function sprintf;
4346
use function str_contains;
4447
use function str_starts_with;
4548
use function strcmp;
4649
use function uniqid;
50+
use function var_export;
4751

4852
abstract class Model extends BaseModel
4953
{
@@ -695,7 +699,7 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
695699
}
696700

697701
if ($this->isEnumCastable($key) && (! $castValue instanceof Arrayable)) {
698-
$castValue = $castValue !== null ? $this->getStorableEnumValue($castValue) : null;
702+
$castValue = $castValue !== null ? $this->getStorableEnumValueFromLaravel11($this->getCasts()[$key], $castValue) : null;
699703
}
700704

701705
if ($castValue instanceof Arrayable) {
@@ -708,6 +712,23 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
708712
return $attributes;
709713
}
710714

715+
/**
716+
* Duplicate of {@see HasAttributes::getStorableEnumValue()} for Laravel 11 as the signature of the method has
717+
* changed in a non-backward compatible way.
718+
*
719+
* @todo Remove this method when support for Laravel 10 is dropped.
720+
*/
721+
private function getStorableEnumValueFromLaravel11($expectedEnum, $value)
722+
{
723+
if (! $value instanceof $expectedEnum) {
724+
throw new ValueError(sprintf('Value [%s] is not of the expected enum type [%s].', var_export($value, true), $expectedEnum));
725+
}
726+
727+
return $value instanceof BackedEnum
728+
? $value->value
729+
: $value->name;
730+
}
731+
711732
/**
712733
* Is a value a BSON type?
713734
*
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Internal;
6+
7+
use MongoDB\Driver\Monitoring\CommandFailedEvent;
8+
use MongoDB\Driver\Monitoring\CommandStartedEvent;
9+
use MongoDB\Driver\Monitoring\CommandSubscriber;
10+
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
11+
12+
/**
13+
* Track findAndModify command events to detect when a document is inserted or
14+
* updated.
15+
*
16+
* @internal
17+
*/
18+
final class FindAndModifyCommandSubscriber implements CommandSubscriber
19+
{
20+
public bool $created;
21+
22+
public function commandFailed(CommandFailedEvent $event)
23+
{
24+
}
25+
26+
public function commandStarted(CommandStartedEvent $event)
27+
{
28+
}
29+
30+
public function commandSucceeded(CommandSucceededEvent $event)
31+
{
32+
$this->created = ! $event->getReply()->lastErrorObject->updatedExisting;
33+
}
34+
}

src/Query/Builder.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -614,11 +614,7 @@ public function orderBy($column, $direction = 'asc')
614614
return $this;
615615
}
616616

617-
/**
618-
* @param list{mixed, mixed}|CarbonPeriod $values
619-
*
620-
* @inheritdoc
621-
*/
617+
/** @inheritdoc */
622618
public function whereBetween($column, iterable $values, $boolean = 'and', $not = false)
623619
{
624620
$type = 'between';

tests/ModelTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,4 +1044,43 @@ public function testNumericFieldName(): void
10441044
$this->assertInstanceOf(User::class, $found);
10451045
$this->assertEquals([3 => 'two.three'], $found[2]);
10461046
}
1047+
1048+
public function testCreateOrFirst()
1049+
{
1050+
$user1 = User::createOrFirst(['email' => 'taylorotwell@gmail.com']);
1051+
1052+
$this->assertSame('taylorotwell@gmail.com', $user1->email);
1053+
$this->assertNull($user1->name);
1054+
$this->assertTrue($user1->wasRecentlyCreated);
1055+
1056+
$user2 = User::createOrFirst(
1057+
['email' => 'taylorotwell@gmail.com'],
1058+
['name' => 'Taylor Otwell', 'birthday' => new DateTime('1987-05-28')],
1059+
);
1060+
1061+
$this->assertEquals($user1->id, $user2->id);
1062+
$this->assertSame('taylorotwell@gmail.com', $user2->email);
1063+
$this->assertNull($user2->name);
1064+
$this->assertNull($user2->birthday);
1065+
$this->assertFalse($user2->wasRecentlyCreated);
1066+
1067+
$user3 = User::createOrFirst(
1068+
['email' => 'abigailotwell@gmail.com'],
1069+
['name' => 'Abigail Otwell', 'birthday' => new DateTime('1987-05-28')],
1070+
);
1071+
1072+
$this->assertNotEquals($user3->id, $user1->id);
1073+
$this->assertSame('abigailotwell@gmail.com', $user3->email);
1074+
$this->assertSame('Abigail Otwell', $user3->name);
1075+
$this->assertEquals(new DateTime('1987-05-28'), $user3->birthday);
1076+
$this->assertTrue($user3->wasRecentlyCreated);
1077+
1078+
$user4 = User::createOrFirst(
1079+
['name' => 'Dries Vints'],
1080+
['name' => 'Nuno Maduro', 'email' => 'nuno@laravel.com'],
1081+
);
1082+
1083+
$this->assertSame('Nuno Maduro', $user4->name);
1084+
$this->assertTrue($user4->wasRecentlyCreated);
1085+
}
10471086
}

tests/Query/BuilderTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,12 @@ function (Builder $builder) {
564564
yield 'whereBetween CarbonPeriod' => [
565565
[
566566
'find' => [
567-
['created_at' => ['$gte' => new UTCDateTime($period->start), '$lte' => new UTCDateTime($period->end)]],
567+
[
568+
'created_at' => [
569+
'$gte' => new UTCDateTime($period->getStartDate()),
570+
'$lte' => new UTCDateTime($period->getEndDate()),
571+
],
572+
],
568573
[], // options
569574
],
570575
],

0 commit comments

Comments
 (0)