Skip to content

Commit cc667c3

Browse files
authored
Merge branch '4.2' into merge-4.1-into-4.2-1714153363009
2 parents 3bc0c4a + 7654b17 commit cc667c3

17 files changed

+389
-34
lines changed

.github/workflows/build-ci.yml

+15-3
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,22 @@ 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+
os: "ubuntu-latest"
35+
- php: "8.4"
36+
laravel: "11.*"
37+
mongodb: "7.0"
38+
mode: "ignore-php-req"
39+
os: "ubuntu-latest"
40+
exclude:
41+
- php: "8.1"
42+
laravel: "11.*"
3343

3444
steps:
3545
- uses: "actions/checkout@v4"
@@ -76,8 +86,10 @@ jobs:
7686
restore-keys: "${{ matrix.os }}-composer-"
7787

7888
- name: "Install dependencies"
79-
run: composer update --no-interaction $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest --prefer-stable')
80-
89+
run: |
90+
composer update --no-interaction \
91+
$([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest') \
92+
$([[ "${{ matrix.mode }}" == ignore-php-req ]] && echo ' --ignore-platform-req=php+')
8193
- name: "Run tests"
8294
run: "./vendor/bin/phpunit --coverage-clover coverage.xml"
8395
env:

CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [4.2.2] - 2024-04-25
5+
6+
* Add return types to `FindAndModifyCommandSubscriber`, used by `firstOrCreate` by @wivaku in [#2913](https://github.com/mongodb/laravel-mongodb/pull/2913)
7+
8+
## [4.2.1] - 2024-04-25
9+
10+
* Set timestamps when using `Model::createOrFirst()` by @GromNaN in [#2905](https://github.com/mongodb/laravel-mongodb/pull/2905)
11+
12+
## [4.2.0] - 2024-03-14
13+
14+
* Add support for Laravel 11 by @GromNaN in [#2735](https://github.com/mongodb/laravel-mongodb/pull/2735)
15+
* Implement `Model::createOrFirst()` using findOneAndUpdate operation by @GromNaN in [#2742](https://github.com/mongodb/laravel-mongodb/pull/2742)
16+
417
## [4.1.3] - 2024-03-05
518

619
* 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

+6-8
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
}

docs/fundamentals/read-operations.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ documents.
383383
Runtime: 95
384384
IMDB Rating: 4
385385
IMDB Votes: 9296
386-
Plot: A sci-fi update of the famous 6th Century poem. In a beseiged land, Beowulf must
386+
Plot: A sci-fi update of the famous 6th Century poem. In a besieged land, Beowulf must
387387
battle against the hideous creature Grendel and his vengeance seeking mother.
388388

389389
.. _laravel-sort:

docs/includes/framework-compatibility-laravel.rst

+8
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
:stub-columns: 1
44

55
* - {+odm-short+} Version
6+
- Laravel 11.x
67
- Laravel 10.x
78
- Laravel 9.x
89

10+
* - 4.2
11+
- ✓
12+
- ✓
13+
-
14+
915
* - 4.1
16+
-
1017
- ✓
1118
-
1219

1320
* - 4.0
21+
-
1422
- ✓
1523
-
1624

docs/quick-start.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ that connects to a MongoDB deployment.
5353
.. tip::
5454

5555
You can download the complete web application project by cloning the
56-
`laravel-quickstart <https://github.com/mongodb-university/laravel-quickstart>`__
56+
`laravel-quickstart <https://github.com/mongodb-university/laravel-quickstart/>`__
5757
GitHub repository.
5858

5959
.. button:: Next: Download and Install

docs/quick-start/configure-mongodb.txt

+7-2
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ Configure Your MongoDB Connection
7373

7474
.. step:: Add the {+odm-short+} provider
7575

76-
Open the ``app.php`` file in the ``config`` directory and
77-
add the following entry into the ``providers`` array:
76+
Open the ``providers.php`` file in the ``bootstrap`` directory and add
77+
the following entry into the array:
7878

7979
.. code-block::
8080

8181
MongoDB\Laravel\MongoDBServiceProvider::class,
8282

83+
.. tip::
84+
85+
To learn how to register the provider in Laravel 10.x, see
86+
`Registering Providers <https://laravel.com/docs/10.x/providers#registering-providers>`__.
87+
8388
After completing these steps, your Laravel web application is ready to
8489
connect to MongoDB.
8590

docs/quick-start/view-data.txt

+7-7
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ View MongoDB Data
6767

6868
public function show()
6969
{
70-
return view('browse_movies', [
71-
'movies' => Movie::where('runtime', '<', 60)
72-
->where('imdb.rating', '>', 8.5)
73-
->orderBy('imdb.rating', 'desc')
74-
->take(10)
75-
->get()
76-
]);
70+
return view('browse_movies', [
71+
'movies' => Movie::where('runtime', '<', 60)
72+
->where('imdb.rating', '>', 8.5)
73+
->orderBy('imdb.rating', 'desc')
74+
->take(10)
75+
->get()
76+
]);
7777
}
7878

7979
.. step:: Add a web route

docs/quick-start/write-data.txt

+14-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ Write Data to MongoDB
3232

3333
.. step:: Add an API route that calls the controller function
3434

35+
Generate an API route file by running the following command:
36+
37+
.. code-block:: bash
38+
39+
php artisan install:api
40+
41+
.. tip::
42+
43+
Skip this step if you are using Laravel 10.x because the file that
44+
the command generates already exists.
45+
3546
Import the controller and add an API route that calls the ``store()``
3647
method in the ``routes/api.php`` file:
3748

@@ -42,7 +53,7 @@ Write Data to MongoDB
4253
// ...
4354

4455
Route::resource('movies', MovieController::class)->only([
45-
'store'
56+
'store'
4657
]);
4758

4859

@@ -57,8 +68,8 @@ Write Data to MongoDB
5768

5869
class Movie extends Model
5970
{
60-
protected $connection = 'mongodb';
61-
protected $fillable = ['title', 'year', 'runtime', 'imdb', 'plot'];
71+
protected $connection = 'mongodb';
72+
protected $fillable = ['title', 'year', 'runtime', 'imdb', 'plot'];
6273
}
6374

6475
.. step:: Post a request to the API

src/Connection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ private static function lookupVersion(): string
328328
try {
329329
return self::$version = InstalledVersions::getPrettyVersion('mongodb/laravel-mongodb');
330330
} catch (Throwable) {
331-
// Ignore exceptions and return unknown version
331+
return self::$version = 'error';
332332
}
333333
}
334334

src/Eloquent/Builder.php

+57
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66

77
use Illuminate\Database\ConnectionInterface;
88
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
9+
use InvalidArgumentException;
910
use MongoDB\Driver\Cursor;
11+
use MongoDB\Laravel\Collection;
1012
use MongoDB\Laravel\Helpers\QueriesRelationships;
13+
use MongoDB\Laravel\Internal\FindAndModifyCommandSubscriber;
1114
use MongoDB\Model\BSONDocument;
15+
use MongoDB\Operation\FindOneAndUpdate;
1216

17+
use function array_intersect_key;
1318
use function array_key_exists;
1419
use function array_merge;
1520
use function collect;
@@ -183,6 +188,58 @@ public function raw($value = null)
183188
return $results;
184189
}
185190

191+
/**
192+
* Attempt to create the record if it does not exist with the matching attributes.
193+
* If the record exists, it will be returned.
194+
*
195+
* @param array $attributes The attributes to check for duplicate records
196+
* @param array $values The attributes to insert if no matching record is found
197+
*/
198+
public function createOrFirst(array $attributes = [], array $values = []): Model
199+
{
200+
if ($attributes === []) {
201+
throw new InvalidArgumentException('You must provide attributes to check for duplicates');
202+
}
203+
204+
// Apply casting and default values to the attributes
205+
// In case of duplicate key between the attributes and the values, the values have priority
206+
$instance = $this->newModelInstance($values + $attributes);
207+
208+
/* @see \Illuminate\Database\Eloquent\Model::performInsert */
209+
if ($instance->usesTimestamps()) {
210+
$instance->updateTimestamps();
211+
}
212+
213+
$values = $instance->getAttributes();
214+
$attributes = array_intersect_key($attributes, $values);
215+
216+
return $this->raw(function (Collection $collection) use ($attributes, $values) {
217+
$listener = new FindAndModifyCommandSubscriber();
218+
$collection->getManager()->addSubscriber($listener);
219+
220+
try {
221+
$document = $collection->findOneAndUpdate(
222+
$attributes,
223+
// Before MongoDB 5.0, $setOnInsert requires a non-empty document.
224+
// This is should not be an issue as $values includes the query filter.
225+
['$setOnInsert' => (object) $values],
226+
[
227+
'upsert' => true,
228+
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
229+
'typeMap' => ['root' => 'array', 'document' => 'array'],
230+
],
231+
);
232+
} finally {
233+
$collection->getManager()->removeSubscriber($listener);
234+
}
235+
236+
$model = $this->model->newFromBuilder($document);
237+
$model->wasRecentlyCreated = $listener->created;
238+
239+
return $model;
240+
});
241+
}
242+
186243
/**
187244
* Add the "updated at" column to an array of values.
188245
* TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e

src/Eloquent/Model.php

+22-1
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
*
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): void
23+
{
24+
}
25+
26+
public function commandStarted(CommandStartedEvent $event): void
27+
{
28+
}
29+
30+
public function commandSucceeded(CommandSucceededEvent $event): void
31+
{
32+
$this->created = ! $event->getReply()->lastErrorObject->updatedExisting;
33+
}
34+
}

0 commit comments

Comments
 (0)