diff --git a/.github/release.yml b/.github/release.yml index aabd8e4f2..0ca62014a 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -12,10 +12,14 @@ changelog: - title: New Features labels: - enhancement + - feature - title: Fixed labels: - bug - fixed + - title: Documentation Changes + labels: + - docs - title: Other Changes labels: - "*" diff --git a/.github/workflows/build-ci-atlas.yml b/.github/workflows/build-ci-atlas.yml index 7a4ebd03f..a4ab45e1d 100644 --- a/.github/workflows/build-ci-atlas.yml +++ b/.github/workflows/build-ci-atlas.yml @@ -1,74 +1,100 @@ name: "Atlas CI" on: - push: - pull_request: + push: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" + pull_request: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" + +env: + MONGODB_EXT_V1: mongodb-1.21.0 + MONGODB_EXT_V2: stable jobs: - build: - runs-on: "${{ matrix.os }}" - - name: "PHP ${{ matrix.php }} Laravel ${{ matrix.laravel }} Atlas" - - strategy: - matrix: - os: - - "ubuntu-latest" - php: - - "8.2" - - "8.3" - - "8.4" - laravel: - - "11.*" - - steps: - - uses: "actions/checkout@v4" - - - name: "Create MongoDB Atlas Local" - run: | - docker run --name mongodb -p 27017:27017 --detach mongodb/mongodb-atlas-local:latest - until docker exec --tty mongodb mongosh --eval "db.runCommand({ ping: 1 })"; do - sleep 1 - done - until docker exec --tty mongodb mongosh --eval "db.createCollection('connection_test') && db.getCollection('connection_test').createSearchIndex({mappings:{dynamic: true}})"; do - sleep 1 - done - - - name: "Show MongoDB server status" - run: | - docker exec --tty mongodb mongosh --eval "db.runCommand({ serverStatus: 1 })" - - - name: "Installing php" - uses: "shivammathur/setup-php@v2" - with: - php-version: ${{ matrix.php }} - extensions: "curl,mbstring,xdebug" - coverage: "xdebug" - tools: "composer" - - - name: "Show Docker version" - if: ${{ runner.debug }} - run: "docker version && env" - - - name: "Restrict Laravel version" - run: "composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'" - - - name: "Download Composer cache dependencies from cache" - id: "composer-cache" - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: "Cache Composer dependencies" - uses: "actions/cache@v4" - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: "${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "${{ matrix.os }}-composer-" - - - name: "Install dependencies" - run: | - composer update --no-interaction - - - name: "Run tests" - run: | - export MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" - ./vendor/bin/phpunit --coverage-clover coverage.xml --group atlas-search + build: + runs-on: "${{ matrix.os }}" + + name: "PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }}" + + strategy: + matrix: + os: + - "ubuntu-latest" + php: + - "8.2" + - "8.3" + - "8.4" + laravel: + - "11.*" + - "12.*" + driver: + - 1 + include: + - php: "8.4" + laravel: "12.*" + os: "ubuntu-latest" + driver: 2 + + steps: + - uses: "actions/checkout@v5" + + - name: "Create MongoDB Atlas Local" + run: | + docker run --name mongodb -p 27017:27017 --detach mongodb/mongodb-atlas-local:latest + until docker exec --tty mongodb mongosh --eval "db.runCommand({ ping: 1 })"; do + sleep 1 + done + until docker exec --tty mongodb mongosh --eval "db.createCollection('connection_test') && db.getCollection('connection_test').createSearchIndex({mappings:{dynamic: true}})"; do + sleep 1 + done + + - name: "Show MongoDB server status" + run: | + docker exec --tty mongodb mongosh --eval "db.runCommand({ serverStatus: 1 })" + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }} + key: "extcache-v1" + + - name: "Installing php" + uses: "shivammathur/setup-php@v2" + with: + php-version: ${{ matrix.php }} + extensions: "curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}" + coverage: "xdebug" + tools: "composer" + + - name: "Show Docker version" + if: ${{ runner.debug }} + run: "docker version && env" + + - name: "Restrict Laravel version" + run: "composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'" + + - name: "Download Composer cache dependencies from cache" + id: "composer-cache" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Cache Composer dependencies" + uses: "actions/cache@v4" + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: "${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "${{ matrix.os }}-composer-" + + - name: "Install dependencies" + run: | + composer update --no-interaction + + - name: "Run tests" + run: | + export MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" + php -d zend.assertions=1 ./vendor/bin/phpunit --coverage-clover coverage.xml --group atlas-search diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index d16a5885f..f55bb3104 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -1,97 +1,127 @@ name: "CI" on: - push: - pull_request: + push: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" + pull_request: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" + +env: + MONGODB_EXT_V1: mongodb-1.21.0 + MONGODB_EXT_V2: stable jobs: - build: - runs-on: "${{ matrix.os }}" - - name: "PHP ${{ matrix.php }} Laravel ${{ matrix.laravel }} MongoDB ${{ matrix.mongodb }} ${{ matrix.mode }}" - - strategy: - matrix: - os: - - "ubuntu-latest" - mongodb: - - "4.4" - - "5.0" - - "6.0" - - "7.0" - - "8.0" - php: - - "8.1" - - "8.2" - - "8.3" - - "8.4" - laravel: - - "10.*" - - "11.*" - include: - - php: "8.1" - laravel: "10.*" - mongodb: "5.0" - mode: "low-deps" - os: "ubuntu-latest" - - php: "8.4" - laravel: "11.*" - mongodb: "7.0" - os: "ubuntu-latest" - exclude: - - php: "8.1" - laravel: "11.*" - - steps: - - uses: "actions/checkout@v4" - - - name: "Create MongoDB Replica Set" - run: | - docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${{ matrix.mongodb }} mongod --replSet rs --setParameter transactionLifetimeLimitSeconds=5 - - if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi - until docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ ping: 1 })"; do - sleep 1 - done - sudo docker exec --tty mongodb $MONGOSH_BIN --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})" - - - name: "Show MongoDB server status" - run: | - if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi - docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ serverStatus: 1 })" - - - name: "Installing php" - uses: "shivammathur/setup-php@v2" - with: - php-version: ${{ matrix.php }} - extensions: "curl,mbstring,xdebug" - coverage: "xdebug" - tools: "composer" - - - name: "Show Docker version" - if: ${{ runner.debug }} - run: "docker version && env" - - - name: "Restrict Laravel version" - run: "composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'" - - - name: "Download Composer cache dependencies from cache" - id: "composer-cache" - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: "Cache Composer dependencies" - uses: "actions/cache@v4" - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: "${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "${{ matrix.os }}-composer-" - - - name: "Install dependencies" - run: | - composer update --no-interaction \ - $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest') \ - $([[ "${{ matrix.mode }}" == ignore-php-req ]] && echo ' --ignore-platform-req=php+') - - name: "Run tests" - run: | - export MONGODB_URI="mongodb://127.0.0.1:27017/?replicaSet=rs" - ./vendor/bin/phpunit --coverage-clover coverage.xml --exclude-group atlas-search + build: + runs-on: "${{ matrix.os }}" + + name: "PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }} Server/${{ matrix.mongodb }} ${{ matrix.mode }}" + + strategy: + matrix: + os: + - "ubuntu-latest" + mongodb: + - "4.4" + - "5.0" + - "6.0" + - "7.0" + - "8.0" + php: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + laravel: + - "10.*" + - "11.*" + - "12.*" + driver: + - 2 + include: + - php: "8.1" + laravel: "10.*" + mongodb: "5.0" + mode: "low-deps" + os: "ubuntu-latest" + driver: 1 + - php: "8.3" + laravel: "11.*" + mongodb: "8.0" + os: "ubuntu-latest" + driver: 1 + - php: "8.4" + laravel: "12.*" + mongodb: "8.0" + os: "ubuntu-latest" + driver: 1 + exclude: + - php: "8.1" + laravel: "11.*" + - php: "8.1" + laravel: "12.*" + + steps: + - uses: "actions/checkout@v5" + + - name: "Create MongoDB Replica Set" + run: | + docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${{ matrix.mongodb }} mongod --replSet rs --setParameter transactionLifetimeLimitSeconds=5 + + if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi + until docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ ping: 1 })"; do + sleep 1 + done + sudo docker exec --tty mongodb $MONGOSH_BIN --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})" + + - name: "Show MongoDB server status" + run: | + if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi + docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ serverStatus: 1 })" + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }} + key: "extcache-v1" + + - name: "Installing php" + uses: "shivammathur/setup-php@v2" + with: + php-version: ${{ matrix.php }} + extensions: "curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}" + coverage: "xdebug" + tools: "composer" + + - name: "Show Docker version" + if: ${{ runner.debug }} + run: "docker version && env" + + - name: "Restrict Laravel version" + run: "composer require --dev --no-update 'laravel/framework:${{ matrix.laravel }}'" + + - name: "Download Composer cache dependencies from cache" + id: "composer-cache" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Cache Composer dependencies" + uses: "actions/cache@v4" + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: "${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "${{ matrix.os }}-composer-" + + - name: "Install dependencies" + run: | + composer update --no-interaction \ + $([[ "${{ matrix.mode }}" == low-deps ]] && echo ' --prefer-lowest') \ + $([[ "${{ matrix.mode }}" == ignore-php-req ]] && echo ' --ignore-platform-req=php+') + - name: "Run tests" + run: | + export MONGODB_URI="mongodb://127.0.0.1:27017/?replicaSet=rs" + php -d zend.assertions=1 ./vendor/bin/phpunit --coverage-clover coverage.xml --exclude-group atlas-search diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 24d397294..e9cddccfe 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -2,10 +2,16 @@ name: "Coding Standards" on: push: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" pull_request: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" env: - PHP_VERSION: "8.2" + PHP_VERSION: "8.4" DRIVER_VERSION: "stable" jobs: @@ -20,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Setup cache environment" id: "extcache" @@ -49,21 +55,15 @@ jobs: run: "php --ri mongodb" - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@3.0.0" + uses: "ramsey/composer-install@3.1.1" with: composer-options: "--no-suggest" - - name: "Format the code" - continue-on-error: true - run: | - mkdir .cache - ./vendor/bin/phpcbf + - name: "Validate PSR class names" + run: "composer dump-autoload --optimize --strict-psr" # The -q option is required until phpcs v4 is released - name: "Run PHP_CodeSniffer" - run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" - - - name: "Commit the changes" - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "apply phpcbf formatting" + run: | + mkdir .cache + vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr diff --git a/.github/workflows/merge-up.yml b/.github/workflows/merge-up.yml index 1ddbb7228..ad92517b5 100644 --- a/.github/workflows/merge-up.yml +++ b/.github/workflows/merge-up.yml @@ -4,6 +4,7 @@ on: push: branches: - "[0-9]+.[0-9x]+" + - "feature/*" env: GH_TOKEN: ${{ secrets.MERGE_UP_TOKEN }} @@ -16,7 +17,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: # fetch-depth 0 is required to fetch all branches, not just the branch being built fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4afbe78f1..bc60a79cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: run: | echo RELEASE_VERSION=${{ inputs.version }} >> $GITHUB_ENV echo RELEASE_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' -f-2) >> $GITHUB_ENV - echo DEV_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' - f-1).x >> $GITHUB_ENV + echo DEV_BRANCH=$(echo ${{ inputs.version }} | cut -d '.' -f-1).x >> $GITHUB_ENV - name: "Ensure release tag does not already exist" run: | diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index a66100d93..fe76fb466 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -2,7 +2,13 @@ name: "Static Analysis" on: push: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" pull_request: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" workflow_call: inputs: ref: @@ -13,9 +19,12 @@ on: env: PHP_VERSION: "8.2" DRIVER_VERSION: "stable" + MONGODB_EXT_V1: mongodb-1.21.0 + MONGODB_EXT_V2: mongodb-mongodb/mongo-php-driver@v2.x jobs: phpstan: + name: "PHP/${{ matrix.php }} Driver/${{ matrix.driver }}" runs-on: "ubuntu-22.04" continue-on-error: true strategy: @@ -24,9 +33,13 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' + driver: + - 1 + - 2 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }} @@ -35,11 +48,19 @@ jobs: run: | echo CHECKED_OUT_SHA=$(git rev-parse HEAD) >> $GITHUB_ENV + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }} + key: "extcache-v1" + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: curl, mbstring + extensions: "curl,mbstring,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}" tools: composer:v2 coverage: none diff --git a/Dockerfile b/Dockerfile index 43529d9e4..39e37531d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG PHP_VERSION=8.1 +ARG PHP_VERSION=8.2 FROM php:${PHP_VERSION}-cli diff --git a/composer.json b/composer.json index 2855a9546..6edd8d484 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ }, "authors": [ { "name": "Andreas Braun", "email": "andreas.braun@mongodb.com", "role": "Leader" }, + { "name": "Pauline Vos", "email": "pauline.vos@mongodb.com", "role": "Maintainer" }, { "name": "Jérôme Tamarelle", "email": "jerome.tamarelle@mongodb.com", "role": "Maintainer" }, { "name": "Jeremy Mikola", "email": "jmikola@gmail.com", "role": "Maintainer" }, { "name": "Jens Segers", "homepage": "https://jenssegers.com", "role": "Creator" } @@ -23,14 +24,14 @@ "license": "MIT", "require": { "php": "^8.1", - "ext-mongodb": "^1.15", + "ext-mongodb": "^1.21|^2", "composer-runtime-api": "^2.0.0", - "illuminate/cache": "^10.36|^11", - "illuminate/container": "^10.0|^11", - "illuminate/database": "^10.30|^11", - "illuminate/events": "^10.0|^11", - "illuminate/support": "^10.0|^11", - "mongodb/mongodb": "^1.21", + "illuminate/cache": "^10.36|^11|^12", + "illuminate/container": "^10.0|^11|^12", + "illuminate/database": "^10.30|^11|^12", + "illuminate/events": "^10.0|^11|^12", + "illuminate/support": "^10.0|^11|^12", + "mongodb/mongodb": "^1.21|^2", "symfony/http-foundation": "^6.4|^7" }, "require-dev": { @@ -38,8 +39,8 @@ "league/flysystem-gridfs": "^3.28", "league/flysystem-read-only": "^3.0", "phpunit/phpunit": "^10.3|^11.5.3", - "orchestra/testbench": "^8.0|^9.0", - "mockery/mockery": "^1.4.4@stable", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "mockery/mockery": "^1.4.4", "doctrine/coding-standard": "12.0.x-dev", "spatie/laravel-query-builder": "^5.6|^6", "phpstan/phpstan": "^1.10", @@ -49,8 +50,7 @@ "illuminate/bus": "< 10.37.2" }, "suggest": { - "league/flysystem-gridfs": "Filesystem storage in MongoDB with GridFS", - "mongodb/builder": "Provides a fluent aggregation builder for MongoDB pipelines" + "league/flysystem-gridfs": "Filesystem storage in MongoDB with GridFS" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/docker-compose.yml b/docker-compose.yml index fc0f0e49a..463da5f79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: mongodb: container_name: mongodb - image: mongodb/mongodb-atlas-local:latest + image: mongodb/mongodb-atlas-local:8 ports: - "27017:27017" healthcheck: diff --git a/docs/cache.txt b/docs/cache.txt index d3fd0f6e6..629065f09 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -14,7 +14,7 @@ Cache and Locks Configuration ------------- -To use MongoDB as a backend for `Laravel Cache and Locks `__, +To use MongoDB as a back end for `Laravel Cache and Locks `__, add a store configuration by specifying the ``mongodb`` driver in ``config/cache.php``: .. code-block:: php diff --git a/docs/compatibility.txt b/docs/compatibility.txt index fd3e2da02..55971c9dd 100644 --- a/docs/compatibility.txt +++ b/docs/compatibility.txt @@ -15,7 +15,7 @@ Compatibility :class: singlecol .. meta:: - :keywords: laravel 9, laravel 10, laravel 11, 4.0, 4.1, 4.2, 5.0, 5.1 + :keywords: laravel 9, laravel 10, laravel 11, laravel 12, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2, 5.3, 5.4 Laravel Compatibility --------------------- @@ -28,3 +28,15 @@ the {+odm-short+} that you can use together. To find compatibility information for unmaintained versions of the {+odm-short+}, see `Laravel Version Compatibility <{+mongodb-laravel-gh+}/blob/3.9/README.md#installation>`__ on GitHub. + +PHP Driver Compatibility +------------------------ + +To use {+odm-long+} v5.2 or later, you must install v1.21 of the +{+php-library+} and {+php-extension+}. + +.. important:: {+php-extension+} v2.0 Compatibility + + If you upgrade the {+php-extension+} to v2.0, you must also upgrade + {+odm-long+} to v5.2.1. {+odm-long+} v5.2.1 still supports v1.x + versions of the extension. diff --git a/docs/database-collection.txt b/docs/database-collection.txt index d42a0d52a..e8d97711a 100644 --- a/docs/database-collection.txt +++ b/docs/database-collection.txt @@ -213,13 +213,13 @@ methods in your application: .. note:: - MongoDB is a schemaless database, so the preceding schema builder methods - query the database data rather than the schema. + MongoDB is a schema-flexible database, so the preceding schema + builder methods query the database data rather than the schema. Example ``````` -The following example accesses a database connection, then calls the +The following example accesses the database of the connection, then calls the ``listCollections()`` query builder method to retrieve information about the collections in the database: @@ -269,9 +269,10 @@ collection fields: - ``Schema::hasColumns(string $, string[] $)``: checks if each specified field exists in at least one document -MongoDB is a schemaless database, so the preceding methods query the collection -data rather than the database schema. If the specified collection doesn't exist -or is empty, these methods return a value of ``false``. +MongoDB is a schema-flexible database, so the preceding methods query +the collection data rather than the database schema. If the specified +collection doesn't exist or is empty, these methods return a value of +``false``. .. note:: id Alias diff --git a/docs/eloquent-models/model-class.txt b/docs/eloquent-models/model-class.txt index a2a9861bc..da820b18c 100644 --- a/docs/eloquent-models/model-class.txt +++ b/docs/eloquent-models/model-class.txt @@ -168,7 +168,7 @@ Eloquent includes a soft delete feature that changes the behavior of the database. It sets a timestamp on the ``deleted_at`` field to exclude it from retrieve operations automatically. -To enable soft deletes on a class, add the ``MongoDB\Laravel\Eloquent\SoftDeletes`` +To enable soft deletes on a class, add the ``Illuminate\Database\Eloquent\SoftDeletes`` trait as shown in the following code example: .. literalinclude:: /includes/eloquent-models/PlanetSoftDelete.php @@ -200,9 +200,10 @@ model attribute, stored in MongoDB as a :php:`MongoDB\\BSON\\UTCDateTime .. 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: + Starting 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 diff --git a/docs/eloquent-models/schema-builder.txt b/docs/eloquent-models/schema-builder.txt index 3cdec0f03..a3e1df913 100644 --- a/docs/eloquent-models/schema-builder.txt +++ b/docs/eloquent-models/schema-builder.txt @@ -21,8 +21,9 @@ Overview -------- Laravel provides a **facade** to access the schema builder class ``Schema``, -which lets you create and modify tables. Facades are static interfaces to -classes that make the syntax more concise and improve testability. +which lets you create and modify tables, or collections in MongoDB. +Facades are static interfaces to classes that make the syntax more +concise and improve testability. The {+odm-short+} supports a subset of the index and collection management methods in the Laravel ``Schema`` facade. @@ -33,16 +34,10 @@ in the Laravel documentation. The following sections describe the Laravel schema builder features available in the {+odm-short+} and show examples of how to use them: -- :ref:`` -- :ref:`` -- :ref:`` - -.. note:: - - The {+odm-short+} supports managing indexes and collections, but - excludes support for MongoDB JSON schemas for data validation. To learn - more about JSON schema validation, see :manual:`Schema Validation ` - in the {+server-docs-name+}. +- :ref:`laravel-eloquent-migrations` +- :ref:`laravel-eloquent-schema-validation` +- :ref:`laravel-eloquent-collection-exists` +- :ref:`laravel-eloquent-indexes` .. _laravel-eloquent-migrations: @@ -117,6 +112,60 @@ To learn more about Laravel migrations, see `Database: Migrations `__ in the Laravel documentation. +.. _laravel-eloquent-schema-validation: + +Implement Schema Validation +--------------------------- + +Starting in {+odm-short+} v5.5, you can use the ``jsonSchema()`` method +to implement :manual:`schema validation ` when +using the following schema builder methods: + +- ``Schema::create()``: When creating a new collection +- ``Schema::table()``: When updating collection properties + +You can use schema validation to restrict data types and value ranges of +document fields in a specified collection. After you implement schema +validation, the server restricts write operations that don't follow the +validation rules. + +You can pass the following parameters to ``jsonSchema()``: + +- ``schema``: Array that specifies the validation rules for the + collection. To learn more about constructing a schema, see + the :manual:`$jsonSchema ` + reference in the {+server-docs-name+}. + +- ``validationLevel``: Sets the level of validation enforcement. + Accepted values are ``"strict"`` (default) and ``"moderate"``. + +- ``validationAction``: Specifies the action to take when invalid + operations are attempted. Accepted values are ``"error"`` (default) and + ``"warn"``. + +This example demonstrates how to specify a schema in the +``jsonSchema()`` method when creating a collection. The schema +validation has the following specifications: + +- Documents in the ``pilots`` collection must + contain the ``license_number`` field. + +- The ``license_number`` field must have an integer value between + ``1000`` and ``9999``. + +- If you attempt to perform invalid write operations, the server raises + an error. + +.. literalinclude:: /includes/schema-builder/flights_migration.php + :language: php + :dedent: + :start-after: begin-json-schema + :end-before: end-json-schema + +If you attempt to insert a document into the ``pilots`` collection that +violates the schema validation rule, {+odm-long+} returns a +:php:`BulkWriteException `. + .. _laravel-eloquent-collection-exists: Check Whether a Collection Exists diff --git a/docs/feature-compatibility.txt b/docs/feature-compatibility.txt index 57c8c7486..cce0932cc 100644 --- a/docs/feature-compatibility.txt +++ b/docs/feature-compatibility.txt @@ -21,17 +21,24 @@ Overview -------- This guide describes the Laravel features that are supported by -{+odm-long+}. This page discusses Laravel version 11.x feature +{+odm-long+}. This page discusses Laravel version 12.x feature availability in the {+odm-short+}. The following sections contain tables that describe whether individual features are available in the {+odm-short+}. +.. tip:: SQL Concepts in MongoDB + + To learn about how MongoDB represents SQL terminology, concepts, and + functionality, see the :manual:`SQL to MongoDB Mapping Chart + `. + Database Features ----------------- .. list-table:: :header-rows: 1 + :widths: 40 60 * - Eloquent Feature - Availability @@ -63,6 +70,13 @@ Database Features * - Database Monitoring - *Unsupported* + * - Multi-database Support / Multiple Schemas + - | *Unsupported* + | Laravel uses a dot separator (``.``) + between SQL schema and table names, but MongoDB allows ``.`` + characters within collection names, which might lead to + unexpected namespace parsing. + Query Features -------------- @@ -109,24 +123,32 @@ The following Eloquent methods are not supported in the {+odm-short+}: - *Unsupported* * - Joins - - *Unsupported* + - Use the ``$lookup`` aggregation stage. To learn more, see the + :manual:`$lookup reference + ` in the + {+server-docs-name+}. {+odm-long+} provides the + :ref:`laravel-aggregation-builder` to perform aggregations. * - Unions - - *Unsupported* + - Use the ``$unionWith`` aggregation stage. To learn more, see the + :manual:`$unionWith reference + ` in the + {+server-docs-name+}. {+odm-long+} provides the + :ref:`laravel-aggregation-builder` to perform aggregations. - * - `Basic Where Clauses `__ + * - `Basic Where Clauses `__ - ✓ - * - `Additional Where Clauses `__ + * - `Additional Where Clauses `__ - ✓ * - Logical Grouping - ✓ - * - `Advanced Where Clauses `__ + * - `Advanced Where Clauses `__ - ✓ - * - `Subquery Where Clauses `__ + * - `Subquery Where Clauses `__ - *Unsupported* * - Ordering @@ -136,7 +158,11 @@ The following Eloquent methods are not supported in the {+odm-short+}: - *Unsupported* * - Grouping - - Partially supported, use :ref:`Aggregations `. + - Use the ``$group`` aggregation stage. To learn more, see the + :manual:`$group reference + ` in the + {+server-docs-name+}. {+odm-long+} provides the + :ref:`laravel-aggregation-builder` to perform aggregations. * - Limit and Offset - ✓ @@ -175,7 +201,7 @@ Migration Features ------------------ The {+odm-short+} supports all Laravel migration features, but the -implementation is specific to MongoDB's schemaless model. +implementation is specific to MongoDB's schema-flexible model. Seeding Features ---------------- diff --git a/docs/filesystems.txt b/docs/filesystems.txt index 3ec7ee41f..c62853f58 100644 --- a/docs/filesystems.txt +++ b/docs/filesystems.txt @@ -79,7 +79,7 @@ You can configure the following settings in ``config/filesystems.php``: * - ``throw`` - If ``true``, exceptions are thrown when an operation cannot be performed. If ``false``, - operations return ``true`` on success and ``false`` on error. Defaults to ``false``. + operations return ``true`` on success and ``false`` on error. Defaults to ``false``. You can also use a factory or a service name to create an instance of ``MongoDB\GridFS\Bucket``. In this case, the options ``connection`` and ``database`` are ignored: @@ -133,7 +133,7 @@ metadata, including the file name and a unique ObjectId. If multiple documents share the same file name, they are considered "revisions" and further distinguished by creation timestamps. -The Laravel MongoDB integration uses the GridFS Flysystem adapter. It interacts +{+odm-long+} uses the GridFS Flysystem adapter. It interacts with file revisions in the following ways: - Reading a file reads the last revision of this file name diff --git a/docs/fundamentals/aggregation-builder.txt b/docs/fundamentals/aggregation-builder.txt index 3169acfeb..47994ce9e 100644 --- a/docs/fundamentals/aggregation-builder.txt +++ b/docs/fundamentals/aggregation-builder.txt @@ -37,7 +37,6 @@ The {+odm-long+} aggregation builder lets you build aggregation stages and aggregation pipelines. The following sections show examples of how to use the aggregation builder to create the stages of an aggregation pipeline: -- :ref:`laravel-add-aggregation-dependency` - :ref:`laravel-build-aggregation` - :ref:`laravel-aggregation-examples` - :ref:`laravel-create-custom-operator-factory` @@ -49,27 +48,6 @@ aggregation builder to create the stages of an aggregation pipeline: aggregation builder, see :ref:`laravel-query-builder-aggregations` in the Query Builder guide. -.. _laravel-add-aggregation-dependency: - -Add the Aggregation Builder Dependency --------------------------------------- - -The aggregation builder is part of the {+agg-builder-package-name+} package. -You must add this package as a dependency to your project to use it. Run the -following command to add the aggregation builder dependency to your -application: - -.. code-block:: bash - - composer require {+agg-builder-package-name+}:{+agg-builder-version+} - -When the installation completes, verify that the ``composer.json`` file -includes the following line in the ``require`` object: - -.. code-block:: json - - "{+agg-builder-package-name+}": "{+agg-builder-version+}", - .. _laravel-build-aggregation: Create Aggregation Stages @@ -88,12 +66,6 @@ to build aggregation stages: - ``MongoDB\Builder\Query`` - ``MongoDB\Builder\Type`` -.. tip:: - - To learn more about builder classes, see the - :github:`mongodb/mongodb-php-builder ` - GitHub repository. - This section features the following examples that show how to use common aggregation stages: diff --git a/docs/fundamentals/atlas-search.txt b/docs/fundamentals/atlas-search.txt index ab957f9fa..a41385fda 100644 --- a/docs/fundamentals/atlas-search.txt +++ b/docs/fundamentals/atlas-search.txt @@ -179,7 +179,7 @@ following optional parameters to ``search()``: * - ``returnStoredSource`` - ``bool`` - Specifies whether to perform a full document lookup on the - backend database or return only stored source fields directly + back end database or return only stored source fields directly from Atlas Search * - ``tracking`` diff --git a/docs/fundamentals/connection/connection-options.txt b/docs/fundamentals/connection/connection-options.txt index 03e98ed06..1a2cdb085 100644 --- a/docs/fundamentals/connection/connection-options.txt +++ b/docs/fundamentals/connection/connection-options.txt @@ -32,6 +32,7 @@ This guide covers the following topics: - :ref:`laravel-connection-auth-options` - :ref:`laravel-driver-options` +- :ref:`laravel-disable-id-alias` .. _laravel-connection-auth-options: @@ -349,3 +350,47 @@ item, as shown in the following example: See the `$driverOptions: array `__ section of the {+php-library+} documentation for a list of driver options. + +.. _laravel-disable-id-alias: + +Disable Use of id Field Name Conversion +--------------------------------------- + +Starting in {+odm-long+} v5.0, ``id`` is an alias for the ``_id`` field +in MongoDB documents, and the library automatically converts ``id`` +to ``_id`` for both top level and embedded fields when querying and +storing data. + +When using {+odm-long+} v5.3 or later, you can disable the automatic +conversion of ``id`` to ``_id`` for embedded documents. To do so, +perform either of the following actions: + +1. Set the ``rename_embedded_id_field`` setting to ``false`` in your + ``config/database.php`` file: + + .. code-block:: php + :emphasize-lines: 6 + + 'connections' => [ + 'mongodb' => [ + 'dsn' => 'mongodb+srv://mongodb0.example.com/', + 'driver' => 'mongodb', + 'database' => 'sample_mflix', + 'rename_embedded_id_field' => false, + // Other settings + ], + ], + +#. Pass ``false`` to the ``setRenameEmbeddedIdField()`` method in your + application: + + .. code-block:: php + + DB::connection('mongodb')->setRenameEmbeddedIdField(false); + +.. important:: + + We recommend using this option only to provide backwards + compatibility with existing document schemas. In new projects, + avoid using ``id`` for field names in embedded documents so that + you can maintain {+odm-long+}'s default behavior. diff --git a/docs/fundamentals/read-operations.txt b/docs/fundamentals/read-operations.txt index f3b02c5ec..674615ffb 100644 --- a/docs/fundamentals/read-operations.txt +++ b/docs/fundamentals/read-operations.txt @@ -10,7 +10,15 @@ Read Operations :values: tutorial .. meta:: - :keywords: find one, find many, code example + :keywords: find one, find many, skip, limit, paginate, string, code example + +.. toctree:: + + Retrieve Data + Search Text + Modify Query Results + Read Preference + Query Logging .. contents:: On this page :local: @@ -21,697 +29,169 @@ Read Operations Overview -------- -In this guide, you can learn how to use {+odm-long+} to perform **find operations** -on your MongoDB collections. Find operations allow you to retrieve documents based on -criteria that you specify. - -This guide shows you how to perform the following tasks: - -- :ref:`laravel-retrieve-matching` -- :ref:`laravel-retrieve-all` -- :ref:`laravel-retrieve-text-search` -- :ref:`Modify Find Operation Behavior ` - -Before You Get Started ----------------------- - -To run the code examples in this guide, complete the :ref:`Quick Start ` -tutorial. This tutorial provides instructions on setting up a MongoDB Atlas instance with -sample data and creating the following files in your Laravel web application: - -- ``Movie.php`` file, which contains a ``Movie`` model to represent documents in the ``movies`` - collection -- ``MovieController.php`` file, which contains a ``show()`` function to run database operations -- ``browse_movies.blade.php`` file, which contains HTML code to display the results of database - operations - -The following sections describe how to edit the files in your Laravel application to run -the find operation code examples and view the expected output. - -.. _laravel-retrieve-matching: - -Retrieve Documents that Match a Query -------------------------------------- - -You can use Laravel's Eloquent object-relational mapper (ORM) to create models -that represent MongoDB collections and chain methods on them to specify -query criteria. - -To retrieve documents that match a set of criteria, call the ``where()`` -method on the collection's corresponding Eloquent model, then pass a query -filter to the method. - -A query filter specifies field value requirements and instructs the find -operation to return only documents that meet these requirements. - -You can use one of the following ``where()`` method calls to build a query: - -- ``where('', )`` builds a query that matches documents in - which the target field has the exact specified value - -- ``where('', '', )`` builds a query - that matches documents in which the target field's value meets the comparison - criteria - -To apply multiple sets of criteria to the find operation, you can chain a series -of ``where()`` methods together. - -After building your query by using the ``where()`` method, chain the ``get()`` -method to retrieve the query results. - -This example calls two ``where()`` methods on the ``Movie`` Eloquent model to -retrieve documents that meet the following criteria: - -- ``year`` field has a value of ``2010`` -- ``imdb.rating`` nested field has a value greater than ``8.5`` - -.. tabs:: - - .. tab:: Query Syntax - :tabid: query-syntax - - Use the following syntax to specify the query: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-query - :end-before: end-query - - .. tab:: Controller Method - :tabid: controller - - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: - - .. io-code-block:: - :copyable: true - - .. input:: - :language: php - - class MovieController - { - public function show() - { - $movies = Movie::where('year', 2010) - ->where('imdb.rating', '>', 8.5) - ->get(); - - return view('browse_movies', [ - 'movies' => $movies - ]); - } - } - - .. output:: - :language: none - :visible: false - - Title: Inception - Year: 2010 - Runtime: 148 - IMDB Rating: 8.8 - IMDB Votes: 1294646 - Plot: A thief who steals corporate secrets through use of dream-sharing - technology is given the inverse task of planting an idea into the mind of a CEO. - - Title: Senna - Year: 2010 - Runtime: 106 - IMDB Rating: 8.6 - IMDB Votes: 41904 - Plot: A documentary on Brazilian Formula One racing driver Ayrton Senna, who won the - F1 world championship three times before his death at age 34. - -To learn how to query by using the Laravel query builder instead of the -Eloquent ORM, see the :ref:`laravel-query-builder` page. - -Match Array Field Elements -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can specify a query filter to match array field elements when -retrieving documents. If your documents contain an array field, you can -match documents based on if the value contains all or some specified -array elements. - -You can use one of the following ``where()`` method calls to build a -query on an array field: - -- ``where('', )`` builds a query that matches documents in - which the array field value is exactly the specified array +In this guide, you can see code templates of common +methods that you can use to read data from MongoDB by using +{+odm-long+}. -- ``where('', 'in', )`` builds a query - that matches documents in which the array field value contains one or - more of the specified array elements - -After building your query by using the ``where()`` method, chain the ``get()`` -method to retrieve the query results. - -Select from the following :guilabel:`Exact Array Match` and -:guilabel:`Element Match` tabs to view the query syntax for each pattern: - -.. tabs:: - - .. tab:: Exact Array Match - :tabid: exact-array - - This example retrieves documents in which the ``countries`` array is - exactly ``['Indonesia', 'Canada']``: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-exact-array - :end-before: end-exact-array - - .. tab:: Element Match - :tabid: element-match - - This example retrieves documents in which the ``countries`` array - contains one of the values in the array ``['Canada', 'Egypt']``: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-elem-match - :end-before: end-elem-match - -To learn how to query array fields by using the Laravel query builder instead of the -Eloquent ORM, see the :ref:`laravel-query-builder-elemMatch` section in -the Query Builder guide. - -.. _laravel-retrieve-all: +.. tip:: -Retrieve All Documents in a Collection --------------------------------------- + To learn more about any of the methods included in this guide, + see the links provided in each section. -You can retrieve all documents in a collection by omitting the query filter. -To return the documents, call the ``get()`` method on an Eloquent model that -represents your collection. Alternatively, you can use the ``get()`` method's -alias ``all()`` to perform the same operation. +Find One +-------- -Use the following syntax to run a find operation that matches all documents: +The following code shows how to retrieve the first matching document +from a collection: .. code-block:: php - $movies = Movie::get(); - -.. warning:: - - The ``movies`` collection in the Atlas sample dataset contains a large amount of data. - Retrieving and displaying all documents in this collection might cause your web - application to time out. - - To avoid this issue, specify a document limit by using the ``take()`` method. For - more information about ``take()``, see the :ref:`laravel-modify-find` section of this - guide. - -.. _laravel-retrieve-text-search: - -Search Text Fields ------------------- - -A text search retrieves documents that contain a **term** or a **phrase** in the -text-indexed fields. A term is a sequence of characters that excludes -whitespace characters. A phrase is a sequence of terms with any number -of whitespace characters. + SampleModel::where('', '') + ->first(); -.. note:: +To view a runnable example that finds one document, see the +:ref:`laravel-find-one-usage` usage example. - Before you can perform a text search, you must create a :manual:`text - index ` on - the text-valued field. To learn more about creating - indexes, see the :ref:`laravel-eloquent-indexes` section of the - Schema Builder guide. +To learn more about retrieving documents and the ``first()`` method, see +the :ref:`laravel-fundamentals-read-retrieve` guide. -You can perform a text search by using the :manual:`$text -` operator followed -by the ``$search`` field in your query filter that you pass to the -``where()`` method. The ``$text`` operator performs a text search on the -text-indexed fields. The ``$search`` field specifies the text to search for. +Find Multiple +------------- -After building your query by using the ``where()`` method, chain the ``get()`` -method to retrieve the query results. +The following code shows how to retrieve all documents that match a +query filter from a collection: -This example calls the ``where()`` method on the ``Movie`` Eloquent model to -retrieve documents in which the ``plot`` field contains the phrase -``"love story"``. To perform this text search, the collection must have -a text index on the ``plot`` field. - -.. tabs:: - - .. tab:: Query Syntax - :tabid: query-syntax +.. code-block:: php - Use the following syntax to specify the query: + SampleModel::where('', '') + ->get(); - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-text - :end-before: end-text +To view a runnable example that finds documents, see the +:ref:`laravel-find-usage` usage example. - .. tab:: Controller Method - :tabid: controller +To learn more about retrieving documents, see the +:ref:`laravel-fundamentals-read-retrieve` guide. - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: +Return All Documents +-------------------- - .. io-code-block:: - :copyable: true +The following code shows how to retrieve all documents from a +collection: - .. input:: - :language: php +.. code-block:: php - class MovieController - { - public function show() - { - $movies = Movie::where('$text', ['$search' => '"love story"']) - ->get(); + SampleModel::get(); - return view('browse_movies', [ - 'movies' => $movies - ]); - } - } + // Or, use the all() method. + SampleModel::all(); - .. output:: - :language: none - :visible: false +To view a runnable example that finds documents, see the +:ref:`laravel-find-usage` usage example. - Title: Cafè de Flore - Year: 2011 - Runtime: 120 - IMDB Rating: 7.4 - IMDB Votes: 9663 - Plot: A love story between a man and woman ... +To learn more about retrieving documents, see the +:ref:`laravel-fundamentals-read-retrieve` guide. - Title: Paheli - Year: 2005 - Runtime: 140 - IMDB Rating: 6.7 - IMDB Votes: 8909 - Plot: A folk tale - supernatural love story about a ghost ... +Search Text +----------- - Title: Por un puèado de besos - Year: 2014 - Runtime: 98 - IMDB Rating: 6.1 - IMDB Votes: 223 - Plot: A girl. A boy. A love story ... - - ... - -A text search assigns a numerical :manual:`text score ` to indicate how closely -each result matches the string in your query filter. You can sort the -results by relevance by using the ``orderBy()`` method to sort on the -``textScore`` metadata field. You can access this metadata by using the -:manual:`$meta ` operator: - -.. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-text-relevance - :end-before: end-text-relevance - :emphasize-lines: 2 +The following code shows how to perform a full-text search on a string +field in a collection's documents: -.. tip:: +.. code-block:: php - To learn more about the ``orderBy()`` method, see the - :ref:`laravel-sort` section of this guide. + SampleModel::where('$text', ['$search' => '']) + ->get(); -.. _laravel-modify-find: +To learn more about searching on text fields, see the +:ref:`laravel-retrieve-text-search` guide. -Modify Behavior +Count Documents --------------- -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()`` -method: - -- :ref:`laravel-skip-limit` uses the ``skip()`` method to set the number of documents - to skip and the ``take()`` method to set the total number of documents to return -- :ref:`laravel-sort` uses the ``orderBy()`` method to return query - results in a specified order based on field values -- :ref:`laravel-retrieve-one` uses the ``first()`` method to return the first document - that matches the query filter -- :ref:`laravel-read-pref` uses the ``readPreference()`` method to direct the query - to specific replica set members - -.. _laravel-skip-limit: - -Skip and Limit Results -~~~~~~~~~~~~~~~~~~~~~~ - -This example queries for documents in which the ``year`` value is ``1999``. -The operation skips the first ``2`` matching documents and outputs a total of ``3`` -documents. - -.. tabs:: - - .. tab:: Query Syntax - :tabid: query-syntax - - Use the following syntax to specify the query: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-skip-limit - :end-before: end-skip-limit - - .. tab:: Controller Method - :tabid: controller +The following code shows how to count documents in a collection: - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: - - .. io-code-block:: - :copyable: true - - .. input:: - :language: php - - class MovieController - { - public function show() - { - $movies = Movie::where('year', 1999) - ->skip(2) - ->take(3) - ->get(); - - return view('browse_movies', [ - 'movies' => $movies - ]); - } - } - - .. output:: - :language: none - :visible: false - - Title: Three Kings - Year: 1999 - Runtime: 114 - IMDB Rating: 7.2 - IMDB Votes: 130677 - Plot: In the aftermath of the Persian Gulf War, 4 soldiers set out to steal gold - that was stolen from Kuwait, but they discover people who desperately need their help. - - Title: Toy Story 2 - Year: 1999 - Runtime: 92 - IMDB Rating: 7.9 - IMDB Votes: 346655 - Plot: When Woody is stolen by a toy collector, Buzz and his friends vow to rescue him, - but Woody finds the idea of immortality in a museum tempting. - - Title: Beowulf - Year: 1999 - Runtime: 95 - IMDB Rating: 4 - IMDB Votes: 9296 - Plot: A sci-fi update of the famous 6th Century poem. In a besieged land, Beowulf must - battle against the hideous creature Grendel and his vengeance seeking mother. - -.. _laravel-sort: - -Sort Query Results -~~~~~~~~~~~~~~~~~~ - -To order query results based on the values of specified fields, use the ``where()`` method -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. - -If your documents contain duplicate values in a specific field, you can -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 -``year`` field, then a descending sort on the ``title`` field. - -.. tabs:: - - .. tab:: Query Syntax - :tabid: query-syntax - - Use the following syntax to specify the query: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-sort - :end-before: end-sort - - .. tab:: Controller Method - :tabid: controller - - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: - - .. io-code-block:: - :copyable: true - - .. input:: - :language: php - - class MovieController - { - public function show() - { - $movies = Movie::where('countries', 'Indonesia') - ->orderBy('year') - ->orderBy('title', 'desc') - ->get(); - - return view('browse_movies', [ - 'movies' => $movies - ]); - } - } - - .. output:: - :language: none - :visible: false - - Title: Joni's Promise - Year: 2005 - Runtime: 83 - 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 - IMDB Rating: 6.6 - IMDB Votes: 316 - Plot: Setyo (Martinus Miroto) and Siti (Artika Sari Dewi) - are young married couple ... - - ... +.. code-block:: php -.. tip:: + SampleModel::count(); - To learn more about sorting, see the following resources: + // You can also count documents that match a filter. + SampleModel::where('', '') + ->count(); - - :manual:`Natural order ` - in the {+server-docs-name+} glossary - - `Ordering, Grouping, Limit, and Offset `__ - in the Laravel documentation +To view a runnable example that counts documents, see the +:ref:`laravel-count-usage` usage example. -.. _laravel-retrieve-one: +Retrieve Distinct Values +------------------------ -Return the First Result -~~~~~~~~~~~~~~~~~~~~~~~ +The following code shows how to retrieve the distinct values of a +specified field: -To retrieve the first document that matches a set of criteria, use the ``where()`` method -followed by the ``first()`` method. +.. code-block:: php -Chain the ``orderBy()`` method to ``first()`` to get consistent results when you query on a unique -value. If you omit the ``orderBy()`` method, MongoDB returns the matching documents according to -the documents' natural order, or as they appear in the collection. + SampleModel::select('') + ->distinct() + ->get(); -This example queries for documents in which the value of the ``runtime`` field is -``30`` and returns the first matching document according to the value of the ``_id`` -field. +To view a runnable example that returns distinct field values, see the +:ref:`laravel-distinct-usage` usage example. -.. tabs:: +Skip Results +------------ - .. tab:: Query Syntax - :tabid: query-syntax +The following code shows how to skip a specified number of documents +returned from MongoDB: - Use the following syntax to specify the query: +.. code-block:: php - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-first - :end-before: end-first + SampleModel::where('', '') + ->skip() + ->get(); - .. tab:: Controller Method - :tabid: controller +To learn more about modifying how {+odm-long+} returns results, see the +:ref:`laravel-read-modify-results` guide. - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: +Limit Results +------------- - .. io-code-block:: - :copyable: true +The following code shows how to return only a specified number of +documents from MongoDB: - .. input:: - :language: php +.. code-block:: php - class MovieController - { - public function show() - { - $movie = Movie::where('runtime', 30) - ->orderBy('_id') - ->first(); + SampleModel::where('', '') + ->take() + ->get(); - return view('browse_movies', [ - 'movies' => $movie - ]); - } - } +To learn more about modifying how {+odm-long+} returns results, see the +:ref:`laravel-read-modify-results` guide. - .. output:: - :language: none - :visible: false +Sort Results +------------ - Title: Statues also Die - Year: 1953 - Runtime: 30 - IMDB Rating: 7.6 - IMDB Votes: 620 - Plot: A documentary of black art. +The following code shows how to set a sort order on results returned +from MongoDB: -.. tip:: +.. code-block:: php - To learn more about the ``orderBy()`` method, see the - :ref:`laravel-sort` section of this guide. + SampleModel::where('field name', '') + ->orderBy('') + ->get(); -.. _laravel-read-pref: +To learn more about modifying how {+odm-long+} returns results, see the +:ref:`laravel-read-modify-results` guide. Set a Read Preference -~~~~~~~~~~~~~~~~~~~~~ +--------------------- -To specify which replica set members receive your read operations, -set a read preference by using the ``readPreference()`` method. +The following code shows how to set a read preference when performing a +find operation: -The ``readPreference()`` method accepts the following parameters: - -- ``mode``: *(Required)* A string value specifying the read preference - mode. - -- ``tagSets``: *(Optional)* An array value specifying key-value tags that correspond to - certain replica set members. - -- ``options``: *(Optional)* An array value specifying additional read preference options. +.. code-block:: php -.. tip:: + SampleModel::where('field name', '') + ->readPreference(ReadPreference::SECONDARY_PREFERRED) + ->get(); - To view a full list of available read preference modes and options, see - :php:`MongoDB\Driver\ReadPreference::__construct ` - in the MongoDB PHP extension documentation. - -The following example queries for documents in which the value of the ``title`` -field is ``"Carrie"`` and sets the read preference to ``ReadPreference::SECONDARY_PREFERRED``. -As a result, the query retrieves the results from secondary replica set -members or the primary member if no secondaries are available: - -.. tabs:: - - .. tab:: Query Syntax - :tabid: query-syntax - - Use the following syntax to specify the query: - - .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php - :language: php - :dedent: - :start-after: start-read-pref - :end-before: end-read-pref - - .. tab:: Controller Method - :tabid: controller - - To see the query results in the ``browse_movies`` view, edit the ``show()`` function - in the ``MovieController.php`` file to resemble the following code: - - .. io-code-block:: - :copyable: true - - .. input:: - :language: php - - class MovieController - { - public function show() - { - $movies = Movie::where('title', 'Carrie') - ->readPreference(ReadPreference::SECONDARY_PREFERRED) - ->get(); - - return view('browse_movies', [ - 'movies' => $movies - ]); - } - } - - .. output:: - :language: none - :visible: false - - Title: Carrie - Year: 1952 - Runtime: 118 - IMDB Rating: 7.5 - IMDB Votes: 1458 - Plot: Carrie boards the train to Chicago with big ambitions. She gets a - job stitching shoes and her sister's husband takes almost all of her pay - for room and board. Then she injures a finger and ... - - Title: Carrie - Year: 1976 - Runtime: 98 - IMDB Rating: 7.4 - IMDB Votes: 115528 - Plot: A shy, outcast 17-year old girl is humiliated by her classmates for the - last time. - - Title: Carrie - Year: 2002 - Runtime: 132 - IMDB Rating: 5.5 - IMDB Votes: 7412 - Plot: Carrie White is a lonely and painfully shy teenage girl with telekinetic - powers who is slowly pushed to the edge of insanity by frequent bullying from - both her classmates and her domineering, religious mother. - - Title: Carrie - Year: 2013 - Runtime: 100 - IMDB Rating: 6 - IMDB Votes: 98171 - Plot: A reimagining of the classic horror tale about Carrie White, a shy girl - outcast by her peers and sheltered by her deeply religious mother, who unleashes - telekinetic terror on her small town after being pushed too far at her senior prom. +To learn more about read preferences, see the :ref:`laravel-read-pref` +guide. diff --git a/docs/fundamentals/read-operations/modify-results.txt b/docs/fundamentals/read-operations/modify-results.txt new file mode 100644 index 000000000..fd67422ae --- /dev/null +++ b/docs/fundamentals/read-operations/modify-results.txt @@ -0,0 +1,227 @@ +.. _laravel-modify-find: +.. _laravel-read-modify-results: + +==================== +Modify Query Results +==================== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: filter, criteria, CRUD, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to customize the way that {+odm-long+} +returns results from queries. You can modify the results of a find +operation by chaining more methods to the ``where()`` method. + +The following sections demonstrate how to modify the behavior of the +``where()`` method: + +- :ref:`laravel-skip-limit` uses the ``skip()`` method to set the number of documents + to skip and the ``take()`` method to set the total number of documents to return +- :ref:`laravel-sort` uses the ``orderBy()`` method to return query + results in a specified order based on field values + +To learn more about Eloquent models in the {+odm-short+}, see the +:ref:`laravel-eloquent-models` section. + +.. include:: /includes/fundamentals/read-operations/before-you-get-started.rst + +.. _laravel-skip-limit: + +Skip and Limit Results +---------------------- + +This example queries for documents in which the ``year`` value is ``1999``. +The operation skips the first ``2`` matching documents and outputs a total of ``3`` +documents. + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-skip-limit + :end-before: end-skip-limit + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movies = Movie::where('year', 1999) + ->skip(2) + ->take(3) + ->get(); + + return view('browse_movies', [ + 'movies' => $movies + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Three Kings + Year: 1999 + Runtime: 114 + IMDB Rating: 7.2 + IMDB Votes: 130677 + Plot: In the aftermath of the Persian Gulf War, 4 soldiers set out to steal gold + that was stolen from Kuwait, but they discover people who desperately need their help. + + Title: Toy Story 2 + Year: 1999 + Runtime: 92 + IMDB Rating: 7.9 + IMDB Votes: 346655 + Plot: When Woody is stolen by a toy collector, Buzz and his friends vow to rescue him, + but Woody finds the idea of immortality in a museum tempting. + + Title: Beowulf + Year: 1999 + Runtime: 95 + IMDB Rating: 4 + IMDB Votes: 9296 + Plot: A sci-fi update of the famous 6th Century poem. In a besieged land, Beowulf must + battle against the hideous creature Grendel and his vengeance seeking mother. + +.. _laravel-sort: + +Sort Query Results +------------------ + +To order query results based on the values of specified fields, use the ``where()`` method +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. + +If your documents contain duplicate values in a specific field, you can +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 +``year`` field, then a descending sort on the ``title`` field. + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-sort + :end-before: end-sort + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movies = Movie::where('countries', 'Indonesia') + ->orderBy('year') + ->orderBy('title', 'desc') + ->get(); + + return view('browse_movies', [ + 'movies' => $movies + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Joni's Promise + Year: 2005 + Runtime: 83 + 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 + IMDB Rating: 6.6 + IMDB Votes: 316 + Plot: Setyo (Martinus Miroto) and Siti (Artika Sari Dewi) + are young married couple ... + + ... + +.. tip:: + + To learn more about sorting, see the following resources: + + - :manual:`Natural order ` + in the {+server-docs-name+} glossary + - `Ordering, Grouping, Limit, and Offset `__ + in the Laravel documentation + +Additional Information +---------------------- + +To view runnable code examples that demonstrate how to perform find +operations by using the {+odm-short+}, see the following usage examples: + +- :ref:`laravel-find-one-usage` +- :ref:`laravel-find-usage` + +To learn how to retrieve data based on filter criteria, see the +:ref:`laravel-fundamentals-read-retrieve` guide. diff --git a/docs/fundamentals/read-operations/query-logging.txt b/docs/fundamentals/read-operations/query-logging.txt new file mode 100644 index 000000000..27816b298 --- /dev/null +++ b/docs/fundamentals/read-operations/query-logging.txt @@ -0,0 +1,82 @@ +.. _laravel-query-logging: + +==================== +Enable Query Logging +==================== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: monitoring, CRUD, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to enable query logging in +{+odm-long+}. Query logging can help you debug your queries and monitor +database interactions. + +.. include:: /includes/fundamentals/read-operations/before-you-get-started.rst + +Enable Logs On a Connection +--------------------------- + +To enable logs on a connection, you can use the ``enableQueryLog()`` +method on the ``DB`` facade. This method enables MongoDB command logging +on any queries that you perform on the database connection. + +After you enable query logging, any queries you perform are stored in +memory. To retrieve the logs, use one of the following methods: + +- ``getQueryLog()``: Returns a log of MongoDB queries +- ``getRawQueryLog()``: Returns a log of raw MongoDB queries + +The following example enables query logging, performs some queries, then +prints the query log: + +.. io-code-block:: + :copyable: true + + .. input:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-query-log + :end-before: end-query-log + :emphasize-lines: 1, 7 + + .. output:: + :language: json + :visible: false + + { + "query": "{ \"find\" : \"movies\", \"filter\" : { \"title\" : \"Carrie\" } }", + "bindings": [], + "time": 29476 + } + { + "query": "{ \"find\" : \"movies\", \"filter\" : { \"year\" : { \"$lt\" : { \"$numberInt\" : \"2005\" } } } }", + "bindings": [], + "time": 29861 + } + { + "query": "{ \"find\" : \"movies\", \"filter\" : { \"imdb.rating\" : { \"$gt\" : { \"$numberDouble\" : \"8.5\" } } } }", + "bindings": [], + "time": 27251 + } + +Additional Information +---------------------- + +To learn more about connecting to MongoDB, see the +:ref:`laravel-connect-to-mongodb`. + +To learn how to retrieve data based on filter criteria, see the +:ref:`laravel-fundamentals-read-retrieve` guide. diff --git a/docs/fundamentals/read-operations/read-pref.txt b/docs/fundamentals/read-operations/read-pref.txt new file mode 100644 index 000000000..075c74380 --- /dev/null +++ b/docs/fundamentals/read-operations/read-pref.txt @@ -0,0 +1,141 @@ +.. _laravel-read-pref: + +===================== +Set a Read Preference +===================== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: consistency, durability, CRUD, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to set a read preference when +performing find operations with {+odm-long+}. + +.. include:: /includes/fundamentals/read-operations/before-you-get-started.rst + +Set a Read Preference +--------------------- + +To specify which replica set members receive your read operations, +set a read preference by using the ``readPreference()`` method. + +The ``readPreference()`` method accepts the following parameters: + +- ``mode``: *(Required)* A string value specifying the read preference + mode. + +- ``tagSets``: *(Optional)* An array value specifying key-value tags that correspond to + certain replica set members. + +- ``options``: *(Optional)* An array value specifying additional read preference options. + +.. tip:: + + To view a full list of available read preference modes and options, see + :php:`MongoDB\Driver\ReadPreference::__construct ` + in the MongoDB PHP extension documentation. + +The following example queries for documents in which the value of the ``title`` +field is ``"Carrie"`` and sets the read preference to ``ReadPreference::SECONDARY_PREFERRED``. +As a result, the query retrieves the results from secondary replica set +members or the primary member if no secondaries are available: + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-read-pref + :end-before: end-read-pref + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movies = Movie::where('title', 'Carrie') + ->readPreference(ReadPreference::SECONDARY_PREFERRED) + ->get(); + + return view('browse_movies', [ + 'movies' => $movies + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Carrie + Year: 1952 + Runtime: 118 + IMDB Rating: 7.5 + IMDB Votes: 1458 + Plot: Carrie boards the train to Chicago with big ambitions. She gets a + job stitching shoes and her sister's husband takes almost all of her pay + for room and board. Then she injures a finger and ... + + Title: Carrie + Year: 1976 + Runtime: 98 + IMDB Rating: 7.4 + IMDB Votes: 115528 + Plot: A shy, outcast 17-year old girl is humiliated by her classmates for the + last time. + + Title: Carrie + Year: 2002 + Runtime: 132 + IMDB Rating: 5.5 + IMDB Votes: 7412 + Plot: Carrie White is a lonely and painfully shy teenage girl with telekinetic + powers who is slowly pushed to the edge of insanity by frequent bullying from + both her classmates and her domineering, religious mother. + + Title: Carrie + Year: 2013 + Runtime: 100 + IMDB Rating: 6 + IMDB Votes: 98171 + Plot: A reimagining of the classic horror tale about Carrie White, a shy girl + outcast by her peers and sheltered by her deeply religious mother, who unleashes + telekinetic terror on her small town after being pushed too + far at her senior prom. + +Additional Information +---------------------- + +To learn how to retrieve data based on filter criteria, see the +:ref:`laravel-fundamentals-read-retrieve` guide. + +To learn how to modify the way that the {+odm-short+} returns results, +see the :ref:`laravel-read-modify-results` guide. diff --git a/docs/fundamentals/read-operations/retrieve.txt b/docs/fundamentals/read-operations/retrieve.txt new file mode 100644 index 000000000..a4ca31091 --- /dev/null +++ b/docs/fundamentals/read-operations/retrieve.txt @@ -0,0 +1,304 @@ +.. _laravel-fundamentals-retrieve-documents: +.. _laravel-fundamentals-read-retrieve: + +============= +Retrieve Data +============= + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: filter, criteria, CRUD, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to retrieve data from MongoDB +collections by using {+odm-long+}. This guide describes the Eloquent +model methods that you can use to retrieve data and provides examples +of different types of find operations. + +To learn more about Eloquent models in the {+odm-short+}, see the +:ref:`laravel-eloquent-models` section. + +.. include:: /includes/fundamentals/read-operations/before-you-get-started.rst + +.. _laravel-retrieve-matching: + +Retrieve Documents that Match a Query +------------------------------------- + +You can use Laravel's Eloquent object-relational mapper (ORM) to create models +that represent MongoDB collections and chain methods on them to specify +query criteria. + +To retrieve documents that match a set of criteria, call the ``where()`` +method on the collection's corresponding Eloquent model, then pass a query +filter to the method. + +.. tip:: Retrieve One Document + + The ``where()`` method retrieves all matching documents. To retrieve + the first matching document, you can chain the ``first()`` method. To + learn more and view an example, see the :ref:`laravel-retrieve-one` + section of this guide. + +A query filter specifies field value requirements and instructs the find +operation to return only documents that meet these requirements. + +You can use one of the following ``where()`` method calls to build a query: + +- ``where('', )`` builds a query that matches documents in + which the target field has the exact specified value + +- ``where('', '', )`` builds a query + that matches documents in which the target field's value meets the comparison + criteria + +To apply multiple sets of criteria to the find operation, you can chain a series +of ``where()`` methods together. + +After building your query by using the ``where()`` method, chain the ``get()`` +method to retrieve the query results. + +This example calls two ``where()`` methods on the ``Movie`` Eloquent model to +retrieve documents that meet the following criteria: + +- ``year`` field has a value of ``2010`` +- ``imdb.rating`` nested field has a value greater than ``8.5`` + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-query + :end-before: end-query + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movies = Movie::where('year', 2010) + ->where('imdb.rating', '>', 8.5) + ->get(); + + return view('browse_movies', [ + 'movies' => $movies + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Inception + Year: 2010 + Runtime: 148 + IMDB Rating: 8.8 + IMDB Votes: 1294646 + Plot: A thief who steals corporate secrets through use of dream-sharing + technology is given the inverse task of planting an idea into the mind of a CEO. + + Title: Senna + Year: 2010 + Runtime: 106 + IMDB Rating: 8.6 + IMDB Votes: 41904 + Plot: A documentary on Brazilian Formula One racing driver Ayrton Senna, who won the + F1 world championship three times before his death at age 34. + +To learn how to query by using the Laravel query builder instead of the +Eloquent ORM, see the :ref:`laravel-query-builder` page. + +Match Array Field Elements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can specify a query filter to match array field elements when +retrieving documents. If your documents contain an array field, you can +match documents based on if the value contains all or some specified +array elements. + +You can use one of the following ``where()`` method calls to build a +query on an array field: + +- ``where('', )`` builds a query that matches documents in + which the array field value is exactly the specified array + +- ``where('', 'in', )`` builds a query + that matches documents in which the array field value contains one or + more of the specified array elements + +After building your query by using the ``where()`` method, chain the ``get()`` +method to retrieve the query results. + +Select from the following :guilabel:`Exact Array Match` and +:guilabel:`Element Match` tabs to view the query syntax for each pattern: + +.. tabs:: + + .. tab:: Exact Array Match + :tabid: exact-array + + This example retrieves documents in which the ``countries`` array is + exactly ``['Indonesia', 'Canada']``: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-exact-array + :end-before: end-exact-array + + .. tab:: Element Match + :tabid: element-match + + This example retrieves documents in which the ``countries`` array + contains one of the values in the array ``['Canada', 'Egypt']``: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-elem-match + :end-before: end-elem-match + +To learn how to query array fields by using the Laravel query builder instead of the +Eloquent ORM, see the :ref:`laravel-query-builder-elemMatch` section in +the Query Builder guide. + +.. _laravel-retrieve-one: + +Retrieve the First Result +------------------------- + +To retrieve the first document that matches a set of criteria, use the ``where()`` method +followed by the ``first()`` method. + +Chain the ``orderBy()`` method to ``first()`` to get consistent results when you query on a unique +value. If you omit the ``orderBy()`` method, MongoDB returns the matching documents according to +the documents' natural order, or as they appear in the collection. + +This example queries for documents in which the value of the ``runtime`` field is +``30`` and returns the first matching document according to the value of the ``_id`` +field. + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-first + :end-before: end-first + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movie = Movie::where('runtime', 30) + ->orderBy('_id') + ->first(); + + return view('browse_movies', [ + 'movies' => $movie + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Statues also Die + Year: 1953 + Runtime: 30 + IMDB Rating: 7.6 + IMDB Votes: 620 + Plot: A documentary of black art. + +.. tip:: + + To learn more about the ``orderBy()`` method, see the + :ref:`laravel-sort` section of the Modify Query Results guide. + +.. _laravel-retrieve-all: + +Retrieve All Documents in a Collection +-------------------------------------- + +You can retrieve all documents in a collection by omitting the query filter. +To return the documents, call the ``get()`` method on an Eloquent model that +represents your collection. Alternatively, you can use the ``get()`` method's +alias ``all()`` to perform the same operation. + +Use the following syntax to run a find operation that matches all documents: + +.. code-block:: php + + $movies = Movie::get(); + +.. warning:: + + The ``movies`` collection in the Atlas sample dataset contains a large amount of data. + Retrieving and displaying all documents in this collection might cause your web + application to time out. + + To avoid this issue, specify a document limit by using the ``take()`` method. For + more information about ``take()``, see the :ref:`laravel-modify-find` + section of the Modify Query Output guide. + +Additional Information +---------------------- + +To view runnable code examples that demonstrate how to perform find +operations by using the {+odm-short+}, see the following usage examples: + +- :ref:`laravel-find-one-usage` +- :ref:`laravel-find-usage` + +To learn how to insert data into MongoDB, see the +:ref:`laravel-fundamentals-write-ops` guide. + +To learn how to modify the way that the {+odm-short+} returns results, +see the :ref:`laravel-read-modify-results` guide. diff --git a/docs/fundamentals/read-operations/search-text.txt b/docs/fundamentals/read-operations/search-text.txt new file mode 100644 index 000000000..4b465e737 --- /dev/null +++ b/docs/fundamentals/read-operations/search-text.txt @@ -0,0 +1,157 @@ +.. _laravel-fundamentals-search-text: +.. _laravel-retrieve-text-search: + +=========== +Search Text +=========== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: filter, string, CRUD, code example + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to run a **text search** by using +{+odm-long+}. + +You can use a text search to retrieve documents that contain a term or a +phrase in a specified field. A term is a sequence of characters that +excludes whitespace characters. A phrase is a sequence of terms with any +number of whitespace characters. + +This guide describes the Eloquent model methods that you can use to +search text and provides examples. To learn more about Eloquent models +in the {+odm-short+}, see the :ref:`laravel-eloquent-models` section. + +.. include:: /includes/fundamentals/read-operations/before-you-get-started.rst + +Search Text Fields +------------------ + +Before you can perform a text search, you must create a :manual:`text +index ` on +the text-valued field. To learn more about creating +indexes, see the :ref:`laravel-eloquent-indexes` section of the +Schema Builder guide. + +You can perform a text search by using the :manual:`$text +` operator followed +by the ``$search`` field in your query filter that you pass to the +``where()`` method. The ``$text`` operator performs a text search on the +text-indexed fields. The ``$search`` field specifies the text to search for. + +After building your query by using the ``where()`` method, chain the ``get()`` +method to retrieve the query results. + +This example calls the ``where()`` method on the ``Movie`` Eloquent model to +retrieve documents in which the ``plot`` field contains the phrase +``"love story"``. To perform this text search, the collection must have +a text index on the ``plot`` field. + +.. tabs:: + + .. tab:: Query Syntax + :tabid: query-syntax + + Use the following syntax to specify the query: + + .. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-text + :end-before: end-text + + .. tab:: Controller Method + :tabid: controller + + To see the query results in the ``browse_movies`` view, edit the ``show()`` function + in the ``MovieController.php`` file to resemble the following code: + + .. io-code-block:: + :copyable: true + + .. input:: + :language: php + + class MovieController + { + public function show() + { + $movies = Movie::where('$text', ['$search' => '"love story"']) + ->get(); + + return view('browse_movies', [ + 'movies' => $movies + ]); + } + } + + .. output:: + :language: none + :visible: false + + Title: Cafè de Flore + Year: 2011 + Runtime: 120 + 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 + IMDB Rating: 6.1 + IMDB Votes: 223 + Plot: A girl. A boy. A love story ... + + ... + +Search Score +------------ + +A text search assigns a numerical :manual:`text score ` to indicate how closely +each result matches the string in your query filter. You can sort the +results by relevance by using the ``orderBy()`` method to sort on the +``textScore`` metadata field. You can access this metadata by using the +:manual:`$meta ` operator: + +.. literalinclude:: /includes/fundamentals/read-operations/ReadOperationsTest.php + :language: php + :dedent: + :start-after: start-text-relevance + :end-before: end-text-relevance + :emphasize-lines: 2 + +.. tip:: + + To learn more about the ``orderBy()`` method, see the + :ref:`laravel-sort` section of the Modify Query Output guide. + +Additional Information +---------------------- + +To view runnable code examples that demonstrate how to perform find +operations by using the {+odm-short+}, see the following usage examples: + +- :ref:`laravel-find-one-usage` +- :ref:`laravel-find-usage` + +To learn how to retrieve data based on filter criteria, see the +:ref:`laravel-fundamentals-read-retrieve` guide. diff --git a/docs/fundamentals/write-operations.txt b/docs/fundamentals/write-operations.txt index 0a4d8a6ca..1b2f163be 100644 --- a/docs/fundamentals/write-operations.txt +++ b/docs/fundamentals/write-operations.txt @@ -133,8 +133,7 @@ matching document doesn't exist: ['upsert' => true], ); - /* Or, use the upsert() method. */ - + // Or, use the upsert() method. SampleModel::upsert( [], '', diff --git a/docs/includes/eloquent-models/PlanetSoftDelete.php b/docs/includes/eloquent-models/PlanetSoftDelete.php index 05d106206..70ccba24b 100644 --- a/docs/includes/eloquent-models/PlanetSoftDelete.php +++ b/docs/includes/eloquent-models/PlanetSoftDelete.php @@ -2,8 +2,8 @@ namespace App\Models; +use Illuminate\Database\Eloquent\SoftDeletes; use MongoDB\Laravel\Eloquent\Model; -use MongoDB\Laravel\Eloquent\SoftDeletes; class Planet extends Model { diff --git a/docs/includes/framework-compatibility-laravel.rst b/docs/includes/framework-compatibility-laravel.rst index 16c405e21..4b0055692 100644 --- a/docs/includes/framework-compatibility-laravel.rst +++ b/docs/includes/framework-compatibility-laravel.rst @@ -3,21 +3,31 @@ :stub-columns: 1 * - {+odm-long+} Version + - Laravel 12.x - Laravel 11.x - Laravel 10.x - Laravel 9.x + * - 5.2 to 5.4 + - ✓ + - ✓ + - ✓ + - + * - 4.2 to 5.1 + - - ✓ - ✓ - * - 4.1 + - - - ✓ - * - 4.0 + - - - ✓ - diff --git a/docs/includes/fundamentals/read-operations/ReadOperationsTest.php b/docs/includes/fundamentals/read-operations/ReadOperationsTest.php index 207fd442e..414b21d31 100644 --- a/docs/includes/fundamentals/read-operations/ReadOperationsTest.php +++ b/docs/includes/fundamentals/read-operations/ReadOperationsTest.php @@ -9,6 +9,13 @@ use MongoDB\Driver\ReadPreference; use MongoDB\Laravel\Tests\TestCase; +use function json_encode; +use function ob_get_flush; +use function ob_start; + +use const JSON_PRETTY_PRINT; +use const PHP_EOL; + class ReadOperationsTest extends TestCase { protected function setUp(): void @@ -183,4 +190,37 @@ public function testReadPreference(): void $this->assertNotNull($movies); $this->assertCount(2, $movies); } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testQueryLog(): void + { + $output = ''; + ob_start(function (string $buffer) use (&$output) { + $output .= $buffer; + }); + // start-query-log + DB::connection('mongodb')->enableQueryLog(); + + Movie::where('title', 'Carrie')->get(); + Movie::where('year', '<', 2005)->get(); + Movie::where('imdb.rating', '>', 8.5)->get(); + + $logs = DB::connection('mongodb')->getQueryLog(); + foreach ($logs as $log) { + echo json_encode($log, JSON_PRETTY_PRINT) . PHP_EOL; + } + + // end-query-log + $output = ob_get_flush(); + $this->assertNotNull($logs); + $this->assertNotEmpty($output); + + $this->assertStringContainsString('"query": "{ \"find\" : \"movies\", \"filter\" : { \"title\" : \"Carrie\" } }"', $output); + $this->assertStringContainsString('"query": "{ \"find\" : \"movies\", \"filter\" : { \"imdb.rating\" : { \"$gt\" : { \"$numberDouble\" : \"8.5\" } } } }"', $output); + $this->assertStringContainsString('"query": "{ \"find\" : \"movies\", \"filter\" : { \"imdb.rating\" : { \"$gt\" : { \"$numberDouble\" : \"8.5\" } } } }"', $output); + $this->assertMatchesRegularExpression('/"time": \d+/', $output); + } } diff --git a/docs/includes/fundamentals/read-operations/before-you-get-started.rst b/docs/includes/fundamentals/read-operations/before-you-get-started.rst new file mode 100644 index 000000000..9555856fc --- /dev/null +++ b/docs/includes/fundamentals/read-operations/before-you-get-started.rst @@ -0,0 +1,15 @@ +Before You Get Started +---------------------- + +To run the code examples in this guide, complete the :ref:`Quick Start ` +tutorial. This tutorial provides instructions on setting up a MongoDB Atlas instance with +sample data and creating the following files in your Laravel web application: + +- ``Movie.php`` file, which contains a ``Movie`` model to represent documents in the ``movies`` + collection +- ``MovieController.php`` file, which contains a ``show()`` function to run database operations +- ``browse_movies.blade.php`` file, which contains HTML code to display the results of database + operations + +The following sections describe how to edit the files in your Laravel application to run +the find operation code examples and view the expected output. diff --git a/docs/includes/query-builder/QueryBuilderTest.php b/docs/includes/query-builder/QueryBuilderTest.php index 574fe060f..a90f1685f 100644 --- a/docs/includes/query-builder/QueryBuilderTest.php +++ b/docs/includes/query-builder/QueryBuilderTest.php @@ -213,10 +213,10 @@ public function testGroupBy(): void { // begin query groupBy $result = DB::table('movies') - ->where('rated', 'G') - ->groupBy('runtime') - ->orderBy('runtime', 'asc') - ->get(['title']); + ->where('rated', 'G') + ->groupBy('runtime') + ->orderBy('runtime', 'asc') + ->get(['title']); // end query groupBy $this->assertInstanceOf(\Illuminate\Support\Collection::class, $result); @@ -351,7 +351,7 @@ public function testAll(): void { // begin query all $result = DB::table('movies') - ->where('movies', 'all', ['title', 'rated', 'imdb.rating']) + ->where('writers', 'all', ['Ben Affleck', 'Matt Damon']) ->get(); // end query all @@ -420,10 +420,10 @@ public function testWhereRaw(): void // begin query raw $result = DB::table('movies') ->whereRaw([ - 'imdb.votes' => ['$gte' => 1000 ], + 'imdb.votes' => ['$gte' => 1000], '$or' => [ ['imdb.rating' => ['$gt' => 7]], - ['directors' => ['$in' => [ 'Yasujiro Ozu', 'Sofia Coppola', 'Federico Fellini' ]]], + ['directors' => ['$in' => ['Yasujiro Ozu', 'Sofia Coppola', 'Federico Fellini']]], ], ])->get(); // end query raw @@ -470,7 +470,7 @@ public function testNear(): void { $this->importTheaters(); - // begin query near + // begin query near $results = DB::table('theaters') ->where('location.geo', 'near', [ '$geometry' => [ @@ -588,7 +588,7 @@ public function testUpdateUpsert(): void [ 'plot' => 'An autobiographical movie', 'year' => 1998, - 'writers' => [ 'Will Hunting' ], + 'writers' => ['Will Hunting'], ], ['upsert' => true], ); @@ -597,6 +597,29 @@ public function testUpdateUpsert(): void $this->assertIsInt($result); } + public function testMultiplyDivide(): void + { + // begin multiply divide + $result = DB::table('movies') + ->where('year', 2001) + ->multiply('imdb.votes', 5); + + $result = DB::table('movies') + ->where('year', 2001) + ->divide('runtime', 2); + // end multiply divide + + $this->assertIsInt($result); + + // begin multiply with set + $result = DB::table('movies') + ->where('year', 1958) + ->multiply('runtime', 1.5, ['note' => 'Adds recovered footage.']); + // end multiply with set + + $this->assertIsInt($result); + } + public function testIncrement(): void { // begin increment diff --git a/docs/includes/schema-builder/flights_migration.php b/docs/includes/schema-builder/flights_migration.php index 861c339ef..4f776f260 100644 --- a/docs/includes/schema-builder/flights_migration.php +++ b/docs/includes/schema-builder/flights_migration.php @@ -19,6 +19,25 @@ public function up(): void $collection->unique('mission_id', options: ['name' => 'unique_mission_id_idx']); }); // end create index + + // begin-json-schema + Schema::create('pilots', function (Blueprint $collection) { + $collection->jsonSchema( + schema: [ + 'bsonType' => 'object', + 'required' => ['license_number'], + 'properties' => [ + 'license_number' => [ + 'bsonType' => 'int', + 'minimum' => 1000, + 'maximum' => 9999, + ], + ], + ], + validationAction: 'error', + ); + }); + // end-json-schema } public function down(): void diff --git a/docs/index.txt b/docs/index.txt index 2937968a7..0b01b7349 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,3 +1,5 @@ +.. _laravel-docs-landing: + =============== {+odm-long+} =============== @@ -22,6 +24,7 @@ Databases & Collections User Authentication Cache & Locks + Scout Integration HTTP Sessions Queues Transactions @@ -86,6 +89,7 @@ see the following content: - :ref:`laravel-aggregation-builder` - :ref:`laravel-user-authentication` - :ref:`laravel-cache` +- :ref:`laravel-scout` - :ref:`laravel-sessions` - :ref:`laravel-queues` - :ref:`laravel-transactions` diff --git a/docs/query-builder.txt b/docs/query-builder.txt index 76a0d144a..2358ed7d5 100644 --- a/docs/query-builder.txt +++ b/docs/query-builder.txt @@ -195,7 +195,7 @@ the value of the ``title`` field is ``"Back to the Future"``: :start-after: begin query orWhere :end-before: end query orWhere -.. note:: +.. note:: id Alias You can use the ``id`` alias in your queries to represent the ``_id`` field in MongoDB documents, as shown in the preceding @@ -208,6 +208,9 @@ the value of the ``title`` field is ``"Back to the Future"``: Because of this behavior, you cannot have two separate ``id`` and ``_id`` fields in your documents. + To learn how to disable this behavior for embedded documents, see the + :ref:`laravel-disable-id-alias` section of the Connection Options guide. + .. _laravel-query-builder-logical-and: Logical AND Example @@ -227,7 +230,7 @@ value greater than ``8.5`` and a ``year`` value of less than .. tip:: - For compatibility with Laravel, Laravel MongoDB v5.1 supports both arrow + For compatibility with Laravel, {+odm-long+} v5.1 supports both arrow (``->``) and dot (``.``) notation to access nested fields in a query filter. The preceding example uses dot notation to query the ``imdb.rating`` nested field, which is the recommended syntax. @@ -869,7 +872,8 @@ Contains All Fields Example The following example shows how to use the ``all`` query operator with the ``where()`` query builder method to match -documents that contain all the specified fields: +documents that have a ``writers`` array field containing all +the specified values: .. literalinclude:: /includes/query-builder/QueryBuilderTest.php :language: php @@ -1165,6 +1169,7 @@ This section includes query builder examples that show how to use the following MongoDB-specific write operations: - :ref:`Upsert a document ` +- :ref:`Multiply and divide values ` - :ref:`Increment a numerical value ` - :ref:`Decrement a numerical value ` - :ref:`Add an array element ` @@ -1248,6 +1253,41 @@ and the ``title`` field and value specified in the ``where()`` query operation: The ``update()`` query builder method returns the number of documents that the operation updated or inserted. +.. _laravel-mongodb-query-builder-mul-div: + +Multiply and Divide Numerical Values Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting in {+odm-short+} v5.5, you can perform multiplication and +division operations on numerical values by using the ``multiply()`` and +``divide()`` query builder methods. + +The following example shows how to use the ``multiply()`` and +``divide()`` methods to manipulate the values of the +``imdb.votes`` and ``runtime`` fields: + +.. literalinclude:: /includes/query-builder/QueryBuilderTest.php + :language: php + :dedent: + :start-after: begin multiply divide + :end-before: end multiply divide + +.. tip:: update() Method + + You can perform the same operations by using the ``update()`` + method and passing an update document that includes the :manual:`$mul + ` operator. To learn more about + ``update()``, see the :ref:`laravel-fundamentals-write-modify` guide. + +You can optionally pass an array parameter to perform a ``$set`` update +in the same operation, as shown in the following example: + +.. literalinclude:: /includes/query-builder/QueryBuilderTest.php + :language: php + :dedent: + :start-after: begin multiply with set + :end-before: end multiply with set + .. _laravel-mongodb-query-builder-increment: Increment a Numerical Value Example diff --git a/docs/quick-start.txt b/docs/quick-start.txt index 1d188ad84..5bd04f353 100644 --- a/docs/quick-start.txt +++ b/docs/quick-start.txt @@ -26,6 +26,7 @@ Quick Start View Data Write Data Next Steps + Tutorial: Build a Back End Overview -------- @@ -41,13 +42,8 @@ read and write operations on the data. `How to Build a Laravel + MongoDB Back End Service `__ MongoDB Developer Center tutorial. - You can learn how to set up a local Laravel development environment - and perform CRUD operations by viewing the - :mdbu-course:`Getting Started with Laravel and MongoDB ` - MongoDB University Learning Byte. - If you prefer to connect to MongoDB by using the {+php-library+} without - Laravel, see `Connecting to MongoDB `__ + Laravel, see `Connect to MongoDB `__ in the {+php-library+} documentation. The {+odm-short+} extends the Laravel Eloquent and Query Builder syntax to diff --git a/docs/quick-start/backend-service-tutorial.txt b/docs/quick-start/backend-service-tutorial.txt new file mode 100644 index 000000000..7ecdf8cf8 --- /dev/null +++ b/docs/quick-start/backend-service-tutorial.txt @@ -0,0 +1,481 @@ +.. _laravel-tutorial-backend-service: + +=========================================================== +Tutorial: Build a Back End Service by Using {+odm-long+} +=========================================================== + +.. facet:: + :name: genre + :values: tutorial + +.. meta:: + :keywords: php framework, odm, code example, crud + :description: Learn how to set up a back end and perform CRUD operations by using Laravel MongoDB. + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this tutorial, you create a simple REST back end for a front-end app +by using {+odm-long+}. The tutorial uses Laravel's built-in API routing +features. + +Prerequisites +------------- + +Before you can start this tutorial, you need the following software +installed in your development environment: + +- MongoDB Atlas cluster with sample data loaded. To learn how to create + a cluster, see the :ref:`laravel-quick-start-create-deployment` step + of the Quick Start guide. +- `PHP `__. +- `Composer `__. +- `{+php-extension+} `__. +- A terminal app and shell. For MacOS users, use Terminal or a similar app. + For Windows users, use PowerShell. + +Steps +----- + +.. procedure:: + :style: connected + + .. step:: Create a Laravel project. + + First, create a Laravel project directory. Then, run the following + command to create a new Laravel project called ``laraproject``: + + .. code-block:: bash + + composer create-project laravel/laravel laraproject + + .. step:: Install {+odm-long+}. + + To check that {+odm-long+} is running in the web server, + add a webpage to your Laravel website. In your project, + navigate to ``/routes/web.php`` and add the following route: + + .. code-block:: php + + Route::get('/info', function () { + phpinfo(); + }); + + Then, run the following command in your shell to start + your application: + + .. code-block:: bash + + php artisan serve + + After the application begins running, navigate to + http://127.0.0.1:8000/info to view the PHPinfo page. Scroll down + to or search for the **mongodb** entry to verify that + the {+php-extension+} is installed. + + Run the following command in your shell to install {+odm-long+}: + + .. code-block:: bash + + composer require mongodb/laravel-mongodb:^{+package-version+} + + .. step:: Configure your MongoDB connection. + + Open your project's ``config/database.php`` file and update the + ``connections`` array as shown in the following code: + + .. code-block:: php + + 'connections' => [ + 'mongodb' => [ + 'driver' => 'mongodb', + 'dsn' => '', + 'database' => 'db', + ], + + Ensure that you replace the connection string placeholder + in the preceding code with your connection string before you run + your application. To learn how to locate your connection string, see + :ref:`laravel-quick-start-connection-string` in the Quick Start + guide. + + You can also set the default database connection. At the top of + the ``config/database.php`` file, change 'default' to the following: + + .. code-block:: php + + 'default' => 'mongodb', + + The Laravel application can now connect to the ``db`` database in + your MongoDB cluster. + + .. step:: Create an endpoint to ping your deployment. + + Run the following shell command to set up API routing: + + .. code-block:: bash + + php artisan install:api + + In the newly created ``routes/api.php`` file, add the following + route: + + .. code-block:: php + + // Add the DB use statement to the top of the file. + use Illuminate\Support\Facades\DB; + + Route::get('/ping', function (Request $request) { + $connection = DB::connection('mongodb'); + $msg = 'MongoDB is accessible!'; + try { + $connection->command(['ping' => 1]); + } catch (\Exception $e) { + $msg = 'MongoDB is not accessible. Error: ' . $e->getMessage(); + } + return ['msg' => $msg]; + }); + + Reload the application, then verify that + http://127.0.0.1:8000/api/ping shows the succesful ping message. + + .. step:: Create Eloquent models. + + Laravel is integrated with Eloquent, an ORM that abstracts the + database back end so that you can connect to different databases by + using a common interface. + + Eloquent provides a ``Model`` class to serve as the interface + between your code and a specific collection. Instances of the + ``Model`` classes represent rows of tables in relational + databases. In MongoDB, they are documents in the collection. + + .. tip:: + + You can define fillable fields in your Eloquent models + to enforce a document schema in your application and prevent + errors such as name typos. To learn more, see the + :ref:`laravel-model-mass-assignment` section of the Eloquent + Model Class guide. + + Create an Eloquent model called ``CustomerMongoDB`` by running + the following command from the project root: + + .. code-block:: bash + + php artisan make:model CustomerMongoDB + + Laravel creates the ``CustomerMongoDB`` class in the ``/models`` + directory. By default, models use the ``default`` database + connection, but you can specify which connection to use by adding + the ``$connection`` member to the class. You can also + specify the collection name by adding the ``$collection`` member. + + Ensure you include the ``use`` statement for the MongoDB Eloquent + model. This is necessary to set ``_id`` as the primary key. + + Replace the contents of the ``CustomerMongoDB.php`` file with the + following code: + + .. code-block:: php + + use MongoDB\Laravel\Eloquent\Model; + + class CustomerMongoDB extends Model + { + // the selected database as defined in /config/database.php + protected $connection = 'mongodb'; + + // equivalent to $table for MySQL + protected $collection = 'laracoll'; + + // defines the schema for top-level properties (optional). + protected $fillable = ['guid', 'first_name', 'family_name', 'email', 'address']; + } + + .. step:: Perform CRUD operations. + + After you create your models, you can perform data operations. + + Create the following route in your ``api.php`` file: + + .. code-block:: php + + Route::get('/create_eloquent_mongo/', function (Request $request) { + $success = CustomerMongoDB::create([ + 'guid'=> 'cust_1111', + 'first_name'=> 'John', + 'family_name' => 'Doe', + 'email' => 'j.doe@gmail.com', + 'address' => '123 my street, my city, zip, state, country' + ]); + }); + + After you insert the document, you can retrieve it by using the + ``where()`` method as shown in the following code: + + .. code-block:: php + + Route::get('/find_eloquent/', function (Request $request) { + $customer = CustomerMongoDB::where('guid', 'cust_1111')->get(); + }); + + Eloquent allows you to find data by using complex queries with + multiple matching conditions. + + You can also update and delete data shown in the following routes: + + .. code-block:: php + + Route::get('/update_eloquent/', function (Request $request) { + $result = CustomerMongoDB::where('guid', 'cust_1111')->update( ['first_name' => 'Jimmy'] ); + }); + + Route::get('/delete_eloquent/', function (Request $request) { + $result = CustomerMongoDB::where('guid', 'cust_1111')->delete(); + }); + + At this point, your MongoDB-connected back-end service is + running, but MongoDB provides more functionality to support your + operations. + + .. step:: Perform operations on nested data. + + {+odm-long+} offers MongoDB-specific operations for nested data. + However, adding nested data is also intuitive without using + the ``embedsMany()`` and ``embedsOne()`` methods. + + As shown in the preceding step, you can define top-level schema + attributes. However, it is more complicated when to define these + attribute if your documents include arrays and embedded documents. + + You can create the model's data structures in PHP. In the + following example, the ``address`` field is an object type. + The ``email`` field is an array of strings: + + .. code-block:: php + + Route::get('/create_nested/', function (Request $request) { + $message = "executed"; + $success = null; + + $address = new stdClass; + $address->street = '123 my street name'; + $address->city = 'my city'; + $address->zip= '12345'; + $emails = ['j.doe@gmail.com', 'j.doe@work.com']; + + try { + $customer = new CustomerMongoDB(); + $customer->guid = 'cust_2222'; + $customer->first_name = 'John'; + $customer->family_name= 'Doe'; + $customer->email= $emails; + $customer->address= $address; + $success = $customer->save(); // save() returns 1 or 0 + } + catch (\Exception $e) { + $message = $e->getMessage(); + } + return ['msg' => $message, 'data' => $success]; + }); + + When you access the ``/api/create_nested/`` endpoint, it creates a + document in MongoDB: + + .. code-block:: json + + { + "_id": {...}, + "guid": "cust_2222", + "first_name": "John", + "family_name": "Doe", + "email": [ + "j.doe@gmail.com", + "j.doe@work.com" + ], + "address": { + "street": "123 my street name", + "city": "my city", + "zip": "12345" + }, + "updated_at": { + "$date": "2025-05-27T17:38:28.793Z" + }, + "created_at": { + "$date": "2025-05-27T17:38:28.793Z" + } + } + + .. step:: Use the MongoDB Query API. + + MongoDB provides the Query API for optimized queries. + + You can begin to build a query by using a ``collection`` object. + Eloquent exposes the full capabilities of the underlying database + by using "raw queries," which Laravel sends to the database + without any processing from the Eloquent Query Builder. + + You can perform a raw native MongoDB query from the model as shown + in the following code: + + .. code-block:: php + + $mongodbquery = ['guid' => 'cust_1111']; + + // returns a "Illuminate\Database\Eloquent\Collection" Object + $results = CustomerMongoDB::whereRaw( $mongodbquery )->get(); + + You can also access the native MongoDB collection object and + perform a query that returns objects such as native MongoDB + documents or cursors: + + .. code-block:: php + + $mongodbquery = ['guid' => 'cust_1111', ]; + $mongodb_native_collection = DB::connection('mongodb')->getCollection('laracoll'); + $document = $mongodb_native_collection->findOne( $mongodbquery ); + $cursor = $mongodb_native_collection->find( $mongodbquery ); + + The following code demonstrates multiple ways to perform queries: + + .. code-block:: php + + Route::get('/find_native/', function (Request $request) { + // a simple MongoDB query that looks for a customer based on the guid + $mongodbquery = ['guid' => 'cust_2222']; + + // Option #1 + // ========= + // use Eloquent's whereRaw() function + // returns a "Illuminate\Database\Eloquent\Collection" Object + + $results = CustomerMongoDB::whereRaw( $mongodbquery )->get(); + + // Option #2 & #3 + // ============== + // use the native MongoDB driver Collection object and the Query API + + $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); + + // find the first document that matches the query + $mdb_bsondoc = $mdb_collection->findOne( $mongodbquery ); // returns a "MongoDB\Model\BSONDocument" Object + + // to convert the MongoDB Document to a Laravel Model, use the Model's newFromBuilder() method + $cust = new CustomerMongoDB(); + $one_doc = $cust->newFromBuilder((array) $mdb_bsondoc); + + // find all documents because you pass an empty query + $mdb_cursor = $mdb_collection->find(); // returns a "MongoDB\Driver\Cursor" object + $cust_array = array(); + foreach ($mdb_cursor->toArray() as $bson) { + $cust_array[] = $cust->newFromBuilder( $bson ); + } + + return ['msg' => 'executed', 'whereraw' => $results, 'document' => $one_doc, 'cursor_array' => $cust_array]; + }); + + The following code demonstrates how to use the ``updateOne()`` + method to update documents: + + .. code-block:: php + + Route::get('/update_native/', function (Request $request) { + $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); + $match = ['guid' => 'cust_2222']; + $update = ['$set' => ['first_name' => 'Henry', 'address.street' => '777 new street name'] ]; + $result = $mdb_collection->updateOne($match, $update ); + return ['msg' => 'executed', 'matched_docs' => $result->getMatchedCount(), 'modified_docs' => $result->getModifiedCount()]; + }); + + The following code demonstrates how to use the ``deleteOne()`` + method to delete documents: + + .. code-block:: php + + Route::get('/delete_native/', function (Request $request) { + $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); + $match = ['guid' => 'cust_2222']; + $result = $mdb_collection->deleteOne($match ); + return ['msg' => 'executed', 'deleted_docs' => + $result->getDeletedCount() ]; + }); + + To learn more about how to perform CRUD operations, see the + :ref:`laravel-fundamentals-write-ops` and + :ref:`laravel-fundamentals-read-ops` guides. + + .. step:: Use the aggregation framework. + + An aggregation pipeline is a task in MongoDB's aggregation + framework. You can use the aggregation framework to perform + various tasks such as real-time dashboards and big data analysis. + + An aggregation pipeline consists of multiple stages in which the + output of each stage is the input of the following stage. + This step uses the ``sample_mflix`` from the :atlas:`Atlas sample + datasets `. Laravel allows you to access multiple + MongoDB databases in the same app, so add the ``sample_mflix`` + database connection to ``database.php``: + + .. code-block:: php + + 'mongodb_mflix' => [ + 'driver' => 'mongodb', + 'dsn' => env('DB_URI'), + 'database' => 'sample_mflix', + ], + + Next, create the ``/aggregate/`` API endpoint and define an + aggregation pipeline to retrieve data from the ``movies`` + collection, compute the average movie rating for each genre, and + return a list. + + .. code-block:: php + + Route::get('/aggregate/', function (Request $request) { + $mdb_collection = DB::connection('mongodb_mflix')->getCollection('movies'); + + $stage0 = ['$unwind' => ['path' => '$genres']]; + $stage1 = ['$group' => ['_id' => '$genres', 'averageGenreRating' => ['$avg' => '$imdb.rating']]]; + $stage2 = ['$sort' => ['averageGenreRating' => -1]]; + $aggregation = [$stage0, $stage1, $stage2]; + + $mdb_cursor = $mdb_collection->aggregate( $aggregation ); + + return ['msg' => 'executed', 'data' => $mdb_cursor->toArray() ]; + }); + + {+odm-long+} provides the :ref:`laravel-aggregation-builder` to + build type-safe aggregation pipelines directly from your models. + We recommend using the aggregation builder to perform + aggregations. + + .. step:: Use indexes to optimize query performance. + + You can create indexes to support your queries and improve + performance. To learn more about how to create indexes + programmatically, see the :ref:`laravel-eloquent-indexes` section + of the Schema Builder guide. + +Conclusion +---------- + +In this tutorial, you learned how to create a back-end service by using +Laravel and MongoDB for a front-end web application. +This tutorial also showed how you can use the document model to improve +database efficiency and scalability. You can use the document model with the +MongoDB Query API to create better apps with less downtime. + +You can access the full code for this tutorial in the +:github:`laravel-mongodb-tutorial +` repository on GitHub. + +Navigate through the rest of the :ref:`laravel-docs-landing` +documentation to learn more about {+odm-long+}'s features. diff --git a/docs/quick-start/download-and-install.txt b/docs/quick-start/download-and-install.txt index 696861a43..f6a44a264 100644 --- a/docs/quick-start/download-and-install.txt +++ b/docs/quick-start/download-and-install.txt @@ -31,7 +31,7 @@ to a Laravel web application. .. tip:: As an alternative to the following installation steps, you can use Laravel Herd - to install MongoDB and configure a Laravel MongoDB development environment. For + to install MongoDB and configure a development environment for {+odm-long+}. For more information about using Laravel Herd with MongoDB, see the following resources: - `Installing MongoDB via Herd Pro @@ -47,9 +47,9 @@ to a Laravel web application. .. step:: Install the {+php-extension+} {+odm-long+} requires the {+php-extension+} to manage MongoDB - connections and commands. - Follow the :php:`Installing the MongoDB PHP Driver with PECL - ` guide to install the {+php-extension+}. + connections and commands. To learn how to install the {+php-extension+}, see + the `Install the MongoDB PHP extension `__ + step of the Get Started with the PHP Library guide. .. step:: Install Laravel diff --git a/docs/quick-start/next-steps.txt b/docs/quick-start/next-steps.txt index 1a7f45c6e..dc155326f 100644 --- a/docs/quick-start/next-steps.txt +++ b/docs/quick-start/next-steps.txt @@ -21,6 +21,19 @@ You can download the web application project by cloning the `laravel-quickstart `__ GitHub repository. +.. tip:: Tutorials + + Learn how to implement more CRUD functionality in a {+odm-long+} + application by following the :ref:`Build a Back End Service by Using + {+odm-long+} ` tutorial. + + Learn how to build a full stack application that uses {+odm-long+} by + following along with the `Full Stack Instagram Clone with Laravel and + MongoDB `__ tutorial on YouTube. + +Further Learning +---------------- + Learn more about {+odm-long+} features from the following resources: - :ref:`laravel-fundamentals-connection`: learn how to configure your MongoDB @@ -34,4 +47,3 @@ Learn more about {+odm-long+} features from the following resources: - :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 f29b2bd12..34be94e9e 100644 --- a/docs/quick-start/view-data.txt +++ b/docs/quick-start/view-data.txt @@ -105,9 +105,9 @@ View MongoDB Data .. code-block:: none :copyable: false - INFO View [resources/views/browse_movie.blade.php] created successfully. + INFO View [resources/views/browse_movies.blade.php] created successfully. - Open the ``browse_movie.blade.php`` view file in the ``resources/views`` + Open the ``browse_movies.blade.php`` view file in the ``resources/views`` directory. Replace the contents with the following code and save the changes: @@ -141,7 +141,7 @@ View MongoDB Data .. step:: Optionally, view your results as JSON documents - Rather than generating a view and editing the ``browse_movie.blade.php`` file, you can + Rather than generating a view and editing the ``browse_movies.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 diff --git a/docs/scout.txt b/docs/scout.txt new file mode 100644 index 000000000..8f409148b --- /dev/null +++ b/docs/scout.txt @@ -0,0 +1,259 @@ +.. _laravel-scout: + +=========================== +Full-Text Search with Scout +=========================== + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: php framework, odm, code example, text search, atlas + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to use the Laravel Scout feature in +your {+odm-long+} application. Scout enables you to implement full-text +search on your Eloquent models. To learn more, see `Laravel Scout +`__ in the +Laravel documentation. + +The Scout integration for {+odm-long+} provides the following +functionality: + +- Provides an abstraction to create :atlas:`Atlas Search indexes + ` from any MongoDB or SQL model. + + .. important:: Use Schema Builder to Create Search Indexes + + If your documents are already in MongoDB, create Search indexes + by using {+php-library+} or ``Schema`` builder methods to improve + search query performance. To learn more about creating Search + indexes, see the :ref:`laravel-as-index` section of the Atlas + Search guide. + +- Enables you to automatically replicate data from MongoDB into a + search engine such as `Meilisearch `__ + or `Algolia `__. You can use a MongoDB Eloquent + model as the source to import and index. To learn more about indexing + to a search engine, see the `Indexing + `__ + section of the Laravel Scout documentation. + +.. important:: Deployment Compatibility + + You can use Laravel Scout only when you connect to MongoDB Atlas + deployments. This feature is not available for self-managed + deployments. + +Scout for Atlas Search Tutorial +------------------------------- + +This tutorial demonstrates how to use Scout to compound and index +documents for MongoDB Atlas Search from Eloquent models (MongoDB or SQL). + +.. procedure:: + :style: connected + + .. step:: Install the Scout package + + Before you can use Scout in your application, run the following + command from your application's root directory to install the + ``laravel/scout`` package: + + .. code-block:: bash + + composer require laravel/scout + + .. step:: Add the Searchable trait to your model + + Add the ``Laravel\Scout\Searchable`` trait to an Eloquent model to make + it searchable. The following example adds this trait to the ``Movie`` + model, which represents documents in the ``sample_mflix.movies`` + collection: + + .. code-block:: php + :emphasize-lines: 6, 10 + + `__ + section of the Laravel Scout documentation. + + .. step:: Configure Scout in your application + + Ensure that your application is configured to use MongoDB as its + database connection. To learn how to configure MongoDB, see the + :ref:`laravel-quick-start-connect-to-mongodb` section of the Quick Start + guide. + + To configure Scout in your application, create a file named + ``scout.php`` in your application's ``config`` directory. Paste the + following code into the file to configure Scout: + + .. code-block:: php + :caption: config/scout.php + + env('SCOUT_DRIVER', 'mongodb'), + 'mongodb' => [ + 'connection' => env('SCOUT_MONGODB_CONNECTION', 'mongodb'), + ], + 'prefix' => env('SCOUT_PREFIX', 'scout_'), + ]; + + The preceding code specifies the following configuration: + + - Uses the value of the ``SCOUT_DRIVER`` environment variable as + the default search driver, or ``mongodb`` if the environment + variable is not set + + - Specifies ``scout_`` as the prefix for the collection name of the + searchable collection + + In the ``config/scout.php`` file, you can also specify a custom + Atlas Search index definition. To learn more, see the :ref:`custom + index definition example ` in the + following step. + + Set the following environment variable in your application's + ``.env`` file to select ``mongodb`` as the default search driver: + + .. code-block:: none + :caption: .env + + SCOUT_DRIVER=mongodb + + .. tip:: Queueing + + When using Scout, consider configuring a queue driver to reduce + response times for your application's web interface. To learn more, + see the `Queuing section + `__ + of the Laravel Scout documentation and the :ref:`laravel-queues` guide. + + .. step:: Create the Atlas Search index + + After you configure Scout and set your default search driver, you can + create your searchable collection and search index by running the + following command from your application's root directory: + + .. code-block:: bash + + php artisan scout:index 'App\Models\Movie' + + Because you set MongoDB as the default search driver, the preceding + command creates the search collection with an Atlas Search index in your + MongoDB database. The collection is named ``scout_movies``, based on the prefix + set in the preceding step. The Atlas Search index is named ``scout`` + and has the following configuration by default: + + .. code-block:: json + + { + "mappings": { + "dynamic": true + } + } + + .. _laravel-scout-custom-index: + + To customize the index definition, add the ``index-definitions`` + configuration to the ``mongodb`` entry in your + ``config/scout.php`` file. The following code demonstrates how to + specify a custom index definition to create on the + ``scout_movies`` collection: + + .. code-block:: php + + 'mongodb' => [ + 'connection' => env('SCOUT_MONGODB_CONNECTION', 'mongodb'), + 'index-definitions' => [ + 'scout_movies' => [ + 'mappings' => [ + 'dynamic' => false, + 'fields' => ['title' => ['type' => 'string']] + ] + ] + ] + ], ... + + To learn more about defining Atlas Search index definitions, see the + :atlas:`Define Field Mappings + ` guide in the Atlas + documentation. + + .. note:: + + MongoDB can take up to a minute to create and finalize + an Atlas Search index, so the ``scout:index`` command might not + return a success message immediately. + + .. step:: Import data into the searchable collection + + You can use Scout to replicate data from a source collection + modeled by your Eloquent model into a searchable collection. The + following command replicates and indexes data from the ``movies`` + collection into the ``scout_movies`` collection indexed in the + preceding step: + + .. code-block:: bash + + php artisan scout:import 'App\Models\Movie' + + The documents are automatically indexed for Atlas Search queries. + + .. tip:: Select Fields to Import + + You might not need all the fields from your source documents in your + searchable collection. Limiting the amount of data you replicate can improve + your application's speed and performance. + + You can select specific fields to import by defining the + ``toSearchableArray()`` method in your Eloquent model class. The + following code demonstrates how to define ``toSearchableArray()`` to + select only the ``plot`` and ``title`` fields for replication: + + .. code-block:: php + + class Movie extends Model + { + .... + public function toSearchableArray(): array + { + return [ + 'plot' => $this->plot, + 'title' => $this->title, + ]; + } + } + +After completing these steps, you can perform Atlas Search queries on the +``scout_movies`` collection in your {+odm-long+} application. To learn +how to perform full-text searches, see the :ref:`laravel-atlas-search` +guide. diff --git a/docs/sessions.txt b/docs/sessions.txt index e8ed10e7a..0f334b873 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -24,9 +24,18 @@ In this guide, you can learn how to set up HTTP sessions by using {+odm-long+}. Sessions allow your application to store information about a user across multiple server requests. Your application stores this information in a specified location that it can access in future -requests that the user makes. The session driver in {+odm-long+} uses -the ``MongoDbSessionHandler`` class from the Symfony framework to store -session information. +requests that the user makes. + +.. note:: Session Handler Implementation + + The v5.4 {+odm-long+} introduces the dedicated + ``MongoDbSessionHandler`` class that extends the Laravel + ``DatabaseSessionHandler`` class to store session information. + The ``mongodb`` session driver saves user IDs, IP addresses, and user + agents if present. + + In v5.3 and earlier, the session driver uses the + ``MongoDbSessionHandler`` class from the Symfony framework. To learn more about support for sessions, see `HTTP Session `__ in the diff --git a/docs/transactions.txt b/docs/transactions.txt index b4a7827ba..d42151d41 100644 --- a/docs/transactions.txt +++ b/docs/transactions.txt @@ -60,20 +60,11 @@ This guide contains the following sections: Requirements and Limitations ---------------------------- -To perform transactions in MongoDB, you must use the following MongoDB -version and topology: - -- MongoDB version 4.0 or later -- A replica set deployment or sharded cluster - MongoDB Server and the {+odm-short+} have the following limitations: -- In MongoDB versions 4.2 and earlier, write operations performed within a - transaction must be on existing collections. In MongoDB versions 4.4 and - later, the server automatically creates collections as necessary when - you perform write operations in a transaction. To learn more about this - limitation, see :manual:`Create Collections and Indexes in a Transaction ` - in the {+server-docs-name+}. +- MongoDB standalone deployments do not support transactions. To use + transactions, your deployment must be a multiple node replica set or + sharded cluster. - MongoDB does not support nested transactions. If you attempt to start a transaction within another one, the extension raises a ``RuntimeException``. diff --git a/docs/upgrade.txt b/docs/upgrade.txt index a87d314a2..3c6ec40a4 100644 --- a/docs/upgrade.txt +++ b/docs/upgrade.txt @@ -127,6 +127,11 @@ This library version introduces the following breaking changes: method results before hydrating a Model instance. When passing a complex query filter, use the ``DB::where()`` method instead of ``Model::raw()``. + Starting in v5.3, you can disable automatic conversion of ``id`` to + ``_id`` for embedded documents. To learn more, see the + :ref:`laravel-disable-id-alias` section of the Connection Options + guide. + - Removes support for the ``$collection`` property. The following code shows how to assign a MongoDB collection to a variable in your ``User`` class in older versions compared to v5.0: diff --git a/docs/user-authentication.txt b/docs/user-authentication.txt index 88b0da603..63e883d13 100644 --- a/docs/user-authentication.txt +++ b/docs/user-authentication.txt @@ -224,7 +224,7 @@ to the ``guards`` array: ], ], -Use Laravel Passport with Laravel MongoDB +Use Laravel Passport with {+odm-long+} ````````````````````````````````````````` After installing Laravel Passport, you must enable Passport compatibility with MongoDB by @@ -300,4 +300,5 @@ Additional Information To learn more about user authentication, see `Authentication `__ in the Laravel documentation. -To learn more about Eloquent models, see the :ref:`laravel-eloquent-model-class` guide. \ No newline at end of file +To learn more about Eloquent models, see the +:ref:`laravel-eloquent-model-class` guide. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 67fdd4154..ba1f3b7aa 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,15 @@ parameters: ignoreErrors: + - + message: "#^Class MongoDB\\\\Laravel\\\\Query\\\\Grammar does not have a constructor and must be instantiated without any parameters\\.$#" + count: 1 + path: src/Connection.php + + - + message: "#^Class MongoDB\\\\Laravel\\\\Schema\\\\Grammar does not have a constructor and must be instantiated without any parameters\\.$#" + count: 1 + path: src/Connection.php + - message: "#^Access to an undefined property Illuminate\\\\Container\\\\Container\\:\\:\\$config\\.$#" count: 3 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 926d9e726..03228b162 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,6 +11,9 @@ parameters: editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' + universalObjectCratesClasses: + - MongoDB\BSON\Document + ignoreErrors: - '#Unsafe usage of new static#' - '#Call to an undefined method [a-zA-Z0-9\\_\<\>\(\)]+::[a-zA-Z]+\(\)#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7044f9069..d7f066483 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,6 +23,8 @@ + + collection->deleteMany( @@ -229,6 +230,7 @@ public function pruneUnfinished(DateTimeInterface $before): int } /** Prune all the cancelled entries older than the given date. */ + #[Override] public function pruneCancelled(DateTimeInterface $before): int { $result = $this->collection->deleteMany( diff --git a/src/Cache/MongoLock.php b/src/Cache/MongoLock.php index d273b4d99..50d04c7ce 100644 --- a/src/Cache/MongoLock.php +++ b/src/Cache/MongoLock.php @@ -41,6 +41,7 @@ public function __construct( /** * Attempt to acquire the lock. */ + #[Override] public function acquire(): bool { // The lock can be acquired if: it doesn't exist, it has expired, diff --git a/src/CommandSubscriber.php b/src/CommandSubscriber.php index 569c7c909..1cad23280 100644 --- a/src/CommandSubscriber.php +++ b/src/CommandSubscriber.php @@ -7,6 +7,7 @@ use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber as CommandSubscriberInterface; use MongoDB\Driver\Monitoring\CommandSucceededEvent; +use Override; use function get_object_vars; use function in_array; @@ -21,16 +22,19 @@ public function __construct(private Connection $connection) { } + #[Override] public function commandStarted(CommandStartedEvent $event): void { $this->commands[$event->getOperationId()] = $event; } + #[Override] public function commandFailed(CommandFailedEvent $event): void { $this->logQuery($event); } + #[Override] public function commandSucceeded(CommandSucceededEvent $event): void { $this->logQuery($event); @@ -48,6 +52,6 @@ private function logQuery(CommandSucceededEvent|CommandFailedEvent $event): void } } - $this->connection->logQuery(Document::fromPHP($command)->toCanonicalExtendedJSON(), [], $event->getDurationMicros()); + $this->connection->logQuery(Document::fromPHP($command)->toCanonicalExtendedJSON(), [], $event->getDurationMicros() / 1000); } } diff --git a/src/Connection.php b/src/Connection.php index 980750093..780cad321 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -16,6 +16,7 @@ use MongoDB\Driver\ReadPreference; use MongoDB\Laravel\Concerns\ManagesTransactions; use OutOfBoundsException; +use Override; use Throwable; use function filter_var; @@ -53,6 +54,9 @@ class Connection extends BaseConnection private ?CommandSubscriber $commandSubscriber = null; + /** @var bool Whether to rename the rename "id" into "_id" for embedded documents. */ + private bool $renameEmbeddedIdField; + /** * Create a new database connection instance. */ @@ -80,6 +84,8 @@ public function __construct(array $config) $this->useDefaultSchemaGrammar(); $this->useDefaultQueryGrammar(); + + $this->renameEmbeddedIdField = $config['rename_embedded_id_field'] ?? true; } /** @@ -90,6 +96,7 @@ public function __construct(array $config) * * @return Query\Builder */ + #[Override] public function table($table, $as = null) { $query = new Query\Builder($this, $this->getQueryGrammar(), $this->getPostProcessor()); @@ -110,6 +117,7 @@ public function getCollection($name): Collection } /** @inheritdoc */ + #[Override] public function getSchemaBuilder() { return new Schema\Builder($this); @@ -167,6 +175,8 @@ public function getClient(): ?Client return $this->connection; } + /** @inheritdoc */ + #[Override] public function enableQueryLog() { parent::enableQueryLog(); @@ -177,6 +187,7 @@ public function enableQueryLog() } } + #[Override] public function disableQueryLog() { parent::disableQueryLog(); @@ -187,6 +198,7 @@ public function disableQueryLog() } } + #[Override] protected function withFreshQueryLog($callback) { try { @@ -209,7 +221,7 @@ protected function withFreshQueryLog($callback) protected function getDefaultDatabaseName(string $dsn, array $config): string { if (empty($config['database'])) { - if (! preg_match('/^mongodb(?:[+]srv)?:\\/\\/.+\\/([^?&]+)/s', $dsn, $matches)) { + if (! preg_match('/^mongodb(?:[+]srv)?:\\/\\/.+?\\/([^?&]+)/s', $dsn, $matches)) { throw new InvalidArgumentException('Database is not properly configured.'); } @@ -335,6 +347,7 @@ protected function getDsn(array $config): string } /** @inheritdoc */ + #[Override] public function getDriverName() { return 'mongodb'; @@ -347,21 +360,26 @@ public function getDriverTitle() } /** @inheritdoc */ + #[Override] protected function getDefaultPostProcessor() { return new Query\Processor(); } /** @inheritdoc */ + #[Override] protected function getDefaultQueryGrammar() { - return new Query\Grammar(); + // Argument added in Laravel 12 + return new Query\Grammar($this); } /** @inheritdoc */ + #[Override] protected function getDefaultSchemaGrammar() { - return new Schema\Grammar(); + // Argument added in Laravel 12 + return new Schema\Grammar($this); } /** @@ -393,6 +411,18 @@ public function __call($method, $parameters) return $this->db->$method(...$parameters); } + /** Set whether to rename "id" field into "_id" for embedded documents. */ + public function setRenameEmbeddedIdField(bool $rename): void + { + $this->renameEmbeddedIdField = $rename; + } + + /** Get whether to rename "id" field into "_id" for embedded documents. */ + public function getRenameEmbeddedIdField(): bool + { + return $this->renameEmbeddedIdField; + } + /** * Return the server version of one of the MongoDB servers: primary for * replica sets and standalone, and the selected server for sharded clusters. diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index eedbe8712..3152a9699 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -4,21 +4,25 @@ namespace MongoDB\Laravel\Eloquent; +use Closure; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use MongoDB\BSON\Document; +use MongoDB\Builder\Expression; use MongoDB\Builder\Type\QueryInterface; use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Driver\CursorInterface; -use MongoDB\Driver\Exception\WriteException; +use MongoDB\Driver\Exception\BulkWriteException; use MongoDB\Laravel\Connection; use MongoDB\Laravel\Helpers\QueriesRelationships; use MongoDB\Laravel\Query\AggregationBuilder; use MongoDB\Model\BSONDocument; +use Override; use function array_key_exists; -use function array_merge; +use function array_map; +use function array_replace; use function collect; use function is_array; use function is_object; @@ -66,7 +70,7 @@ class Builder extends EloquentBuilder ]; /** - * @return ($function is null ? AggregationBuilder : self) + * @return ($function is null ? AggregationBuilder : $this) * * @inheritdoc */ @@ -124,7 +128,12 @@ public function vectorSearch( return $this->model->hydrate($results->all()); } - /** @inheritdoc */ + /** + * @param array $options + * + * @inheritdoc + */ + #[Override] public function update(array $values, array $options = []) { // Intercept operations on embedded models and delegate logic @@ -228,7 +237,13 @@ public function decrement($column, $amount = 1, array $extra = []) return parent::decrement($column, $amount, $extra); } - /** @inheritdoc */ + /** + * @param (Closure():T)|Expression|null $value + * + * @return ($value is Closure ? T : ($value is null ? Collection : Expression)) + * + * @template T + */ public function raw($value = null) { // Get raw results from the query builder. @@ -237,7 +252,7 @@ public function raw($value = null) // Convert MongoCursor results to a collection of models. if ($results instanceof CursorInterface) { $results->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']); - $results = $this->query->aliasIdForResult(iterator_to_array($results)); + $results = array_map(fn ($document) => $this->query->aliasIdForResult($document), iterator_to_array($results)); return $this->model->hydrate($results); } @@ -261,6 +276,7 @@ public function raw($value = null) return $results; } + #[Override] public function firstOrCreate(array $attributes = [], array $values = []) { $instance = (clone $this)->where($attributes)->first(); @@ -270,12 +286,13 @@ public function firstOrCreate(array $attributes = [], array $values = []) // createOrFirst is not supported in transaction. if ($this->getConnection()->getSession()?->isInTransaction()) { - return $this->create(array_merge($attributes, $values)); + return $this->create(array_replace($attributes, $values)); } return $this->createOrFirst($attributes, $values); } + #[Override] public function createOrFirst(array $attributes = [], array $values = []) { // The duplicate key error would abort the transaction. Using the regular firstOrCreate in that case. @@ -284,8 +301,8 @@ public function createOrFirst(array $attributes = [], array $values = []) } try { - return $this->create(array_merge($attributes, $values)); - } catch (WriteException $e) { + return $this->create(array_replace($attributes, $values)); + } catch (BulkWriteException $e) { if ($e->getCode() === self::DUPLICATE_KEY_ERROR) { return $this->where($attributes)->first() ?? throw $e; } @@ -299,9 +316,8 @@ public function createOrFirst(array $attributes = [], array $values = []) * TODO Remove if https://github.com/laravel/framework/commit/6484744326531829341e1ff886cc9b628b20d73e * will be reverted * Issue in laravel/frawework https://github.com/laravel/framework/issues/27791. - * - * @return array */ + #[Override] protected function addUpdatedAtColumn(array $values) { if (! $this->model->usesTimestamps() || $this->model->getUpdatedAtColumn() === null) { @@ -309,7 +325,11 @@ protected function addUpdatedAtColumn(array $values) } $column = $this->model->getUpdatedAtColumn(); - $values = array_merge( + if (isset($values['$set'][$column])) { + return $values; + } + + $values = array_replace( [$column => $this->model->freshTimestampString()], $values, ); @@ -323,6 +343,7 @@ public function getConnection(): Connection } /** @inheritdoc */ + #[Override] protected function ensureOrderForCursorPagination($shouldReverse = false) { if (empty($this->query->orders)) { diff --git a/src/Eloquent/DocumentModel.php b/src/Eloquent/DocumentModel.php index 930ed6286..f8d399e62 100644 --- a/src/Eloquent/DocumentModel.php +++ b/src/Eloquent/DocumentModel.php @@ -5,7 +5,6 @@ namespace MongoDB\Laravel\Eloquent; use BackedEnum; -use Carbon\Carbon; use Carbon\CarbonInterface; use DateTimeInterface; use DateTimeZone; @@ -30,7 +29,7 @@ use function array_key_exists; use function array_keys; -use function array_merge; +use function array_replace; use function array_unique; use function array_values; use function class_basename; @@ -51,6 +50,7 @@ use function strlen; use function var_export; +/** @mixin Builder */ trait DocumentModel { use HybridRelations; @@ -128,7 +128,7 @@ public function fromDateTime($value): UTCDateTime * * @param mixed $value */ - protected function asDateTime($value): Carbon + protected function asDateTime($value): DateTimeInterface { // Convert UTCDateTime instances to Carbon. if ($value instanceof UTCDateTime) { @@ -192,7 +192,7 @@ protected function transformModelValue($key, $value) // to a Carbon or CarbonImmutable instance. // @see Model::setAttribute() if ($this->hasCast($key) && $value instanceof CarbonInterface) { - $value->settings(array_merge($value->getSettings(), ['toStringFormat' => $this->getDateFormat()])); + $value->settings(array_replace($value->getSettings(), ['toStringFormat' => $this->getDateFormat()])); // "date" cast resets the time to 00:00:00. $castType = $this->getCasts()[$key]; diff --git a/src/Eloquent/MassPrunable.php b/src/Eloquent/MassPrunable.php index 98e947842..ecf033a3b 100644 --- a/src/Eloquent/MassPrunable.php +++ b/src/Eloquent/MassPrunable.php @@ -5,6 +5,7 @@ namespace MongoDB\Laravel\Eloquent; use Illuminate\Database\Eloquent\MassPrunable as EloquentMassPrunable; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Events\ModelsPruned; use function class_uses_recursive; diff --git a/src/Eloquent/SoftDeletes.php b/src/Eloquent/SoftDeletes.php index 135c55dcf..438219f3c 100644 --- a/src/Eloquent/SoftDeletes.php +++ b/src/Eloquent/SoftDeletes.php @@ -4,6 +4,14 @@ namespace MongoDB\Laravel\Eloquent; +use function sprintf; +use function trigger_error; + +use const E_USER_DEPRECATED; + +trigger_error(sprintf('Since mongodb/laravel-mongodb:5.5, trait "%s" is deprecated, use "%s" instead.', SoftDeletes::class, \Illuminate\Database\Eloquent\SoftDeletes::class), E_USER_DEPRECATED); + +/** @deprecated since mongodb/laravel-mongodb:5.5, use \Illuminate\Database\Eloquent\SoftDeletes instead */ trait SoftDeletes { use \Illuminate\Database\Eloquent\SoftDeletes; diff --git a/src/Helpers/QueriesRelationships.php b/src/Helpers/QueriesRelationships.php index 1f1ffa34b..29d708e3c 100644 --- a/src/Helpers/QueriesRelationships.php +++ b/src/Helpers/QueriesRelationships.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasOneOrMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; +use LogicException; use MongoDB\Laravel\Eloquent\Model; use MongoDB\Laravel\Relations\MorphToMany; @@ -104,6 +105,8 @@ protected function isAcrossConnections(Relation $relation) */ public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) { + $this->assertHybridRelationSupported($relation); + $hasQuery = $relation->getQuery(); if ($callback) { $hasQuery->callScope($callback); @@ -128,6 +131,26 @@ public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $ return $this->whereIn($this->getRelatedConstraintKey($relation), $relatedIds, $boolean, $not); } + /** + * @param Relation $relation + * + * @return void + * + * @throws Exception + */ + private function assertHybridRelationSupported(Relation $relation): void + { + if ( + $relation instanceof HasOneOrMany + || $relation instanceof BelongsTo + || ($relation instanceof BelongsToMany && ! $this->isAcrossConnections($relation)) + ) { + return; + } + + throw new LogicException(class_basename($relation) . ' is not supported for hybrid query constraints.'); + } + /** * @param Builder $hasQuery * @param Relation $relation @@ -213,6 +236,8 @@ protected function getConstrainedRelatedIds($relations, $operator, $count) */ protected function getRelatedConstraintKey(Relation $relation) { + $this->assertHybridRelationSupported($relation); + if ($relation instanceof HasOneOrMany) { return $relation->getLocalKeyName(); } @@ -221,7 +246,7 @@ protected function getRelatedConstraintKey(Relation $relation) return $relation->getForeignKeyName(); } - if ($relation instanceof BelongsToMany && ! $this->isAcrossConnections($relation)) { + if ($relation instanceof BelongsToMany) { return $this->model->getKeyName(); } diff --git a/src/MongoDBBusServiceProvider.php b/src/MongoDBBusServiceProvider.php index d3d6f25fc..ab0afb588 100644 --- a/src/MongoDBBusServiceProvider.php +++ b/src/MongoDBBusServiceProvider.php @@ -10,6 +10,7 @@ use Illuminate\Support\ServiceProvider; use InvalidArgumentException; use MongoDB\Laravel\Bus\MongoBatchRepository; +use Override; use function sprintf; @@ -18,6 +19,7 @@ class MongoDBBusServiceProvider extends ServiceProvider implements DeferrablePro /** * Register the service provider. */ + #[Override] public function register() { $this->app->singleton(MongoBatchRepository::class, function (Container $app) { @@ -46,6 +48,8 @@ public function register() }); } + /** @inheritdoc */ + #[Override] public function provides() { return [ diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index 349abadc7..644eb7a56 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -23,8 +23,9 @@ use MongoDB\Laravel\Eloquent\Model; use MongoDB\Laravel\Queue\MongoConnector; use MongoDB\Laravel\Scout\ScoutEngine; +use MongoDB\Laravel\Session\MongoDbSessionHandler; +use Override; use RuntimeException; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; use function assert; use function class_exists; @@ -47,6 +48,7 @@ public function boot() /** * Register the service provider. */ + #[Override] public function register() { // Add database driver. @@ -67,12 +69,10 @@ public function register() assert($connection instanceof Connection, new InvalidArgumentException(sprintf('The database connection "%s" used for the session does not use the "mongodb" driver.', $connectionName))); return new MongoDbSessionHandler( - $connection->getClient(), - $app->config->get('session.options', []) + [ - 'database' => $connection->getDatabaseName(), - 'collection' => $app->config->get('session.table') ?: 'sessions', - 'ttl' => $app->config->get('session.lifetime'), - ], + $connection, + $app->config->get('session.table', 'sessions'), + $app->config->get('session.lifetime'), + $app, ); }); }); diff --git a/src/Query/Builder.php b/src/Query/Builder.php index f613b6467..5e0413929 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -29,6 +29,7 @@ use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Driver\Cursor; use MongoDB\Driver\ReadPreference; +use MongoDB\Laravel\Connection; use Override; use RuntimeException; use stdClass; @@ -40,6 +41,7 @@ use function array_key_exists; use function array_map; use function array_merge; +use function array_replace; use function array_values; use function assert; use function blank; @@ -82,6 +84,7 @@ use function trait_exists; use function var_export; +/** @property Connection $connection */ class Builder extends BaseBuilder { private const REGEX_DELIMITERS = ['/', '#', '~']; @@ -240,12 +243,14 @@ public function hint($index) } /** @inheritdoc */ + #[Override] public function find($id, $columns = []) { return $this->where('_id', '=', $this->convertKey($id))->first($columns); } /** @inheritdoc */ + #[Override] public function value($column) { $result = (array) $this->first([$column]); @@ -254,12 +259,14 @@ public function value($column) } /** @inheritdoc */ + #[Override] public function get($columns = []) { return $this->getFresh($columns); } /** @inheritdoc */ + #[Override] public function cursor($columns = []) { $result = $this->getFresh($columns, true); @@ -426,7 +433,7 @@ public function toMql(): array // Add custom query options if (count($this->options)) { - $options = array_merge($options, $this->options); + $options = array_replace($options, $this->options); } $options = $this->inheritConnectionOptions($options); @@ -450,7 +457,7 @@ public function toMql(): array // Add custom projections. if ($this->projections) { - $projection = array_merge($projection, $this->projections); + $projection = array_replace($projection, $this->projections); } $options = []; @@ -484,7 +491,7 @@ public function toMql(): array // Add custom query options if (count($this->options)) { - $options = array_merge($options, $this->options); + $options = array_replace($options, $this->options); } $options = $this->inheritConnectionOptions($options); @@ -576,6 +583,7 @@ public function generateCacheKey() } /** @return ($function is null ? AggregationBuilder : mixed) */ + #[Override] public function aggregate($function = null, $columns = ['*']) { assert(is_array($columns), new TypeError(sprintf('Argument #2 ($columns) must be of type array, %s given', get_debug_type($columns)))); @@ -637,9 +645,10 @@ public function aggregate($function = null, $columns = ['*']) } /** - * {@inheritDoc} + * @param string $function + * @param array $columns * - * @see \Illuminate\Database\Query\Builder::aggregateByGroup() + * @return mixed */ public function aggregateByGroup(string $function, array $columns = ['*']) { @@ -651,6 +660,7 @@ public function aggregateByGroup(string $function, array $columns = ['*']) } /** @inheritdoc */ + #[Override] public function exists() { return $this->first(['id']) !== null; @@ -673,6 +683,7 @@ public function distinct($column = false) * * @inheritdoc */ + #[Override] public function orderBy($column, $direction = 'asc') { if (is_string($direction)) { @@ -694,6 +705,7 @@ public function orderBy($column, $direction = 'asc') } /** @inheritdoc */ + #[Override] public function whereBetween($column, iterable $values, $boolean = 'and', $not = false) { $type = 'between'; @@ -718,6 +730,7 @@ public function whereBetween($column, iterable $values, $boolean = 'and', $not = } /** @inheritdoc */ + #[Override] public function insert(array $values) { // Allow empty insert batch for consistency with Eloquent SQL @@ -742,7 +755,10 @@ public function insert(array $values) $values = [$values]; } - $values = $this->aliasIdForQuery($values); + $values = array_map( + $this->aliasIdForQuery(...), + $values, + ); $options = $this->inheritConnectionOptions(); @@ -752,6 +768,7 @@ public function insert(array $values) } /** @inheritdoc */ + #[Override] public function insertGetId(array $values, $sequence = null) { $options = $this->inheritConnectionOptions(); @@ -771,6 +788,7 @@ public function insertGetId(array $values, $sequence = null) } /** @inheritdoc */ + #[Override] public function update(array $values, array $options = []) { // Use $set as default operator for field names that are not in an operator @@ -783,17 +801,11 @@ public function update(array $values, array $options = []) unset($values[$key]); } - // Since "id" is an alias for "_id", we prevent updating it - foreach ($values as $fields) { - if (array_key_exists('id', $fields)) { - throw new InvalidArgumentException('Cannot update "id" field.'); - } - } - return $this->performUpdate($values, $options); } /** @inheritdoc */ + #[Override] public function upsert(array $values, $uniqueBy, $update = null): int { if ($values === []) { @@ -840,6 +852,7 @@ public function upsert(array $values, $uniqueBy, $update = null): int } /** @inheritdoc */ + #[Override] public function increment($column, $amount = 1, array $extra = [], array $options = []) { $query = ['$inc' => [(string) $column => $amount]]; @@ -860,6 +873,12 @@ public function increment($column, $amount = 1, array $extra = [], array $option return $this->performUpdate($query, $options); } + /** + * @param array $options + * + * @inheritdoc + */ + #[Override] public function incrementEach(array $columns, array $extra = [], array $options = []) { $stage['$addFields'] = $extra; @@ -877,12 +896,14 @@ public function incrementEach(array $columns, array $extra = [], array $options } /** @inheritdoc */ + #[Override] public function decrement($column, $amount = 1, array $extra = [], array $options = []) { return $this->increment($column, -1 * $amount, $extra, $options); } /** @inheritdoc */ + #[Override] public function decrementEach(array $columns, array $extra = [], array $options = []) { $decrement = []; @@ -894,7 +915,49 @@ public function decrementEach(array $columns, array $extra = [], array $options return $this->incrementEach($decrement, $extra, $options); } + /** + * Multiply a column's value by a given amount. + * + * @param string $column + * @param float|int $amount + * + * @return int + */ + public function multiply($column, $amount, array $extra = [], array $options = []) + { + $query = ['$mul' => [(string) $column => $amount]]; + + if (! empty($extra)) { + $query['$set'] = $extra; + } + + // Protect + $this->where(function ($query) use ($column) { + $query->where($column, 'exists', true); + + $query->whereNotNull($column); + }); + + $options = $this->inheritConnectionOptions($options); + + return $this->performUpdate($query, $options); + } + + /** + * Divide a column's value by a given amount. + * + * @param string $column + * @param float|int $amount + * + * @return int + */ + public function divide($column, $amount, array $extra = [], array $options = []) + { + return $this->multiply($column, 1 / $amount, $extra, $options); + } + /** @inheritdoc */ + #[Override] public function pluck($column, $key = null) { $results = $this->get($key === null ? [$column] : [$column, $key]); @@ -905,6 +968,7 @@ public function pluck($column, $key = null) } /** @inheritdoc */ + #[Override] public function delete($id = null) { // If an ID is passed to the method, we will set the where clause to check @@ -936,6 +1000,7 @@ public function delete($id = null) } /** @inheritdoc */ + #[Override] public function from($collection, $as = null) { if ($collection) { @@ -968,7 +1033,14 @@ public function lists($column, $key = null) return $this->pluck($column, $key); } - /** @inheritdoc */ + /** + * @param (Closure():T)|Expression|null $value + * + * @return ($value is Closure ? T : ($value is null ? Collection : Expression)) + * + * @template T + */ + #[Override] public function raw($value = null) { // Execute the closure on the mongodb collection @@ -1071,11 +1143,13 @@ public function drop($columns) * * @inheritdoc */ + #[Override] public function newQuery() { return new static($this->connection, $this->grammar, $this->processor); } + #[Override] public function runPaginationCountQuery($columns = ['*']) { if ($this->distinct) { @@ -1158,6 +1232,7 @@ public function convertKey($id) * * @return $this */ + #[Override] public function where($column, $operator = null, $value = null, $boolean = 'and') { $params = func_get_args(); @@ -1671,6 +1746,7 @@ private function inheritConnectionOptions(array $options = []): array } /** @inheritdoc */ + #[Override] public function __call($method, $parameters) { if ($method === 'unset') { @@ -1681,98 +1757,113 @@ public function __call($method, $parameters) } /** @internal This method is not supported by MongoDB. */ + #[Override] public function toSql() { throw new BadMethodCallException('This method is not supported by MongoDB. Try "toMql()" instead.'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function toRawSql() { throw new BadMethodCallException('This method is not supported by MongoDB. Try "toMql()" instead.'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function whereColumn($first, $operator = null, $second = null, $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function whereFullText($columns, $value, array $options = [], $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function groupByRaw($sql, array $bindings = []) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function orderByRaw($sql, $bindings = []) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function unionAll($query) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function union($query, $all = false) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function having($column, $operator = null, $value = null, $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function havingRaw($sql, array $bindings = [], $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function havingBetween($column, iterable $values, $boolean = 'and', $not = false) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function orWhereIntegerInRaw($column, $values) { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function whereIntegerNotInRaw($column, $values, $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } /** @internal This method is not supported by MongoDB. */ + #[Override] public function orWhereIntegerNotInRaw($column, $values, $boolean = 'and') { throw new BadMethodCallException('This method is not supported by MongoDB'); } - private function aliasIdForQuery(array $values): array + private function aliasIdForQuery(array $values, bool $root = true): array { - if (array_key_exists('id', $values)) { + if (array_key_exists('id', $values) && ($root || $this->connection->getRenameEmbeddedIdField())) { if (array_key_exists('_id', $values) && $values['id'] !== $values['_id']) { throw new InvalidArgumentException('Cannot have both "id" and "_id" fields.'); } @@ -1799,20 +1890,20 @@ private function aliasIdForQuery(array $values): array } // ".id" subfield are alias for "._id" - if (str_ends_with($key, '.id')) { + if (str_ends_with($key, '.id') && $this->connection->getRenameEmbeddedIdField()) { $newkey = substr($key, 0, -3) . '._id'; if (array_key_exists($newkey, $values) && $value !== $values[$newkey]) { throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey)); } - $values[substr($key, 0, -3) . '._id'] = $value; + $values[$newkey] = $value; unset($values[$key]); } } foreach ($values as &$value) { if (is_array($value)) { - $value = $this->aliasIdForQuery($value); + $value = $this->aliasIdForQuery($value, false); } elseif ($value instanceof DateTimeInterface) { $value = new UTCDateTime($value); } @@ -1830,10 +1921,13 @@ private function aliasIdForQuery(array $values): array * * @template T of array|object */ - public function aliasIdForResult(array|object $values): array|object + public function aliasIdForResult(array|object $values, bool $root = true): array|object { if (is_array($values)) { - if (array_key_exists('_id', $values) && ! array_key_exists('id', $values)) { + if ( + array_key_exists('_id', $values) && ! array_key_exists('id', $values) + && ($root || $this->connection->getRenameEmbeddedIdField()) + ) { $values['id'] = $values['_id']; unset($values['_id']); } @@ -1843,13 +1937,16 @@ public function aliasIdForResult(array|object $values): array|object $values[$key] = Date::instance($value->toDateTime()) ->setTimezone(new DateTimeZone(date_default_timezone_get())); } elseif (is_array($value) || is_object($value)) { - $values[$key] = $this->aliasIdForResult($value); + $values[$key] = $this->aliasIdForResult($value, false); } } } if ($values instanceof stdClass) { - if (property_exists($values, '_id') && ! property_exists($values, 'id')) { + if ( + property_exists($values, '_id') && ! property_exists($values, 'id') + && ($root || $this->connection->getRenameEmbeddedIdField()) + ) { $values->id = $values->_id; unset($values->_id); } @@ -1859,7 +1956,7 @@ public function aliasIdForResult(array|object $values): array|object $values->{$key} = Date::instance($value->toDateTime()) ->setTimezone(new DateTimeZone(date_default_timezone_get())); } elseif (is_array($value) || is_object($value)) { - $values->{$key} = $this->aliasIdForResult($value); + $values->{$key} = $this->aliasIdForResult($value, false); } } } diff --git a/src/Queue/MongoQueue.php b/src/Queue/MongoQueue.php index 7810aab92..1e353bd65 100644 --- a/src/Queue/MongoQueue.php +++ b/src/Queue/MongoQueue.php @@ -8,6 +8,7 @@ use Illuminate\Queue\DatabaseQueue; use MongoDB\Laravel\Connection; use MongoDB\Operation\FindOneAndUpdate; +use Override; use stdClass; class MongoQueue extends DatabaseQueue @@ -34,7 +35,12 @@ public function __construct(Connection $database, $table, $default = 'default', $this->retryAfter = $retryAfter; } - /** @inheritdoc */ + /** + * @return MongoJob|null + * + * @inheritdoc + */ + #[Override] public function pop($queue = null) { $queue = $this->getQueue($queue); @@ -138,12 +144,14 @@ protected function releaseJob($id, $attempts) } /** @inheritdoc */ + #[Override] public function deleteReserved($queue, $id) { $this->database->table($this->table)->where('_id', $id)->delete(); } /** @inheritdoc */ + #[Override] public function deleteAndRelease($queue, $job, $delay) { $this->deleteReserved($queue, $job->getJobId()); diff --git a/src/Relations/BelongsTo.php b/src/Relations/BelongsTo.php index 93eb11f8e..15447c219 100644 --- a/src/Relations/BelongsTo.php +++ b/src/Relations/BelongsTo.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo as EloquentBelongsTo; +use Override; /** * @template TRelatedModel of Model @@ -26,6 +27,7 @@ public function getHasCompareKey() } /** @inheritdoc */ + #[Override] public function addConstraints() { if (static::$constraints) { @@ -37,6 +39,7 @@ public function addConstraints() } /** @inheritdoc */ + #[Override] public function addEagerConstraints(array $models) { // We'll grab the primary key name of the related models since it could be set to @@ -46,6 +49,7 @@ public function addEagerConstraints(array $models) } /** @inheritdoc */ + #[Override] public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { return $query; @@ -58,11 +62,13 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @return string */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; } + #[Override] public function getQualifiedForeignKeyName(): string { return $this->foreignKey; diff --git a/src/Relations/BelongsToMany.php b/src/Relations/BelongsToMany.php index a150fccf7..8978483ec 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -10,11 +10,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentBelongsToMany; use Illuminate\Support\Arr; use MongoDB\Laravel\Eloquent\Model as DocumentModel; +use Override; use function array_diff; use function array_keys; use function array_map; -use function array_merge; +use function array_replace; use function array_values; use function assert; use function count; @@ -39,12 +40,14 @@ public function getHasCompareKey() } /** @inheritdoc */ + #[Override] public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { return $query; } /** @inheritdoc */ + #[Override] protected function hydratePivotRelation(array $models) { // Do nothing. @@ -61,12 +64,14 @@ protected function getSelectColumns(array $columns = ['*']) } /** @inheritdoc */ + #[Override] protected function shouldSelect(array $columns = ['*']) { return $columns; } /** @inheritdoc */ + #[Override] public function addConstraints() { if (static::$constraints) { @@ -89,6 +94,7 @@ protected function setWhere() } /** @inheritdoc */ + #[Override] public function save(Model $model, array $pivotAttributes = [], $touch = true) { $model->save(['touch' => false]); @@ -99,6 +105,7 @@ public function save(Model $model, array $pivotAttributes = [], $touch = true) } /** @inheritdoc */ + #[Override] public function create(array $attributes = [], array $joining = [], $touch = true) { $instance = $this->related->newInstance($attributes); @@ -114,6 +121,7 @@ public function create(array $attributes = [], array $joining = [], $touch = tru } /** @inheritdoc */ + #[Override] public function sync($ids, $detaching = true) { $changes = [ @@ -164,7 +172,7 @@ public function sync($ids, $detaching = true) // Now we are finally ready to attach the new records. Note that we'll disable // touching until after the entire operation is complete so we don't fire a // ton of touch operations until we are totally done syncing the records. - $changes = array_merge( + $changes = array_replace( $changes, $this->attachNew($records, $current, false), ); @@ -177,6 +185,7 @@ public function sync($ids, $detaching = true) } /** @inheritdoc */ + #[Override] public function updateExistingPivot($id, array $attributes, $touch = true) { // Do nothing, we have no pivot table. @@ -184,6 +193,7 @@ public function updateExistingPivot($id, array $attributes, $touch = true) } /** @inheritdoc */ + #[Override] public function attach($id, array $attributes = [], $touch = true) { if ($id instanceof Model) { @@ -224,6 +234,7 @@ public function attach($id, array $attributes = [], $touch = true) } /** @inheritdoc */ + #[Override] public function detach($ids = [], $touch = true) { if ($ids instanceof Model) { @@ -264,6 +275,7 @@ public function detach($ids = [], $touch = true) } /** @inheritdoc */ + #[Override] protected function buildDictionary(Collection $results) { $foreign = $this->foreignPivotKey; @@ -283,6 +295,7 @@ protected function buildDictionary(Collection $results) } /** @inheritdoc */ + #[Override] public function newPivotQuery() { return $this->newRelatedQuery(); @@ -309,12 +322,14 @@ public function getForeignKey() } /** @inheritdoc */ + #[Override] public function getQualifiedForeignPivotKeyName() { return $this->foreignPivotKey; } /** @inheritdoc */ + #[Override] public function getQualifiedRelatedPivotKeyName() { return $this->relatedPivotKey; @@ -323,10 +338,9 @@ public function getQualifiedRelatedPivotKeyName() /** * Get the name of the "where in" method for eager loading. * - * @param string $key - * - * @return string + * @inheritdoc */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Relations/EmbedsOneOrMany.php b/src/Relations/EmbedsOneOrMany.php index a46593cf4..cc9376dcc 100644 --- a/src/Relations/EmbedsOneOrMany.php +++ b/src/Relations/EmbedsOneOrMany.php @@ -12,6 +12,7 @@ use Illuminate\Database\Query\Expression; use MongoDB\Driver\Exception\LogicException; use MongoDB\Laravel\Eloquent\Model as DocumentModel; +use Override; use Throwable; use function array_merge; @@ -78,6 +79,7 @@ public function __construct(Builder $query, Model $parent, Model $related, strin } /** @inheritdoc */ + #[Override] public function addConstraints() { if (static::$constraints) { @@ -86,12 +88,14 @@ public function addConstraints() } /** @inheritdoc */ + #[Override] public function addEagerConstraints(array $models) { // There are no eager loading constraints. } /** @inheritdoc */ + #[Override] public function match(array $models, Collection $results, $relation) { foreach ($models as $model) { @@ -105,13 +109,7 @@ public function match(array $models, Collection $results, $relation) return $models; } - /** - * Shorthand to get the results of the relationship. - * - * @param array $columns - * - * @return Collection - */ + #[Override] public function get($columns = ['*']) { return $this->getResults(); @@ -324,6 +322,7 @@ protected function getParentRelation() } /** @inheritdoc */ + #[Override] public function getQuery() { // Because we are sharing this relation instance to models, we need @@ -332,6 +331,7 @@ public function getQuery() } /** @inheritdoc */ + #[Override] public function toBase() { // Because we are sharing this relation instance to models, we need @@ -367,6 +367,7 @@ protected function getPathHierarchy($glue = '.') } /** @inheritdoc */ + #[Override] public function getQualifiedParentKeyName() { $parentRelation = $this->getParentRelation(); @@ -425,10 +426,10 @@ public function getQualifiedForeignKeyName() * Get the name of the "where in" method for eager loading. * * @param EloquentModel $model - * @param string $key * - * @return string + * @inheritdoc */ + #[Override] protected function whereInMethod(EloquentModel $model, $key) { return 'whereIn'; diff --git a/src/Relations/HasMany.php b/src/Relations/HasMany.php index c8e7e0590..052230495 100644 --- a/src/Relations/HasMany.php +++ b/src/Relations/HasMany.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany as EloquentHasMany; +use Override; /** * @template TRelatedModel of Model @@ -20,6 +21,7 @@ class HasMany extends EloquentHasMany * * @return string */ + #[Override] public function getForeignKeyName() { return $this->foreignKey; @@ -36,6 +38,7 @@ public function getHasCompareKey() } /** @inheritdoc */ + #[Override] public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { $foreignKey = $this->getHasCompareKey(); @@ -46,10 +49,9 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, /** * Get the name of the "where in" method for eager loading. * - * @param string $key - * - * @return string + * @inheritdoc */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Relations/HasOne.php b/src/Relations/HasOne.php index ea26761d3..bfa297c4e 100644 --- a/src/Relations/HasOne.php +++ b/src/Relations/HasOne.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasOne as EloquentHasOne; +use Override; /** * @template TRelatedModel of Model @@ -20,6 +21,7 @@ class HasOne extends EloquentHasOne * * @return string */ + #[Override] public function getForeignKeyName() { return $this->foreignKey; @@ -36,6 +38,7 @@ public function getHasCompareKey() } /** @inheritdoc */ + #[Override] public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { $foreignKey = $this->getForeignKeyName(); @@ -43,13 +46,8 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, return $query->select($foreignKey)->where($foreignKey, 'exists', true); } - /** - * Get the name of the "where in" method for eager loading. - * - * @param string $key - * - * @return string - */ + /** Get the name of the "where in" method for eager loading. */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Relations/MorphMany.php b/src/Relations/MorphMany.php index 5f395950f..925ebcfa9 100644 --- a/src/Relations/MorphMany.php +++ b/src/Relations/MorphMany.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany as EloquentMorphMany; +use Override; /** * @template TRelatedModel of Model @@ -14,13 +15,7 @@ */ class MorphMany extends EloquentMorphMany { - /** - * Get the name of the "where in" method for eager loading. - * - * @param string $key - * - * @return string - */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Relations/MorphTo.php b/src/Relations/MorphTo.php index 4888b2d97..9f1bf1441 100644 --- a/src/Relations/MorphTo.php +++ b/src/Relations/MorphTo.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo as EloquentMorphTo; +use Override; /** * @template TRelatedModel of Model @@ -15,6 +16,7 @@ class MorphTo extends EloquentMorphTo { /** @inheritdoc */ + #[Override] public function addConstraints() { if (static::$constraints) { @@ -30,6 +32,7 @@ public function addConstraints() } /** @inheritdoc */ + #[Override] protected function getResultsByType($type) { $instance = $this->createModelByType($type); @@ -41,13 +44,8 @@ protected function getResultsByType($type) return $query->whereIn($key, $this->gatherKeysByType($type, $instance->getKeyType()))->get(); } - /** - * Get the name of the "where in" method for eager loading. - * - * @param string $key - * - * @return string - */ + /** Get the name of the "where in" method for eager loading. */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Relations/MorphToMany.php b/src/Relations/MorphToMany.php index 929738360..724dad912 100644 --- a/src/Relations/MorphToMany.php +++ b/src/Relations/MorphToMany.php @@ -10,13 +10,14 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany as EloquentMorphToMany; use Illuminate\Support\Arr; use MongoDB\BSON\ObjectId; +use Override; use function array_diff; use function array_key_exists; use function array_keys; use function array_map; -use function array_merge; use function array_reduce; +use function array_replace; use function array_values; use function collect; use function count; @@ -31,25 +32,25 @@ */ class MorphToMany extends EloquentMorphToMany { - /** @inheritdoc */ + #[Override] public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { return $query; } - /** @inheritdoc */ + #[Override] protected function hydratePivotRelation(array $models) { // Do nothing. } - /** @inheritdoc */ + #[Override] protected function shouldSelect(array $columns = ['*']) { return $columns; } - /** @inheritdoc */ + #[Override] public function addConstraints() { if (static::$constraints) { @@ -57,7 +58,7 @@ public function addConstraints() } } - /** @inheritdoc */ + #[Override] public function addEagerConstraints(array $models) { // To load relation's data, we act normally on MorphToMany relation, @@ -102,6 +103,7 @@ protected function setWhere() } /** @inheritdoc */ + #[Override] public function save(Model $model, array $pivotAttributes = [], $touch = true) { $model->save(['touch' => false]); @@ -112,6 +114,7 @@ public function save(Model $model, array $pivotAttributes = [], $touch = true) } /** @inheritdoc */ + #[Override] public function create(array $attributes = [], array $joining = [], $touch = true) { $instance = $this->related->newInstance($attributes); @@ -127,6 +130,7 @@ public function create(array $attributes = [], array $joining = [], $touch = tru } /** @inheritdoc */ + #[Override] public function sync($ids, $detaching = true) { $changes = [ @@ -190,7 +194,7 @@ public function sync($ids, $detaching = true) // Now we are finally ready to attach the new records. Note that we'll disable // touching until after the entire operation is complete so we don't fire a // ton of touch operations until we are totally done syncing the records. - $changes = array_merge( + $changes = array_replace( $changes, $this->attachNew($records, $current, false), ); @@ -203,12 +207,14 @@ public function sync($ids, $detaching = true) } /** @inheritdoc */ + #[Override] public function updateExistingPivot($id, array $attributes, $touch = true): void { // Do nothing, we have no pivot table. } /** @inheritdoc */ + #[Override] public function attach($id, array $attributes = [], $touch = true) { if ($id instanceof Model) { @@ -302,6 +308,7 @@ public function attach($id, array $attributes = [], $touch = true) } /** @inheritdoc */ + #[Override] public function detach($ids = [], $touch = true) { if ($ids instanceof Model) { @@ -376,6 +383,7 @@ public function detach($ids = [], $touch = true) } /** @inheritdoc */ + #[Override] protected function buildDictionary(Collection $results) { $foreign = $this->foreignPivotKey; @@ -403,6 +411,7 @@ protected function buildDictionary(Collection $results) } /** @inheritdoc */ + #[Override] public function newPivotQuery() { return $this->newRelatedQuery(); @@ -418,19 +427,13 @@ public function newRelatedQuery() return $this->related->newQuery(); } - /** @inheritdoc */ + #[Override] public function getQualifiedRelatedPivotKeyName() { return $this->relatedPivotKey; } - /** - * Get the name of the "where in" method for eager loading. - * - * @param string $key - * - * @return string - */ + #[Override] protected function whereInMethod(Model $model, $key) { return 'whereIn'; diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index a525a9cee..24e23d50e 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -4,11 +4,13 @@ namespace MongoDB\Laravel\Schema; -use Illuminate\Database\Connection; -use Illuminate\Database\Schema\Blueprint as SchemaBlueprint; +use Illuminate\Database\Schema\Blueprint as BaseBlueprint; use MongoDB\Collection; +use MongoDB\Laravel\Connection; +use Override; use function array_flip; +use function array_merge; use function implode; use function in_array; use function is_array; @@ -16,17 +18,14 @@ use function is_string; use function key; -class Blueprint extends SchemaBlueprint +/** @property Connection $connection */ +class Blueprint extends BaseBlueprint { - /** - * The MongoConnection object for this blueprint. - * - * @var Connection - */ - protected $connection; + // Import $connection property and constructor for Laravel 12 compatibility + use BlueprintLaravelCompatibility; /** - * The Collection object for this blueprint. + * The MongoDB collection object for this blueprint. * * @var Collection */ @@ -39,19 +38,8 @@ class Blueprint extends SchemaBlueprint */ protected $columns = []; - /** - * Create a new schema blueprint. - */ - public function __construct(Connection $connection, string $collection) - { - parent::__construct($collection); - - $this->connection = $connection; - - $this->collection = $this->connection->getCollection($collection); - } - /** @inheritdoc */ + #[Override] public function index($columns = null, $name = null, $algorithm = null, $options = []) { $columns = $this->fluent($columns); @@ -78,12 +66,14 @@ public function index($columns = null, $name = null, $algorithm = null, $options } /** @inheritdoc */ + #[Override] public function primary($columns = null, $name = null, $algorithm = null, $options = []) { return $this->unique($columns, $name, $algorithm, $options); } /** @inheritdoc */ + #[Override] public function dropIndex($index = null) { $index = $this->transformColumns($index); @@ -132,6 +122,24 @@ public function hasIndex($indexOrColumns = null) return false; } + public function jsonSchema( + array $schema = [], + ?string $validationLevel = null, + ?string $validationAction = null, + ): void { + $options = array_merge( + [ + 'validator' => [ + '$jsonSchema' => $schema, + ], + ], + $validationLevel ? ['validationLevel' => $validationLevel] : [], + $validationAction ? ['validationAction' => $validationAction] : [], + ); + + $this->connection->getDatabase()->modifyCollection($this->collection->getCollectionName(), $options); + } + /** * @param string|array $indexOrColumns * @@ -166,6 +174,7 @@ protected function transformColumns($indexOrColumns) } /** @inheritdoc */ + #[Override] public function unique($columns = null, $name = null, $algorithm = null, $options = []) { $columns = $this->fluent($columns); @@ -247,6 +256,7 @@ public function expire($columns, $seconds) * * @return void */ + #[Override] public function create($options = []) { $collection = $this->collection->getCollectionName(); @@ -258,6 +268,7 @@ public function create($options = []) } /** @inheritdoc */ + #[Override] public function drop() { $this->collection->drop(); @@ -266,6 +277,7 @@ public function drop() } /** @inheritdoc */ + #[Override] public function renameColumn($from, $to) { $this->collection->updateMany([$from => ['$exists' => true]], ['$rename' => [$from => $to]]); @@ -274,6 +286,7 @@ public function renameColumn($from, $to) } /** @inheritdoc */ + #[Override] public function addColumn($type, $name, array $parameters = []) { $this->fluent($name); diff --git a/src/Schema/BlueprintLaravelCompatibility.php b/src/Schema/BlueprintLaravelCompatibility.php new file mode 100644 index 000000000..bf288eae8 --- /dev/null +++ b/src/Schema/BlueprintLaravelCompatibility.php @@ -0,0 +1,50 @@ +connection = $connection; + $this->collection = $connection->getCollection($collection); + } + } +} else { + /** @internal For compatibility with Laravel 12+ */ + trait BlueprintLaravelCompatibility + { + public function __construct(Connection $connection, string $collection, ?Closure $callback = null) + { + parent::__construct($connection, $collection, $callback); + + $this->collection = $connection->getCollection($collection); + } + } +} diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 4af15f1f9..207f4f1b3 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -7,27 +7,40 @@ use Closure; use MongoDB\Collection; use MongoDB\Driver\Exception\ServerException; +use MongoDB\Laravel\Connection; use MongoDB\Model\CollectionInfo; use MongoDB\Model\IndexInfo; +use Override; use function array_column; use function array_fill_keys; use function array_filter; +use function array_key_exists; use function array_keys; use function array_map; use function array_merge; +use function array_values; use function assert; use function count; use function current; +use function explode; use function implode; use function in_array; +use function is_array; +use function is_bool; +use function is_string; use function iterator_to_array; use function sort; use function sprintf; +use function str_contains; use function str_ends_with; use function substr; +use function trigger_error; use function usort; +use const E_USER_DEPRECATED; + +/** @property Connection $connection */ class Builder extends \Illuminate\Database\Schema\Builder { /** @@ -42,7 +55,7 @@ public function hasColumn($table, $column): bool } /** - * Check if columns exists in the collection schema. + * Check if columns exist in the collection schema. * * @param string $table * @param string[] $columns @@ -86,12 +99,14 @@ public function hasCollection($name) } /** @inheritdoc */ + #[Override] public function hasTable($table) { return $this->hasCollection($table); } /** @inheritdoc */ + #[Override] public function table($table, Closure $callback) { $blueprint = $this->createBlueprint($table); @@ -102,6 +117,7 @@ public function table($table, Closure $callback) } /** @inheritdoc */ + #[Override] public function create($table, ?Closure $callback = null, array $options = []) { $blueprint = $this->createBlueprint($table); @@ -114,6 +130,7 @@ public function create($table, ?Closure $callback = null, array $options = []) } /** @inheritdoc */ + #[Override] public function dropIfExists($table) { if ($this->hasCollection($table)) { @@ -122,6 +139,7 @@ public function dropIfExists($table) } /** @inheritdoc */ + #[Override] public function drop($table) { $blueprint = $this->createBlueprint($table); @@ -129,54 +147,82 @@ public function drop($table) $blueprint->drop(); } - /** @inheritdoc */ + /** + * @inheritdoc + * + * Drops the entire database instead of deleting each collection individually. + * + * In MongoDB, dropping the whole database is much faster than dropping collections + * one by one. The database will be automatically recreated when a new connection + * writes to it. + */ + #[Override] public function dropAllTables() { - foreach ($this->getAllCollections() as $collection) { - $this->drop($collection); - } + $this->connection->getDatabase()->drop(); } - public function getTables() + /** + * @param string|null $schema Database name + * + * @inheritdoc + */ + #[Override] + public function getTables($schema = null) { - $db = $this->connection->getDatabase(); - $collections = []; + return $this->getCollectionRows('collection', $schema); + } - foreach ($db->listCollectionNames() as $collectionName) { - $stats = $db->selectCollection($collectionName)->aggregate([ - ['$collStats' => ['storageStats' => ['scale' => 1]]], - ['$project' => ['storageStats.totalSize' => 1]], - ])->toArray(); + /** + * @param string|null $schema Database name + * + * @inheritdoc + */ + #[Override] + public function getViews($schema = null) + { + return $this->getCollectionRows('view', $schema); + } - $collections[] = [ - 'name' => $collectionName, - 'schema' => null, - 'size' => $stats[0]?->storageStats?->totalSize ?? null, - 'comment' => null, - 'collation' => null, - 'engine' => null, - ]; - } + /** + * @param string|null $schema + * @param bool $schemaQualified If a schema is provided, prefix the collection names with the schema name + * + * @return array + */ + #[Override] + public function getTableListing($schema = null, $schemaQualified = false) + { + $collections = []; - usort($collections, function ($a, $b) { - return $a['name'] <=> $b['name']; - }); + if ($schema === null || is_string($schema)) { + $collections[$schema ?? 0] = iterator_to_array($this->connection->getDatabase($schema)->listCollectionNames()); + } elseif (is_array($schema)) { + foreach ($schema as $db) { + $collections[$db] = iterator_to_array($this->connection->getDatabase($db)->listCollectionNames()); + } + } - return $collections; - } + if ($schema && $schemaQualified) { + $collections = array_map(fn ($db, $collections) => array_map(static fn ($collection) => $db . '.' . $collection, $collections), array_keys($collections), $collections); + } - public function getTableListing() - { - $collections = iterator_to_array($this->connection->getDatabase()->listCollectionNames()); + $collections = array_merge(...array_values($collections)); sort($collections); return $collections; } + #[Override] public function getColumns($table) { - $stats = $this->connection->getDatabase()->selectCollection($table)->aggregate([ + $db = null; + if (str_contains($table, '.')) { + [$db, $table] = explode('.', $table, 2); + } + + $stats = $this->connection->getDatabase($db)->getCollection($table)->aggregate([ // Sample 1,000 documents to get a representative sample of the collection ['$sample' => ['size' => 1_000]], // Convert each document to an array of fields @@ -227,6 +273,7 @@ public function getColumns($table) return $columns; } + #[Override] public function getIndexes($table) { $collection = $this->connection->getDatabase()->selectCollection($table); @@ -281,12 +328,18 @@ public function getIndexes($table) return $indexList; } + #[Override] public function getForeignKeys($table) { return []; } - /** @inheritdoc */ + /** + * @return Blueprint + * + * @inheritdoc + */ + #[Override] protected function createBlueprint($table, ?Closure $callback = null) { return new Blueprint($this->connection, $table); @@ -311,12 +364,16 @@ public function getCollection($name) } /** - * Get all of the collections names for the database. + * Get all the collections names for the database. + * + * @deprecated * * @return array */ protected function getAllCollections() { + trigger_error(sprintf('Since mongodb/laravel-mongodb:5.4, Method "%s()" is deprecated without replacement.', __METHOD__), E_USER_DEPRECATED); + $collections = []; foreach ($this->connection->getDatabase()->listCollections() as $collection) { $collections[] = $collection->getName(); @@ -336,4 +393,68 @@ public static function isAtlasSearchNotSupportedException(ServerException $e): b 31082, // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. ], true); } + + /** @param string|null $schema Database name */ + private function getCollectionRows(string $collectionType, $schema = null) + { + $db = $this->connection->getDatabase($schema); + $collections = []; + + foreach ($db->listCollections() as $collectionInfo) { + $collectionName = $collectionInfo->getName(); + + if ($collectionInfo->getType() !== $collectionType) { + continue; + } + + $options = $collectionInfo->getOptions(); + $collation = $options['collation'] ?? []; + + // Aggregation is not supported on views + $stats = $collectionType !== 'view' ? $db->selectCollection($collectionName)->aggregate([ + ['$collStats' => ['storageStats' => ['scale' => 1]]], + ['$project' => ['storageStats.totalSize' => 1]], + ])->toArray() : null; + + $collections[] = [ + 'name' => $collectionName, + 'schema' => $db->getDatabaseName(), + 'schema_qualified_name' => $db->getDatabaseName() . '.' . $collectionName, + 'size' => $stats[0]?->storageStats?->totalSize ?? null, + 'comment' => null, + 'collation' => $this->collationToString($collation), + 'engine' => null, + ]; + } + + usort($collections, fn ($a, $b) => $a['name'] <=> $b['name']); + + return $collections; + } + + private function collationToString(array $collation): string + { + $map = [ + 'locale' => 'l', + 'strength' => 's', + 'caseLevel' => 'cl', + 'caseFirst' => 'cf', + 'numericOrdering' => 'no', + 'alternate' => 'a', + 'maxVariable' => 'mv', + 'normalization' => 'n', + 'backwards' => 'b', + ]; + + $parts = []; + foreach ($collation as $key => $value) { + if (array_key_exists($key, $map)) { + $shortKey = $map[$key]; + $shortValue = is_bool($value) ? ($value ? '1' : '0') : $value; + $parts[] = $shortKey . '=' . $shortValue; + } + } + + return implode(';', $parts); + } } diff --git a/src/Scout/ScoutEngine.php b/src/Scout/ScoutEngine.php index dc70a39e2..9455608bb 100644 --- a/src/Scout/ScoutEngine.php +++ b/src/Scout/ScoutEngine.php @@ -29,7 +29,7 @@ use function array_column; use function array_flip; use function array_map; -use function array_merge; +use function array_replace; use function assert; use function call_user_func; use function class_uses_recursive; @@ -117,7 +117,7 @@ public function update($models) unset($searchableData['_id']); - $searchableData = array_merge($searchableData, $model->scoutMetadata()); + $searchableData = array_replace($searchableData, $model->scoutMetadata()); /** Convert the __soft_deleted set by {@see Searchable::pushSoftDeleteMetadata()} * into a boolean for efficient storage and indexing. */ diff --git a/src/Session/MongoDbSessionHandler.php b/src/Session/MongoDbSessionHandler.php new file mode 100644 index 000000000..3677ea758 --- /dev/null +++ b/src/Session/MongoDbSessionHandler.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MongoDB\Laravel\Session; + +use Illuminate\Session\DatabaseSessionHandler; +use MongoDB\BSON\Binary; +use MongoDB\BSON\Document; +use MongoDB\BSON\UTCDateTime; +use MongoDB\Collection; +use Override; + +use function tap; +use function time; + +/** + * Session handler using the MongoDB driver extension. + */ +final class MongoDbSessionHandler extends DatabaseSessionHandler +{ + private Collection $collection; + + public function close(): bool + { + return true; + } + + #[Override] + public function gc($lifetime): int + { + $result = $this->getCollection()->deleteMany(['last_activity' => ['$lt' => $this->getUTCDateTime(-$lifetime)]]); + + return $result->getDeletedCount() ?? 0; + } + + #[Override] + public function destroy($sessionId): bool + { + $this->getCollection()->deleteOne(['_id' => (string) $sessionId]); + + return true; + } + + #[Override] + public function read($sessionId): string|false + { + $result = $this->getCollection()->findOne( + ['_id' => (string) $sessionId, 'expires_at' => ['$gte' => $this->getUTCDateTime()]], + [ + 'projection' => ['_id' => false, 'payload' => true], + 'typeMap' => ['root' => 'bson'], + ], + ); + + if ($result instanceof Document) { + return (string) $result->payload; + } + + return false; + } + + #[Override] + public function write($sessionId, $data): bool + { + $payload = $this->getDefaultPayload($data); + + $this->getCollection()->replaceOne( + ['_id' => (string) $sessionId], + $payload, + ['upsert' => true], + ); + + return true; + } + + /** Creates a TTL index that automatically deletes expired objects. */ + public function createTTLIndex(): void + { + $this->collection->createIndex( + // UTCDateTime field that holds the expiration date + ['expires_at' => 1], + // Delay to remove items after expiration + ['expireAfterSeconds' => 0], + ); + } + + #[Override] + protected function getDefaultPayload($data): array + { + $payload = [ + 'payload' => new Binary($data), + 'last_activity' => $this->getUTCDateTime(), + 'expires_at' => $this->getUTCDateTime($this->minutes * 60), + ]; + + if (! $this->container) { + return $payload; + } + + return tap($payload, function (&$payload) { + $this->addUserInformation($payload) + ->addRequestInformation($payload); + }); + } + + private function getCollection(): Collection + { + return $this->collection ??= $this->connection->getCollection($this->table); + } + + private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime + { + return new UTCDateTime((time() + $additionalSeconds) * 1000); + } +} diff --git a/src/Validation/DatabasePresenceVerifier.php b/src/Validation/DatabasePresenceVerifier.php index c5c378539..fdd783ab5 100644 --- a/src/Validation/DatabasePresenceVerifier.php +++ b/src/Validation/DatabasePresenceVerifier.php @@ -5,6 +5,7 @@ namespace MongoDB\Laravel\Validation; use MongoDB\BSON\Regex; +use Override; use function array_map; use function implode; @@ -12,17 +13,8 @@ class DatabasePresenceVerifier extends \Illuminate\Validation\DatabasePresenceVerifier { - /** - * Count the number of objects in a collection having the given value. - * - * @param string $collection - * @param string $column - * @param string $value - * @param int $excludeId - * @param string $idColumn - * - * @return int - */ + /** Count the number of objects in a collection having the given value. */ + #[Override] public function getCount($collection, $column, $value, $excludeId = null, $idColumn = null, array $extra = []) { $query = $this->table($collection)->where($column, new Regex('^' . preg_quote($value) . '$', '/i')); @@ -38,16 +30,8 @@ public function getCount($collection, $column, $value, $excludeId = null, $idCol return $query->count(); } - /** - * Count the number of objects in a collection with the given values. - * - * @param string $collection - * @param string $column - * @param array $values - * @param array $extra - * - * @return int - */ + /** Count the number of objects in a collection with the given values. */ + #[Override] public function getMultiCount($collection, $column, array $values, array $extra = []) { // Nothing can match an empty array. Return early to avoid matching an empty string. diff --git a/src/Validation/ValidationServiceProvider.php b/src/Validation/ValidationServiceProvider.php index 1095e93a3..6f7ebd980 100644 --- a/src/Validation/ValidationServiceProvider.php +++ b/src/Validation/ValidationServiceProvider.php @@ -5,9 +5,11 @@ namespace MongoDB\Laravel\Validation; use Illuminate\Validation\ValidationServiceProvider as BaseProvider; +use Override; class ValidationServiceProvider extends BaseProvider { + #[Override] protected function registerPresenceVerifier() { $this->app->singleton('validation.presence', function ($app) { diff --git a/tests/Casts/EncryptionTest.php b/tests/Casts/EncryptionTest.php index 0c40254f1..acb7520cc 100644 --- a/tests/Casts/EncryptionTest.php +++ b/tests/Casts/EncryptionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Casts; +namespace MongoDB\Laravel\Tests\Casts; use Illuminate\Database\Eloquent\Casts\Json; use Illuminate\Encryption\Encrypter; diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index ba5e09804..de77da7f7 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -63,7 +63,7 @@ public function testDisconnectAndCreateNewConnection() public function testDb() { $connection = DB::connection('mongodb'); - $this->assertInstanceOf(Database::class, $connection->getMongoDB()); + $this->assertInstanceOf(Database::class, $connection->getDatabase()); $this->assertInstanceOf(Client::class, $connection->getClient()); } @@ -190,6 +190,12 @@ public static function dataConnectionConfig(): Generator 'expectedDatabaseName' => 'tests', 'config' => ['dsn' => 'mongodb://some-host:12345/tests'], ]; + + yield 'Database is extracted from DSN with CA path in options' => [ + 'expectedUri' => 'mongodb://some-host:12345/tests?tls=true&tlsCAFile=/path/to/ca.pem&retryWrites=false', + 'expectedDatabaseName' => 'tests', + 'config' => ['dsn' => 'mongodb://some-host:12345/tests?tls=true&tlsCAFile=/path/to/ca.pem&retryWrites=false'], + ]; } #[DataProvider('dataConnectionConfig')] @@ -199,7 +205,7 @@ public function testConnectionConfig(string $expectedUri, string $expectedDataba $client = $connection->getClient(); $this->assertSame($expectedUri, (string) $client); - $this->assertSame($expectedDatabaseName, $connection->getMongoDB()->getDatabaseName()); + $this->assertSame($expectedDatabaseName, $connection->getDatabase()->getDatabaseName()); $this->assertSame('foo', $connection->getCollection('foo')->getCollectionName()); $this->assertSame('foo', $connection->table('foo')->raw()->getCollectionName()); } @@ -289,6 +295,8 @@ public function testQueryLog() DB::table('items')->get(); $this->assertCount(1, $logs = DB::getQueryLog()); $this->assertJsonStringEqualsJsonString('{"find":"items","filter":{}}', $logs[0]['query']); + $this->assertLessThan(10, $logs[0]['time'], 'Query time is in milliseconds'); + $this->assertGreaterThan(0.01, $logs[0]['time'], 'Query time is in milliseconds'); DB::table('items')->insert(['id' => $id = new ObjectId(), 'name' => 'test']); $this->assertCount(2, $logs = DB::getQueryLog()); diff --git a/tests/DateTimeImmutableTest.php b/tests/DateTimeImmutableTest.php new file mode 100644 index 000000000..7fd6fa2b1 --- /dev/null +++ b/tests/DateTimeImmutableTest.php @@ -0,0 +1,42 @@ + 'John', + 'anniversary' => new CarbonImmutable('2020-01-01 00:00:00'), + ]); + + $anniversary = Anniversary::sole(); + assert($anniversary instanceof Anniversary); + self::assertInstanceOf(CarbonImmutable::class, $anniversary->anniversary); + } +} diff --git a/tests/FilesystemsTest.php b/tests/FilesystemsTest.php index 3b9fa8e5f..7b8141905 100644 --- a/tests/FilesystemsTest.php +++ b/tests/FilesystemsTest.php @@ -56,7 +56,7 @@ public static function provideValidOptions(): Generator 'driver' => 'gridfs', 'bucket' => static fn (Application $app) => $app['db'] ->connection('mongodb') - ->getMongoDB() + ->getDatabase() ->selectGridFSBucket(), ], ]; @@ -68,7 +68,7 @@ public function testValidOptions(array $options) // Service used by "bucket-service" $this->app->singleton('bucket', static fn (Application $app) => $app['db'] ->connection('mongodb') - ->getMongoDB() + ->getDatabase() ->selectGridFSBucket()); $this->app['config']->set('filesystems.disks.' . $this->dataName(), $options); @@ -145,6 +145,6 @@ public function testPrefix() private function getBucket(): Bucket { - return DB::connection('mongodb')->getMongoDB()->selectGridFSBucket(); + return DB::connection('mongodb')->getDatabase()->selectGridFSBucket(); } } diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 08423007c..71fb0830b 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -78,7 +78,7 @@ public function testSqlRelations() $this->assertEquals('John Doe', $role->sqlUser->name); // MongoDB User - $user = new User(); + $user = new User(); $user->name = 'John Doe'; $user->save(); @@ -105,7 +105,7 @@ public function testSqlRelations() public function testHybridWhereHas() { - $user = new SqlUser(); + $user = new SqlUser(); $otherUser = new SqlUser(); $this->assertInstanceOf(SqlUser::class, $user); $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection()); @@ -114,11 +114,11 @@ public function testHybridWhereHas() // SQL User $user->name = 'John Doe'; - $user->id = 2; + $user->id = 2; $user->save(); // Other user $otherUser->name = 'Other User'; - $otherUser->id = 3; + $otherUser->id = 3; $otherUser->save(); // Make sure they are created $this->assertIsInt($user->id); @@ -159,7 +159,7 @@ public function testHybridWhereHas() public function testHybridWith() { - $user = new SqlUser(); + $user = new SqlUser(); $otherUser = new SqlUser(); $this->assertInstanceOf(SqlUser::class, $user); $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection()); @@ -168,11 +168,11 @@ public function testHybridWith() // SQL User $user->name = 'John Doe'; - $user->id = 2; + $user->id = 2; $user->save(); // Other user $otherUser->name = 'Other User'; - $otherUser->id = 3; + $otherUser->id = 3; $otherUser->save(); // Make sure they are created $this->assertIsInt($user->id); @@ -268,6 +268,23 @@ public function testHybridBelongsToMany() $this->assertEquals(1, $check->skills->count()); } + public function testQueryingHybridBelongsToManyRelationFails() + { + $user = new SqlUser(); + $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection()); + + // Create Mysql Users + $user->fill(['name' => 'John Doe'])->save(); + $skill = Skill::query()->create(['name' => 'MongoDB']); + $user->skills()->save($skill); + + $this->expectExceptionMessage('BelongsToMany is not supported for hybrid query constraints.'); + + SqlUser::whereHas('skills', function ($query) { + return $query->where('name', 'LIKE', 'MongoDB'); + }); + } + public function testHybridMorphToManySqlModelToMongoModel() { // SqlModel -> MorphToMany -> MongoModel diff --git a/tests/ModelTest.php b/tests/ModelTest.php index ecfcb2b6a..8fcd0b272 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -25,6 +25,7 @@ use MongoDB\Laravel\Tests\Models\IdIsString; use MongoDB\Laravel\Tests\Models\Item; use MongoDB\Laravel\Tests\Models\MemberStatus; +use MongoDB\Laravel\Tests\Models\NonIncrementing; use MongoDB\Laravel\Tests\Models\Soft; use MongoDB\Laravel\Tests\Models\SqlUser; use MongoDB\Laravel\Tests\Models\User; @@ -56,6 +57,7 @@ public function tearDown(): void Book::truncate(); Item::truncate(); Guarded::truncate(); + NonIncrementing::truncate(); parent::tearDown(); } @@ -106,6 +108,26 @@ public function testInsert(): void $this->assertEquals(35, $user->age); } + public function testInsertNonIncrementable(): void + { + $connection = DB::connection('mongodb'); + $connection->setRenameEmbeddedIdField(false); + + $nonIncrementing = new NonIncrementing(); + $nonIncrementing->id = '12345'; + $nonIncrementing->name = 'John Doe'; + + $nonIncrementing->save(); + + $this->assertTrue($nonIncrementing->exists); + $this->assertEquals(1, NonIncrementing::count()); + + $check = NonIncrementing::find($nonIncrementing->id); + $this->assertInstanceOf(NonIncrementing::class, $check); + $this->assertSame('12345', $check->id); + $this->assertEquals('John Doe', $check->name); + } + public function testUpdate(): void { $user = new User(); @@ -147,6 +169,21 @@ public function testUpdate(): void $this->assertEquals('Hans Thomas', $check->fullname); } + public function testUpdateTroughSetUpdatedAt(): void + { + $user = new User(); + $user->name = 'John Doe'; + $user->title = 'admin'; + $user->age = 35; + $user->save(); + + $updatedAt = Carbon::yesterday(); + User::query()->update(['$set' => ['updated_at' => new UTCDateTime($updatedAt)]]); + + $user->refresh(); + $this->assertEquals($updatedAt, $user->updated_at); + } + public function testUpsert() { $result = User::upsert([ diff --git a/tests/Models/Anniversary.php b/tests/Models/Anniversary.php new file mode 100644 index 000000000..c37196c16 --- /dev/null +++ b/tests/Models/Anniversary.php @@ -0,0 +1,24 @@ + 'immutable_datetime']; +} diff --git a/tests/Models/HiddenAnimal.php b/tests/Models/HiddenAnimal.php index 240238da0..f6217177c 100644 --- a/tests/Models/HiddenAnimal.php +++ b/tests/Models/HiddenAnimal.php @@ -6,17 +6,11 @@ use Illuminate\Database\Eloquent\Model; use MongoDB\Laravel\Eloquent\DocumentModel; -use MongoDB\Laravel\Eloquent\Model as Eloquent; -use MongoDB\Laravel\Query\Builder; /** * @property string $name * @property string $country * @property bool $can_be_eaten - * @mixin Eloquent - * @method static Builder create(...$values) - * @method static Builder truncate() - * @method static Eloquent sole(...$parameters) */ final class HiddenAnimal extends Model { diff --git a/tests/Models/NonIncrementing.php b/tests/Models/NonIncrementing.php new file mode 100644 index 000000000..8a9ab31e2 --- /dev/null +++ b/tests/Models/NonIncrementing.php @@ -0,0 +1,26 @@ +markTestSkipped(sprintf('Method "%s::%s()" does not exist.', Builder::class, $requiredMethod)); } - $builder = $build(self::getBuilder()); + $builder = $build($this->getBuilder()); $this->assertInstanceOf(Builder::class, $builder); $mql = $builder->toMql(); @@ -869,7 +868,7 @@ function (Builder $builder) { [], ], ], - fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +02:00')), + fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +00:00')), ]; yield 'where date !=' => [ @@ -1447,7 +1446,7 @@ function (Builder $elemMatchQuery): void { #[DataProvider('provideExceptions')] public function testException($class, $message, Closure $build): void { - $builder = self::getBuilder(); + $builder = $this->getBuilder(); $this->expectException($class); $this->expectExceptionMessage($message); @@ -1545,7 +1544,7 @@ public static function provideExceptions(): iterable #[DataProvider('getEloquentMethodsNotSupported')] public function testEloquentMethodsNotSupported(Closure $callback) { - $builder = self::getBuilder(); + $builder = $this->getBuilder(); $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage('This method is not supported by MongoDB'); @@ -1600,12 +1599,95 @@ public static function getEloquentMethodsNotSupported() yield 'orWhereIntegerNotInRaw' => [fn (Builder $builder) => $builder->orWhereIntegerNotInRaw('id', ['1a', 2])]; } - private static function getBuilder(): Builder + #[DataProvider('provideDisableRenameEmbeddedIdField')] + public function testDisableRenameEmbeddedIdField(array $expected, Closure $build) { - $connection = m::mock(Connection::class); - $processor = m::mock(Processor::class); - $connection->shouldReceive('getSession')->andReturn(null); - $connection->shouldReceive('getQueryGrammar')->andReturn(new Grammar()); + $builder = $this->getBuilder(false); + $this->assertFalse($builder->getConnection()->getRenameEmbeddedIdField()); + + $mql = $build($builder)->toMql(); + + $this->assertEquals($expected, $mql); + } + + public static function provideDisableRenameEmbeddedIdField() + { + yield 'rename embedded id field' => [ + [ + 'find' => [ + [ + '$and' => [ + ['_id' => 10], + ['nested.id' => 20], + ['embed' => ['id' => 30]], + ], + ], + ['typeMap' => ['root' => 'object', 'document' => 'array']], + ], + ], + fn (Builder $builder) => $builder->where('id', '=', 10) + ->where('nested.id', '=', 20) + ->where('embed', '=', ['id' => 30]), + ]; + + yield 'rename root id' => [ + ['find' => [['_id' => 10], ['typeMap' => ['root' => 'object', 'document' => 'array']]]], + fn (Builder $builder) => $builder->where('id', '=', 10), + ]; + + yield 'nested id not renamed' => [ + ['find' => [['nested.id' => 20], ['typeMap' => ['root' => 'object', 'document' => 'array']]]], + fn (Builder $builder) => $builder->where('nested.id', '=', 20), + ]; + + yield 'embed id not renamed' => [ + ['find' => [['embed' => ['id' => 30]], ['typeMap' => ['root' => 'object', 'document' => 'array']]]], + fn (Builder $builder) => $builder->where('embed', '=', ['id' => 30]), + ]; + + yield 'nested $and in $or' => [ + [ + 'find' => [ + [ + '$or' => [ + [ + '$and' => [ + ['_id' => 10], + ['nested.id' => 20], + ['embed' => ['id' => 30]], + ], + ], + [ + '$and' => [ + ['_id' => 40], + ['nested.id' => 50], + ['embed' => ['id' => 60]], + ], + ], + ], + ], + ['typeMap' => ['root' => 'object', 'document' => 'array']], + ], + ], + fn (Builder $builder) => $builder->orWhere(function (Builder $builder) { + return $builder->where('id', '=', 10) + ->where('nested.id', '=', 20) + ->where('embed', '=', ['id' => 30]); + })->orWhere(function (Builder $builder) { + return $builder->where('id', '=', 40) + ->where('nested.id', '=', 50) + ->where('embed', '=', ['id' => 60]); + }), + ]; + } + + private function getBuilder(bool $renameEmbeddedIdField = true): Builder + { + $connection = $this->createStub(Connection::class); + $connection->method('getRenameEmbeddedIdField')->willReturn($renameEmbeddedIdField); + $processor = $this->createStub(Processor::class); + $connection->method('getSession')->willReturn(null); + $connection->method('getQueryGrammar')->willReturn(new Grammar($connection)); return new Builder($connection, null, $processor); } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 9592bbe7c..158fb5084 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -20,10 +20,12 @@ use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; use MongoDB\Driver\Cursor; +use MongoDB\Driver\CursorInterface; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; use MongoDB\Driver\Monitoring\CommandSucceededEvent; +use MongoDB\Laravel\Connection; use MongoDB\Laravel\Query\Builder; use MongoDB\Laravel\Tests\Models\Item; use MongoDB\Laravel\Tests\Models\User; @@ -124,6 +126,22 @@ public function testInsert() $this->assertIsArray($user->tags); } + #[TestWith([true])] + #[TestWith([false])] + public function testInsertWithCustomId(bool $renameEmbeddedIdField) + { + $connection = DB::connection('mongodb'); + $connection->setRenameEmbeddedIdField($renameEmbeddedIdField); + + $data = ['id' => 'abcdef', 'name' => 'John Doe']; + + DB::table('users')->insert($data); + + $user = User::find('abcdef'); + $this->assertInstanceOf(User::class, $user); + $this->assertSame('abcdef', $user->id); + } + public function testInsertGetId() { $id = DB::table('users')->insertGetId(['name' => 'John Doe']); @@ -161,7 +179,7 @@ public function testFindWithTimeout() $id = DB::table('users')->insertGetId(['name' => 'John Doe']); $subscriber = new class implements CommandSubscriber { - public function commandStarted(CommandStartedEvent $event) + public function commandStarted(CommandStartedEvent $event): void { if ($event->getCommandName() !== 'find') { return; @@ -171,11 +189,11 @@ public function commandStarted(CommandStartedEvent $event) Assert::assertSame(1000, $event->getCommand()->maxTimeMS); } - public function commandFailed(CommandFailedEvent $event) + public function commandFailed(CommandFailedEvent $event): void { } - public function commandSucceeded(CommandSucceededEvent $event) + public function commandSucceeded(CommandSucceededEvent $event): void { } }; @@ -336,6 +354,93 @@ public function testRaw() $this->assertEquals('Jane Doe', $results[0]->name); } + public function testRawResultRenameId() + { + $connection = DB::connection('mongodb'); + self::assertInstanceOf(Connection::class, $connection); + + $date = Carbon::createFromDate(1986, 12, 31)->setTime(12, 0, 0); + User::insert([ + ['id' => 1, 'name' => 'Jane Doe', 'address' => ['id' => 11, 'city' => 'Ghent'], 'birthday' => $date], + ['id' => 2, 'name' => 'John Doe', 'address' => ['id' => 12, 'city' => 'Brussels'], 'birthday' => $date], + ]); + + // Using raw database query, result is not altered + $results = $connection->table('users')->raw(fn (Collection $collection) => $collection->find([])); + self::assertInstanceOf(CursorInterface::class, $results); + $results = $results->toArray(); + self::assertCount(2, $results); + + self::assertObjectHasProperty('_id', $results[0]); + self::assertObjectNotHasProperty('id', $results[0]); + self::assertSame(1, $results[0]->_id); + + self::assertObjectHasProperty('_id', $results[0]->address); + self::assertObjectNotHasProperty('id', $results[0]->address); + self::assertSame(11, $results[0]->address->_id); + + self::assertInstanceOf(UTCDateTime::class, $results[0]->birthday); + + // Using Eloquent query, result is transformed + self::assertTrue($connection->getRenameEmbeddedIdField()); + $results = User::raw(fn (Collection $collection) => $collection->find([])); + self::assertInstanceOf(LaravelCollection::class, $results); + self::assertCount(2, $results); + + $attributes = $results->first()->getAttributes(); + self::assertArrayHasKey('id', $attributes); + self::assertArrayNotHasKey('_id', $attributes); + self::assertSame(1, $attributes['id']); + + self::assertArrayHasKey('id', $attributes['address']); + self::assertArrayNotHasKey('_id', $attributes['address']); + self::assertSame(11, $attributes['address']['id']); + + self::assertEquals($date, $attributes['birthday']); + + // Single result + $result = User::raw(fn (Collection $collection) => $collection->findOne([], ['typeMap' => ['root' => 'object', 'document' => 'array']])); + self::assertInstanceOf(User::class, $result); + + $attributes = $result->getAttributes(); + self::assertArrayHasKey('id', $attributes); + self::assertArrayNotHasKey('_id', $attributes); + self::assertSame(1, $attributes['id']); + + self::assertArrayHasKey('id', $attributes['address']); + self::assertArrayNotHasKey('_id', $attributes['address']); + self::assertSame(11, $attributes['address']['id']); + + // Change the renameEmbeddedIdField option + $connection->setRenameEmbeddedIdField(false); + + $results = User::raw(fn (Collection $collection) => $collection->find([])); + self::assertInstanceOf(LaravelCollection::class, $results); + self::assertCount(2, $results); + + $attributes = $results->first()->getAttributes(); + self::assertArrayHasKey('id', $attributes); + self::assertArrayNotHasKey('_id', $attributes); + self::assertSame(1, $attributes['id']); + + self::assertArrayHasKey('_id', $attributes['address']); + self::assertArrayNotHasKey('id', $attributes['address']); + self::assertSame(11, $attributes['address']['_id']); + + // Single result + $result = User::raw(fn (Collection $collection) => $collection->findOne([])); + self::assertInstanceOf(User::class, $result); + + $attributes = $result->getAttributes(); + self::assertArrayHasKey('id', $attributes); + self::assertArrayNotHasKey('_id', $attributes); + self::assertSame(1, $attributes['id']); + + self::assertArrayHasKey('_id', $attributes['address']); + self::assertArrayNotHasKey('id', $attributes['address']); + self::assertSame(11, $attributes['address']['_id']); + } + public function testPush() { $id = DB::table('users')->insertGetId([ @@ -963,6 +1068,55 @@ public function testIncrement() $this->assertEquals(1, $user->age); } + public function testMultiplyAndDivide() + { + DB::table('users')->insert([ + ['name' => 'John Doe', 'salary' => 88000, 'note' => 'senior'], + ['name' => 'Jane Doe', 'salary' => 64000, 'note' => 'junior'], + ['name' => 'Robert Roe', 'salary' => null], + ['name' => 'Mark Moe'], + ]); + + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(88000, $user->salary); + + DB::table('users')->where('name', 'John Doe')->multiply('salary', 1); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(88000, $user->salary); + + DB::table('users')->where('name', 'John Doe')->divide('salary', 1); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(88000, $user->salary); + + DB::table('users')->where('name', 'John Doe')->multiply('salary', 2); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(176000, $user->salary); + + DB::table('users')->where('name', 'John Doe')->divide('salary', 2); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(88000, $user->salary); + + DB::table('users')->where('name', 'Jane Doe')->multiply('salary', 10, ['note' => 'senior']); + $user = DB::table('users')->where('name', 'Jane Doe')->first(); + $this->assertEquals(640000, $user->salary); + $this->assertEquals('senior', $user->note); + + DB::table('users')->where('name', 'John Doe')->divide('salary', 2, ['note' => 'junior']); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(44000, $user->salary); + $this->assertEquals('junior', $user->note); + + DB::table('users')->multiply('salary', 1); + $user = DB::table('users')->where('name', 'John Doe')->first(); + $this->assertEquals(44000, $user->salary); + $user = DB::table('users')->where('name', 'Jane Doe')->first(); + $this->assertEquals(640000, $user->salary); + $user = DB::table('users')->where('name', 'Robert Roe')->first(); + $this->assertNull($user->salary); + $user = DB::table('users')->where('name', 'Mark Moe')->first(); + $this->assertFalse(isset($user->salary)); + } + public function testProjections() { DB::table('items')->insert([ diff --git a/tests/QueueTest.php b/tests/QueueTest.php index efc8f07ff..4de63391d 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -15,7 +15,7 @@ use MongoDB\Laravel\Queue\MongoQueue; use function app; -use function json_encode; +use function json_decode; class QueueTest extends TestCase { @@ -42,17 +42,16 @@ public function testQueueJobLifeCycle(): void $job = Queue::pop('test'); $this->assertInstanceOf(MongoJob::class, $job); $this->assertEquals(1, $job->isReserved()); - $this->assertEquals(json_encode([ - 'uuid' => $uuid, - 'displayName' => 'test', - 'job' => 'test', - 'maxTries' => null, - 'maxExceptions' => null, - 'failOnTimeout' => false, - 'backoff' => null, - 'timeout' => null, - 'data' => ['action' => 'QueueJobLifeCycle'], - ]), $job->getRawBody()); + $payload = json_decode($job->getRawBody(), true); + $this->assertEquals($uuid, $payload['uuid']); + $this->assertEquals('test', $payload['displayName']); + $this->assertEquals('test', $payload['job']); + $this->assertNull($payload['maxTries']); + $this->assertNull($payload['maxExceptions']); + $this->assertFalse($payload['failOnTimeout']); + $this->assertNull($payload['backoff']); + $this->assertNull($payload['timeout']); + $this->assertEquals(['action' => 'QueueJobLifeCycle'], $payload['data']); // Remove reserved job $job->delete(); diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index a55c8c0e0..643e00e6a 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -35,6 +35,7 @@ public function tearDown(): void Photo::truncate(); Label::truncate(); Skill::truncate(); + Soft::truncate(); parent::tearDown(); } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index e2f4f7b7e..9726eb705 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -20,216 +20,255 @@ class SchemaTest extends TestCase { + private const COLL_1 = 'new_collection'; + private const COLL_2 = 'new_collection_two'; + private const COLL_WITH_COLLATION = 'collection_with_collation'; + public function tearDown(): void { - $database = $this->getConnection('mongodb')->getMongoDB(); + $database = $this->getConnection('mongodb')->getDatabase(); assert($database instanceof Database); - $database->dropCollection('newcollection'); - $database->dropCollection('newcollection_two'); + $database->dropCollection(self::COLL_1); + $database->dropCollection(self::COLL_2); + $database->dropCollection(self::COLL_WITH_COLLATION); + $database->dropCollection('test_view'); parent::tearDown(); } public function testCreate(): void { - Schema::create('newcollection'); - $this->assertTrue(Schema::hasCollection('newcollection')); - $this->assertTrue(Schema::hasTable('newcollection')); + Schema::create(self::COLL_1); + $this->assertTrue(Schema::hasCollection(self::COLL_1)); + $this->assertTrue(Schema::hasTable(self::COLL_1)); } public function testCreateWithCallback(): void { - Schema::create('newcollection', static function ($collection) { + Schema::create(self::COLL_1, static function ($collection) { self::assertInstanceOf(Blueprint::class, $collection); }); - $this->assertTrue(Schema::hasCollection('newcollection')); + $this->assertTrue(Schema::hasCollection(self::COLL_1)); } public function testCreateWithOptions(): void { - Schema::create('newcollection_two', null, ['capped' => true, 'size' => 1024]); - $this->assertTrue(Schema::hasCollection('newcollection_two')); - $this->assertTrue(Schema::hasTable('newcollection_two')); + Schema::create(self::COLL_2, null, ['capped' => true, 'size' => 1024]); + $this->assertTrue(Schema::hasCollection(self::COLL_2)); + $this->assertTrue(Schema::hasTable(self::COLL_2)); - $collection = Schema::getCollection('newcollection_two'); + $collection = Schema::getCollection(self::COLL_2); $this->assertTrue($collection['options']['capped']); $this->assertEquals(1024, $collection['options']['size']); } + public function testCreateWithSchemaValidator(): void + { + $schema = [ + 'bsonType' => 'object', + 'required' => [ 'username' ], + 'properties' => [ + 'username' => [ + 'bsonType' => 'string', + 'description' => 'must be a string and is required', + ], + ], + ]; + + Schema::create(self::COLL_2, function (Blueprint $collection) use ($schema) { + $collection->string('username'); + $collection->jsonSchema(schema: $schema, validationAction: 'warn'); + }); + + $this->assertTrue(Schema::hasCollection(self::COLL_2)); + $this->assertTrue(Schema::hasTable(self::COLL_2)); + + $collection = Schema::getCollection(self::COLL_2); + $this->assertEquals( + ['$jsonSchema' => $schema], + $collection['options']['validator'], + ); + + $this->assertEquals( + 'warn', + $collection['options']['validationAction'], + ); + } + public function testDrop(): void { - Schema::create('newcollection'); - Schema::drop('newcollection'); - $this->assertFalse(Schema::hasCollection('newcollection')); + Schema::create(self::COLL_1); + Schema::drop(self::COLL_1); + $this->assertFalse(Schema::hasCollection(self::COLL_1)); } public function testBluePrint(): void { - Schema::table('newcollection', static function ($collection) { + Schema::table(self::COLL_1, static function ($collection) { self::assertInstanceOf(Blueprint::class, $collection); }); - Schema::table('newcollection', static function ($collection) { + Schema::table(self::COLL_1, static function ($collection) { self::assertInstanceOf(Blueprint::class, $collection); }); } public function testIndex(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->index('mykey1'); }); - $index = $this->assertIndexExists('newcollection', 'mykey1_1'); + $index = $this->assertIndexExists(self::COLL_1, 'mykey1_1'); $this->assertEquals(1, $index['key']['mykey1']); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->index(['mykey2']); }); - $index = $this->assertIndexExists('newcollection', 'mykey2_1'); + $index = $this->assertIndexExists(self::COLL_1, 'mykey2_1'); $this->assertEquals(1, $index['key']['mykey2']); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->string('mykey3')->index(); }); - $index = $this->assertIndexExists('newcollection', 'mykey3_1'); + $index = $this->assertIndexExists(self::COLL_1, 'mykey3_1'); $this->assertEquals(1, $index['key']['mykey3']); } public function testPrimary(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->string('mykey', 100)->primary(); }); - $index = $this->assertIndexExists('newcollection', 'mykey_1'); + $index = $this->assertIndexExists(self::COLL_1, 'mykey_1'); $this->assertEquals(1, $index['unique']); } public function testUnique(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->unique('uniquekey'); }); - $index = $this->assertIndexExists('newcollection', 'uniquekey_1'); + $index = $this->assertIndexExists(self::COLL_1, 'uniquekey_1'); $this->assertEquals(1, $index['unique']); } public function testDropIndex(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->unique('uniquekey'); $collection->dropIndex('uniquekey_1'); }); - $this->assertIndexNotExists('newcollection', 'uniquekey_1'); + $this->assertIndexNotExists(self::COLL_1, 'uniquekey_1'); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->unique('uniquekey'); $collection->dropIndex(['uniquekey']); }); - $this->assertIndexNotExists('newcollection', 'uniquekey_1'); + $this->assertIndexNotExists(self::COLL_1, 'uniquekey_1'); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->index(['field_a', 'field_b']); }); - $this->assertIndexExists('newcollection', 'field_a_1_field_b_1'); + $this->assertIndexExists(self::COLL_1, 'field_a_1_field_b_1'); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->dropIndex(['field_a', 'field_b']); }); - $this->assertIndexNotExists('newcollection', 'field_a_1_field_b_1'); + $this->assertIndexNotExists(self::COLL_1, 'field_a_1_field_b_1'); $indexName = 'field_a_-1_field_b_1'; - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->index(['field_a' => -1, 'field_b' => 1]); }); - $this->assertIndexExists('newcollection', $indexName); + $this->assertIndexExists(self::COLL_1, $indexName); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->dropIndex(['field_a' => -1, 'field_b' => 1]); }); - $this->assertIndexNotExists('newcollection', $indexName); + $this->assertIndexNotExists(self::COLL_1, $indexName); $indexName = 'custom_index_name'; - Schema::table('newcollection', function ($collection) use ($indexName) { + Schema::table(self::COLL_1, function ($collection) use ($indexName) { $collection->index(['field_a', 'field_b'], $indexName); }); - $this->assertIndexExists('newcollection', $indexName); + $this->assertIndexExists(self::COLL_1, $indexName); - Schema::table('newcollection', function ($collection) use ($indexName) { + Schema::table(self::COLL_1, function ($collection) use ($indexName) { $collection->dropIndex($indexName); }); - $this->assertIndexNotExists('newcollection', $indexName); + $this->assertIndexNotExists(self::COLL_1, $indexName); } public function testDropIndexIfExists(): void { - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->unique('uniquekey'); $collection->dropIndexIfExists('uniquekey_1'); }); - $this->assertIndexNotExists('newcollection', 'uniquekey'); + $this->assertIndexNotExists(self::COLL_1, 'uniquekey'); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->unique('uniquekey'); $collection->dropIndexIfExists(['uniquekey']); }); - $this->assertIndexNotExists('newcollection', 'uniquekey'); + $this->assertIndexNotExists(self::COLL_1, 'uniquekey'); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->index(['field_a', 'field_b']); }); - $this->assertIndexExists('newcollection', 'field_a_1_field_b_1'); + $this->assertIndexExists(self::COLL_1, 'field_a_1_field_b_1'); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->dropIndexIfExists(['field_a', 'field_b']); }); - $this->assertIndexNotExists('newcollection', 'field_a_1_field_b_1'); + $this->assertIndexNotExists(self::COLL_1, 'field_a_1_field_b_1'); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->index(['field_a', 'field_b'], 'custom_index_name'); }); - $this->assertIndexExists('newcollection', 'custom_index_name'); + $this->assertIndexExists(self::COLL_1, 'custom_index_name'); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->dropIndexIfExists('custom_index_name'); }); - $this->assertIndexNotExists('newcollection', 'custom_index_name'); + $this->assertIndexNotExists(self::COLL_1, 'custom_index_name'); } public function testHasIndex(): void { - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->index('myhaskey1'); $this->assertTrue($collection->hasIndex('myhaskey1_1')); $this->assertFalse($collection->hasIndex('myhaskey1')); }); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->index('myhaskey2'); $this->assertTrue($collection->hasIndex(['myhaskey2'])); $this->assertFalse($collection->hasIndex(['myhaskey2_1'])); }); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->index(['field_a', 'field_b']); $this->assertTrue($collection->hasIndex(['field_a_1_field_b'])); $this->assertFalse($collection->hasIndex(['field_a_1_field_b_1'])); @@ -238,74 +277,74 @@ public function testHasIndex(): void public function testSparse(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->sparse('sparsekey'); }); - $index = $this->assertIndexExists('newcollection', 'sparsekey_1'); + $index = $this->assertIndexExists(self::COLL_1, 'sparsekey_1'); $this->assertEquals(1, $index['sparse']); } public function testExpire(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->expire('expirekey', 60); }); - $index = $this->assertIndexExists('newcollection', 'expirekey_1'); + $index = $this->assertIndexExists(self::COLL_1, 'expirekey_1'); $this->assertEquals(60, $index['expireAfterSeconds']); } public function testSoftDeletes(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->softDeletes(); }); - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->string('email')->nullable()->index(); }); - $index = $this->assertIndexExists('newcollection', 'email_1'); + $index = $this->assertIndexExists(self::COLL_1, 'email_1'); $this->assertEquals(1, $index['key']['email']); } public function testFluent(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->string('email')->index(); $collection->string('token')->index(); $collection->timestamp('created_at'); }); - $index = $this->assertIndexExists('newcollection', 'email_1'); + $index = $this->assertIndexExists(self::COLL_1, 'email_1'); $this->assertEquals(1, $index['key']['email']); - $index = $this->assertIndexExists('newcollection', 'token_1'); + $index = $this->assertIndexExists(self::COLL_1, 'token_1'); $this->assertEquals(1, $index['key']['token']); } public function testGeospatial(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->geospatial('point'); $collection->geospatial('area', '2d'); $collection->geospatial('continent', '2dsphere'); }); - $index = $this->assertIndexExists('newcollection', 'point_2d'); + $index = $this->assertIndexExists(self::COLL_1, 'point_2d'); $this->assertEquals('2d', $index['key']['point']); - $index = $this->assertIndexExists('newcollection', 'area_2d'); + $index = $this->assertIndexExists(self::COLL_1, 'area_2d'); $this->assertEquals('2d', $index['key']['area']); - $index = $this->assertIndexExists('newcollection', 'continent_2dsphere'); + $index = $this->assertIndexExists(self::COLL_1, 'continent_2dsphere'); $this->assertEquals('2dsphere', $index['key']['continent']); } public function testDummies(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->boolean('activated')->default(0); $collection->integer('user_id')->unsigned(); }); @@ -314,22 +353,22 @@ public function testDummies(): void public function testSparseUnique(): void { - Schema::table('newcollection', function ($collection) { + Schema::table(self::COLL_1, function ($collection) { $collection->sparse_and_unique('sparseuniquekey'); }); - $index = $this->assertIndexExists('newcollection', 'sparseuniquekey_1'); + $index = $this->assertIndexExists(self::COLL_1, 'sparseuniquekey_1'); $this->assertEquals(1, $index['sparse']); $this->assertEquals(1, $index['unique']); } public function testRenameColumn(): void { - DB::connection()->table('newcollection')->insert(['test' => 'value']); - DB::connection()->table('newcollection')->insert(['test' => 'value 2']); - DB::connection()->table('newcollection')->insert(['column' => 'column value']); + DB::connection()->table(self::COLL_1)->insert(['test' => 'value']); + DB::connection()->table(self::COLL_1)->insert(['test' => 'value 2']); + DB::connection()->table(self::COLL_1)->insert(['column' => 'column value']); - $check = DB::connection()->table('newcollection')->get(); + $check = DB::connection()->table(self::COLL_1)->get(); $this->assertCount(3, $check); $this->assertObjectHasProperty('test', $check[0]); @@ -342,11 +381,11 @@ public function testRenameColumn(): void $this->assertObjectNotHasProperty('test', $check[2]); $this->assertObjectNotHasProperty('newtest', $check[2]); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->renameColumn('test', 'newtest'); }); - $check2 = DB::connection()->table('newcollection')->get(); + $check2 = DB::connection()->table(self::COLL_1)->get(); $this->assertCount(3, $check2); $this->assertObjectHasProperty('newtest', $check2[0]); @@ -365,36 +404,46 @@ public function testRenameColumn(): void public function testHasColumn(): void { - $this->assertTrue(Schema::hasColumn('newcollection', '_id')); - $this->assertTrue(Schema::hasColumn('newcollection', 'id')); + $this->assertTrue(Schema::hasColumn(self::COLL_1, '_id')); + $this->assertTrue(Schema::hasColumn(self::COLL_1, 'id')); - DB::connection()->table('newcollection')->insert(['column1' => 'value', 'embed' => ['_id' => 1]]); + DB::connection()->table(self::COLL_1)->insert(['column1' => 'value', 'embed' => ['_id' => 1]]); - $this->assertTrue(Schema::hasColumn('newcollection', 'column1')); - $this->assertFalse(Schema::hasColumn('newcollection', 'column2')); - $this->assertTrue(Schema::hasColumn('newcollection', 'embed._id')); - $this->assertTrue(Schema::hasColumn('newcollection', 'embed.id')); + $this->assertTrue(Schema::hasColumn(self::COLL_1, 'column1')); + $this->assertFalse(Schema::hasColumn(self::COLL_1, 'column2')); + $this->assertTrue(Schema::hasColumn(self::COLL_1, 'embed._id')); + $this->assertTrue(Schema::hasColumn(self::COLL_1, 'embed.id')); } public function testHasColumns(): void { - $this->assertTrue(Schema::hasColumns('newcollection', ['_id'])); - $this->assertTrue(Schema::hasColumns('newcollection', ['id'])); + $this->assertTrue(Schema::hasColumns(self::COLL_1, ['_id'])); + $this->assertTrue(Schema::hasColumns(self::COLL_1, ['id'])); // Insert documents with both column1 and column2 - DB::connection()->table('newcollection')->insert([ + DB::connection()->table(self::COLL_1)->insert([ ['column1' => 'value1', 'column2' => 'value2'], ['column1' => 'value3'], ]); - $this->assertTrue(Schema::hasColumns('newcollection', ['column1', 'column2'])); - $this->assertFalse(Schema::hasColumns('newcollection', ['column1', 'column3'])); + $this->assertTrue(Schema::hasColumns(self::COLL_1, ['column1', 'column2'])); + $this->assertFalse(Schema::hasColumns(self::COLL_1, ['column1', 'column3'])); } public function testGetTables() { - DB::connection('mongodb')->table('newcollection')->insert(['test' => 'value']); - DB::connection('mongodb')->table('newcollection_two')->insert(['test' => 'value']); + $db = DB::connection('mongodb')->getDatabase(); + $db->createCollection(self::COLL_WITH_COLLATION, [ + 'collation' => [ + 'locale' => 'fr', + 'strength' => 2, + ], + ]); + + DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']); + DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']); + $db->createCollection('test_view', ['viewOn' => self::COLL_1]); + $dbName = DB::connection('mongodb')->getDatabaseName(); $tables = Schema::getTables(); $this->assertIsArray($tables); @@ -403,38 +452,103 @@ public function testGetTables() foreach ($tables as $table) { $this->assertArrayHasKey('name', $table); $this->assertArrayHasKey('size', $table); + $this->assertArrayHasKey('schema', $table); + $this->assertArrayHasKey('collation', $table); + $this->assertArrayHasKey('schema_qualified_name', $table); + $this->assertNotEquals('test_view', $table['name'], 'Standard views should not be included in the result of getTables.'); - if ($table['name'] === 'newcollection') { + if ($table['name'] === self::COLL_1) { $this->assertEquals(8192, $table['size']); + $this->assertEquals($dbName, $table['schema']); + $this->assertEquals($dbName . '.' . self::COLL_1, $table['schema_qualified_name']); + $found = true; + } + + if ($table['name'] === self::COLL_WITH_COLLATION) { + $this->assertEquals('l=fr;cl=0;cf=off;s=2;no=0;a=non-ignorable;mv=punct;n=0;b=0', $table['collation']); + } + } + + if (! $found) { + $this->fail('Collection "' . self::COLL_1 . '" not found'); + } + } + + public function testGetViews() + { + DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']); + DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']); + $dbName = DB::connection('mongodb')->getDatabaseName(); + + DB::connection('mongodb')->getDatabase()->createCollection('test_view', ['viewOn' => self::COLL_1]); + + $tables = Schema::getViews(); + + $this->assertIsArray($tables); + $this->assertGreaterThanOrEqual(1, count($tables)); + $found = false; + foreach ($tables as $table) { + $this->assertArrayHasKey('name', $table); + $this->assertArrayHasKey('size', $table); + $this->assertArrayHasKey('schema', $table); + $this->assertArrayHasKey('schema_qualified_name', $table); + + // Ensure "normal collections" are not in the views list + $this->assertNotEquals(self::COLL_1, $table['name'], 'Normal collections should not be included in the result of getViews.'); + + if ($table['name'] === 'test_view') { + $this->assertEquals($dbName, $table['schema']); + $this->assertEquals($dbName . '.test_view', $table['schema_qualified_name']); $found = true; } } if (! $found) { - $this->fail('Collection "newcollection" not found'); + $this->fail('Collection "test_view" not found'); } } public function testGetTableListing() { - DB::connection('mongodb')->table('newcollection')->insert(['test' => 'value']); - DB::connection('mongodb')->table('newcollection_two')->insert(['test' => 'value']); + DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']); + DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']); $tables = Schema::getTableListing(); $this->assertIsArray($tables); $this->assertGreaterThanOrEqual(2, count($tables)); - $this->assertContains('newcollection', $tables); - $this->assertContains('newcollection_two', $tables); + $this->assertContains(self::COLL_1, $tables); + $this->assertContains(self::COLL_2, $tables); + } + + public function testGetTableListingBySchema() + { + DB::connection('mongodb')->table(self::COLL_1)->insert(['test' => 'value']); + DB::connection('mongodb')->table(self::COLL_2)->insert(['test' => 'value']); + $dbName = DB::connection('mongodb')->getDatabaseName(); + + $tables = Schema::getTableListing([$dbName, 'database__that_does_not_exists'], schemaQualified: true); + + $this->assertIsArray($tables); + $this->assertGreaterThanOrEqual(2, count($tables)); + $this->assertContains($dbName . '.' . self::COLL_1, $tables); + $this->assertContains($dbName . '.' . self::COLL_2, $tables); + + $tables = Schema::getTableListing([$dbName, 'database__that_does_not_exists'], schemaQualified: false); + + $this->assertIsArray($tables); + $this->assertGreaterThanOrEqual(2, count($tables)); + $this->assertContains(self::COLL_1, $tables); + $this->assertContains(self::COLL_2, $tables); } public function testGetColumns() { - $collection = DB::connection('mongodb')->table('newcollection'); + $collection = DB::connection('mongodb')->table(self::COLL_1); $collection->insert(['text' => 'value', 'mixed' => ['key' => 'value']]); $collection->insert(['date' => new UTCDateTime(), 'binary' => new Binary('binary'), 'mixed' => true]); - $columns = Schema::getColumns('newcollection'); + $columns = Schema::getColumns(self::COLL_1); $this->assertIsArray($columns); $this->assertCount(5, $columns); @@ -463,17 +577,22 @@ public function testGetColumns() // Non-existent collection $columns = Schema::getColumns('missing'); $this->assertSame([], $columns); + + // Qualified table name + $columns = Schema::getColumns(DB::getDatabaseName() . '.' . self::COLL_1); + $this->assertIsArray($columns); + $this->assertCount(5, $columns); } /** @see AtlasSearchTest::testGetIndexes() */ public function testGetIndexes() { - Schema::create('newcollection', function (Blueprint $collection) { + Schema::create(self::COLL_1, function (Blueprint $collection) { $collection->index('mykey1'); $collection->string('mykey2')->unique('unique_index'); $collection->string('mykey3')->index(); }); - $indexes = Schema::getIndexes('newcollection'); + $indexes = Schema::getIndexes(self::COLL_1); self::assertIsArray($indexes); self::assertCount(4, $indexes); @@ -519,7 +638,7 @@ public function testSearchIndex(): void { $this->skipIfSearchIndexManagementIsNotSupported(); - Schema::create('newcollection', function (Blueprint $collection) { + Schema::create(self::COLL_1, function (Blueprint $collection) { $collection->searchIndex([ 'mappings' => [ 'dynamic' => false, @@ -530,7 +649,7 @@ public function testSearchIndex(): void ]); }); - $index = $this->getSearchIndex('newcollection', 'default'); + $index = $this->getSearchIndex(self::COLL_1, 'default'); self::assertNotNull($index); self::assertSame('default', $index['name']); @@ -538,11 +657,11 @@ public function testSearchIndex(): void self::assertFalse($index['latestDefinition']['mappings']['dynamic']); self::assertSame('lucene.whitespace', $index['latestDefinition']['mappings']['fields']['foo']['analyzer']); - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->dropSearchIndex('default'); }); - $index = $this->getSearchIndex('newcollection', 'default'); + $index = $this->getSearchIndex(self::COLL_1, 'default'); self::assertNull($index); } @@ -550,7 +669,7 @@ public function testVectorSearchIndex() { $this->skipIfSearchIndexManagementIsNotSupported(); - Schema::create('newcollection', function (Blueprint $collection) { + Schema::create(self::COLL_1, function (Blueprint $collection) { $collection->vectorSearchIndex([ 'fields' => [ ['type' => 'vector', 'path' => 'foo', 'numDimensions' => 128, 'similarity' => 'euclidean', 'quantization' => 'none'], @@ -558,7 +677,7 @@ public function testVectorSearchIndex() ], 'vector'); }); - $index = $this->getSearchIndex('newcollection', 'vector'); + $index = $this->getSearchIndex(self::COLL_1, 'vector'); self::assertNotNull($index); self::assertSame('vector', $index['name']); @@ -566,11 +685,11 @@ public function testVectorSearchIndex() self::assertSame('vector', $index['latestDefinition']['fields'][0]['type']); // Drop the index - Schema::table('newcollection', function (Blueprint $collection) { + Schema::table(self::COLL_1, function (Blueprint $collection) { $collection->dropSearchIndex('vector'); }); - $index = $this->getSearchIndex('newcollection', 'vector'); + $index = $this->getSearchIndex(self::COLL_1, 'vector'); self::assertNull($index); } diff --git a/tests/Scout/Models/ScoutUser.php b/tests/Scout/Models/ScoutUser.php index 50fa39a94..581606f75 100644 --- a/tests/Scout/Models/ScoutUser.php +++ b/tests/Scout/Models/ScoutUser.php @@ -5,11 +5,11 @@ namespace MongoDB\Laravel\Tests\Scout\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\SQLiteBuilder; use Illuminate\Support\Facades\Schema; use Laravel\Scout\Searchable; -use MongoDB\Laravel\Eloquent\SoftDeletes; use function assert; diff --git a/tests/Scout/ScoutEngineTest.php b/tests/Scout/ScoutEngineTest.php index 40d943ffb..7b254ec9c 100644 --- a/tests/Scout/ScoutEngineTest.php +++ b/tests/Scout/ScoutEngineTest.php @@ -11,13 +11,11 @@ use Laravel\Scout\Builder; use Laravel\Scout\Jobs\RemoveFromSearch; use LogicException; -use Mockery as m; use MongoDB\BSON\Document; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; use MongoDB\Database; use MongoDB\Driver\CursorInterface; -use MongoDB\Laravel\Eloquent\Model; use MongoDB\Laravel\Scout\ScoutEngine; use MongoDB\Laravel\Tests\Scout\Models\ScoutUser; use MongoDB\Laravel\Tests\Scout\Models\SearchableModel; @@ -36,7 +34,7 @@ class ScoutEngineTest extends TestCase public function testCreateIndexInvalidDefinition(): void { - $database = m::mock(Database::class); + $database = $this->createMock(Database::class); $engine = new ScoutEngine($database, false, ['collection_invalid' => ['foo' => 'bar']]); $this->expectException(LogicException::class); @@ -53,21 +51,22 @@ public function testCreateIndex(): void ], ]; - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('createCollection') - ->once() + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('createCollection') ->with($collectionName); - $database->shouldReceive('selectCollection') + $database->expects($this->once()) + ->method('selectCollection') ->with($collectionName) - ->andReturn($collection); - $collection->shouldReceive('createSearchIndex') - ->once() + ->willReturn($collection); + $collection->expects($this->once()) + ->method('createSearchIndex') ->with($expectedDefinition, ['name' => 'scout']); - $collection->shouldReceive('listSearchIndexes') - ->once() + $collection->expects($this->once()) + ->method('listSearchIndexes') ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']]) - ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); + ->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); $engine = new ScoutEngine($database, false, []); $engine->createIndex($collectionName); @@ -90,21 +89,22 @@ public function testCreateIndexCustomDefinition(): void ], ]; - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('createCollection') - ->once() + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('createCollection') ->with($collectionName); - $database->shouldReceive('selectCollection') + $database->expects($this->once()) + ->method('selectCollection') ->with($collectionName) - ->andReturn($collection); - $collection->shouldReceive('createSearchIndex') - ->once() + ->willReturn($collection); + $collection->expects($this->once()) + ->method('createSearchIndex') ->with($expectedDefinition, ['name' => 'scout']); - $collection->shouldReceive('listSearchIndexes') - ->once() + $collection->expects($this->once()) + ->method('listSearchIndexes') ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']]) - ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); + ->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); $engine = new ScoutEngine($database, false, [$collectionName => $expectedDefinition]); $engine->createIndex($collectionName); @@ -115,26 +115,28 @@ public function testCreateIndexCustomDefinition(): void public function testSearch(Closure $builder, array $expectedPipeline): void { $data = [['_id' => 'key_1', '__count' => 15], ['_id' => 'key_2', '__count' => 15]]; - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('selectCollection') ->with('collection_searchable') - ->andReturn($collection); - $cursor = m::mock(CursorInterface::class); - $cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP); - $cursor->shouldReceive('toArray')->once()->with()->andReturn($data); - - $collection->shouldReceive('getCollectionName') - ->zeroOrMoreTimes() - ->andReturn('collection_searchable'); - $collection->shouldReceive('aggregate') - ->once() - ->withArgs(function ($pipeline) use ($expectedPipeline) { - self::assertEquals($expectedPipeline, $pipeline); - - return true; - }) - ->andReturn($cursor); + ->willReturn($collection); + $cursor = $this->createMock(CursorInterface::class); + $cursor->expects($this->once()) + ->method('setTypeMap') + ->with(self::EXPECTED_TYPEMAP); + $cursor->expects($this->once()) + ->method('toArray') + ->with() + ->willReturn($data); + + $collection->expects($this->any()) + ->method('getCollectionName') + ->willReturn('collection_searchable'); + $collection->expects($this->once()) + ->method('aggregate') + ->with($expectedPipeline) + ->willReturn($cursor); $engine = new ScoutEngine($database, softDelete: false); $result = $engine->search($builder()); @@ -414,15 +416,15 @@ public function testPaginate() $perPage = 5; $page = 3; - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $cursor = m::mock(CursorInterface::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $cursor = $this->createMock(CursorInterface::class); + $database->method('selectCollection') ->with('collection_searchable') - ->andReturn($collection); - $collection->shouldReceive('aggregate') - ->once() - ->withArgs(function (...$args) { + ->willReturn($collection); + $collection->expects($this->once()) + ->method('aggregate') + ->willReturnCallback(function (...$args) use ($cursor) { self::assertSame([ [ '$search' => [ @@ -468,14 +470,11 @@ public function testPaginate() ], ], $args[0]); - return true; - }) - ->andReturn($cursor); - $cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP); - $cursor->shouldReceive('toArray') - ->once() - ->with() - ->andReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]); + return $cursor; + }); + $cursor->expects($this->once())->method('setTypeMap')->with(self::EXPECTED_TYPEMAP); + $cursor->expects($this->once())->method('toArray')->with() + ->willReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]); $engine = new ScoutEngine($database, softDelete: false); $builder = new Builder(new SearchableModel(), 'mustang'); @@ -485,20 +484,27 @@ public function testPaginate() public function testMapMethodRespectsOrder() { - $database = m::mock(Database::class); + $database = $this->createMock(Database::class); + $query = $this->createMock(Builder::class); $engine = new ScoutEngine($database, false); - $model = m::mock(Model::class); - $model->shouldReceive(['getScoutKeyName' => 'id']); - $model->shouldReceive('queryScoutModelsByIds->get') - ->andReturn(LaravelCollection::make([ + $model = $this->createMock(SearchableModel::class); + $model->expects($this->any()) + ->method('getScoutKeyName') + ->willReturn('id'); + $model->expects($this->once()) + ->method('queryScoutModelsByIds') + ->willReturn($query); + $query->expects($this->once()) + ->method('get') + ->willReturn(LaravelCollection::make([ new ScoutUser(['id' => 1]), new ScoutUser(['id' => 2]), new ScoutUser(['id' => 3]), new ScoutUser(['id' => 4]), ])); - $builder = m::mock(Builder::class); + $builder = $this->createMock(Builder::class); $results = $engine->map($builder, [ ['_id' => 1, '__count' => 4], @@ -518,21 +524,27 @@ public function testMapMethodRespectsOrder() public function testLazyMapMethodRespectsOrder() { - $lazy = false; - $database = m::mock(Database::class); + $database = $this->createMock(Database::class); + $query = $this->createMock(Builder::class); $engine = new ScoutEngine($database, false); - $model = m::mock(Model::class); - $model->shouldReceive(['getScoutKeyName' => 'id']); - $model->shouldReceive('queryScoutModelsByIds->cursor') - ->andReturn(LazyCollection::make([ + $model = $this->createMock(SearchableModel::class); + $model->expects($this->any()) + ->method('getScoutKeyName') + ->willReturn('id'); + $model->expects($this->once()) + ->method('queryScoutModelsByIds') + ->willReturn($query); + $query->expects($this->once()) + ->method('cursor') + ->willReturn(LazyCollection::make([ new ScoutUser(['id' => 1]), new ScoutUser(['id' => 2]), new ScoutUser(['id' => 3]), new ScoutUser(['id' => 4]), ])); - $builder = m::mock(Builder::class); + $builder = $this->createMock(Builder::class); $results = $engine->lazyMap($builder, [ ['_id' => 1, '__count' => 4], @@ -553,13 +565,14 @@ public function testLazyMapMethodRespectsOrder() public function testUpdate(): void { $date = new DateTimeImmutable('2000-01-02 03:04:05'); - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('selectCollection') ->with('collection_indexable') - ->andReturn($collection); - $collection->shouldReceive('bulkWrite') - ->once() + ->willReturn($collection); + $collection->expects($this->once()) + ->method('bulkWrite') ->with([ [ 'updateOne' => [ @@ -592,26 +605,23 @@ public function testUpdate(): void public function testUpdateWithSoftDelete(): void { $date = new DateTimeImmutable('2000-01-02 03:04:05'); - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('selectCollection') ->with('collection_indexable') - ->andReturn($collection); - $collection->shouldReceive('bulkWrite') - ->once() - ->withArgs(function ($pipeline) { - $this->assertSame([ - [ - 'updateOne' => [ - ['_id' => 'key_1'], - ['$set' => ['id' => 1, '__soft_deleted' => false]], - ['upsert' => true], - ], + ->willReturn($collection); + $collection->expects($this->once()) + ->method('bulkWrite') + ->with([ + [ + 'updateOne' => [ + ['_id' => 'key_1'], + ['$set' => ['id' => 1, '__soft_deleted' => false]], + ['upsert' => true], ], - ], $pipeline); - - return true; - }); + ], + ]); $model = new SearchableModel(['id' => 1]); $model->delete(); @@ -622,13 +632,14 @@ public function testUpdateWithSoftDelete(): void public function testDelete(): void { - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('selectCollection') ->with('collection_indexable') - ->andReturn($collection); - $collection->shouldReceive('deleteMany') - ->once() + ->willReturn($collection); + $collection->expects($this->once()) + ->method('deleteMany') ->with(['_id' => ['$in' => ['key_1', 'key_2']]]); $engine = new ScoutEngine($database, softDelete: false); @@ -646,13 +657,14 @@ public function testDeleteWithRemoveableScoutCollection(): void $job = unserialize(serialize($job)); - $database = m::mock(Database::class); - $collection = m::mock(Collection::class); - $database->shouldReceive('selectCollection') + $database = $this->createMock(Database::class); + $collection = $this->createMock(Collection::class); + $database->expects($this->once()) + ->method('selectCollection') ->with('collection_indexable') - ->andReturn($collection); - $collection->shouldReceive('deleteMany') - ->once() + ->willReturn($collection); + $collection->expects($this->once()) + ->method('deleteMany') ->with(['_id' => ['$in' => ['key_5']]]); $engine = new ScoutEngine($database, softDelete: false); diff --git a/tests/SessionTest.php b/tests/SessionTest.php index ee086f5b8..f334dc746 100644 --- a/tests/SessionTest.php +++ b/tests/SessionTest.php @@ -5,7 +5,9 @@ use Illuminate\Session\DatabaseSessionHandler; use Illuminate\Session\SessionManager; use Illuminate\Support\Facades\DB; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; +use MongoDB\Laravel\Session\MongoDbSessionHandler; +use PHPUnit\Framework\Attributes\TestWith; +use SessionHandlerInterface; class SessionTest extends TestCase { @@ -16,21 +18,31 @@ protected function tearDown(): void parent::tearDown(); } - public function testDatabaseSessionHandlerCompatibility() + /** @param class-string $class */ + #[TestWith([DatabaseSessionHandler::class])] + #[TestWith([MongoDbSessionHandler::class])] + public function testSessionHandlerFunctionality(string $class) { - $sessionId = '123'; - - $handler = new DatabaseSessionHandler( + $handler = new $class( $this->app['db']->connection('mongodb'), 'sessions', 10, ); + $sessionId = '123'; + $handler->write($sessionId, 'foo'); $this->assertEquals('foo', $handler->read($sessionId)); $handler->write($sessionId, 'bar'); $this->assertEquals('bar', $handler->read($sessionId)); + + $handler->destroy($sessionId); + $this->assertEmpty($handler->read($sessionId)); + + $handler->write($sessionId, 'bar'); + $handler->gc(-1); + $this->assertEmpty($handler->read($sessionId)); } public function testDatabaseSessionHandlerRegistration() @@ -70,5 +82,13 @@ private function assertSessionCanStoreInMongoDB(SessionManager $session): void self::assertIsObject($data); self::assertSame($session->getId(), $data->_id); + + $session->remove('foo'); + $data = DB::connection('mongodb') + ->getCollection('sessions') + ->findOne(['_id' => $session->getId()]); + + self::assertIsObject($data); + self::assertSame($session->getId(), $data->_id); } } diff --git a/tests/Ticket/GH3326Test.php b/tests/Ticket/GH3326Test.php new file mode 100644 index 000000000..d3f339acc --- /dev/null +++ b/tests/Ticket/GH3326Test.php @@ -0,0 +1,42 @@ +foo = 'bar'; + $model->save(); + + $fresh = $model->fresh(); + + $this->assertEquals('bar', $fresh->foo); + $this->assertEquals('written-in-created', $fresh->extra); + } +} + +class GH3326Model extends Model +{ + protected $connection = 'mongodb'; + protected $collection = 'test_gh3326'; + protected $guarded = []; + + protected static function booted(): void + { + static::created(function ($model) { + $model->extra = 'written-in-created'; + $model->saveQuietly(); + }); + } +} diff --git a/tests/Ticket/GH3335Test.php b/tests/Ticket/GH3335Test.php new file mode 100644 index 000000000..f37782a4b --- /dev/null +++ b/tests/Ticket/GH3335Test.php @@ -0,0 +1,33 @@ +id = 'foo'; + $model->save(); + + $model = Location::find('foo'); + $model->{'38'} = 'PHP'; + $model->save(); + + $model = Location::find('foo'); + self::assertSame('PHP', $model->{'38'}); + } +}