diff --git a/.github/workflows/build-ci-atlas.yml b/.github/workflows/build-ci-atlas.yml index 46f30150d..a4ab45e1d 100644 --- a/.github/workflows/build-ci-atlas.yml +++ b/.github/workflows/build-ci-atlas.yml @@ -1,94 +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 }} 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@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: 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 + 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 bbc8b53d1..f55bb3104 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -1,121 +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 }} 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@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: 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 + 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 2f991c76b..e9cddccfe 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -2,7 +2,13 @@ name: "Coding Standards" on: push: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" pull_request: + branches: + - "[0-9]+.[0-9x]+" + - "feature/*" env: PHP_VERSION: "8.4" @@ -20,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Setup cache environment" id: "extcache" @@ -49,7 +55,12 @@ jobs: run: "php --ri mongodb" - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@3.1.0" + uses: "ramsey/composer-install@3.1.1" + with: + composer-options: "--no-suggest" + + - 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" 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/static-analysis.yml b/.github/workflows/static-analysis.yml index e0c907953..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: @@ -33,7 +39,7 @@ jobs: - 2 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }} 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 2542b51bb..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" } 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/eloquent-models/model-class.txt b/docs/eloquent-models/model-class.txt index 6f686e88a..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 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/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/query-builder/QueryBuilderTest.php b/docs/includes/query-builder/QueryBuilderTest.php index 3f7ea2274..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); @@ -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/query-builder.txt b/docs/query-builder.txt index a73d5e791..2358ed7d5 100644 --- a/docs/query-builder.txt +++ b/docs/query-builder.txt @@ -1169,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 ` @@ -1252,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/backend-service-tutorial.txt b/docs/quick-start/backend-service-tutorial.txt index 9236c698a..7ecdf8cf8 100644 --- a/docs/quick-start/backend-service-tutorial.txt +++ b/docs/quick-start/backend-service-tutorial.txt @@ -1,8 +1,8 @@ .. _laravel-tutorial-backend-service: -========================================================== +=========================================================== Tutorial: Build a Back End Service by Using {+odm-long+} -========================================================== +=========================================================== .. facet:: :name: genre diff --git a/src/Bus/MongoBatchRepository.php b/src/Bus/MongoBatchRepository.php index 2656bbc30..afd95e0b2 100644 --- a/src/Bus/MongoBatchRepository.php +++ b/src/Bus/MongoBatchRepository.php @@ -216,6 +216,7 @@ public function prune(DateTimeInterface $before): int } /** Prune all the unfinished entries older than the given date. */ + #[Override] public function pruneUnfinished(DateTimeInterface $before): int { $result = $this->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 29b72ae44..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; @@ -95,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()); @@ -115,6 +117,7 @@ public function getCollection($name): Collection } /** @inheritdoc */ + #[Override] public function getSchemaBuilder() { return new Schema\Builder($this); @@ -172,6 +175,8 @@ public function getClient(): ?Client return $this->connection; } + /** @inheritdoc */ + #[Override] public function enableQueryLog() { parent::enableQueryLog(); @@ -182,6 +187,7 @@ public function enableQueryLog() } } + #[Override] public function disableQueryLog() { parent::disableQueryLog(); @@ -192,6 +198,7 @@ public function disableQueryLog() } } + #[Override] protected function withFreshQueryLog($callback) { try { @@ -214,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.'); } @@ -340,6 +347,7 @@ protected function getDsn(array $config): string } /** @inheritdoc */ + #[Override] public function getDriverName() { return 'mongodb'; @@ -352,12 +360,14 @@ public function getDriverTitle() } /** @inheritdoc */ + #[Override] protected function getDefaultPostProcessor() { return new Query\Processor(); } /** @inheritdoc */ + #[Override] protected function getDefaultQueryGrammar() { // Argument added in Laravel 12 @@ -365,6 +375,7 @@ protected function getDefaultQueryGrammar() } /** @inheritdoc */ + #[Override] protected function getDefaultSchemaGrammar() { // Argument added in Laravel 12 diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index 1b13de314..3152a9699 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -4,10 +4,12 @@ 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; @@ -16,6 +18,7 @@ use MongoDB\Laravel\Helpers\QueriesRelationships; use MongoDB\Laravel\Query\AggregationBuilder; use MongoDB\Model\BSONDocument; +use Override; use function array_key_exists; use function array_map; @@ -67,7 +70,7 @@ class Builder extends EloquentBuilder ]; /** - * @return ($function is null ? AggregationBuilder : self) + * @return ($function is null ? AggregationBuilder : $this) * * @inheritdoc */ @@ -125,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 @@ -229,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. @@ -262,6 +276,7 @@ public function raw($value = null) return $results; } + #[Override] public function firstOrCreate(array $attributes = [], array $values = []) { $instance = (clone $this)->where($attributes)->first(); @@ -277,6 +292,7 @@ public function firstOrCreate(array $attributes = [], array $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. @@ -300,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) { @@ -328,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 965b1a444..f8d399e62 100644 --- a/src/Eloquent/DocumentModel.php +++ b/src/Eloquent/DocumentModel.php @@ -50,6 +50,7 @@ use function strlen; use function var_export; +/** @mixin Builder */ trait DocumentModel { use HybridRelations; 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 a51a63919..644eb7a56 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -24,6 +24,7 @@ use MongoDB\Laravel\Queue\MongoConnector; use MongoDB\Laravel\Scout\ScoutEngine; use MongoDB\Laravel\Session\MongoDbSessionHandler; +use Override; use RuntimeException; use function assert; @@ -47,6 +48,7 @@ public function boot() /** * Register the service provider. */ + #[Override] public function register() { // Add database driver. diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 321672e6f..5e0413929 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -243,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]); @@ -257,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); @@ -579,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)))); @@ -640,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 = ['*']) { @@ -654,6 +660,7 @@ public function aggregateByGroup(string $function, array $columns = ['*']) } /** @inheritdoc */ + #[Override] public function exists() { return $this->first(['id']) !== null; @@ -676,6 +683,7 @@ public function distinct($column = false) * * @inheritdoc */ + #[Override] public function orderBy($column, $direction = 'asc') { if (is_string($direction)) { @@ -697,6 +705,7 @@ public function orderBy($column, $direction = 'asc') } /** @inheritdoc */ + #[Override] public function whereBetween($column, iterable $values, $boolean = 'and', $not = false) { $type = 'between'; @@ -721,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 @@ -758,6 +768,7 @@ public function insert(array $values) } /** @inheritdoc */ + #[Override] public function insertGetId(array $values, $sequence = null) { $options = $this->inheritConnectionOptions(); @@ -777,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 @@ -793,6 +805,7 @@ public function update(array $values, array $options = []) } /** @inheritdoc */ + #[Override] public function upsert(array $values, $uniqueBy, $update = null): int { if ($values === []) { @@ -839,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]]; @@ -859,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; @@ -876,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 = []; @@ -893,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]); @@ -904,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 @@ -935,6 +1000,7 @@ public function delete($id = null) } /** @inheritdoc */ + #[Override] public function from($collection, $as = null) { if ($collection) { @@ -967,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 @@ -1070,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) { @@ -1157,6 +1232,7 @@ public function convertKey($id) * * @return $this */ + #[Override] public function where($column, $operator = null, $value = null, $boolean = 'and') { $params = func_get_args(); @@ -1670,6 +1746,7 @@ private function inheritConnectionOptions(array $options = []): array } /** @inheritdoc */ + #[Override] public function __call($method, $parameters) { if ($method === 'unset') { @@ -1680,90 +1757,105 @@ 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'); @@ -1804,7 +1896,7 @@ private function aliasIdForQuery(array $values, bool $root = true): array 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]); } } 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 042ec22ce..8978483ec 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -10,6 +10,7 @@ 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; @@ -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 = [ @@ -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 a1514d235..724dad912 100644 --- a/src/Relations/MorphToMany.php +++ b/src/Relations/MorphToMany.php @@ -10,6 +10,7 @@ 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; @@ -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 = [ @@ -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 1197bfde1..24e23d50e 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -7,8 +7,10 @@ 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; @@ -37,6 +39,7 @@ class Blueprint extends BaseBlueprint protected $columns = []; /** @inheritdoc */ + #[Override] public function index($columns = null, $name = null, $algorithm = null, $options = []) { $columns = $this->fluent($columns); @@ -63,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); @@ -117,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 * @@ -151,6 +174,7 @@ protected function transformColumns($indexOrColumns) } /** @inheritdoc */ + #[Override] public function unique($columns = null, $name = null, $algorithm = null, $options = []) { $columns = $this->fluent($columns); @@ -232,6 +256,7 @@ public function expire($columns, $seconds) * * @return void */ + #[Override] public function create($options = []) { $collection = $this->collection->getCollectionName(); @@ -243,6 +268,7 @@ public function create($options = []) } /** @inheritdoc */ + #[Override] public function drop() { $this->collection->drop(); @@ -251,6 +277,7 @@ public function drop() } /** @inheritdoc */ + #[Override] public function renameColumn($from, $to) { $this->collection->updateMany([$from => ['$exists' => true]], ['$rename' => [$from => $to]]); @@ -259,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/Builder.php b/src/Schema/Builder.php index 746fda99e..207f4f1b3 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -10,10 +10,12 @@ 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; @@ -25,6 +27,7 @@ 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; @@ -96,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); @@ -112,6 +117,7 @@ public function table($table, Closure $callback) } /** @inheritdoc */ + #[Override] public function create($table, ?Closure $callback = null, array $options = []) { $blueprint = $this->createBlueprint($table); @@ -124,6 +130,7 @@ public function create($table, ?Closure $callback = null, array $options = []) } /** @inheritdoc */ + #[Override] public function dropIfExists($table) { if ($this->hasCollection($table)) { @@ -132,6 +139,7 @@ public function dropIfExists($table) } /** @inheritdoc */ + #[Override] public function drop($table) { $blueprint = $this->createBlueprint($table); @@ -148,74 +156,32 @@ public function drop($table) * one by one. The database will be automatically recreated when a new connection * writes to it. */ + #[Override] public function dropAllTables() { $this->connection->getDatabase()->drop(); } - /** @param string|null $schema Database name */ + /** + * @param string|null $schema Database name + * + * @inheritdoc + */ + #[Override] public function getTables($schema = null) { - $db = $this->connection->getDatabase($schema); - $collections = []; - - foreach ($db->listCollections() as $collectionInfo) { - $collectionName = $collectionInfo->getName(); - - // Skip views, which don't support aggregate - if ($collectionInfo->getType() === 'view') { - continue; - } - - $stats = $db->selectCollection($collectionName)->aggregate([ - ['$collStats' => ['storageStats' => ['scale' => 1]]], - ['$project' => ['storageStats.totalSize' => 1]], - ])->toArray(); - - $collections[] = [ - 'name' => $collectionName, - 'schema' => $db->getDatabaseName(), - 'schema_qualified_name' => $db->getDatabaseName() . '.' . $collectionName, - 'size' => $stats[0]?->storageStats?->totalSize ?? null, - 'comment' => null, - 'collation' => null, - 'engine' => null, - ]; - } - - usort($collections, fn ($a, $b) => $a['name'] <=> $b['name']); - - return $collections; + return $this->getCollectionRows('collection', $schema); } - /** @param string|null $schema Database name */ + /** + * @param string|null $schema Database name + * + * @inheritdoc + */ + #[Override] public function getViews($schema = null) { - $db = $this->connection->getDatabase($schema); - $collections = []; - - foreach ($db->listCollections() as $collectionInfo) { - $collectionName = $collectionInfo->getName(); - - // Skip normal type collection - if ($collectionInfo->getType() !== 'view') { - continue; - } - - $collections[] = [ - 'name' => $collectionName, - 'schema' => $db->getDatabaseName(), - 'schema_qualified_name' => $db->getDatabaseName() . '.' . $collectionName, - 'size' => null, - 'comment' => null, - 'collation' => null, - 'engine' => null, - ]; - } - - usort($collections, fn ($a, $b) => $a['name'] <=> $b['name']); - - return $collections; + return $this->getCollectionRows('view', $schema); } /** @@ -224,6 +190,7 @@ public function getViews($schema = null) * * @return array */ + #[Override] public function getTableListing($schema = null, $schemaQualified = false) { $collections = []; @@ -247,6 +214,7 @@ public function getTableListing($schema = null, $schemaQualified = false) return $collections; } + #[Override] public function getColumns($table) { $db = null; @@ -254,7 +222,7 @@ public function getColumns($table) [$db, $table] = explode('.', $table, 2); } - $stats = $this->connection->getDatabase($db)->selectCollection($table)->aggregate([ + $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 @@ -305,6 +273,7 @@ public function getColumns($table) return $columns; } + #[Override] public function getIndexes($table) { $collection = $this->connection->getDatabase()->selectCollection($table); @@ -359,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); @@ -389,7 +364,7 @@ public function getCollection($name) } /** - * Get all of the collections names for the database. + * Get all the collections names for the database. * * @deprecated * @@ -418,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/Session/MongoDbSessionHandler.php b/src/Session/MongoDbSessionHandler.php index dd57f2a3c..3677ea758 100644 --- a/src/Session/MongoDbSessionHandler.php +++ b/src/Session/MongoDbSessionHandler.php @@ -16,6 +16,7 @@ use MongoDB\BSON\Document; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; +use Override; use function tap; use function time; @@ -32,6 +33,7 @@ public function close(): bool return true; } + #[Override] public function gc($lifetime): int { $result = $this->getCollection()->deleteMany(['last_activity' => ['$lt' => $this->getUTCDateTime(-$lifetime)]]); @@ -39,6 +41,7 @@ public function gc($lifetime): int return $result->getDeletedCount() ?? 0; } + #[Override] public function destroy($sessionId): bool { $this->getCollection()->deleteOne(['_id' => (string) $sessionId]); @@ -46,6 +49,7 @@ public function destroy($sessionId): bool return true; } + #[Override] public function read($sessionId): string|false { $result = $this->getCollection()->findOne( @@ -63,6 +67,7 @@ public function read($sessionId): string|false return false; } + #[Override] public function write($sessionId, $data): bool { $payload = $this->getDefaultPayload($data); @@ -87,6 +92,7 @@ public function createTTLIndex(): void ); } + #[Override] protected function getDefaultPayload($data): array { $payload = [ 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/ConnectionTest.php b/tests/ConnectionTest.php index 75761080e..de77da7f7 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -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')] @@ -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/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/Models/Anniversary.php b/tests/Models/Anniversary.php index fb78c9a55..c37196c16 100644 --- a/tests/Models/Anniversary.php +++ b/tests/Models/Anniversary.php @@ -6,16 +6,10 @@ 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 $anniversary - * @mixin Eloquent - * @method static Builder create(...$values) - * @method static Builder truncate() - * @method static Eloquent sole(...$parameters) */ class Anniversary extends Model { 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/Soft.php b/tests/Models/Soft.php index f887d05a9..999d13fd4 100644 --- a/tests/Models/Soft.php +++ b/tests/Models/Soft.php @@ -6,10 +6,10 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use MongoDB\Laravel\Eloquent\Builder; use MongoDB\Laravel\Eloquent\DocumentModel; use MongoDB\Laravel\Eloquent\MassPrunable; -use MongoDB\Laravel\Eloquent\SoftDeletes; /** @property Carbon $deleted_at */ class Soft extends Model diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 2f3d77eec..158fb5084 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -1068,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/SchemaTest.php b/tests/SchemaTest.php index 3257a671e..9726eb705 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -20,12 +20,17 @@ 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')->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(); @@ -33,204 +38,237 @@ public function tearDown(): void 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'])); @@ -239,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(); }); @@ -315,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]); @@ -343,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]); @@ -366,37 +404,45 @@ 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::connection('mongodb')->getDatabase()->createCollection('test_view', ['viewOn' => 'newcollection']); + $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(); @@ -407,29 +453,34 @@ public function testGetTables() $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 . '.newcollection', $table['schema_qualified_name']); + $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 "newcollection" not found'); + $this->fail('Collection "' . self::COLL_1 . '" not found'); } } public function testGetViews() { - 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']); $dbName = DB::connection('mongodb')->getDatabaseName(); - DB::connection('mongodb')->getDatabase()->createCollection('test_view', ['viewOn' => 'newcollection']); + DB::connection('mongodb')->getDatabase()->createCollection('test_view', ['viewOn' => self::COLL_1]); $tables = Schema::getViews(); @@ -443,7 +494,7 @@ public function testGetViews() $this->assertArrayHasKey('schema_qualified_name', $table); // Ensure "normal collections" are not in the views list - $this->assertNotEquals('newcollection', $table['name'], 'Normal collections should not be included in the result of getViews.'); + $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']); @@ -459,45 +510,45 @@ public function testGetViews() 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('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']); $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 . '.newcollection', $tables); - $this->assertContains($dbName . '.newcollection_two', $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('newcollection', $tables); - $this->assertContains('newcollection_two', $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); @@ -528,7 +579,7 @@ public function testGetColumns() $this->assertSame([], $columns); // Qualified table name - $columns = Schema::getColumns(DB::getDatabaseName() . '.newcollection'); + $columns = Schema::getColumns(DB::getDatabaseName() . '.' . self::COLL_1); $this->assertIsArray($columns); $this->assertCount(5, $columns); } @@ -536,12 +587,12 @@ public function testGetColumns() /** @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); @@ -587,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, @@ -598,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']); @@ -606,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); } @@ -618,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'], @@ -626,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']); @@ -634,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;