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/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/.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/.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 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/CHANGELOG.md b/CHANGELOG.md index ffd240b43..d8626f27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.3.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) +* 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] - 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) +* 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 * 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/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: 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. 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/compatibility.txt b/docs/compatibility.txt index 1ab0f6c91..dc0b33148 100644 --- a/docs/compatibility.txt +++ b/docs/compatibility.txt @@ -14,6 +14,9 @@ Compatibility :depth: 1 :class: singlecol +.. meta:: + :keywords: laravel 9, laravel 10, laravel 11, 4.0, 4.1, 4.2 + Laravel Compatibility --------------------- diff --git a/docs/eloquent-models.txt b/docs/eloquent-models.txt index e7edadcfe..95fe24d15 100644 --- a/docs/eloquent-models.txt +++ b/docs/eloquent-models.txt @@ -26,7 +26,7 @@ This section contains guidance on how to use Eloquent models in between models - :ref:`laravel-schema-builder` shows how to manage indexes on your MongoDB collections by using Laravel migrations - + .. toctree:: /eloquent-models/model-class/ diff --git a/docs/eloquent-models/model-class.txt b/docs/eloquent-models/model-class.txt index 5542b35ea..f1d1fbdda 100644 --- a/docs/eloquent-models/model-class.txt +++ b/docs/eloquent-models/model-class.txt @@ -194,6 +194,24 @@ type, to the Laravel ``datetime`` type. :emphasize-lines: 9-11 :dedent: +.. tip:: Casts in Laravel 11 + + In Laravel 11, you can define a ``casts()`` method to specify data type conversions + instead of using the ``$casts`` attribute. The following code performs the same + conversion as the preceding example by using a ``casts()`` method: + + .. code-block:: php + + protected function casts(): array + { + return [ + 'discovery_dt' => 'datetime', + ]; + } + + To learn more, see `Attribute Casting `__ + in the Laravel documentation. + This conversion lets you use the PHP `DateTime `__ or the `Carbon class `__ to work with dates in this field. The following example shows a Laravel query that uses the diff --git a/docs/eloquent-models/relationships.txt b/docs/eloquent-models/relationships.txt index 2ae716132..b71b8b8c2 100644 --- a/docs/eloquent-models/relationships.txt +++ b/docs/eloquent-models/relationships.txt @@ -90,7 +90,7 @@ method: :dedent: The following sample code shows how to instantiate a model for each class -and add the relationship between them. Click the :guilabel:`VIEW OUTPUT` +and add the relationship between them. Click the :guilabel:`{+code-output-label+}` button to see the data created by running the code: .. io-code-block:: @@ -175,7 +175,7 @@ model by using the ``belongsTo()`` method: :dedent: The following sample code shows how to instantiate a model for each class -and add the relationship between them. Click the :guilabel:`VIEW OUTPUT` +and add the relationship between them. Click the :guilabel:`{+code-output-label+}` button to see the data created by running the code: .. io-code-block:: @@ -275,7 +275,7 @@ using the ``belongsToMany()`` method: :dedent: The following sample code shows how to instantiate a model for each class -and add the relationship between them. Click the :guilabel:`VIEW OUTPUT` +and add the relationship between them. Click the :guilabel:`{+code-output-label+}` button to see the data created by running the code: .. io-code-block:: @@ -405,7 +405,7 @@ following ``Cargo`` model class: The following sample code shows how to create a ``SpaceShip`` model and embed multiple ``Cargo`` models and the MongoDB document created by running the -code. Click the :guilabel:`VIEW OUTPUT` button to see the data created by +code. Click the :guilabel:`{+code-output-label+}` button to see the data created by running the code: .. io-code-block:: @@ -496,7 +496,7 @@ model by using the ``belongsTo()`` method: The following sample code shows how to create a ``SpaceShip`` model in a MySQL database and related ``Passenger`` models in a MongoDB database as well -as the data created by running the code. Click the :guilabel:`VIEW OUTPUT` button +as the data created by running the code. Click the :guilabel:`{+code-output-label+}` button to see the data created by running the code: .. io-code-block:: diff --git a/docs/eloquent-models/schema-builder.txt b/docs/eloquent-models/schema-builder.txt index c6c7e64cc..0003d3e7b 100644 --- a/docs/eloquent-models/schema-builder.txt +++ b/docs/eloquent-models/schema-builder.txt @@ -75,8 +75,9 @@ following changes to perform the schema changes on your MongoDB database: MongoDB database, update the following setting to make sure the migration specifies the correct database: - - Specify ``mongodb`` in the ``$connection`` field of your migration class - - Set ``DB_CONNECTION=mongodb`` in your ``.env`` configuration file + - Make sure your ``connections`` array item contains a valid ``mongodb`` + entry in your ``config/database.php`` file + - Specify ``"mongodb"`` in the ``$connection`` field of your migration class The following example migration class contains the following methods: @@ -125,8 +126,8 @@ To check whether a collection exists, call the ``hasCollection()`` method on the ``Schema`` facade in your migration file. You can use this to perform migration logic conditionally. -The following example migration creates a ``stars`` collection if a collection -named ``telescopes`` exists: +The following example migration creates a ``telescopes`` collection if a collection +named ``stars`` exists: .. literalinclude:: /includes/schema-builder/stars_migration.php :language: php @@ -166,9 +167,9 @@ fields: - Single field index on ``mission_type`` - Compound index on ``launch_location`` and ``launch_date``, specifying a descending sort order on ``launch_date`` -- Unique index on the ``mission_id`` field, specifying the index name "unique_mission_id_idx" +- Unique index on the ``mission_id`` field, specifying the index name ``"unique_mission_id_idx"`` -Click the :guilabel:`VIEW OUTPUT` button to see the indexes created by running +Click the :guilabel:`{+code-output-label+}` button to see the indexes created by running the migration, including the default index on the ``_id`` field: .. io-code-block:: @@ -207,7 +208,7 @@ You can specify index options when calling an index creation method, such as ``index()``, on a ``Blueprint`` instance. The following migration code shows how to add a collation to an index as an -index option. Click the :guilabel:`VIEW OUTPUT` button to see the indexes +index option. Click the :guilabel:`{+code-output-label+}` button to see the indexes created by running the migration, including the default index on the ``_id`` field: @@ -266,7 +267,7 @@ appropriate helper method on the ``Blueprint`` instance and pass the index creation details. The following migration code shows how to create a sparse and a TTL index -by using the index helpers. Click the :guilabel:`VIEW OUTPUT` button to see +by using the index helpers. Click the :guilabel:`{+code-output-label+}` button to see the indexes created by running the migration, including the default index on the ``_id`` field: @@ -297,7 +298,7 @@ You can specify sparse, TTL, and unique indexes on either a single field or compound index by specifying them in the index options. The following migration code shows how to create all three types of indexes -on a single field. Click the :guilabel:`VIEW OUTPUT` button to see the indexes +on a single field. Click the :guilabel:`{+code-output-label+}` button to see the indexes created by running the migration, including the default index on the ``_id`` field: @@ -342,7 +343,7 @@ method with a ``MongoDB\Laravel\Schema\Blueprint`` parameter. Specify the geospatial index creation details on the ``Blueprint`` instance. The following example migration creates a ``2d`` and ``2dsphere`` geospatial -index on the ``spaceports`` collection. Click the :guilabel:`VIEW OUTPUT` +index on the ``spaceports`` collection. Click the :guilabel:`{+code-output-label+}` button to see the indexes created by running the migration, including the default index on the ``_id`` field: diff --git a/docs/fundamentals.txt b/docs/fundamentals.txt index a850d44f0..d5ee9e796 100644 --- a/docs/fundamentals.txt +++ b/docs/fundamentals.txt @@ -9,7 +9,7 @@ Fundamentals :values: reference .. meta:: - :keywords: php framework, odm + :keywords: php framework, odm, concepts .. toctree:: :titlesonly: @@ -21,7 +21,7 @@ Fundamentals /fundamentals/write-operations /fundamentals/aggregation-builder -Learn how to use the {+odm-long+} to perform the following tasks: +Learn more about the following concepts related to the {+odm-long+}: - :ref:`laravel-fundamentals-connection` - :ref:`laravel-db-coll` 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, diff --git a/docs/fundamentals/connection.txt b/docs/fundamentals/connection.txt index 17b849ae9..3141cfeaf 100644 --- a/docs/fundamentals/connection.txt +++ b/docs/fundamentals/connection.txt @@ -4,10 +4,18 @@ Connections =========== +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: options, deployment, connection behavior + .. toctree:: /fundamentals/connection/connect-to-mongodb /fundamentals/connection/connection-options + /fundamentals/connection/tls .. contents:: On this page :local: @@ -23,3 +31,4 @@ and specify connection behavior in the following sections: - :ref:`laravel-connect-to-mongodb` - :ref:`laravel-fundamentals-connection-options` +- :ref:`laravel-tls` diff --git a/docs/fundamentals/connection/connect-to-mongodb.txt b/docs/fundamentals/connection/connect-to-mongodb.txt index 7de96ad76..9f7e07b26 100644 --- a/docs/fundamentals/connection/connect-to-mongodb.txt +++ b/docs/fundamentals/connection/connect-to-mongodb.txt @@ -78,6 +78,9 @@ options. In the example, we set the following connection options and values: - ``maxPoolSize=20`` - ``w=majority`` +To learn more about connection options, see +:ref:`laravel-fundamentals-connection-options`. + .. _laravel-database-config: Laravel Database Connection Configuration @@ -151,11 +154,13 @@ For a MongoDB database connection, you can specify the following details: * - ``options`` - Specifies connection options to pass to MongoDB that determine the - connection behavior. + connection behavior. To learn more about connection options, see + :ref:`laravel-connection-auth-options`. * - ``driverOptions`` - - Specifies options specific to pass to the MongoDB PHP Library driver - that determine the driver behavior for that connection. + - Specifies options specific to pass to the {+php-library+} that + determine the driver behavior for that connection. To learn more about + driver options, see :ref:`laravel-driver-options`. .. note:: @@ -323,6 +328,8 @@ To learn more about setting up a MongoDB replica set, see :manual:`Deploy a Replica Set ` in the {+server-docs-name+}. +.. _laravel-direct-connection: + Direct Connection ````````````````` diff --git a/docs/fundamentals/connection/connection-options.txt b/docs/fundamentals/connection/connection-options.txt index 9d873a406..d73cb33d4 100644 --- a/docs/fundamentals/connection/connection-options.txt +++ b/docs/fundamentals/connection/connection-options.txt @@ -155,7 +155,9 @@ and their default values: - Boolean - ``false`` - Specifies whether to directly connect to a single host instead of - discovering and connecting to all servers in the cluster. + discovering and connecting to all servers in the cluster. To learn more + about this setting, see :ref:`laravel-direct-connection` in the + Connection Guide. * - **heartbeatFrequencyMS** - Integer greater than or equal to ``500`` @@ -295,7 +297,7 @@ and their default values: - | Requests acknowledgment that the operation has propagated to a specific number or variety of servers. | To learn more, see :manual:`Write Concern ` - in the Server manual. + in the {+server-docs-name+}. * - **wTimeoutMS** - Non-negative integer diff --git a/docs/fundamentals/connection/tls.txt b/docs/fundamentals/connection/tls.txt new file mode 100644 index 000000000..793157286 --- /dev/null +++ b/docs/fundamentals/connection/tls.txt @@ -0,0 +1,199 @@ +.. _laravel-tls: + +======================== +Enable and Configure TLS +======================== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: code example, security, connection options, ssl + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to use the TLS protocol to secure +your connection to a MongoDB deployment. To configure your connection to +use TLS, enable the TLS option and optionally provide your certificates for +validation in your application's ``config/database.php`` file. + +.. tip:: + + To learn more about TLS, see the Wikipedia entry on + :wikipedia:`Transport Layer Security `. + +Enable TLS +---------- + +In your application's ``config/database.php`` file, you can enable TLS +on a connection to your MongoDB deployment in one of the following ways: + +- Setting the ``tls`` option to ``true`` in your connection string +- Setting the ``tls`` option to ``true`` in the ``options`` property of + your ``mongodb`` connection entry + +Select from the following :guilabel:`Connection String` and +:guilabel:`Connection Options` tabs to see a corresponding code sample: + +.. tabs:: + + .. tab:: Connection String + :tabid: connection string tls true + + .. code-block:: php + :emphasize-lines: 5 + + 'connections' => [ + + 'mongodb' => [ + 'driver' => 'mongodb', + 'dsn' => 'mongodb://:/?tls=true', + 'database' => 'myDB', + ] + ] + + .. tab:: Connection Options + :tabid: options tls true + + .. code-block:: php + :emphasize-lines: 8 + + 'connections' => [ + + 'mongodb' => [ + 'driver' => 'mongodb', + 'dsn' => '', + 'database' => 'myDB', + 'options' => [ + 'tls' => true, + ], + ] + ] + + To view a full list of connection options, see + :ref:`laravel-fundamentals-connection-options`. + +.. note:: + + If your connection string uses a DNS SRV record by including + the ``mongodb+srv`` prefix, TLS is enabled on your connection by + default. + +Configure Certificates +---------------------- + +To successfully initiate a TLS request, your application might need to present +cryptographic certificates to prove its identity. Your application's +certificates must be stored as PEM files to enable TLS when connecting. + +.. important:: + + For production use, we recommend that your MongoDB deployment use valid + certificates generated and signed by the same certificate authority. + For testing, your deployment can use self-signed certificates. + +The following list describes the components that your client can +present to establish a TLS-enabled connection: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - TLS Component + - Description + + * - Certificate Authority (CA) + - One or more certificate authorities to + trust when making a TLS connection. You can pass this file's path + to the ``tlsCAFile`` option. + + * - Client Certificate + - A digital certificate that allows the server to verify the identity + of your application to establish an encrypted network connection. + You can pass this file's path to the ``tlsCertificateKeyFile`` option. + + * - Certificate Key + - The client certificate private key file. This key is often + included within the certificate file itself. If you must + provide this item, the certificate and key should be concatenated + in one file that you can pass to the ``tlsCertificateKeyFile`` + option. + + * - Passphrase + - The password to decrypt the private client key if it is + encrypted. You can pass this file's path to the + ``tlsCertificateKeyFilePassword`` option. + +Reference Certificates +---------------------- + +If required, you must reference your certificates when configuring your ``mongodb`` +connection so that the server can validate them before the client connects. + +We recommend that you reference your certificates and set other TLS +options in the ``options`` property of your connection configuration +instead of in the connection string. This improves code readability in +your application. + +Set the following options in the ``options`` property to reference your +certificates: + +- ``tlsCAFile`` +- ``tlsCertificateKeyFile`` +- ``tlsCertificateKeyFilePassword`` + +.. note:: + + For **testing purposes**, you can set the following options to + ``true`` to disable validation: + + - ``tlsAllowInvalidCertificates`` + - ``tlsAllowInvalidHostnames`` + + Or, you can set the ``tlsInsecure`` option to ``true`` to implicitly set + both of the preceding options. + + Specifying these options in a production environment might make + your application insecure. To learn more, see the :manual:`Connection + Options ` + reference in the Server manual. + +The following example configures a connection with TLS enabled: + +.. code-block:: php + + 'connections' => [ + + 'mongodb' => [ + 'driver' => 'mongodb', + 'dsn' => '', + 'database' => 'myDB', + 'options' => [ + 'tls' => true, + 'tlsCAFile' => '', + 'tlsCertificateKeyFile' => '', + 'tlsCertificateKeyFilePassword' => '', + ] + ] + ] + +Additional Information +---------------------- + +To learn more about setting URI options, see the `MongoDB\Driver\Manager::__construct() +`__ +API documentation. + +To learn more about enabling TLS on a connection, see the +following Server manual documentation: + +- :manual:`TLS/SSL (Transport Encryption) ` +- :manual:`TLS/SSL Configuration for Clients ` diff --git a/docs/fundamentals/database-collection.txt b/docs/fundamentals/database-collection.txt index db74b045b..6b629d79e 100644 --- a/docs/fundamentals/database-collection.txt +++ b/docs/fundamentals/database-collection.txt @@ -30,7 +30,7 @@ data as **documents** that contain field-and-value pairs. In {+odm-short+}, you can access documents through Eloquent models. To learn more about the document data format, -see :manual:`Documents ` in the Server manual. +see :manual:`Documents ` in the {+server-docs-name+}. .. _laravel-access-db: @@ -39,7 +39,7 @@ Specify the Database in a Connection Configuration You can specify a database name that a connection uses in your application's ``config/database.php`` file. The ``connections`` property -in this file stores all of your database connection information, such as +in this file stores all your database connection information, such as your connection string, database name, and optionally, authentication details. After you specify a database connection, you can perform database-level operations and access collections that the database @@ -98,8 +98,8 @@ The following example shows how to specify multiple database connections The MongoDB PHP driver reuses the same connection when you create two clients with the same connection string. There is no - overhead in using two connections for two distinct databases, so you - do not need to optimize your connections. + overhead in using two connections for two distinct databases, so it is + unnecessary to optimize your connections. If your application contains multiple database connections and you want to store your model in a database other than the default, override the diff --git a/docs/fundamentals/read-operations.txt b/docs/fundamentals/read-operations.txt index 23793711f..fea24ea38 100644 --- a/docs/fundamentals/read-operations.txt +++ b/docs/fundamentals/read-operations.txt @@ -256,14 +256,14 @@ a text index on the ``plot`` field. IMDB Rating: 7.4 IMDB Votes: 9663 Plot: A love story between a man and woman ... - + Title: Paheli Year: 2005 Runtime: 140 IMDB Rating: 6.7 IMDB Votes: 8909 Plot: A folk tale - supernatural love story about a ghost ... - + Title: Por un puรจado de besos Year: 2014 Runtime: 98 @@ -296,7 +296,7 @@ results by relevance by using the ``orderBy()`` method to sort on the Modify Behavior --------------- -You can modify the results of a find operation by chaining additional methods +You can modify the results of a find operation by chaining more methods to ``where()``. The following sections demonstrate how to modify the behavior of the ``where()`` @@ -397,15 +397,15 @@ followed by the ``orderBy()`` method. You can set an **ascending** or **descending** sort direction on results. By default, the ``orderBy()`` method sets an ascending sort on the supplied field name, but you can explicitly specify an ascending -sort by passing ``'asc'`` as the second parameter. To -specify a descending sort, pass ``'desc'`` as the second parameter. +sort by passing ``"asc"`` as the second parameter. To +specify a descending sort, pass ``"desc"`` as the second parameter. If your documents contain duplicate values in a specific field, you can -handle the tie by specifying additional fields to sort on. This ensures consistent -results if the additional fields contain unique values. +handle the tie by specifying more fields to sort on. This ensures consistent +results if the other fields contain unique values. This example queries for documents in which the value of the ``countries`` field contains -``'Indonesia'`` and orders results first by an ascending sort on the +``"Indonesia"`` and orders results first by an ascending sort on the ``year`` field, then a descending sort on the ``title`` field. .. tabs:: @@ -458,14 +458,14 @@ This example queries for documents in which the value of the ``countries`` field IMDB Rating: 7.6 IMDB Votes: 702 Plot: A film delivery man promises ... - + Title: Gie Year: 2005 Runtime: 147 IMDB Rating: 7.5 IMDB Votes: 470 Plot: Soe Hok Gie is an activist who lived in the sixties ... - + Title: Requiem from Java Year: 2006 Runtime: 120 @@ -473,7 +473,7 @@ This example queries for documents in which the value of the ``countries`` field IMDB Votes: 316 Plot: Setyo (Martinus Miroto) and Siti (Artika Sari Dewi) are young married couple ... - + ... .. tip:: @@ -481,8 +481,8 @@ This example queries for documents in which the value of the ``countries`` field To learn more about sorting, see the following resources: - :manual:`Natural order ` - in the Server manual glossary - - `Ordering, Grouping, Limit and Offset `__ + in the {+server-docs-name+} glossary + - `Ordering, Grouping, Limit, and Offset `__ in the Laravel documentation .. _laravel-retrieve-one: diff --git a/docs/fundamentals/write-operations.txt b/docs/fundamentals/write-operations.txt index 242d4e941..57bbcd8bc 100644 --- a/docs/fundamentals/write-operations.txt +++ b/docs/fundamentals/write-operations.txt @@ -283,7 +283,7 @@ Upsert Example This example shows how to pass the ``upsert`` option to the ``update()`` method to perform an update or insert in a single operation. Click the -:guilabel:`VIEW OUTPUT` button to see the example document inserted when no +:guilabel:`{+code-output-label+}` button to see the example document inserted when no matching documents exist: .. io-code-block:: @@ -353,7 +353,7 @@ method call: The following example shows how to add the value ``"baroque"`` to the ``genres`` array field of a matching document. Click the -:guilabel:`VIEW OUTPUT` button to see the updated document: +:guilabel:`{+code-output-label+}` button to see the updated document: .. io-code-block:: @@ -400,7 +400,7 @@ from the array. The following code example shows the structure of a The following example shows how to remove array values ``"classical"`` and ``"dance-pop"`` from the ``genres`` array field. Click the -:guilabel:`VIEW OUTPUT` button to see the updated document: +:guilabel:`{+code-output-label+}` button to see the updated document: .. io-code-block:: @@ -451,7 +451,7 @@ structure of a positional operator update call on a single matching document: The following example shows how to replace the array value ``"dance-pop"`` with ``"contemporary"`` in the ``genres`` array field. Click the -:guilabel:`VIEW OUTPUT` button to see the updated document: +:guilabel:`{+code-output-label+}` button to see the updated document: .. io-code-block:: diff --git a/docs/includes/framework-compatibility-laravel.rst b/docs/includes/framework-compatibility-laravel.rst index 1efdce964..1305cf8e0 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.3 + - โœ“ + - โœ“ + - + * - 4.2 - โœ“ - โœ“ @@ -21,4 +26,3 @@ - - โœ“ - - diff --git a/docs/includes/query-builder/QueryBuilderTest.php b/docs/includes/query-builder/QueryBuilderTest.php index 40705102d..a7d7a591e 100644 --- a/docs/includes/query-builder/QueryBuilderTest.php +++ b/docs/includes/query-builder/QueryBuilderTest.php @@ -228,7 +228,6 @@ public function testAggAvg(): void // begin aggregation avg $result = DB::collection('movies') ->avg('imdb.rating'); - //->avg('year'); // end aggregation avg $this->assertIsFloat($result); diff --git a/docs/includes/usage-examples/fact-edit-laravel-app.rst b/docs/includes/usage-examples/fact-edit-laravel-app.rst new file mode 100644 index 000000000..ad6839cfb --- /dev/null +++ b/docs/includes/usage-examples/fact-edit-laravel-app.rst @@ -0,0 +1,2 @@ +To learn how to edit your Laravel application to run the usage example, see the +:ref:`Usage Examples landing page `. diff --git a/docs/includes/usage-examples/operation-description.rst b/docs/includes/usage-examples/operation-description.rst new file mode 100644 index 000000000..68119a249 --- /dev/null +++ b/docs/includes/usage-examples/operation-description.rst @@ -0,0 +1,2 @@ +|operator-description| by creating a query builder, using a method such +as ``Model::where()`` or the ``DB`` facade to match documents in a collection, and then calling |result-operation|. diff --git a/docs/index.txt b/docs/index.txt index b6b61b54d..a4be7dcea 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,5 +1,5 @@ =============== -Laravel MongoDB +{+odm-short+} =============== .. facet:: @@ -95,7 +95,6 @@ compatible, see the :ref:`laravel-compatibility` section. Upgrade Versions ---------------- -Learn what changes you might need to make to your application to upgrade -versions in the :ref:`laravel-upgrading` section. - +Learn what changes you must make to your application to upgrade versions in +the :ref:`laravel-upgrading` section. diff --git a/docs/issues-and-help.txt b/docs/issues-and-help.txt index ff4a1dbb9..197f0a5b1 100644 --- a/docs/issues-and-help.txt +++ b/docs/issues-and-help.txt @@ -4,8 +4,15 @@ Issues & Help ============= +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: debug, report bug, request, contribute, github, support + We are lucky to have a vibrant PHP community that includes users of varying -experience with MongoDB PHP Library and {+odm-short+}. To get support for +experience with {+php-library+} and {+odm-short+}. To get support for general questions, search or post in the :community-forum:`MongoDB PHP Community Forums `. diff --git a/docs/query-builder.txt b/docs/query-builder.txt index 9547b1676..8b4be3245 100644 --- a/docs/query-builder.txt +++ b/docs/query-builder.txt @@ -73,7 +73,10 @@ Before You Get Started To run the code examples in this guide, complete the :ref:`Quick Start ` tutorial to configure a web application, load sample datasets into your MongoDB deployment, and -run the example code from a controller method. +run the example code from a controller method. To see the expected code +output as JSON documents, use the ``toJson()`` method shown in the optional +:ref:`View your results as JSON documents ` step +of the Quick Start. To perform read and write operations by using the query builder, import the ``Illuminate\Support\Facades\DB`` facade and compose your query. 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/docs/quick-start.txt b/docs/quick-start.txt index fb8ad6fe2..30587454e 100644 --- a/docs/quick-start.txt +++ b/docs/quick-start.txt @@ -36,9 +36,9 @@ read and write operations on the data. :mdbu-course:`Getting Started with Laravel and MongoDB ` MongoDB University Learning Byte. - If you prefer to connect to MongoDB by using the PHP Library driver without + If you prefer to connect to MongoDB by using the {+php-library+} without Laravel, see `Connecting to MongoDB `__ - in the PHP Library documentation. + in the {+php-library+} documentation. {+odm-short+} extends the Laravel Eloquent and Query Builder syntax to store and retrieve data from MongoDB. diff --git a/docs/quick-start/configure-mongodb.txt b/docs/quick-start/configure-mongodb.txt index 2f6dd0e36..6f72455a6 100644 --- a/docs/quick-start/configure-mongodb.txt +++ b/docs/quick-start/configure-mongodb.txt @@ -14,20 +14,50 @@ Configure Your MongoDB Connection .. procedure:: :style: connected + .. step:: Configure the application environment variable file + + Copy the ``.env.example`` file to a file named ``.env`` in the project + root directory by running the following shell command: + + .. code-block:: sh + + cp .env.example .env + + Open the ``.env`` file and add or edit the following variables and values. + Replace the ```` placeholder with your connection + string from the :ref:`laravel-quick-start-connection-string` step: + + .. code-block:: bash + + DB_CONNECTION=mongodb + DB_URI="" + + For example, if your connection string is + ``"mongodb+srv://myUser:myPass123@mongo0.example.com/"``, + your ``DB_URI`` variable matches the following line: + + .. code-block:: bash + + DB_URI="mongodb+srv://myUser:myPass123@mongo0.example.com/" + + .. note:: + + Make sure these variables in your ``.env`` file are undefined in the + shell in which you run your application. Environment variables in + the shell take precedence over the ones in the ``.env`` file. + .. step:: Set the connection string in the database configuration - Open the ``database.php`` file in the ``config`` directory and - set the default database connection to ``mongodb`` as shown - in the following line: + Open the ``database.php`` file in the ``config`` directory and set the + default database connection to the ``DB_CONNECTION`` environment + variable as shown in the following line: .. code-block:: php - 'default' => env('DB_CONNECTION', 'mongodb'), + 'default' => env('DB_CONNECTION'), Add the following highlighted ``mongodb`` entry to the ``connections`` array - in the same file. Replace the ```` placeholder with the - connection string that you copied from the :ref:`laravel-quick-start-connection-string` - step in the following code example: + in the same file: .. code-block:: php :emphasize-lines: 2-6 @@ -35,13 +65,13 @@ Configure Your MongoDB Connection 'connections' => [ 'mongodb' => [ 'driver' => 'mongodb', - 'dsn' => env('DB_URI', ''), + 'dsn' => env('DB_URI'), 'database' => 'sample_mflix', ], // ... - .. step:: Add the MongoDB provider + .. step:: Add the {+odm-short+} provider Open the ``providers.php`` file in the ``bootstrap`` directory and add the following entry into the array: diff --git a/docs/quick-start/next-steps.txt b/docs/quick-start/next-steps.txt index 0284720a3..1afcc2f7e 100644 --- a/docs/quick-start/next-steps.txt +++ b/docs/quick-start/next-steps.txt @@ -23,8 +23,15 @@ GitHub repository. Learn more about {+odm-short+} features from the following resources: +- :ref:`laravel-fundamentals-connection`: learn how to configure your MongoDB + connection. + +- :ref:`laravel-usage-examples`: see code examples of frequently used MongoDB + operations. + - :ref:`laravel-eloquent-models`: use Eloquent model classes to work with MongoDB data. - :ref:`laravel-query-builder`: use the query builder to specify MongoDB queries and aggregations. + diff --git a/docs/quick-start/view-data.txt b/docs/quick-start/view-data.txt index 9f086c774..ecd5206a0 100644 --- a/docs/quick-start/view-data.txt +++ b/docs/quick-start/view-data.txt @@ -39,7 +39,7 @@ View MongoDB Data make the following edits: - Replace the ``Illuminate\Database\Eloquent\Model`` import with ``MongoDB\Laravel\Eloquent\Model`` - - Specify ``mongodb`` in the ``$connection`` field + - Specify ``"mongodb"`` in the ``$connection`` field The edited ``Movie.php`` file contains the following code: @@ -136,6 +136,29 @@ View MongoDB Data + + .. _laravel-quick-start-json: + + .. step:: Optionally, view your results as JSON documents + + Rather than generating a view and editing the ``browse_movie.blade.php`` file, you can + use the ``toJson()`` method to display your results in JSON format. + + Replace the ``show()`` function with the following code to retrieve results and + return them as JSON documents: + + .. code-block:: php + + public function show() + { + $results = Movie::where('runtime', '<', 60) + ->where('imdb.rating', '>', 8.5) + ->orderBy('imdb.rating', 'desc') + ->take(10) + ->get(); + + return $results->toJson(); + } .. step:: Start your Laravel application diff --git a/docs/quick-start/write-data.txt b/docs/quick-start/write-data.txt index ddf2a98d9..3ede2f8c5 100644 --- a/docs/quick-start/write-data.txt +++ b/docs/quick-start/write-data.txt @@ -79,14 +79,14 @@ Write Data to MongoDB .. code-block:: json { - "title": "The Laravel MongoDB Quick Start", + "title": "The {+odm-short+} Quick Start", "year": 2024, "runtime": 15, "imdb": { "rating": 9.5, "votes": 1 }, - "plot": "This movie entry was created by running through the Laravel MongoDB Quick Start tutorial." + "plot": "This movie entry was created by running through the {+odm-short+} Quick Start tutorial." } Send the JSON payload to the endpoint as a ``POST`` request by running diff --git a/docs/transactions.txt b/docs/transactions.txt index 3cb3c2c5b..5ef3df19d 100644 --- a/docs/transactions.txt +++ b/docs/transactions.txt @@ -24,7 +24,7 @@ In this guide, you can learn how to perform a **transaction** in MongoDB by using the {+odm-long+}. Transactions let you run a sequence of write operations that update the data only after the transaction is committed. -If the transaction fails, the PHP library that manages MongoDB operations +If the transaction fails, the {+php-library+} that manages MongoDB operations for {+odm-short+} ensures that MongoDB discards all the changes made within the transaction before they become visible. This property of transactions that ensures that all changes within a transaction are either applied or diff --git a/docs/upgrade.txt b/docs/upgrade.txt index 1aeba2be3..46308d6de 100644 --- a/docs/upgrade.txt +++ b/docs/upgrade.txt @@ -34,7 +34,7 @@ Before you upgrade, perform the following actions: application runs on. See the :ref:`` page for this information. - Address any breaking changes between the version of {+odm-short+} that - your application currently uses and your planned upgrade version in the + your application now uses and your planned upgrade version in the :ref:`` section of this guide. To upgrade your library version, run the following command in your application's diff --git a/docs/usage-examples.txt b/docs/usage-examples.txt index 629ba5eca..a17fd1b70 100644 --- a/docs/usage-examples.txt +++ b/docs/usage-examples.txt @@ -42,7 +42,7 @@ Before You Get Started ~~~~~~~~~~~~~~~~~~~~~~ You can run the usage examples from your own Laravel application or from the -``{+quickstart-app-name+}`` application created in the :ref:`laravel-quick-start` guide. +``{+quickstart-app-name+}`` application created in the :ref:`laravel-quick-start` guide. The usage examples are designed to run operations on a MongoDB deployment that contains the MongoDB Atlas sample datasets. Before running the usage examples, ensure that you load @@ -50,10 +50,14 @@ the sample data into the MongoDB cluster to which your application connects. Oth operation output might not match the text included in the ``{+code-output-label+}`` tab of the usage example page. -.. tip:: +Unless otherwise mentioned, usage examples use the ``Movie.php`` model class +created in the Quick Start to demonstrate operations on the ``movies`` MongoDB +collection. + +.. tip:: For instructions on loading the sample data into a MongoDB cluster, see - :atlas:`Load Sample Data ` in the Atlas documentation. + :atlas:`Load Sample Data ` in the Atlas documentation. .. _run-usage-examples: @@ -67,6 +71,25 @@ Laravel application. To view the expected output of the operation, you can add a web route to your application that calls the controller function and returns the result to a web interface. +.. _usage-example-list: + +Usage Example List +------------------ + +See code examples of the following operations in this section: + +- :ref:`laravel-find-one-usage` +- :ref:`laravel-find-usage` +- :ref:`laravel-insert-one-usage` +- :ref:`laravel-insert-many-usage` +- :ref:`laravel-update-one-usage` +- :ref:`laravel-update-many-usage` +- :ref:`laravel-delete-one-usage` +- :ref:`laravel-delete-many-usage` +- :ref:`laravel-count-usage` +- :ref:`laravel-distinct-usage` +- :ref:`laravel-run-command-usage` + .. toctree:: :titlesonly: :maxdepth: 1 diff --git a/docs/usage-examples/count.txt b/docs/usage-examples/count.txt index dc3720fc0..c3af477ee 100644 --- a/docs/usage-examples/count.txt +++ b/docs/usage-examples/count.txt @@ -17,11 +17,15 @@ Count Documents :depth: 1 :class: singlecol -You can count the number of documents returned by a query by calling the ``where()`` and -``count()`` methods on a collection of models or a query builder. +.. include:: /includes/usage-examples/operation-description.rst -To return the number of documents that match a filter, pass the query filter to the ``where()`` -method and call the ``count()`` method. + .. replacement:: operator-description + + You can count the number of documents returned by a query + + .. replacement:: result-operation + + the ``count()`` method to retrieve the results. Example ------- @@ -50,8 +54,6 @@ The example calls the following methods on the ``Movie`` model: :language: console :visible: false - Matching documents: 1267 - + Number of documents: 1267 -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst diff --git a/docs/usage-examples/deleteMany.txt b/docs/usage-examples/deleteMany.txt index ec80f1140..14a1091f8 100644 --- a/docs/usage-examples/deleteMany.txt +++ b/docs/usage-examples/deleteMany.txt @@ -37,8 +37,8 @@ The example calls the following methods on the ``Movie`` model: - ``where()``: matches documents in which the value of the ``year`` field is less than or equal to ``1910``. -- ``delete()``: deletes the retrieved documents. This method returns the number of documents - that were successfully deleted. +- ``delete()``: deletes the matched documents. This method returns the number + of documents that the method successfully deletes. .. io-code-block:: :copyable: true @@ -55,14 +55,10 @@ The example calls the following methods on the ``Movie`` model: Deleted documents: 7 -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: To learn more about deleting documents with {+odm-short+}, see the :ref:`laravel-fundamentals-delete-documents` section of the Write Operations guide. - For more information about query filters, see the :ref:`laravel-retrieve-matching` section of - the Read Operations guide. - diff --git a/docs/usage-examples/deleteOne.txt b/docs/usage-examples/deleteOne.txt index 3f934b273..9c8d6b127 100644 --- a/docs/usage-examples/deleteOne.txt +++ b/docs/usage-examples/deleteOne.txt @@ -36,7 +36,7 @@ This usage example performs the following actions: The example calls the following methods on the ``Movie`` model: -- ``where()``: matches documents in which the value of the ``title`` field is ``'Quiz Show'`` +- ``where()``: matches documents in which the value of the ``title`` field is ``"Quiz Show"`` - ``orderBy()``: sorts matched documents by their ascending ``_id`` values - ``limit()``: retrieves only the first matching document - ``delete()``: deletes the retrieved document @@ -56,8 +56,7 @@ The example calls the following methods on the ``Movie`` model: Deleted documents: 1 -For instructions on editing your Laravel application to run the usage example, see the -:ref:`Usage Example landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: @@ -65,6 +64,3 @@ For instructions on editing your Laravel application to run the usage example, s `__ section of the Laravel documentation. - For more information about query filters, see the :ref:`laravel-retrieve-matching` section of - the Read Operations guide. - diff --git a/docs/usage-examples/distinct.txt b/docs/usage-examples/distinct.txt index 8765bea1b..5d62ec8be 100644 --- a/docs/usage-examples/distinct.txt +++ b/docs/usage-examples/distinct.txt @@ -36,10 +36,10 @@ This usage example performs the following actions: The example calls the following methods on the ``Movie`` model: -- ``where()``: matches documents in which the value of the ``directors`` field includes ``'Sofia Coppola'``. +- ``where()``: matches documents in which the value of the ``directors`` field includes ``"Sofia Coppola"``. - ``select()``: retrieves the matching documents' ``imdb.rating`` field values. -- ``distinct()``: accesses the unique values of the ``imdb.rating`` field among the matching - documents. This method returns a list of values. +- ``distinct()``: retrieves the unique values of the selected field and returns + the list of values. - ``get()``: retrieves the query results. .. io-code-block:: @@ -57,8 +57,7 @@ The example calls the following methods on the ``Movie`` model: [[5.6],[6.4],[7.2],[7.8]] -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: diff --git a/docs/usage-examples/find.txt b/docs/usage-examples/find.txt index 3e9115661..b12c97f41 100644 --- a/docs/usage-examples/find.txt +++ b/docs/usage-examples/find.txt @@ -17,9 +17,15 @@ Find Multiple Documents :depth: 1 :class: singlecol -You can retrieve multiple documents from a collection by creating a query -builder using a method such as ``Model::where()`` or by using the ``DB`` -facade, and then chaining the ``get()`` method to retrieve the result. +.. include:: /includes/usage-examples/operation-description.rst + + .. replacement:: operator-description + + You can retrieve multiple documents from a collection + + .. replacement:: result-operation + + the ``get()`` method to retrieve the results. Pass a query filter to the ``where()`` method to retrieve documents that meet a set of criteria. When you call the ``get()`` method, MongoDB returns the @@ -76,8 +82,7 @@ The example calls the following methods on the ``Movie`` model: ... ] -For instructions on editing your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: diff --git a/docs/usage-examples/findOne.txt b/docs/usage-examples/findOne.txt index 2a66726d1..815d7923e 100644 --- a/docs/usage-examples/findOne.txt +++ b/docs/usage-examples/findOne.txt @@ -4,12 +4,24 @@ Find a Document =============== -You can retrieve a single document from a collection by calling the ``where()`` and -``first()`` methods on an Eloquent model or a query builder. +.. facet:: + :name: genre + :values: reference -Pass a query filter to the ``where()`` method and then call the ``first()`` method to -return one document in the collection that matches the filter. If multiple documents match -the query filter, ``first()`` returns the first matching document according to the documents' +.. meta:: + :keywords: find one, retrieve, code example, first + +.. include:: /includes/usage-examples/operation-description.rst + + .. replacement:: operator-description + + You can retrieve a single document from a collection + + .. replacement:: result-operation + + the ``first()`` method to return one document. + +If multiple documents match the query filter, ``first()`` returns the first matching document according to the documents' :term:`natural order` in the database or according to the sort order that you can specify by using the ``orderBy()`` method. @@ -25,7 +37,7 @@ This usage example performs the following actions: The example calls the following methods on the ``Movie`` model: -- ``where()``: matches documents in which the value of the ``directors`` field includes ``'Rob Reiner'``. +- ``where()``: matches documents in which the value of the ``directors`` field includes ``"Rob Reiner"``. - ``orderBy()``: sorts matched documents by their ascending ``_id`` values. - ``first()``: retrieves only the first matching document. @@ -42,34 +54,17 @@ The example calls the following methods on the ``Movie`` model: :visible: false // Result is truncated - + { - "_id": "573a1398f29313caabce94a3", - "plot": "Spinal Tap, one of England's loudest bands, is chronicled by film director - Marty DeBergi on what proves to be a fateful tour.", - "genres": [ - "Comedy", - "Music" - ], - "runtime": 82, - "metacritic": 85, - "rated": "R", - "cast": [ - "Rob Reiner", - "Kimberly Stringer", - "Chazz Dominguez", - "Shari Hall" - ], - "poster": "https://m.media-amazon.com/images/M/MV5BMTQ2MTIzMzg5Nl5BMl5BanBnXkFtZTgwOTc5NDI1MDE@._V1_SY1000_SX677_AL_.jpg", - "title": "This Is Spinal Tap", + "_id": ..., + "title": "This Is Spinal Tap", + "directors": [ "Rob Reiner" ], ... } - -For instructions on editing your Laravel application to run the usage example, see the -:ref:`Usage Example landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: To learn more about retrieving documents with {+odm-short+}, see the - :ref:`laravel-fundamentals-retrieve` guide \ No newline at end of file + :ref:`laravel-fundamentals-retrieve` guide. diff --git a/docs/usage-examples/insertMany.txt b/docs/usage-examples/insertMany.txt index bf771aa8d..2d59a78ab 100644 --- a/docs/usage-examples/insertMany.txt +++ b/docs/usage-examples/insertMany.txt @@ -9,7 +9,7 @@ Insert Multiple Documents :values: reference .. meta:: - :keywords: insert many, add, create, bulk, code example + :keywords: insert many, add multiple, code example .. contents:: On this page :local: @@ -32,11 +32,12 @@ This usage example performs the following actions: - Uses the ``Movie`` Eloquent model to represent the ``movies`` collection in the ``sample_mflix`` database - Inserts documents into the ``movies`` collection -- Prints the result of the insert operation +- Prints whether the insert operation succeeds -The example calls the ``insert()`` method to insert documents that represent movies released -in 2023. This method returns a value of ``1`` if the operation is successful, and it throws -an exception if the operation is unsuccessful. +The example calls the ``insert()`` method to insert documents that contain +information about movies released in ``2023``. If the insert operation is +successful, it returns a value of ``1``. If the operation fails, it throws +an exception. .. io-code-block:: :copyable: true @@ -53,8 +54,7 @@ an exception if the operation is unsuccessful. Insert operation success: yes -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: diff --git a/docs/usage-examples/insertOne.txt b/docs/usage-examples/insertOne.txt index 785bf2578..e28e12090 100644 --- a/docs/usage-examples/insertOne.txt +++ b/docs/usage-examples/insertOne.txt @@ -61,13 +61,12 @@ information: "_id": "..." } -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: You can also use the ``save()`` or ``insert()`` methods to insert a document into a collection. - To learn more about insert operations, see the :ref:`laravel-fundamentals-insert-documents` section + To learn more about insert operations, see the :ref:`laravel-fundamentals-insert-documents` section of the Write Operations guide. diff --git a/docs/usage-examples/runCommand.txt b/docs/usage-examples/runCommand.txt index 51f0cca83..7d3d95ac9 100644 --- a/docs/usage-examples/runCommand.txt +++ b/docs/usage-examples/runCommand.txt @@ -4,6 +4,19 @@ Run a Command ============= +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: server command, list collections, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 1 + :class: singlecol + You can run a MongoDB command directly on a database by calling the ``command()`` method on a database connection instance. @@ -44,8 +57,7 @@ returns a cursor that contains a result document for each collection in the data embedded_movies users -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: diff --git a/docs/usage-examples/updateMany.txt b/docs/usage-examples/updateMany.txt index 3a7482336..7fd5bfd1b 100644 --- a/docs/usage-examples/updateMany.txt +++ b/docs/usage-examples/updateMany.txt @@ -1,4 +1,4 @@ -.. _laravel-update-one-usage: +.. _laravel-update-many-usage: ========================= Update Multiple Documents @@ -37,7 +37,7 @@ This usage example performs the following actions: The example calls the following methods on the ``Movie`` model: - ``where()``: matches documents in which the value of the ``imdb.rating`` nested field - is greater than ``9``. + is greater than ``9.0``. - ``update()``: updates the matching documents by adding an ``acclaimed`` field and setting its value to ``true``. This method returns the number of documents that were successfully updated. @@ -57,8 +57,7 @@ The example calls the following methods on the ``Movie`` model: Updated documents: 20 -To learn how to edit your Laravel application to run the usage example, see the -:ref:`Usage Examples landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: diff --git a/docs/usage-examples/updateOne.txt b/docs/usage-examples/updateOne.txt index 12aec17ff..42fcda477 100644 --- a/docs/usage-examples/updateOne.txt +++ b/docs/usage-examples/updateOne.txt @@ -31,16 +31,16 @@ This usage example performs the following actions: - Uses the ``Movie`` Eloquent model to represent the ``movies`` collection in the ``sample_mflix`` database -- Updates a document from the ``movies`` collection that matches a query filter +- Updates a document from the ``movies`` collection that matches the query filter - Prints the number of updated documents The example calls the following methods on the ``Movie`` model: -- ``where()``: matches documents in which the value of the ``title`` field is ``'Carol'``. +- ``where()``: matches documents in which the value of the ``title`` field is ``"Carol"``. - ``orderBy()``: sorts matched documents by their ascending ``_id`` values. - ``first()``: retrieves only the first matching document. - ``update()``: updates the value of the ``imdb.rating`` nested field to from ``6.9`` to - ``7.3``. This method also updates the ``imdb.votes`` nested field from ``493`` to ``142000``. + ``7.3`` and the value of the ``imdb.votes`` nested field from ``493`` to ``142000``. .. io-code-block:: :copyable: true @@ -57,12 +57,10 @@ The example calls the following methods on the ``Movie`` model: Updated documents: 1 -For instructions on editing your Laravel application to run the usage example, see the -:ref:`Usage Example landing page `. +.. include:: /includes/usage-examples/fact-edit-laravel-app.rst .. tip:: - To learn more about updating data with {+odm-short+}, see the `Updates - `__ section of the - Laravel documentation. + To learn more about updating data with {+odm-short+}, see the :ref:`laravel-fundamentals-modify-documents` + section of the Write Operations guide. 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/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/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'" + } + ] + } + ] + } +} 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/Connection.php b/src/Connection.php index 01232c7ae..2ce5324ee 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -14,6 +14,7 @@ use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\ReadPreference; use MongoDB\Laravel\Concerns\ManagesTransactions; +use OutOfBoundsException; use Throwable; use function filter_var; @@ -65,6 +66,8 @@ public function __construct(array $config) // Select database $this->db = $this->connection->selectDatabase($this->getDefaultDatabaseName($dsn, $config)); + $this->tablePrefix = $config['prefix'] ?? ''; + $this->useDefaultPostProcessor(); $this->useDefaultSchemaGrammar(); @@ -108,7 +111,7 @@ public function table($table, $as = null) */ public function getCollection($name) { - return new Collection($this, $this->db->selectCollection($name)); + return new Collection($this, $this->db->selectCollection($this->tablePrefix . $name)); } /** @inheritdoc */ @@ -207,11 +210,13 @@ public function ping(): void /** @inheritdoc */ public function disconnect() { - unset($this->connection); + $this->connection = null; } /** * Determine if the given configuration array has a dsn string. + * + * @deprecated */ protected function hasDsnString(array $config): bool { @@ -260,9 +265,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 */ @@ -324,7 +335,11 @@ private static function getVersion(): string private static function lookupVersion(): string { try { - return self::$version = InstalledVersions::getPrettyVersion('mongodb/laravel-mongodb') ?? 'unknown'; + try { + return self::$version = InstalledVersions::getPrettyVersion('mongodb/laravel-mongodb') ?? 'unknown'; + } catch (OutOfBoundsException) { + return self::$version = InstalledVersions::getPrettyVersion('jenssegers/mongodb') ?? 'unknown'; + } } catch (Throwable) { return self::$version = 'error'; } diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index 22dcfd081..da96b64f1 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -4,18 +4,14 @@ 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; @@ -25,6 +21,7 @@ /** @method \MongoDB\Laravel\Query\Builder toBase() */ class Builder extends EloquentBuilder { + private const DUPLICATE_KEY_ERROR = 11000; use QueriesRelationships; /** @@ -201,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 === []) { - throw new InvalidArgumentException('You must provide attributes to check for duplicates'); + $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; + } } /** @@ -276,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/Eloquent/Model.php b/src/Eloquent/Model.php index de5ddc3ea..f7b4f1f36 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -46,15 +46,15 @@ use function str_contains; use function str_starts_with; use function strcmp; -use function uniqid; use function var_export; -/** @mixin Builder */ abstract class Model extends BaseModel { use HybridRelations; use EmbedsRelations; + private const TEMPORARY_KEY = '__LARAVEL_TEMPORARY_KEY__'; + /** * The collection associated with the model. * @@ -271,12 +271,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; } @@ -747,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/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/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); + } +} diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 262c4cafc..83097973b 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'); @@ -178,6 +194,8 @@ public function testConnectionConfig(string $expectedUri, string $expectedDataba $this->assertSame($expectedUri, (string) $client); $this->assertSame($expectedDatabaseName, $connection->getMongoDB()->getDatabaseName()); + $this->assertSame('foo', $connection->getCollection('foo')->getCollectionName()); + $this->assertSame('foo', $connection->collection('foo')->raw()->getCollectionName()); } public function testConnectionWithoutConfiguredDatabase(): void @@ -188,6 +206,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'); @@ -200,6 +226,20 @@ public function testCollection() $this->assertInstanceOf(Builder::class, $collection); } + public function testPrefix() + { + $config = [ + 'dsn' => 'mongodb://127.0.0.1/', + 'database' => 'tests', + 'prefix' => 'prefix_', + ]; + + $connection = new Connection($config); + + $this->assertSame('prefix_foo', $connection->getCollection('foo')->getCollectionName()); + $this->assertSame('prefix_foo', $connection->collection('foo')->raw()->getCollectionName()); + } + public function testQueryLog() { DB::enableQueryLog(); diff --git a/tests/ModelTest.php b/tests/ModelTest.php index baa731799..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(); @@ -1151,4 +1180,34 @@ 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()); + } + + /** @param class-string $modelClass */ + private static function registerModelEvents(string $modelClass, array &$events): void + { + $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'; + }); + } } 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; + } +}