From db830f8b3fc272edb03314ffd1235c69b7c48977 Mon Sep 17 00:00:00 2001 From: Mike Woofter Date: Wed, 8 May 2024 15:30:01 -0500 Subject: [PATCH 01/26] typo fix --- docs/fundamentals/aggregation-builder.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fundamentals/aggregation-builder.txt b/docs/fundamentals/aggregation-builder.txt index 0fc55bcf4..79d650499 100644 --- a/docs/fundamentals/aggregation-builder.txt +++ b/docs/fundamentals/aggregation-builder.txt @@ -404,7 +404,7 @@ stages perform the following operations sequentially: extracted from the ``birthday`` field. - Group the documents by the value of the ``occupation`` field and compute the average value of ``birth_year`` for each group by using the - ``Accumulator::avg()`` function. Assign to result of the computation to + ``Accumulator::avg()`` function. Assign the result of the computation to the ``birth_year_avg`` field. - Sort the documents by the group key field in ascending order. - Create the ``profession`` field from the value of the group key field, From ed0a122a529014714cede17376e31455554b06d9 Mon Sep 17 00:00:00 2001 From: Nora Reidy Date: Thu, 9 May 2024 13:32:02 -0400 Subject: [PATCH 02/26] DOCSP-36722: v4.0 minimum version (#2940) Adds a footnote about Laravel MongoDB v4.0's minimum Laravel version --- docs/includes/framework-compatibility-laravel.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/includes/framework-compatibility-laravel.rst b/docs/includes/framework-compatibility-laravel.rst index 9b39db4ea..62c92d666 100644 --- a/docs/includes/framework-compatibility-laravel.rst +++ b/docs/includes/framework-compatibility-laravel.rst @@ -11,6 +11,7 @@ - * - 4.0 - - ✓ + - ✓ [#min-version-note]_ - +.. [#min-version-note] {+odm-short+} v4.0 is compatible with Laravel v9.3.9 and later. \ No newline at end of file From 773e65f703acbd271c57dbe115ad8a36d3a0fa6b Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 10 May 2024 09:28:40 +0200 Subject: [PATCH 03/26] PHPORM-181: Add SBOM lite (#2937) --- sbom.json | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 sbom.json diff --git a/sbom.json b/sbom.json new file mode 100644 index 000000000..432ded6c2 --- /dev/null +++ b/sbom.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:0b622e40-f57d-4c6f-9f63-db415c1a1271", + "version": 1, + "metadata": { + "timestamp": "2024-05-08T09:52:55Z", + "tools": [ + { + "name": "composer", + "version": "2.7.6" + }, + { + "vendor": "cyclonedx", + "name": "cyclonedx-php-composer", + "version": "v5.2.0", + "externalReferences": [ + { + "type": "distribution", + "url": "https://api.github.com/repos/CycloneDX/cyclonedx-php-composer/zipball/f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed", + "comment": "dist reference: f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-php-composer.git", + "comment": "source reference: f3a3cdc1a9e34bf1d5748e4279a24569cbf31fed" + }, + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-php-composer/#readme", + "comment": "as detected from Composer manifest 'homepage'" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-php-composer/issues", + "comment": "as detected from Composer manifest 'support.issues'" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-php-composer/", + "comment": "as detected from Composer manifest 'support.source'" + } + ] + }, + { + "vendor": "cyclonedx", + "name": "cyclonedx-library", + "version": "3.x-dev cad0f92", + "externalReferences": [ + { + "type": "distribution", + "url": "https://api.github.com/repos/CycloneDX/cyclonedx-php-library/zipball/cad0f92b36c85f36b3d3c11ff96002af5f20cd10", + "comment": "dist reference: cad0f92b36c85f36b3d3c11ff96002af5f20cd10" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-php-library.git", + "comment": "source reference: cad0f92b36c85f36b3d3c11ff96002af5f20cd10" + }, + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-php-library/#readme", + "comment": "as detected from Composer manifest 'homepage'" + }, + { + "type": "documentation", + "url": "https://cyclonedx-php-library.readthedocs.io/", + "comment": "as detected from Composer manifest 'support.docs'" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-php-library/issues", + "comment": "as detected from Composer manifest 'support.issues'" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-php-library/", + "comment": "as detected from Composer manifest 'support.source'" + } + ] + } + ] + } +} From d10384955bc4a5a51e903225d255d62ecf45b452 Mon Sep 17 00:00:00 2001 From: Nora Reidy Date: Mon, 20 May 2024 08:48:43 -0400 Subject: [PATCH 04/26] Revert "DOCSP-36722: v4.0 minimum version (#2940)" (#2947) This reverts commit ed0a122a529014714cede17376e31455554b06d9. --- docs/includes/framework-compatibility-laravel.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/includes/framework-compatibility-laravel.rst b/docs/includes/framework-compatibility-laravel.rst index 62c92d666..9b39db4ea 100644 --- a/docs/includes/framework-compatibility-laravel.rst +++ b/docs/includes/framework-compatibility-laravel.rst @@ -11,7 +11,6 @@ - * - 4.0 - - ✓ [#min-version-note]_ + - ✓ - -.. [#min-version-note] {+odm-short+} v4.0 is compatible with Laravel v9.3.9 and later. \ No newline at end of file From b040bef2b6e89e47df0c7d48687dad48ecd913e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 21 May 2024 17:00:47 +0200 Subject: [PATCH 05/26] PHPORM-81 implement `mongodb` driver for batch (#2904) --- CHANGELOG.md | 3 + composer.json | 6 +- docs/queues.txt | 96 ++++++- phpstan-baseline.neon | 5 + src/Bus/MongoBatchRepository.php | 278 +++++++++++++++++++ src/MongoDBBusServiceProvider.php | 46 ++++ src/MongoDBQueueServiceProvider.php | 14 +- src/Queue/MongoConnector.php | 20 +- tests/Bus/Fixtures/ChainHeadJob.php | 15 + tests/Bus/Fixtures/SecondTestJob.php | 15 + tests/Bus/Fixtures/ThirdTestJob.php | 15 + tests/Bus/MongoBatchRepositoryTest.php | 364 +++++++++++++++++++++++++ 12 files changed, 864 insertions(+), 13 deletions(-) create mode 100644 src/Bus/MongoBatchRepository.php create mode 100644 src/MongoDBBusServiceProvider.php create mode 100644 tests/Bus/Fixtures/ChainHeadJob.php create mode 100644 tests/Bus/Fixtures/SecondTestJob.php create mode 100644 tests/Bus/Fixtures/ThirdTestJob.php create mode 100644 tests/Bus/MongoBatchRepositoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec142b46..318e340e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [4.4.0] - unreleased * Support collection name prefix by @GromNaN in [#2930](https://github.com/mongodb/laravel-mongodb/pull/2930) +* Add `mongodb` driver for Batching by @GromNaN in [#2904](https://github.com/mongodb/laravel-mongodb/pull/2904) +* Rename queue option `table` to `collection` +* Replace queue option `expire` with `retry_after` ## [4.3.0] - 2024-04-26 diff --git a/composer.json b/composer.json index 8c038819e..84229b00f 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,9 @@ "spatie/laravel-query-builder": "^5.6", "phpstan/phpstan": "^1.10" }, + "conflict": { + "illuminate/bus": "< 10.37.2" + }, "suggest": { "mongodb/builder": "Provides a fluent aggregation builder for MongoDB pipelines" }, @@ -62,7 +65,8 @@ "laravel": { "providers": [ "MongoDB\\Laravel\\MongoDBServiceProvider", - "MongoDB\\Laravel\\MongoDBQueueServiceProvider" + "MongoDB\\Laravel\\MongoDBQueueServiceProvider", + "MongoDB\\Laravel\\MongoDBBusServiceProvider" ] } }, diff --git a/docs/queues.txt b/docs/queues.txt index 330662913..ccac29ba6 100644 --- a/docs/queues.txt +++ b/docs/queues.txt @@ -11,7 +11,7 @@ Queues .. meta:: :keywords: php framework, odm, code example -If you want to use MongoDB as your database backend for Laravel Queue, change +If you want to use MongoDB as your database backend for Laravel Queue, change the driver in ``config/queue.php``: .. code-block:: php @@ -20,27 +20,107 @@ the driver in ``config/queue.php``: 'database' => [ 'driver' => 'mongodb', // You can also specify your jobs specific database created on config/database.php - 'connection' => 'mongodb-job', - 'table' => 'jobs', + 'connection' => 'mongodb', + 'collection' => 'jobs', 'queue' => 'default', - 'expire' => 60, + 'retry_after' => 60, ], ], -If you want to use MongoDB to handle failed jobs, change the database in +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Setting + - Description + + * - ``driver`` + - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + + * - ``connection`` + - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + + * - ``collection`` + - **Required**. Name of the MongoDB collection to store jobs to process. + + * - ``queue`` + - **Required**. Name of the queue. + + * - ``retry_after`` + - Specifies how many seconds the queue connection should wait before retrying a job that is being processed. Defaults to ``60``. + +If you want to use MongoDB to handle failed jobs, change the database in ``config/queue.php``: .. code-block:: php 'failed' => [ 'driver' => 'mongodb', - // You can also specify your jobs specific database created on config/database.php - 'database' => 'mongodb-job', - 'table' => 'failed_jobs', + 'database' => 'mongodb', + 'collection' => 'failed_jobs', ], +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Setting + - Description + + * - ``driver`` + - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + + * - ``connection`` + - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + + * - ``collection`` + - Name of the MongoDB collection to store failed jobs. Defaults to ``failed_jobs``. + + Add the service provider in ``config/app.php``: .. code-block:: php MongoDB\Laravel\MongoDBQueueServiceProvider::class, + + +Job Batching +------------ + +`Job batching `__ +is a Laravel feature to execute a batch of jobs and subsequent actions before, +after, and during the execution of the jobs from the queue. + +With MongoDB, you don't have to create any collection before using job batching. +The ``job_batches`` collection is created automatically to store meta +information about your job batches, such as their completion percentage. + +.. code-block:: php + + 'batching' => [ + 'driver' => 'mongodb', + 'database' => 'mongodb', + 'collection' => 'job_batches', + ], + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Setting + - Description + + * - ``driver`` + - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + + * - ``connection`` + - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + + * - ``collection`` + - Name of the MongoDB collection to store job batches. Defaults to ``job_batches``. + +Add the service provider in ``config/app.php``: + +.. code-block:: php + + MongoDB\Laravel\MongoDBBusServiceProvider::class, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 99579fa0a..fdef24410 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,10 @@ parameters: ignoreErrors: + - + message: "#^Access to an undefined property Illuminate\\\\Container\\\\Container\\:\\:\\$config\\.$#" + count: 3 + path: src/MongoDBBusServiceProvider.php + - message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#" count: 2 diff --git a/src/Bus/MongoBatchRepository.php b/src/Bus/MongoBatchRepository.php new file mode 100644 index 000000000..dd0713f97 --- /dev/null +++ b/src/Bus/MongoBatchRepository.php @@ -0,0 +1,278 @@ +collection = $connection->getCollection($collection); + + parent::__construct($factory, $connection, $collection); + } + + #[Override] + public function get($limit = 50, $before = null): array + { + if (is_string($before)) { + $before = new ObjectId($before); + } + + return $this->collection->find( + $before ? ['_id' => ['$lt' => $before]] : [], + [ + 'limit' => $limit, + 'sort' => ['_id' => -1], + 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'], + ], + )->toArray(); + } + + #[Override] + public function find(string $batchId): ?Batch + { + $batchId = new ObjectId($batchId); + + $batch = $this->collection->findOne( + ['_id' => $batchId], + [ + // If the select query is executed faster than the database replication takes place, + // then no batch is found. In that case an exception is thrown because jobs are added + // to a null batch. + 'readPreference' => new ReadPreference(ReadPreference::PRIMARY), + 'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array'], + ], + ); + + return $batch ? $this->toBatch($batch) : null; + } + + #[Override] + public function store(PendingBatch $batch): Batch + { + $batch = [ + 'name' => $batch->name, + 'total_jobs' => 0, + 'pending_jobs' => 0, + 'failed_jobs' => 0, + 'failed_job_ids' => [], + // Serialization is required for Closures + 'options' => serialize($batch->options), + 'created_at' => $this->getUTCDateTime(), + 'cancelled_at' => null, + 'finished_at' => null, + ]; + $result = $this->collection->insertOne($batch); + + return $this->toBatch(['_id' => $result->getInsertedId()] + $batch); + } + + #[Override] + public function incrementTotalJobs(string $batchId, int $amount): void + { + $batchId = new ObjectId($batchId); + $this->collection->updateOne( + ['_id' => $batchId], + [ + '$inc' => [ + 'total_jobs' => $amount, + 'pending_jobs' => $amount, + ], + '$set' => [ + 'finished_at' => null, + ], + ], + ); + } + + #[Override] + public function decrementPendingJobs(string $batchId, string $jobId): UpdatedBatchJobCounts + { + $batchId = new ObjectId($batchId); + $values = $this->collection->findOneAndUpdate( + ['_id' => $batchId], + [ + '$inc' => ['pending_jobs' => -1], + '$pull' => ['failed_job_ids' => $jobId], + ], + [ + 'projection' => ['pending_jobs' => 1, 'failed_jobs' => 1], + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, + ], + ); + + return new UpdatedBatchJobCounts( + $values['pending_jobs'], + $values['failed_jobs'], + ); + } + + #[Override] + public function incrementFailedJobs(string $batchId, string $jobId): UpdatedBatchJobCounts + { + $batchId = new ObjectId($batchId); + $values = $this->collection->findOneAndUpdate( + ['_id' => $batchId], + [ + '$inc' => ['failed_jobs' => 1], + '$push' => ['failed_job_ids' => $jobId], + ], + [ + 'projection' => ['pending_jobs' => 1, 'failed_jobs' => 1], + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, + ], + ); + + return new UpdatedBatchJobCounts( + $values['pending_jobs'], + $values['failed_jobs'], + ); + } + + #[Override] + public function markAsFinished(string $batchId): void + { + $batchId = new ObjectId($batchId); + $this->collection->updateOne( + ['_id' => $batchId], + ['$set' => ['finished_at' => $this->getUTCDateTime()]], + ); + } + + #[Override] + public function cancel(string $batchId): void + { + $batchId = new ObjectId($batchId); + $this->collection->updateOne( + ['_id' => $batchId], + [ + '$set' => [ + 'cancelled_at' => $this->getUTCDateTime(), + 'finished_at' => $this->getUTCDateTime(), + ], + ], + ); + } + + #[Override] + public function delete(string $batchId): void + { + $batchId = new ObjectId($batchId); + $this->collection->deleteOne(['_id' => $batchId]); + } + + /** Execute the given Closure within a storage specific transaction. */ + #[Override] + public function transaction(Closure $callback): mixed + { + return $this->connection->transaction($callback); + } + + /** Rollback the last database transaction for the connection. */ + #[Override] + public function rollBack(): void + { + $this->connection->rollBack(); + } + + /** Prune the entries older than the given date. */ + #[Override] + public function prune(DateTimeInterface $before): int + { + $result = $this->collection->deleteMany( + ['finished_at' => ['$ne' => null, '$lt' => new UTCDateTime($before)]], + ); + + return $result->getDeletedCount(); + } + + /** Prune all the unfinished entries older than the given date. */ + public function pruneUnfinished(DateTimeInterface $before): int + { + $result = $this->collection->deleteMany( + [ + 'finished_at' => null, + 'created_at' => ['$lt' => new UTCDateTime($before)], + ], + ); + + return $result->getDeletedCount(); + } + + /** Prune all the cancelled entries older than the given date. */ + public function pruneCancelled(DateTimeInterface $before): int + { + $result = $this->collection->deleteMany( + [ + 'cancelled_at' => ['$ne' => null], + 'created_at' => ['$lt' => new UTCDateTime($before)], + ], + ); + + return $result->getDeletedCount(); + } + + /** @param array $batch */ + #[Override] + protected function toBatch($batch): Batch + { + return $this->factory->make( + $this, + $batch['_id'], + $batch['name'], + $batch['total_jobs'], + $batch['pending_jobs'], + $batch['failed_jobs'], + $batch['failed_job_ids'], + unserialize($batch['options']), + $this->toCarbon($batch['created_at']), + $this->toCarbon($batch['cancelled_at']), + $this->toCarbon($batch['finished_at']), + ); + } + + private function getUTCDateTime(): UTCDateTime + { + // Using Carbon so the current time can be modified for tests + return new UTCDateTime(Carbon::now()); + } + + /** @return ($date is null ? null : CarbonImmutable) */ + private function toCarbon(?UTCDateTime $date): ?CarbonImmutable + { + if ($date === null) { + return null; + } + + return CarbonImmutable::createFromTimestampMsUTC((string) $date); + } +} diff --git a/src/MongoDBBusServiceProvider.php b/src/MongoDBBusServiceProvider.php new file mode 100644 index 000000000..c77ccd118 --- /dev/null +++ b/src/MongoDBBusServiceProvider.php @@ -0,0 +1,46 @@ +app->singleton(MongoBatchRepository::class, function (Container $app) { + return new MongoBatchRepository( + $app->make(BatchFactory::class), + $app->make('db')->connection($app->config->get('queue.batching.database')), + $app->config->get('queue.batching.collection', 'job_batches'), + ); + }); + + /** @see BusServiceProvider::registerBatchServices() */ + $this->app->extend(BatchRepository::class, function (BatchRepository $repository, Container $app) { + $driver = $app->config->get('queue.batching.driver'); + + return match ($driver) { + 'mongodb' => $app->make(MongoBatchRepository::class), + default => $repository, + }; + }); + } + + public function provides() + { + return [ + BatchRepository::class, + MongoBatchRepository::class, + ]; + } +} diff --git a/src/MongoDBQueueServiceProvider.php b/src/MongoDBQueueServiceProvider.php index aa67f7405..ea7a06176 100644 --- a/src/MongoDBQueueServiceProvider.php +++ b/src/MongoDBQueueServiceProvider.php @@ -9,6 +9,9 @@ use MongoDB\Laravel\Queue\Failed\MongoFailedJobProvider; use function array_key_exists; +use function trigger_error; + +use const E_USER_DEPRECATED; class MongoDBQueueServiceProvider extends QueueServiceProvider { @@ -51,6 +54,15 @@ protected function registerFailedJobServices() */ protected function mongoFailedJobProvider(array $config): MongoFailedJobProvider { - return new MongoFailedJobProvider($this->app['db'], $config['database'], $config['table']); + if (! isset($config['collection']) && isset($config['table'])) { + trigger_error('Since mongodb/laravel-mongodb 4.4: Using "table" option for the queue is deprecated. Use "collection" instead.', E_USER_DEPRECATED); + $config['collection'] = $config['table']; + } + + return new MongoFailedJobProvider( + $this->app['db'], + $config['database'] ?? null, + $config['collection'] ?? 'failed_jobs', + ); } } diff --git a/src/Queue/MongoConnector.php b/src/Queue/MongoConnector.php index 4f987694a..be51d4fe1 100644 --- a/src/Queue/MongoConnector.php +++ b/src/Queue/MongoConnector.php @@ -8,6 +8,10 @@ use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Queue\Connectors\ConnectorInterface; +use function trigger_error; + +use const E_USER_DEPRECATED; + class MongoConnector implements ConnectorInterface { /** @@ -32,11 +36,21 @@ public function __construct(ConnectionResolverInterface $connections) */ public function connect(array $config) { + if (! isset($config['collection']) && isset($config['table'])) { + trigger_error('Since mongodb/laravel-mongodb 4.4: Using "table" option in queue configuration is deprecated. Use "collection" instead.', E_USER_DEPRECATED); + $config['collection'] = $config['table']; + } + + if (! isset($config['retry_after']) && isset($config['expire'])) { + trigger_error('Since mongodb/laravel-mongodb 4.4: Using "expire" option in queue configuration is deprecated. Use "retry_after" instead.', E_USER_DEPRECATED); + $config['retry_after'] = $config['expire']; + } + return new MongoQueue( $this->connections->connection($config['connection'] ?? null), - $config['table'], - $config['queue'], - $config['expire'] ?? 60, + $config['collection'] ?? 'jobs', + $config['queue'] ?? 'default', + $config['retry_after'] ?? 60, ); } } diff --git a/tests/Bus/Fixtures/ChainHeadJob.php b/tests/Bus/Fixtures/ChainHeadJob.php new file mode 100644 index 000000000..c964e59f9 --- /dev/null +++ b/tests/Bus/Fixtures/ChainHeadJob.php @@ -0,0 +1,15 @@ +getCollection('job_batches')->drop(); + + unset( + $_SERVER['__catch.batch'], + $_SERVER['__catch.count'], + $_SERVER['__catch.exception'], + $_SERVER['__finally.batch'], + $_SERVER['__finally.count'], + $_SERVER['__progress.batch'], + $_SERVER['__progress.count'], + $_SERVER['__then.batch'], + $_SERVER['__then.count'], + ); + + parent::tearDown(); + } + + /** @see BusBatchTest::test_jobs_can_be_added_to_the_batch */ + public function testJobsCanBeAddedToTheBatch(): void + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $thirdJob = function () { + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once()->with(m::on(function ($args) use ($job, $secondJob) { + return $args[0] === $job && + $args[1] === $secondJob && + $args[2] instanceof CallQueuedClosure + && is_string($args[2]->batchId); + }), '', 'test-queue'); + + $batch = $batch->add([$job, $secondJob, $thirdJob]); + + $this->assertEquals(3, $batch->totalJobs); + $this->assertEquals(3, $batch->pendingJobs); + $this->assertIsString($job->batchId); + $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); + } + + /** @see BusBatchTest::test_successful_jobs_can_be_recorded */ + public function testSuccessfulJobsCanBeRecorded() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once(); + + $batch = $batch->add([$job, $secondJob]); + $this->assertEquals(2, $batch->pendingJobs); + + $batch->recordSuccessfulJob('test-id'); + $batch->recordSuccessfulJob('test-id'); + + $this->assertInstanceOf(Batch::class, $_SERVER['__finally.batch']); + $this->assertInstanceOf(Batch::class, $_SERVER['__progress.batch']); + $this->assertInstanceOf(Batch::class, $_SERVER['__then.batch']); + + $batch = $batch->fresh(); + $this->assertEquals(0, $batch->pendingJobs); + $this->assertTrue($batch->finished()); + $this->assertEquals(1, $_SERVER['__finally.count']); + $this->assertEquals(2, $_SERVER['__progress.count']); + $this->assertEquals(1, $_SERVER['__then.count']); + } + + /** @see BusBatchTest::test_failed_jobs_can_be_recorded_while_not_allowing_failures */ + public function testFailedJobsCanBeRecordedWhileNotAllowingFailures() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue, $allowFailures = false); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once(); + + $batch = $batch->add([$job, $secondJob]); + $this->assertEquals(2, $batch->pendingJobs); + + $batch->recordFailedJob('test-id', new RuntimeException('Something went wrong.')); + $batch->recordFailedJob('test-id', new RuntimeException('Something else went wrong.')); + + $this->assertInstanceOf(Batch::class, $_SERVER['__finally.batch']); + $this->assertFalse(isset($_SERVER['__then.batch'])); + + $batch = $batch->fresh(); + $this->assertEquals(2, $batch->pendingJobs); + $this->assertEquals(2, $batch->failedJobs); + $this->assertTrue($batch->finished()); + $this->assertTrue($batch->cancelled()); + $this->assertEquals(1, $_SERVER['__finally.count']); + $this->assertEquals(0, $_SERVER['__progress.count']); + $this->assertEquals(1, $_SERVER['__catch.count']); + $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); + } + + /** @see BusBatchTest::test_failed_jobs_can_be_recorded_while_allowing_failures */ + public function testFailedJobsCanBeRecordedWhileAllowingFailures() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue, $allowFailures = true); + + $job = new class + { + use Batchable; + }; + + $secondJob = new class + { + use Batchable; + }; + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once(); + + $batch = $batch->add([$job, $secondJob]); + $this->assertEquals(2, $batch->pendingJobs); + + $batch->recordFailedJob('test-id', new RuntimeException('Something went wrong.')); + $batch->recordFailedJob('test-id', new RuntimeException('Something else went wrong.')); + + // While allowing failures this batch never actually completes... + $this->assertFalse(isset($_SERVER['__then.batch'])); + + $batch = $batch->fresh(); + $this->assertEquals(2, $batch->pendingJobs); + $this->assertEquals(2, $batch->failedJobs); + $this->assertFalse($batch->finished()); + $this->assertFalse($batch->cancelled()); + $this->assertEquals(1, $_SERVER['__catch.count']); + $this->assertEquals(2, $_SERVER['__progress.count']); + $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); + } + + /** @see BusBatchTest::test_batch_can_be_cancelled */ + public function testBatchCanBeCancelled() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $batch->cancel(); + + $batch = $batch->fresh(); + + $this->assertTrue($batch->cancelled()); + } + + /** @see BusBatchTest::test_batch_can_be_deleted */ + public function testBatchCanBeDeleted() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $batch->delete(); + + $batch = $batch->fresh(); + + $this->assertNull($batch); + } + + /** @see BusBatchTest::test_batch_state_can_be_inspected */ + public function testBatchStateCanBeInspected() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $this->assertFalse($batch->finished()); + $batch->finishedAt = now(); + $this->assertTrue($batch->finished()); + + $batch->options['progress'] = []; + $this->assertFalse($batch->hasProgressCallbacks()); + $batch->options['progress'] = [1]; + $this->assertTrue($batch->hasProgressCallbacks()); + + $batch->options['then'] = []; + $this->assertFalse($batch->hasThenCallbacks()); + $batch->options['then'] = [1]; + $this->assertTrue($batch->hasThenCallbacks()); + + $this->assertFalse($batch->allowsFailures()); + $batch->options['allowFailures'] = true; + $this->assertTrue($batch->allowsFailures()); + + $this->assertFalse($batch->hasFailures()); + $batch->failedJobs = 1; + $this->assertTrue($batch->hasFailures()); + + $batch->options['catch'] = []; + $this->assertFalse($batch->hasCatchCallbacks()); + $batch->options['catch'] = [1]; + $this->assertTrue($batch->hasCatchCallbacks()); + + $this->assertFalse($batch->cancelled()); + $batch->cancelledAt = now(); + $this->assertTrue($batch->cancelled()); + + $this->assertIsString(json_encode($batch)); + } + + /** @see BusBatchTest:test_chain_can_be_added_to_batch: */ + public function testChainCanBeAddedToBatch() + { + $queue = m::mock(Factory::class); + + $batch = $this->createTestBatch($queue); + + $chainHeadJob = new ChainHeadJob(); + + $secondJob = new SecondTestJob(); + + $thirdJob = new ThirdTestJob(); + + $queue->shouldReceive('connection')->once() + ->with('test-connection') + ->andReturn($connection = m::mock(stdClass::class)); + + $connection->shouldReceive('bulk')->once()->with(m::on(function ($args) use ($chainHeadJob, $secondJob, $thirdJob) { + return $args[0] === $chainHeadJob + && serialize($secondJob) === $args[0]->chained[0] + && serialize($thirdJob) === $args[0]->chained[1]; + }), '', 'test-queue'); + + $batch = $batch->add([ + [$chainHeadJob, $secondJob, $thirdJob], + ]); + + $this->assertEquals(3, $batch->totalJobs); + $this->assertEquals(3, $batch->pendingJobs); + $this->assertSame('test-queue', $chainHeadJob->chainQueue); + $this->assertIsString($chainHeadJob->batchId); + $this->assertIsString($secondJob->batchId); + $this->assertIsString($thirdJob->batchId); + $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); + } + + /** @see BusBatchTest::createTestBatch() */ + private function createTestBatch(Factory $queue, $allowFailures = false) + { + $connection = DB::connection('mongodb'); + $this->assertInstanceOf(Connection::class, $connection); + + $repository = new MongoBatchRepository(new BatchFactory($queue), $connection, 'job_batches'); + + $pendingBatch = (new PendingBatch(new Container(), collect())) + ->progress(function (Batch $batch) { + $_SERVER['__progress.batch'] = $batch; + $_SERVER['__progress.count']++; + }) + ->then(function (Batch $batch) { + $_SERVER['__then.batch'] = $batch; + $_SERVER['__then.count']++; + }) + ->catch(function (Batch $batch, $e) { + $_SERVER['__catch.batch'] = $batch; + $_SERVER['__catch.exception'] = $e; + $_SERVER['__catch.count']++; + }) + ->finally(function (Batch $batch) { + $_SERVER['__finally.batch'] = $batch; + $_SERVER['__finally.count']++; + }) + ->allowFailures($allowFailures) + ->onConnection('test-connection') + ->onQueue('test-queue'); + + return $repository->store($pendingBatch); + } +} From a7aecf838ae6519793493cd1a43d7ea0ad2a5fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 21 May 2024 22:51:43 +0200 Subject: [PATCH 06/26] PHPORM-184 Use fixed key for temporary setting nested field (#2962) --- CHANGELOG.md | 6 +++++- src/Eloquent/Model.php | 11 +++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd240b43..9eff79c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.3.0] - unreleased +## [4.3.1] + +* Fix memory leak when filling nested fields using dot notation by @GromNaN in [#2962](https://github.com/mongodb/laravel-mongodb/pull/2962) + +## [4.3.0] - 2024-04-26 * New aggregation pipeline builder by @GromNaN in [#2738](https://github.com/mongodb/laravel-mongodb/pull/2738) * Drop support for Composer 1.x by @GromNaN in [#2784](https://github.com/mongodb/laravel-mongodb/pull/2784) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index de5ddc3ea..5974e49e1 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -46,7 +46,6 @@ use function str_contains; use function str_starts_with; use function strcmp; -use function uniqid; use function var_export; /** @mixin Builder */ @@ -55,6 +54,8 @@ abstract class Model extends BaseModel use HybridRelations; use EmbedsRelations; + private const TEMPORARY_KEY = '__LARAVEL_TEMPORARY_KEY__'; + /** * The collection associated with the model. * @@ -271,12 +272,10 @@ public function setAttribute($key, $value) // Support keys in dot notation. if (str_contains($key, '.')) { // Store to a temporary key, then move data to the actual key - $uniqueKey = uniqid($key); - - parent::setAttribute($uniqueKey, $value); + parent::setAttribute(self::TEMPORARY_KEY, $value); - Arr::set($this->attributes, $key, $this->attributes[$uniqueKey] ?? null); - unset($this->attributes[$uniqueKey]); + Arr::set($this->attributes, $key, $this->attributes[self::TEMPORARY_KEY] ?? null); + unset($this->attributes[self::TEMPORARY_KEY]); return $this; } From bb8977f1f4344bef17afcacd916b400f51defa72 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Wed, 22 May 2024 16:10:11 +0200 Subject: [PATCH 07/26] Add SARIF output formatter for PHPStan (#2965) --- .github/workflows/coding-standards.yml | 8 +- phpstan.neon.dist | 8 ++ tests/PHPStan/SarifErrorFormatter.php | 129 +++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/SarifErrorFormatter.php diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 0d5ec53cd..1eebcaa5f 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -108,7 +108,13 @@ jobs: phpstan-result-cache- - name: Run PHPStan - run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi + run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi --error-format=sarif > phpstan.sarif + + - name: "Upload SARIF report" + if: always() + uses: "github/codeql-action/upload-sarif@v3" + with: + sarif_file: phpstan.sarif - name: Save cache PHPStan results id: phpstan-cache-save diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 518fe9ab8..539536a11 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,3 +14,11 @@ parameters: ignoreErrors: - '#Unsafe usage of new static#' - '#Call to an undefined method [a-zA-Z0-9\\_\<\>]+::[a-zA-Z]+\(\)#' + +services: + errorFormatter.sarif: + class: MongoDB\Laravel\Tests\PHPStan\SarifErrorFormatter + arguments: + relativePathHelper: @simpleRelativePathHelper + currentWorkingDirectory: %currentWorkingDirectory% + pretty: true diff --git a/tests/PHPStan/SarifErrorFormatter.php b/tests/PHPStan/SarifErrorFormatter.php new file mode 100644 index 000000000..1fb814cde --- /dev/null +++ b/tests/PHPStan/SarifErrorFormatter.php @@ -0,0 +1,129 @@ + [ + 'uri' => 'file://' . $this->currentWorkingDirectory . '/', + ], + ]; + + $results = []; + $rules = []; + + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + $ruleId = $fileSpecificError->getIdentifier(); + $rules[$ruleId] = ['id' => $ruleId]; + + $result = [ + 'ruleId' => $ruleId, + 'level' => 'error', + 'message' => [ + 'text' => $fileSpecificError->getMessage(), + ], + 'locations' => [ + [ + 'physicalLocation' => [ + 'artifactLocation' => [ + 'uri' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), + 'uriBaseId' => self::URI_BASE_ID, + ], + 'region' => [ + 'startLine' => $fileSpecificError->getLine(), + ], + ], + ], + ], + 'properties' => [ + 'ignorable' => $fileSpecificError->canBeIgnored(), + ], + ]; + + if ($fileSpecificError->getTip() !== null) { + $result['properties']['tip'] = $fileSpecificError->getTip(); + } + + $results[] = $result; + } + + foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { + $results[] = [ + 'level' => 'error', + 'message' => [ + 'text' => $notFileSpecificError, + ], + ]; + } + + foreach ($analysisResult->getWarnings() as $warning) { + $results[] = [ + 'level' => 'warning', + 'message' => [ + 'text' => $warning, + ], + ]; + } + + $sarif = [ + '$schema' => 'https://json.schemastore.org/sarif-2.1.0.json', + 'version' => '2.1.0', + 'runs' => [ + [ + 'tool' => [ + 'driver' => [ + 'name' => 'PHPStan', + 'fullName' => 'PHP Static Analysis Tool', + 'informationUri' => 'https://phpstan.org/', + 'version' => $phpstanVersion, + 'semanticVersion' => $phpstanVersion, + 'rules' => array_values($rules), + ], + ], + 'originalUriBaseIds' => $originalUriBaseIds, + 'results' => $results, + ], + ], + ]; + + $json = Json::encode($sarif, $this->pretty ? Json::PRETTY : 0); + + $output->writeRaw($json); + + return $analysisResult->hasErrors() ? 1 : 0; + } +} From 63535200598573971ec423e8608c3fb96f548c48 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 22 May 2024 17:48:59 +0200 Subject: [PATCH 08/26] Prevent Undefined property: MongoDB\Laravel\Connection::$connection (#2967) --- CHANGELOG.md | 1 + src/Connection.php | 2 +- tests/ConnectionTest.php | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eff79c84..500f1ee46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [4.3.1] * Fix memory leak when filling nested fields using dot notation by @GromNaN in [#2962](https://github.com/mongodb/laravel-mongodb/pull/2962) +* Fix PHP error when accessing the connection after disconnect by @SanderMuller in [#2967](https://github.com/mongodb/laravel-mongodb/pull/2967) ## [4.3.0] - 2024-04-26 diff --git a/src/Connection.php b/src/Connection.php index 0c5015489..ec337d524 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -208,7 +208,7 @@ public function ping(): void /** @inheritdoc */ public function disconnect() { - unset($this->connection); + $this->connection = null; } /** diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 262c4cafc..60ee9ee3f 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -38,6 +38,22 @@ public function testReconnect() $this->assertNotEquals(spl_object_hash($c1), spl_object_hash($c2)); } + public function testDisconnectAndCreateNewConnection() + { + $connection = DB::connection('mongodb'); + $this->assertInstanceOf(Connection::class, $connection); + $client = $connection->getMongoClient(); + $this->assertInstanceOf(Client::class, $client); + $connection->disconnect(); + $client = $connection->getMongoClient(); + $this->assertNull($client); + DB::purge('mongodb'); + $connection = DB::connection('mongodb'); + $this->assertInstanceOf(Connection::class, $connection); + $client = $connection->getMongoClient(); + $this->assertInstanceOf(Client::class, $client); + } + public function testDb() { $connection = DB::connection('mongodb'); From 390c9ec2b1ebb14cc44765f6db4ed81cdb5969a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 23 May 2024 11:14:46 +0200 Subject: [PATCH 09/26] Customize generated release notes (#2971) --- .github/release.yml | 21 +++++++++++++++++++++ RELEASING.md | 40 +--------------------------------------- 2 files changed, 22 insertions(+), 39 deletions(-) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..aabd8e4f2 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,21 @@ +changelog: + exclude: + labels: + - ignore-for-release + - minor + authors: + - mongodb-php-bot + categories: + - title: Breaking Changes 🛠 + labels: + - breaking-change + - title: New Features + labels: + - enhancement + - title: Fixed + labels: + - bug + - fixed + - title: Other Changes + labels: + - "*" diff --git a/RELEASING.md b/RELEASING.md index e0b494d08..c4aeecd39 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -87,44 +87,6 @@ tagging. ## Publish release notes -The following template should be used for creating GitHub release notes via -[this form](https://github.com/mongodb/laravel-mongodb/releases/new). - -```markdown -The PHP team is happy to announce that version X.Y.Z of the MongoDB integration for Laravel is now available. - -**Release Highlights** - - - -A complete list of resolved issues in this release may be found in [JIRA]($JIRA_URL). - -**Documentation** - -Documentation for this library may be found in the [Readme](https://github.com/mongodb/laravel-mongodb/blob/$VERSION/README.md). - -**Installation** - -This library may be installed or upgraded with: - - composer require mongodb/laravel-mongodb:X.Y.Z - -Installation instructions for the `mongodb` extension may be found in the [PHP.net documentation](https://php.net/manual/en/mongodb.installation.php). -``` - -The URL for the list of resolved JIRA issues will need to be updated with each -release. You may obtain the list from -[this form](https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=22488). - -If commits from community contributors were included in this release, append the -following section: - -```markdown -**Thanks** - -Thanks for our community contributors for this release: - - * [$CONTRIBUTOR_NAME](https://github.com/$GITHUB_USERNAME) -``` +Use the generated release note in [this form](https://github.com/mongodb/laravel-mongodb/releases/new). Release announcements should also be posted in the [MongoDB Product & Driver Announcements: Driver Releases](https://mongodb.com/community/forums/tags/c/announcements/driver-releases/110/php) forum and shared on Twitter. From d685b6a6aec4261347b340a88ca4ba32904a3c40 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 23 May 2024 13:17:07 +0200 Subject: [PATCH 10/26] PHPORM-153: Add automated release workflow (#2964) * Add automated release workflow * Use stable version of release tooling * Automatically generate release notes for draft release --- .github/workflows/release.yml | 90 +++++++++++++++++++++++++++++++++++ README.md | 20 ++++++++ 2 files changed, 110 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..b8df0df69 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: "Release New Version" +run-name: "Release ${{ inputs.version }}" + +on: + workflow_dispatch: + inputs: + version: + description: "The version to be released. This is checked for consistency with the branch name and configuration" + required: true + type: "string" + +env: + # TODO: Use different token + GH_TOKEN: ${{ secrets.MERGE_UP_TOKEN }} + GIT_AUTHOR_NAME: "DBX PHP Release Bot" + GIT_AUTHOR_EMAIL: "dbx-php@mongodb.com" + +jobs: + prepare-release: + name: "Prepare release" + runs-on: ubuntu-latest + + steps: + - name: "Create release output" + run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY + + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ env.GH_TOKEN }} + + - name: "Store version numbers in env variables" + run: | + echo RELEASE_VERSION=${{ inputs.version }} >> $GITHUB_ENV + echo RELEASE_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' -f-2) >> $GITHUB_ENV + + - name: "Ensure release tag does not already exist" + run: | + if [[ $(git tag -l ${RELEASE_VERSION}) == ${RELEASE_VERSION} ]]; then + echo '❌ Release failed: tag for version ${{ inputs.version }} already exists' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: "Fail if branch names don't match" + if: ${{ github.ref_name != env.RELEASE_BRANCH }} + run: | + echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }}, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY + exit 1 + + # + # Preliminary checks done - commence the release process + # + + - name: "Set git author information" + run: | + git config user.name "${GIT_AUTHOR_NAME}" + git config user.email "${GIT_AUTHOR_EMAIL}" + + # Create draft release with release notes + - name: "Create draft release" + run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --generate-notes --draft)" >> "$GITHUB_ENV" + + # This step creates the signed release tag + - name: "Create release tag" + uses: mongodb-labs/drivers-github-tools/garasign/git-sign@v1 + with: + command: "git tag -m 'Release ${{ inputs.version }}' -s --local-user=${{ vars.GPG_KEY_ID }} ${{ inputs.version }}" + garasign_username: ${{ secrets.GRS_CONFIG_USER1_USERNAME }} + garasign_password: ${{ secrets.GRS_CONFIG_USER1_PASSWORD }} + artifactory_username: ${{ secrets.ARTIFACTORY_USER }} + artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} + + # TODO: Manually merge using ours strategy. This avoids merge-up pull requests being created + # Process is: + # 1. switch to next branch (according to merge-up action) + # 2. merge release branch using --strategy=ours + # 3. push next branch + # 4. switch back to release branch, then push + + - name: "Push changes from release branch" + run: git push + + # Pushing the release tag starts build processes that then produce artifacts for the release + - name: "Push release tag" + run: git push origin ${{ inputs.version }} + + - name: "Set summary" + run: | + echo '🚀 Created tag and drafted release for version [${{ inputs.version }}](${{ env.RELEASE_URL }})' >> $GITHUB_STEP_SUMMARY + echo '✍️ You may now update the release notes and publish the release when ready' >> $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index 9ecf12af0..0619f387c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,26 @@ It is compatible with Laravel 10.x. For older versions of Laravel, please refer - https://www.mongodb.com/docs/drivers/php/laravel-mongodb/ - https://www.mongodb.com/docs/drivers/php/ +## Release Integrity + +Releases are created automatically and the resulting release tag is signed using +the [PHP team's GPG key](https://pgp.mongodb.com/php-driver.asc). To verify the +tag signature, download the key and import it using `gpg`: + +```shell +gpg --import php-driver.asc +``` + +Then, in a local clone, verify the signature of a given tag (e.g. `4.4.0`): + +```shell +git show --show-signature 4.4.0 +``` + +> [!NOTE] +> Composer does not support verifying signatures as part of its installation +> process. + ## Reporting Issues Think you’ve found a bug in the library? Want to see a new feature? Please open a case in our issue management tool, JIRA: From 75451775d84225b67e8004f19755a34d97707e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 23 May 2024 15:21:52 +0200 Subject: [PATCH 11/26] Automatically label docs PR (#2972) --- .github/labeler.yml | 7 +++++++ .github/workflows/labeler.yml | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..d0a7bb123 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,7 @@ +# https://github.com/actions/labeler +docs: + - changed-files: + - any-glob-to-any-file: 'docs/**' +github: + - changed-files: + - any-glob-to-any-file: '.github/**' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..52474c6a6 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: + - pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 From 7b8f0a151e16618b14eb4e19309254996c3c71a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 23 May 2024 16:18:37 +0200 Subject: [PATCH 12/26] Improve error message for invalid configuration (#2975) * Improve error message for invalid configuration * Deprecate Connection::hasDsnString --- CHANGELOG.md | 1 + src/Connection.php | 14 +++++++++++--- tests/ConnectionTest.php | 8 ++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 500f1ee46..6870b75b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. * Fix memory leak when filling nested fields using dot notation by @GromNaN in [#2962](https://github.com/mongodb/laravel-mongodb/pull/2962) * Fix PHP error when accessing the connection after disconnect by @SanderMuller in [#2967](https://github.com/mongodb/laravel-mongodb/pull/2967) +* Improve error message for invalid configuration by @GromNaN in [#2975](https://github.com/mongodb/laravel-mongodb/pull/2975) ## [4.3.0] - 2024-04-26 diff --git a/src/Connection.php b/src/Connection.php index ec337d524..734bd6d55 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -213,6 +213,8 @@ public function disconnect() /** * Determine if the given configuration array has a dsn string. + * + * @deprecated */ protected function hasDsnString(array $config): bool { @@ -261,9 +263,15 @@ protected function getHostDsn(array $config): string */ protected function getDsn(array $config): string { - return $this->hasDsnString($config) - ? $this->getDsnString($config) - : $this->getHostDsn($config); + if (! empty($config['dsn'])) { + return $this->getDsnString($config); + } + + if (! empty($config['host'])) { + return $this->getHostDsn($config); + } + + throw new InvalidArgumentException('MongoDB connection configuration requires "dsn" or "host" key.'); } /** @inheritdoc */ diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 60ee9ee3f..225de611e 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -204,6 +204,14 @@ public function testConnectionWithoutConfiguredDatabase(): void new Connection(['dsn' => 'mongodb://some-host']); } + public function testConnectionWithoutConfiguredDsnOrHost(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('MongoDB connection configuration requires "dsn" or "host" key.'); + + new Connection(['database' => 'hello']); + } + public function testCollection() { $collection = DB::connection('mongodb')->getCollection('unittest'); From 7dc263edd8202528dccdba0e1e568728e2cff1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 28 May 2024 16:00:36 +0200 Subject: [PATCH 13/26] Remove @mixin on Model class (#2981) --- CHANGELOG.md | 1 + src/Eloquent/Model.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6870b75b4..7a3ccf9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. * Fix memory leak when filling nested fields using dot notation by @GromNaN in [#2962](https://github.com/mongodb/laravel-mongodb/pull/2962) * Fix PHP error when accessing the connection after disconnect by @SanderMuller in [#2967](https://github.com/mongodb/laravel-mongodb/pull/2967) * Improve error message for invalid configuration by @GromNaN in [#2975](https://github.com/mongodb/laravel-mongodb/pull/2975) +* Remove `@mixin` annotation from `MongoDB\Laravel\Model` class by @GromNaN in [#2981](https://github.com/mongodb/laravel-mongodb/pull/2981) ## [4.3.0] - 2024-04-26 diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 5974e49e1..d47b62b8c 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -48,7 +48,6 @@ use function strcmp; use function var_export; -/** @mixin Builder */ abstract class Model extends BaseModel { use HybridRelations; From 3f84373b5c589ab753960b4f8eab90c69bc0a631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 28 May 2024 16:04:55 +0200 Subject: [PATCH 14/26] Unset `_id: null` to let it be autogenerated (#2969) --- CHANGELOG.md | 1 + src/Eloquent/Builder.php | 5 +++-- src/Eloquent/Model.php | 6 ++++++ tests/ModelTest.php | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1478732..bb318f301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [4.4.0] - unreleased * Support collection name prefix by @GromNaN in [#2930](https://github.com/mongodb/laravel-mongodb/pull/2930) +* Ignore `_id: null` to let MongoDB generate an `ObjectId` by @GromNaN in [#2969](https://github.com/mongodb/laravel-mongodb/pull/2969) * Add `mongodb` driver for Batching by @GromNaN in [#2904](https://github.com/mongodb/laravel-mongodb/pull/2904) * Rename queue option `table` to `collection` * Replace queue option `expire` with `retry_after` diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index 22dcfd081..678e7095b 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -21,6 +21,7 @@ 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 @@ -210,8 +211,8 @@ public function raw($value = null) */ public function createOrFirst(array $attributes = [], array $values = []): Model { - if ($attributes === []) { - throw new InvalidArgumentException('You must provide attributes to check for duplicates'); + 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 diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index d47b62b8c..f7b4f1f36 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -745,6 +745,12 @@ protected function isBSON(mixed $value): bool */ public function save(array $options = []) { + // SQL databases would use autoincrement the id field if set to null. + // Apply the same behavior to MongoDB with _id only, otherwise null would be stored. + if (array_key_exists('_id', $this->attributes) && $this->attributes['_id'] === null) { + unset($this->attributes['_id']); + } + $saved = parent::save($options); // Clear list of unset fields diff --git a/tests/ModelTest.php b/tests/ModelTest.php index baa731799..ead5847ca 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -1151,4 +1151,21 @@ public function testUpdateOrCreate(array $criteria) $this->assertEquals($createdAt, $checkUser->created_at->getTimestamp()); $this->assertEquals($updatedAt, $checkUser->updated_at->getTimestamp()); } + + public function testCreateWithNullId() + { + $user = User::create(['_id' => null, 'email' => 'foo@bar']); + $this->assertNotNull(ObjectId::class, $user->id); + $this->assertSame(1, User::count()); + } + + public function testUpdateOrCreateWithNullId() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You must provide attributes to check for duplicates'); + User::updateOrCreate( + ['_id' => null], + ['email' => 'jane.doe@example.com'], + ); + } } From 55ca36e758925c132312e9e99fd093db26f3dda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 May 2024 18:01:08 +0200 Subject: [PATCH 15/26] PHPORM-180 Keep createOrFirst in 2 commands to simplify implementation (#2984) --- CHANGELOG.md | 1 + src/Eloquent/Builder.php | 78 +++++++------------ .../FindAndModifyCommandSubscriber.php | 34 -------- tests/ModelTest.php | 76 ++++++++++++++---- 4 files changed, 87 insertions(+), 102 deletions(-) delete mode 100644 src/Internal/FindAndModifyCommandSubscriber.php diff --git a/CHANGELOG.md b/CHANGELOG.md index bb318f301..978523fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. * Add `mongodb` driver for Batching by @GromNaN in [#2904](https://github.com/mongodb/laravel-mongodb/pull/2904) * Rename queue option `table` to `collection` * Replace queue option `expire` with `retry_after` +* Revert behavior of `createOrFirst` to delegate to `firstOrCreate` when in transaction by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2984) ## [4.3.1] diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index 678e7095b..da96b64f1 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -4,28 +4,24 @@ namespace MongoDB\Laravel\Eloquent; -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\Connection; 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; /** @@ -202,56 +198,37 @@ 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 firstOrCreate(array $attributes = [], array $values = []) { - if ($attributes === [] || $attributes === ['_id' => null]) { - throw new InvalidArgumentException('You must provide attributes to check for duplicates. Got ' . json_encode($attributes)); + $instance = (clone $this)->where($attributes)->first(); + if ($instance !== null) { + return $instance; } - // 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); + // createOrFirst is not supported in transaction. + if ($this->getConnection()->getSession()?->isInTransaction()) { + return $this->create(array_merge($attributes, $values)); + } - /* @see \Illuminate\Database\Eloquent\Model::performInsert */ - if ($instance->usesTimestamps()) { - $instance->updateTimestamps(); + return $this->createOrFirst($attributes, $values); + } + + public function createOrFirst(array $attributes = [], array $values = []) + { + // The duplicate key error would abort the transaction. Using the regular firstOrCreate in that case. + if ($this->getConnection()->getSession()?->isInTransaction()) { + return $this->firstOrCreate($attributes, $values); } - $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) { + return $this->where($attributes)->first() ?? throw $e; } - $model = $this->model->newFromBuilder($document); - $model->wasRecentlyCreated = $listener->created; - - return $model; - }); + throw $e; + } } /** @@ -277,8 +254,7 @@ protected function addUpdatedAtColumn(array $values) return $values; } - /** @return ConnectionInterface */ - public function getConnection() + public function getConnection(): Connection { return $this->query->getConnection(); } diff --git a/src/Internal/FindAndModifyCommandSubscriber.php b/src/Internal/FindAndModifyCommandSubscriber.php deleted file mode 100644 index 335e05562..000000000 --- a/src/Internal/FindAndModifyCommandSubscriber.php +++ /dev/null @@ -1,34 +0,0 @@ -created = ! $event->getReply()->lastErrorObject->updatedExisting; - } -} diff --git a/tests/ModelTest.php b/tests/ModelTest.php index ead5847ca..73374ce57 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -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; @@ -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(); @@ -1048,10 +1048,23 @@ public function testNumericFieldName(): void $this->assertEquals([3 => 'two.three'], $found[2]); } - public function testCreateOrFirst() + #[TestWith([true])] + #[TestWith([false])] + public function testCreateOrFirst(bool $transaction) { + $connection = DB::connection('mongodb'); + $connection + ->getCollection('users') + ->createIndex(['email' => 1], ['unique' => true]); + + if ($transaction) { + $connection->beginTransaction(); + } + Carbon::setTestNow('2010-06-22'); $createdAt = Carbon::now()->getTimestamp(); + $events = []; + self::registerModelEvents(User::class, $events); $user1 = User::createOrFirst(['email' => 'john.doe@example.com']); $this->assertSame('john.doe@example.com', $user1->email); @@ -1059,8 +1072,10 @@ public function testCreateOrFirst() $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 = []; $user2 = User::createOrFirst( ['email' => 'john.doe@example.com'], ['name' => 'John Doe', 'birthday' => new DateTime('1987-05-28')], @@ -1073,7 +1088,17 @@ public function testCreateOrFirst() $this->assertFalse($user2->wasRecentlyCreated); $this->assertEquals($createdAt, $user1->created_at->getTimestamp()); $this->assertEquals($createdAt, $user1->updated_at->getTimestamp()); + if ($transaction) { + // In a transaction, firstOrCreate is used instead. + // Since a document is found, "save" is not called. + $this->assertEquals([], $events); + } else { + // The "duplicate key error" exception interrupts the save process + // before triggering "created" and "saved". Consistent with Laravel + $this->assertEquals(['saving', 'creating'], $events); + } + $events = []; $user3 = User::createOrFirst( ['email' => 'jane.doe@example.com'], ['name' => 'Jane Doe', 'birthday' => new DateTime('1987-05-28')], @@ -1086,7 +1111,9 @@ 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' => 'maria.doe@example.com'], @@ -1094,13 +1121,11 @@ public function testCreateOrFirst() $this->assertSame('Maria Doe', $user4->name); $this->assertTrue($user4->wasRecentlyCreated); - } + $this->assertEquals(['saving', 'creating', 'created', 'saved'], $events); - public function testCreateOrFirstRequiresFilter() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('You must provide attributes to check for duplicates'); - User::createOrFirst([]); + if ($transaction) { + $connection->commit(); + } } #[TestWith([['_id' => new ObjectID()]])] @@ -1116,6 +1141,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( @@ -1127,11 +1154,12 @@ 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); Carbon::setTestNow('2010-02-01'); $updatedAt = Carbon::now()->getTimestamp(); // Update + $events = []; $user = User::updateOrCreate( $criteria, ['birthday' => new DateTime('1990-01-12'), 'foo' => 'bar'], @@ -1142,6 +1170,7 @@ public function testUpdateOrCreate(array $criteria) $this->assertEquals(new DateTime('1990-01-12'), $user->birthday); $this->assertEquals($createdAt, $user->created_at->getTimestamp()); $this->assertEquals($updatedAt, $user->updated_at->getTimestamp()); + $this->assertEquals(['saving', 'updating', 'updated', 'saved'], $events); // Stored data $checkUser = User::where($criteria)->first(); @@ -1159,13 +1188,26 @@ public function testCreateWithNullId() $this->assertSame(1, User::count()); } - public function testUpdateOrCreateWithNullId() + /** @param class-string $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' => 'jane.doe@example.com'], - ); + $modelClass::creating(function () use (&$events) { + $events[] = 'creating'; + }); + $modelClass::created(function () use (&$events) { + $events[] = 'created'; + }); + $modelClass::updating(function () use (&$events) { + $events[] = 'updating'; + }); + $modelClass::updated(function () use (&$events) { + $events[] = 'updated'; + }); + $modelClass::saving(function () use (&$events) { + $events[] = 'saving'; + }); + $modelClass::saved(function () use (&$events) { + $events[] = 'saved'; + }); } } From 3c8a3fbfe159375705426cb4c79e1e029502c604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 May 2024 09:01:19 +0200 Subject: [PATCH 16/26] Update changelog for 4.3.1 (#2987) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3ccf9c8..7b92eac87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.3.1] +## [4.3.1] - 2024-05-31 * Fix memory leak when filling nested fields using dot notation by @GromNaN in [#2962](https://github.com/mongodb/laravel-mongodb/pull/2962) * Fix PHP error when accessing the connection after disconnect by @SanderMuller in [#2967](https://github.com/mongodb/laravel-mongodb/pull/2967) From efe1a893761b1d5599cab1924caa6bbf385b73a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 May 2024 09:01:28 +0200 Subject: [PATCH 17/26] Update changelog for 4.4.0 (#2988) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 978523fe2..f25779ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.4.0] - unreleased +## [4.4.0] - 2024-05-31 * Support collection name prefix by @GromNaN in [#2930](https://github.com/mongodb/laravel-mongodb/pull/2930) * Ignore `_id: null` to let MongoDB generate an `ObjectId` by @GromNaN in [#2969](https://github.com/mongodb/laravel-mongodb/pull/2969) From c9d19ad06028755f11f0912e0748dd225999d5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 4 Jun 2024 15:11:04 +0200 Subject: [PATCH 18/26] Update RELEASING.md (#2990) --- RELEASING.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index c4aeecd39..4be9302a4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -31,24 +31,18 @@ Update the version's release date and status from the [Manage Versions](https://jira.mongodb.org/plugins/servlet/project-config/PHPORM/versions) page. -## Update version info +## Trigger the release workflow -This uses [semantic versioning](https://semver.org/). Do not break -backwards compatibility in a non-major release or your users will kill you. +Releases are done automatically through a GitHub Action. Visit the corresponding +[Release New Version](https://github.com/mongodb/laravel-mongodb/actions/workflows/release.yml) +workflow page to trigger a new build. Select the correct branch (e.g. `v4.5`) +and trigger a new run using the "Run workflow" button. In the following prompt, +enter the version number. -Before proceeding, ensure that the default branch is up-to-date with all code -changes in this maintenance branch. This is important because we will later -merge the ensuing release commits with `--strategy=ours`, which will ignore -changes from the merged commits. +The automation will then create and push the necessary commits and tag, and create +a draft release. The release is created in a draft state and can be published +once the release notes have been updated. -## Tag the release - -Create a tag for the release and push: - -```console -$ git tag -a -m "Release X.Y.Z" X.Y.Z -$ git push mongodb --tags -``` ## Branch management From 2daa0ea86d4635a77890eec349ae39208d63cb7f Mon Sep 17 00:00:00 2001 From: Nora Reidy Date: Tue, 4 Jun 2024 12:56:38 -0400 Subject: [PATCH 19/26] v4.4 compat table (#2995) * v4.4 compat table * edit * remove file --- docs/includes/framework-compatibility-laravel.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/includes/framework-compatibility-laravel.rst b/docs/includes/framework-compatibility-laravel.rst index 1305cf8e0..44519e27c 100644 --- a/docs/includes/framework-compatibility-laravel.rst +++ b/docs/includes/framework-compatibility-laravel.rst @@ -7,6 +7,11 @@ - Laravel 10.x - Laravel 9.x + * - 4.4 + - ✓ + - ✓ + - + * - 4.3 - ✓ - ✓ From 798a5baec511f98090507143c7ac7093f76117db Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 7 Jun 2024 14:58:17 +0200 Subject: [PATCH 20/26] PHPORM-190: Update drivers-github-tools to v2 (#2998) --- .github/workflows/release.yml | 39 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8df0df69..1f1b3e44e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,21 +9,30 @@ on: required: true type: "string" -env: - # TODO: Use different token - GH_TOKEN: ${{ secrets.MERGE_UP_TOKEN }} - GIT_AUTHOR_NAME: "DBX PHP Release Bot" - GIT_AUTHOR_EMAIL: "dbx-php@mongodb.com" - jobs: prepare-release: + environment: release name: "Prepare release" runs-on: ubuntu-latest + permissions: + id-token: write + contents: write steps: - name: "Create release output" run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY + - name: "Create temporary app token" + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: "Store GitHub token in environment" + run: echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV" + shell: bash + - uses: actions/checkout@v4 with: submodules: true @@ -51,10 +60,12 @@ jobs: # Preliminary checks done - commence the release process # - - name: "Set git author information" - run: | - git config user.name "${GIT_AUTHOR_NAME}" - git config user.email "${GIT_AUTHOR_EMAIL}" + - name: "Set up drivers-github-tools" + uses: mongodb-labs/drivers-github-tools/setup@v2 + with: + aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} + aws_region_name: ${{ vars.AWS_REGION_NAME }} + aws_secret_id: ${{ secrets.AWS_SECRET_ID }} # Create draft release with release notes - name: "Create draft release" @@ -62,13 +73,9 @@ jobs: # This step creates the signed release tag - name: "Create release tag" - uses: mongodb-labs/drivers-github-tools/garasign/git-sign@v1 + uses: mongodb-labs/drivers-github-tools/git-sign@v2 with: - command: "git tag -m 'Release ${{ inputs.version }}' -s --local-user=${{ vars.GPG_KEY_ID }} ${{ inputs.version }}" - garasign_username: ${{ secrets.GRS_CONFIG_USER1_USERNAME }} - garasign_password: ${{ secrets.GRS_CONFIG_USER1_PASSWORD }} - artifactory_username: ${{ secrets.ARTIFACTORY_USER }} - artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} + command: "git tag -m 'Release ${{ inputs.version }}' -s --local-user=${{ env.GPG_KEY_ID }} ${{ inputs.version }}" # TODO: Manually merge using ours strategy. This avoids merge-up pull requests being created # Process is: From 42f5a49013e894b8b45cf8a7ef02220c4bf11434 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 13 Jun 2024 17:45:58 +0200 Subject: [PATCH 21/26] PHPORM-185, PHPORM-191, PHPORM-192: Publish SSDLC assets on release (#3004) * Run static analysis for tag manually from release workflow * Publish SSDLC assets after release * Use secure-checkout action to generate token and run checkout * Use tag-version action from drivers-github-tools --- .github/workflows/coding-standards.yml | 56 -------------- .github/workflows/release.yml | 101 +++++++++++++++++++------ .github/workflows/static-analysis.yml | 74 ++++++++++++++++++ 3 files changed, 151 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/static-analysis.yml diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 1eebcaa5f..24d397294 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -67,59 +67,3 @@ jobs: uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "apply phpcbf formatting" - - analysis: - runs-on: "ubuntu-22.04" - continue-on-error: true - strategy: - matrix: - php: - - '8.1' - - '8.2' - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, mbstring - tools: composer:v2 - coverage: none - - - name: Cache dependencies - id: composer-cache - uses: actions/cache@v4 - with: - path: ./vendor - key: composer-${{ hashFiles('**/composer.lock') }} - - - name: Install dependencies - run: composer install - - - name: Restore cache PHPStan results - id: phpstan-cache-restore - uses: actions/cache/restore@v4 - with: - path: .cache - key: "phpstan-result-cache-${{ github.run_id }}" - restore-keys: | - phpstan-result-cache- - - - name: Run PHPStan - run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi --error-format=sarif > phpstan.sarif - - - name: "Upload SARIF report" - if: always() - uses: "github/codeql-action/upload-sarif@v3" - with: - sarif_file: phpstan.sarif - - - name: Save cache PHPStan results - id: phpstan-cache-save - if: always() - uses: actions/cache/save@v4 - with: - path: .cache - key: ${{ steps.phpstan-cache-restore.outputs.cache-primary-key }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f1b3e44e..63dea84c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,21 +22,11 @@ jobs: - name: "Create release output" run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY - - name: "Create temporary app token" - uses: actions/create-github-app-token@v1 - id: app-token + - name: "Generate token and checkout repository" + uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: "Store GitHub token in environment" - run: echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV" - shell: bash - - - uses: actions/checkout@v4 - with: - submodules: true - token: ${{ env.GH_TOKEN }} + app_id: ${{ vars.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: "Store version numbers in env variables" run: | @@ -71,11 +61,11 @@ jobs: - name: "Create draft release" run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --generate-notes --draft)" >> "$GITHUB_ENV" - # This step creates the signed release tag - name: "Create release tag" - uses: mongodb-labs/drivers-github-tools/git-sign@v2 + uses: mongodb-labs/drivers-github-tools/tag-version@v2 with: - command: "git tag -m 'Release ${{ inputs.version }}' -s --local-user=${{ env.GPG_KEY_ID }} ${{ inputs.version }}" + version: ${{ inputs.version }} + tag_message_template: 'Release ${VERSION}' # TODO: Manually merge using ours strategy. This avoids merge-up pull requests being created # Process is: @@ -84,14 +74,77 @@ jobs: # 3. push next branch # 4. switch back to release branch, then push - - name: "Push changes from release branch" - run: git push - - # Pushing the release tag starts build processes that then produce artifacts for the release - - name: "Push release tag" - run: git push origin ${{ inputs.version }} - - name: "Set summary" run: | echo '🚀 Created tag and drafted release for version [${{ inputs.version }}](${{ env.RELEASE_URL }})' >> $GITHUB_STEP_SUMMARY echo '✍️ You may now update the release notes and publish the release when ready' >> $GITHUB_STEP_SUMMARY + + static-analysis: + needs: prepare-release + name: "Run Static Analysis" + uses: ./.github/workflows/static-analysis.yml + with: + ref: refs/tags/${{ inputs.version }} + permissions: + security-events: write + id-token: write + + publish-ssdlc-assets: + needs: static-analysis + environment: release + name: "Publish SSDLC Assets" + runs-on: ubuntu-latest + permissions: + security-events: read + id-token: write + contents: write + + steps: + - name: "Generate token and checkout repository" + uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + with: + app_id: ${{ vars.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + ref: refs/tags/${{ inputs.version }} + + # Sets the S3_ASSETS environment variable used later + - name: "Set up drivers-github-tools" + uses: mongodb-labs/drivers-github-tools/setup@v2 + with: + aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} + aws_region_name: ${{ vars.AWS_REGION_NAME }} + aws_secret_id: ${{ secrets.AWS_SECRET_ID }} + + - name: "Generate authorized publication document" + uses: mongodb-labs/drivers-github-tools/authorized-pub@v2 + with: + product_name: "MongoDB Laravel Integration" + release_version: ${{ inputs.version }} + filenames: "" + token: ${{ env.GH_TOKEN }} + + - name: "Download SBOM file from Silk" + uses: mongodb-labs/drivers-github-tools/sbom@v2 + with: + silk_asset_group: mongodb-laravel-integration + + - name: "Upload SBOM as release artifact" + run: gh release upload ${{ inputs.version }} ${{ env.S3_ASSETS }}/cyclonedx.sbom.json + continue-on-error: true + + - name: "Generate SARIF report from code scanning alerts" + uses: mongodb-labs/drivers-github-tools/code-scanning-export@v2 + with: + ref: ${{ inputs.version }} + output-file: ${{ env.S3_ASSETS }}/code-scanning-alerts.json + + - name: "Generate compliance report" + uses: mongodb-labs/drivers-github-tools/compliance-report@v2 + with: + token: ${{ env.GH_TOKEN }} + + - name: Upload S3 assets + uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v2 + with: + version: ${{ inputs.version }} + product_name: laravel-mongodb diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 000000000..240c0aa5b --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,74 @@ +name: "Static Analysis" + +on: + push: + pull_request: + workflow_call: + inputs: + ref: + description: "The git ref to check" + type: string + required: true + +env: + PHP_VERSION: "8.2" + DRIVER_VERSION: "stable" + +jobs: + phpstan: + runs-on: "ubuntu-22.04" + continue-on-error: true + strategy: + matrix: + php: + - '8.1' + - '8.2' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring + tools: composer:v2 + coverage: none + + - name: Cache dependencies + id: composer-cache + uses: actions/cache@v4 + with: + path: ./vendor + key: composer-${{ hashFiles('**/composer.lock') }} + + - name: Install dependencies + run: composer install + + - name: Restore cache PHPStan results + id: phpstan-cache-restore + uses: actions/cache/restore@v4 + with: + path: .cache + key: "phpstan-result-cache-${{ matrix.php }}-${{ github.run_id }}" + restore-keys: | + phpstan-result-cache- + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi --error-format=sarif > phpstan.sarif + + - name: "Upload SARIF report" + if: always() + uses: "github/codeql-action/upload-sarif@v3" + with: + sarif_file: phpstan.sarif + + - name: Save cache PHPStan results + id: phpstan-cache-save + if: always() + uses: actions/cache/save@v4 + with: + path: .cache + key: ${{ steps.phpstan-cache-restore.outputs.cache-primary-key }} From 4940d803e66df0288d653b980d1452edb6ff5a09 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 17 Jun 2024 10:56:54 +0200 Subject: [PATCH 22/26] Upload code scanning results to correct ref when releasing (#3006) --- .github/workflows/static-analysis.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 240c0aa5b..18ea2014e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -29,6 +29,11 @@ jobs: with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }} + - name: "Get SHA hash of checked out ref" + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo CHECKED_OUT_SHA=$(git rev-parse HEAD) >> $GITHUB_ENV + - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -58,12 +63,21 @@ jobs: - name: Run PHPStan run: ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi --error-format=sarif > phpstan.sarif + continue-on-error: true - name: "Upload SARIF report" - if: always() + if: ${{ github.event_name != 'workflow_dispatch' }} + uses: "github/codeql-action/upload-sarif@v3" + with: + sarif_file: phpstan.sarif + + - name: "Upload SARIF report" + if: ${{ github.event_name == 'workflow_dispatch' }} uses: "github/codeql-action/upload-sarif@v3" with: sarif_file: phpstan.sarif + ref: ${{ inputs.ref }} + sha: ${{ env.CHECKED_OUT_SHA }} - name: Save cache PHPStan results id: phpstan-cache-save From 3415f8654a67f1142e49f8d445cd650ce23dda09 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 20 Jun 2024 08:18:21 +0200 Subject: [PATCH 23/26] Use full-report convenience action for SSDLC reports (#3010) --- .github/workflows/release.yml | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63dea84c4..e957b7faf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,34 +115,17 @@ jobs: aws_region_name: ${{ vars.AWS_REGION_NAME }} aws_secret_id: ${{ secrets.AWS_SECRET_ID }} - - name: "Generate authorized publication document" - uses: mongodb-labs/drivers-github-tools/authorized-pub@v2 + - name: "Generate SSDLC Reports" + uses: mongodb-labs/drivers-github-tools/full-report@v2 with: product_name: "MongoDB Laravel Integration" release_version: ${{ inputs.version }} - filenames: "" - token: ${{ env.GH_TOKEN }} - - - name: "Download SBOM file from Silk" - uses: mongodb-labs/drivers-github-tools/sbom@v2 - with: silk_asset_group: mongodb-laravel-integration - name: "Upload SBOM as release artifact" run: gh release upload ${{ inputs.version }} ${{ env.S3_ASSETS }}/cyclonedx.sbom.json continue-on-error: true - - name: "Generate SARIF report from code scanning alerts" - uses: mongodb-labs/drivers-github-tools/code-scanning-export@v2 - with: - ref: ${{ inputs.version }} - output-file: ${{ env.S3_ASSETS }}/code-scanning-alerts.json - - - name: "Generate compliance report" - uses: mongodb-labs/drivers-github-tools/compliance-report@v2 - with: - token: ${{ env.GH_TOKEN }} - - name: Upload S3 assets uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v2 with: From 512c610355bf3294b1159320824143ca6aa99377 Mon Sep 17 00:00:00 2001 From: Rea Rustagi <85902999+rustagir@users.noreply.github.com> Date: Thu, 20 Jun 2024 09:42:05 -0400 Subject: [PATCH 24/26] DOCSP-39849: revise job batching docs (#2994) * DOCSP-39849: revise job batching docs * JT fixes * small fixes * NR PR fixes 1 --- docs/queues.txt | 88 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/docs/queues.txt b/docs/queues.txt index ccac29ba6..5e25d868b 100644 --- a/docs/queues.txt +++ b/docs/queues.txt @@ -9,24 +9,29 @@ Queues :values: tutorial .. meta:: - :keywords: php framework, odm, code example + :keywords: php framework, odm, code example, jobs -If you want to use MongoDB as your database backend for Laravel Queue, change -the driver in ``config/queue.php``: +To use MongoDB as your database for Laravel Queue, change +the driver in your application's ``config/queue.php`` file: .. code-block:: php 'connections' => [ 'database' => [ 'driver' => 'mongodb', - // You can also specify your jobs specific database created on config/database.php + // You can also specify your jobs-specific database + // in the config/database.php file 'connection' => 'mongodb', 'collection' => 'jobs', 'queue' => 'default', - 'retry_after' => 60, + // Optional setting + // 'retry_after' => 60, ], ], +The following table describes properties that you can specify to configure +the behavior of the queue: + .. list-table:: :header-rows: 1 :widths: 25 75 @@ -35,22 +40,29 @@ the driver in ``config/queue.php``: - Description * - ``driver`` - - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + - **Required** Queue driver to use. The value of + this property must be ``mongodb``. * - ``connection`` - - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + - Database connection used to store jobs. It must be a + ``mongodb`` connection. The driver uses the default connection if + a connection is not specified. * - ``collection`` - - **Required**. Name of the MongoDB collection to store jobs to process. + - **Required** Name of the MongoDB collection to + store jobs to process. * - ``queue`` - - **Required**. Name of the queue. + - **Required** Name of the queue. * - ``retry_after`` - - Specifies how many seconds the queue connection should wait before retrying a job that is being processed. Defaults to ``60``. + - Specifies how many seconds the queue connection should wait + before retrying a job that is being processed. The value is + ``60`` by default. -If you want to use MongoDB to handle failed jobs, change the database in -``config/queue.php``: +To use MongoDB to handle failed jobs, create a ``failed`` entry in your +application's ``config/queue.php`` file and specify the database and +collection: .. code-block:: php @@ -60,6 +72,9 @@ If you want to use MongoDB to handle failed jobs, change the database in 'collection' => 'failed_jobs', ], +The following table describes properties that you can specify to configure +how to handle failed jobs: + .. list-table:: :header-rows: 1 :widths: 25 75 @@ -68,32 +83,41 @@ If you want to use MongoDB to handle failed jobs, change the database in - Description * - ``driver`` - - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + - **Required** Queue driver to use. The value of + this property must be ``mongodb``. * - ``connection`` - - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + - Database connection used to store jobs. It must be + a ``mongodb`` connection. The driver uses the default connection + if a connection is not specified. * - ``collection`` - - Name of the MongoDB collection to store failed jobs. Defaults to ``failed_jobs``. - + - Name of the MongoDB collection to store failed + jobs. The value is ``failed_jobs`` by default. -Add the service provider in ``config/app.php``: +Then, add the service provider in your application's +``config/app.php`` file: .. code-block:: php MongoDB\Laravel\MongoDBQueueServiceProvider::class, - Job Batching ------------ -`Job batching `__ -is a Laravel feature to execute a batch of jobs and subsequent actions before, -after, and during the execution of the jobs from the queue. +**Job batching** is a Laravel feature that enables you to execute a +batch of jobs and related actions before, after, and during the +execution of the jobs from the queue. To learn more about this feature, +see `Job Batching `__ +in the Laravel documentation. + +In MongoDB, you don't have to create a designated collection before +using job batching. The ``job_batches`` collection is created +automatically to store metadata about your job batches, such as +their completion percentage. -With MongoDB, you don't have to create any collection before using job batching. -The ``job_batches`` collection is created automatically to store meta -information about your job batches, such as their completion percentage. +To enable job batching, create the ``batching`` entry in your +application's ``config/queue.php`` file: .. code-block:: php @@ -103,6 +127,9 @@ information about your job batches, such as their completion percentage. 'collection' => 'job_batches', ], +The following table describes properties that you can specify to configure +job batching: + .. list-table:: :header-rows: 1 :widths: 25 75 @@ -111,15 +138,20 @@ information about your job batches, such as their completion percentage. - Description * - ``driver`` - - **Required**. Specifies the queue driver to use. Must be ``mongodb``. + - **Required** Queue driver to use. The value of + this property must be ``mongodb``. * - ``connection`` - - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + - Database connection used to store jobs. It must be a + ``mongodb`` connection. The driver uses the default connection if + a connection is not specified. * - ``collection`` - - Name of the MongoDB collection to store job batches. Defaults to ``job_batches``. + - Name of the MongoDB collection to store job + batches. The value is ``job_batches`` by default. -Add the service provider in ``config/app.php``: +Then, add the service provider in your application's ``config/app.php`` +file: .. code-block:: php From 0a143cc31601417ff07290afa7b7a700f6f8d144 Mon Sep 17 00:00:00 2001 From: Nora Reidy Date: Fri, 28 Jun 2024 12:52:23 -0400 Subject: [PATCH 25/26] DOCSP-41010: Fix transactions code example (#3016) --- docs/transactions.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/transactions.txt b/docs/transactions.txt index 5ef3df19d..89562c795 100644 --- a/docs/transactions.txt +++ b/docs/transactions.txt @@ -99,7 +99,8 @@ to another account: :start-after: begin transaction callback :end-before: end transaction callback -You can optionally pass the maximum number of times to retry a failed transaction as the second parameter as shown in the following code example: +You can optionally pass the maximum number of times to retry a failed transaction +as the second parameter, as shown in the following code example: .. code-block:: php :emphasize-lines: 4 @@ -107,7 +108,7 @@ You can optionally pass the maximum number of times to retry a failed transactio DB::transaction(function() { // transaction code }, - retries: 5, + attempts: 5, ); .. _laravel-transaction-commit: From cb3fa4ef27cbf65f8e0edbd7b632c7707ab3cb28 Mon Sep 17 00:00:00 2001 From: Rea Rustagi <85902999+rustagir@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:38:52 -0400 Subject: [PATCH 26/26] DOCSP-38380: array reads (#3028) * DOCSP-38380: array reads * fix * NR PR fixes 1 * remove extra doc in test --- docs/fundamentals/read-operations.txt | 67 ++++++++++++++++--- .../read-operations/ReadOperationsTest.php | 31 +++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/docs/fundamentals/read-operations.txt b/docs/fundamentals/read-operations.txt index 8025f0087..29437aa59 100644 --- a/docs/fundamentals/read-operations.txt +++ b/docs/fundamentals/read-operations.txt @@ -57,16 +57,13 @@ You can use Laravel's Eloquent object-relational mapper (ORM) to create models that represent MongoDB collections and chain methods on them to specify query criteria. -To retrieve documents that match a set of criteria, pass a query filter to the -``where()`` method. +To retrieve documents that match a set of criteria, call the ``where()`` +method on the collection's corresponding Eloquent model, then pass a query +filter to the method. A query filter specifies field value requirements and instructs the find operation to return only documents that meet these requirements. -You can use Laravel's Eloquent object-relational mapper (ORM) to create models -that represent MongoDB collections. To retrieve documents from a collection, -call the ``where()`` method on the collection's corresponding Eloquent model. - You can use one of the following ``where()`` method calls to build a query: - ``where('', )`` builds a query that matches documents in @@ -79,7 +76,7 @@ You can use one of the following ``where()`` method calls to build a query: To apply multiple sets of criteria to the find operation, you can chain a series of ``where()`` methods together. -After building your query with the ``where()`` method, chain the ``get()`` +After building your query by using the ``where()`` method, chain the ``get()`` method to retrieve the query results. This example calls two ``where()`` methods on the ``Movie`` Eloquent model to @@ -150,6 +147,60 @@ retrieve documents that meet the following criteria: To learn how to query by using the Laravel query builder instead of the Eloquent ORM, see the :ref:`laravel-query-builder` page. +Match Array Field Elements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can specify a query filter to match array field elements when +retrieving documents. If your documents contain an array field, you can +match documents based on if the value contains all or some specified +array elements. + +You can use one of the following ``where()`` method calls to build a +query on an array field: + +- ``where('', )`` builds a query that matches documents in + which the array field value is exactly the specified array + +- ``where('', 'in', )`` builds a query + that matches documents in which the array field value contains one or + more of the specified array elements + +After building your query by using the ``where()`` method, chain the ``get()`` +method to retrieve the query results. + +Select from the following :guilabel:`Exact Array Match` and +:guilabel:`Element Match` tabs to view the query syntax for each pattern: + +.. tabs:: + + .. tab:: Exact Array Match + :tabid: exact-array + + This example retrieves documents in which the ``countries`` array is + exactly ``['Indonesia', 'Canada']``: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-exact-array + :end-before: end-exact-array + + .. tab:: Element Match + :tabid: element-match + + This example retrieves documents in which the ``countries`` array + contains one of the values in the array ``['Canada', 'Egypt']``: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-elem-match + :end-before: end-elem-match + +To learn how to query array fields by using the Laravel query builder instead of the +Eloquent ORM, see the :ref:`laravel-query-builder-elemMatch` section in +the Query Builder guide. + .. _laravel-retrieve-all: Retrieve All Documents in a Collection @@ -200,7 +251,7 @@ by the ``$search`` field in your query filter that you pass to the ``where()`` method. The ``$text`` operator performs a text search on the text-indexed fields. The ``$search`` field specifies the text to search for. -After building your query with the ``where()`` method, chain the ``get()`` +After building your query by using the ``where()`` method, chain the ``get()`` method to retrieve the query results. This example calls the ``where()`` method on the ``Movie`` Eloquent model to diff --git a/docs/includes/fundamentals/read-operations/ReadOperationsTest.php b/docs/includes/fundamentals/read-operations/ReadOperationsTest.php index a2080ec8f..c27680fb5 100644 --- a/docs/includes/fundamentals/read-operations/ReadOperationsTest.php +++ b/docs/includes/fundamentals/read-operations/ReadOperationsTest.php @@ -133,4 +133,35 @@ public function testTextRelevance(): void $this->assertCount(1, $movies); $this->assertEquals('this is a love story', $movies[0]->plot); } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function exactArrayMatch(): void + { + // start-exact-array + $movies = Movie::where('countries', ['Indonesia', 'Canada']) + ->get(); + // end-exact-array + + $this->assertNotNull($movies); + $this->assertCount(1, $movies); + $this->assertEquals('Title 1', $movies[0]->title); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function arrayElemMatch(): void + { + // start-elem-match + $movies = Movie::where('countries', 'in', ['Canada', 'Egypt']) + ->get(); + // end-elem-match + + $this->assertNotNull($movies); + $this->assertCount(2, $movies); + } }