diff --git a/.doctrine-project.json b/.doctrine-project.json index 362e4d7f4f..dfb3923004 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -6,86 +6,79 @@ "docsSlug": "doctrine-mongodb-odm", "versions": [ { - "name": "2.13", - "branchName": "2.13.x", + "name": "2.14", + "branchName": "2.14.x", "slug": "latest", "upcoming": true }, { - "name": "2.12", - "branchName": "2.12.x", - "slug": "latest", + "name": "2.13", + "branchName": "2.13.x", + "slug": "2.13", "current": true }, + { + "name": "2.12", + "slug": "2.12", + "maintained": false + }, { "name": "2.11", - "branchName": "2.11.x", "slug": "2.11", "maintained": false }, { "name": "2.10", - "branchName": "2.10.x", "slug": "2.10", "maintained": false }, { "name": "2.9", - "branchName": "2.9.x", "slug": "2.9", "maintained": false }, { "name": "2.8", - "branchName": "2.8.x", "slug": "2.8", "maintained": false }, { "name": "2.7", - "branchName": "2.7.x", "slug": "2.7", "maintained": false }, { "name": "2.6", - "branchName": "2.6.x", "slug": "2.6", "maintained": false }, { "name": "2.5", - "branchName": "2.5.x", "slug": "2.5", "maintained": false }, { "name": "2.4", - "branchName": "2.4.x", "slug": "2.4", "maintained": false }, { "name": "2.3", - "branchName": "2.3.x", "slug": "2.3", "maintained": false }, { "name": "2.2", - "branchName": "2.2.x", "slug": "2.2", "maintained": false }, { "name": "2.1", - "branchName": "2.1.x", "slug": "2.1", "maintained": false }, { "name": "2.0", - "branchName": "2.0.x", "slug": "2.0", "maintained": false }, @@ -97,19 +90,16 @@ }, { "name": "1.2", - "branchName": "1.2.x", "slug": "1.2", "maintained": false }, { "name": "1.1", - "branchName": "1.1.x", "slug": "1.1", "maintained": false }, { "name": "1.0", - "branchName": "1.0.x", "slug": "1.0", "maintained": false } diff --git a/.gitattributes b/.gitattributes index c6f19fd536..f1acdd5dba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,15 +2,11 @@ * text=auto eol=lf # Exclude non-essential files from dist -/.github export-ignore +/.* export-ignore +/*.dist export-ignore +/.neon export-ignore /docs export-ignore +/benchmark export-ignore +/phpbench.json export-ignore /tests export-ignore /tools export-ignore -/.doctrine-project.json export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.gitmodules export-ignore -/.scrutinizer.yml export-ignore -/phpcs.xml.dist export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore diff --git a/.github/workflows/atlas-ci.yml b/.github/workflows/atlas-ci.yml new file mode 100644 index 0000000000..e113452fd0 --- /dev/null +++ b/.github/workflows/atlas-ci.yml @@ -0,0 +1,99 @@ +name: "Atlas CI" + +on: + pull_request: + branches: + - "*.x" + - "feature/*" + push: + +jobs: + atlas-local: + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + php-version: + - "8.4" + symfony: + - "stable" + proxy: + - "lazy-ghost" + include: + # Test with ProxyManager + - php-version: "8.1" + symfony: "6.4" + proxy: "proxy-manager" + os: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + with: + fetch-depth: 2 + + - name: "Create MongoDB Atlas Local" + run: | + docker run --name mongodb -p 27017:27017 --detach mongodb/mongodb-atlas-local:8.2 + 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-version }} + extensions: "mongodb, bcmath" + key: "extcache-v1" + + - name: Cache extensions + uses: actions/cache@v4 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + tools: "pecl" + extensions: "mongodb, bcmath" + coverage: "none" + ini-values: "zend.assertions=1" + + - name: "Show driver information" + run: "php --ri mongodb" + + # Not used, skip transient dependencies + - name: "Remove phpbench/phpbench" + run: composer remove --no-update --dev phpbench/phpbench + + - name: "Configure Symfony ${{ matrix.symfony }}" + if: "${{ matrix.symfony != 'stable' }}" + run: | + composer config minimum-stability dev + # update symfony deps + composer require --no-update symfony/console:^${{ matrix.symfony }} + composer require --no-update symfony/var-dumper:^${{ matrix.symfony }} + composer require --no-update --dev symfony/cache:^${{ matrix.symfony }} + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--prefer-dist" + + - name: "Run PHPUnit with Atlas Local" + run: "vendor/bin/phpunit --group atlas" + env: + DOCTRINE_MONGODB_SERVER: "mongodb://127.0.0.1:27017/?directConnection=true" + USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }} diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 4de5e5ff3b..31983556ce 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -11,4 +11,4 @@ on: jobs: coding-standards: name: "Coding Standards" - uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.0.0" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 54410af6da..16407bfd99 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -148,7 +148,7 @@ jobs: composer-options: "--prefer-dist" - name: "Install latest Python version" - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' @@ -159,7 +159,7 @@ jobs: topology: ${{ matrix.topology }} - name: "Run PHPUnit" - run: "vendor/bin/phpunit" + run: "vendor/bin/phpunit --exclude-group=atlas" env: DOCTRINE_MONGODB_SERVER: ${{ steps.setup-mongodb.outputs.cluster-uri }} USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }}" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 8cf3021054..1e87083d83 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -15,30 +15,6 @@ on: - docs/** jobs: - validate-with-guides: - name: "Validate documentation with phpDocumentor/guides" - runs-on: "ubuntu-24.04" - - steps: - - name: "Checkout code" - uses: "actions/checkout@v5" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.3" - - - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@v3" - with: - working-directory: "docs" - dependency-versions: "highest" - - - name: "Add orphan metadata where needed" - run: | - printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst - printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/reference/annotations-reference.rst)" > docs/en/reference/annotations-reference.rst - - - name: "Run guides-cli" - run: "docs/vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )" + documentation: + name: "Generate documentation" + uses: "doctrine/.github/.github/workflows/documentation.yml@12.0.0" diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index 951002cd7d..07f079fd34 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -8,7 +8,9 @@ on: jobs: release: name: "Git tag, release & create merge-up PR" - uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.0.0" + with: + use-next-minor-as-default-branch: true secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} diff --git a/.github/workflows/website-schema.yml b/.github/workflows/website-schema.yml index 3b30ece554..66202e2005 100644 --- a/.github/workflows/website-schema.yml +++ b/.github/workflows/website-schema.yml @@ -18,4 +18,4 @@ on: jobs: json-validate: name: "Validate JSON schema" - uses: "doctrine/.github/.github/workflows/website-schema.yml@7.3.0" + uses: "doctrine/.github/.github/workflows/website-schema.yml@12.0.0" diff --git a/.gitignore b/.gitignore index 68a3971619..859328211b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +docs/output tools/sandbox/Proxies/* tools/sandbox/Hydrators/* tools/sandbox/PersistentCollections/* diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 2b8f298592..6296fa4c12 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -14,6 +14,11 @@ The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been removed. Custom ID generators must implement the `Doctrine\ODM\MongoDB\Id\IdGenerator` interface. +The `Doctrine\ODM\MongoDB\Id\UuidGenerator` class has been removed. Use a custom +generator to generate string UUIDs. For more efficient storage of UUIDs, use the +`Doctrine\ODM\MongoDB\Types\BinaryUuidType` type in combination with the +`Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator` generator. + ## Metadata The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final and will no longer be extendable. diff --git a/composer.json b/composer.json index 86dc1ad247..0f9d2fd142 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,9 @@ { "name": "Ion Bazan", "email": "ion.bazan@gmail.com" }, { "name": "Fran Moreno", "email": "franmomu@gmail.com" } ], + "scripts": { + "docs": "composer update -d docs && ./docs/vendor/bin/build-docs.sh @additional_args" + }, "require": { "php": "^8.1", "ext-mongodb": "^1.21 || ^2.0", @@ -40,16 +43,17 @@ "require-dev": { "ext-bcmath": "*", "doctrine/annotations": "^1.12 || ^2.0", - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^14.0", "doctrine/orm": "^3.2", "jmikola/geojson": "^1.0", "phpbench/phpbench": "^1.0.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.4", - "squizlabs/php_codesniffer": "^3.5", - "symfony/cache": "^5.4 || ^6.0 || ^7.0" + "phpunit/phpunit": "^10.5.58", + "squizlabs/php_codesniffer": "^4", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "doctrine/annotations": "<1.12 || >=3.0" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000..3226d135fa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +version: '3.8' + +services: + mongodb-atlas-local: + image: mongodb/mongodb-atlas-local + ports: + - "27018:27017" diff --git a/docs/composer.json b/docs/composer.json index 69c28e54a2..89f8a2b480 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -1,5 +1,5 @@ { "require-dev": { - "phpdocumentor/guides-cli": "^1.2" + "doctrine/docs-builder": "^1.0.3" } } diff --git a/docs/en/cookbook/vector-search.rst b/docs/en/cookbook/vector-search.rst new file mode 100644 index 0000000000..e952ddcfea --- /dev/null +++ b/docs/en/cookbook/vector-search.rst @@ -0,0 +1,186 @@ +Vector Search Cookbook +====================== + +This tutorial demonstrates how to use `MongoDB Atlas Vector Search`_ with Doctrine +MongoDB ODM. Vector search enables semantic queries over vector embeddings, +which are typically generated by an embedding system. + +Step 1: Generate Vector Embeddings +---------------------------------- + +Before storing data, you need to generate vector embeddings for your documents. +You can use an embedding system such as Symfony AI to convert text or other +data into a ``float[]`` vector. + +Example using `Voyage AI`_ and `Symfony AI`_: + +.. code-block:: php + + use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory; + + $platform = PlatformFactory::create(getenv('VOYAGE_API_KEY')); + $vectors = $platform->invoke('voyage-3', <<<'TEXT' + Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and rivers. + The people of Japan were very kind and hardworking. They loved their country very much and took care of it. The + country was very peaceful and prosperous. The people lived happily ever after. + TEXT)->asVectors(); + +Step 2: Define the Model +------------------------ + +Annotate your document with :doc:`#[VectorSearchIndex] attribute ` +and define a vector field of type ``float[]``. +The number of dimensions must match the embedding vector size (e.g., 1024). +The similarity metric can be either cosine, euclidean or dotProduct; they all +return the same result because Voyage AI uses normalized vectors to length 1. + +.. code-block:: php + + use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; + use Doctrine\ODM\MongoDB\Types\Type; + use Symfony\AI\Platform\Vector\Vector; + + #[ODM\Document] + #[ODM\VectorSearchIndex( + fields: [ + [ + 'type' => 'vector', + 'path' => 'voyage3Vector', + 'numDimensions' => 1024, + 'similarity' => ClassMetadata::VECTOR_SIMILARITY_DOT_PRODUCT, + ], + [ + 'type' => 'filter', + 'path' => 'published', + ], + ], + name: 'default', + )] + class Guide + { + #[ODM\Id] + public ?string $id = null; + + #[ODM\Field] + public bool $published = false; + + #[ODM\Field] + public ?string $content = null; + + /** @var list|null Embedding vector created from $content */ + #[ODM\Field(type: Type::COLLECTION)] + public ?array $voyage3Vector = null; + + /** @param list|Vector $vector */ + public function setVoyage3Vector(array|Vector $vector): void + { + if ($vector instanceof Vector) { + if ($vector->getDimensions() !== 1024) { + throw new InvalidArgumentException('The embedding vector must have 1024 dimensions.'); + } + + $vector = $vector->getData(); + } + + if (count($vector) !== 1024) { + throw new InvalidArgumentException('The embedding vector must have 1024 dimensions.'); + } + + $this->voyage3Vector = $vector; + } + } + +Step 3: Create the Collection and Insert Documents +-------------------------------------------------- + +Use the ``SchemaManager`` to create the collection and insert documents with vector embeddings. + +.. code-block:: php + + $schemaManager = $dm->getSchemaManager(); + $schemaManager->createDocumentCollection(Guide::class); + +Insert documents: + +.. code-block:: php + + $doc1 = new Guide(); + $doc1->published = true; + $doc1->content = 'First document'; + + $doc2 = new Guide(); + $doc2->published = false; + + $dm->persist($doc1); + $dm->persist($doc2); + $dm->flush(); + +The vector values of each document can be set later, typically using an asynchronous process: + +.. code-block:: php + + $vector1 = $embeddingPlatform->invoke($doc1->content)->asVectors()[0]; + $doc1->setVoyage3Vector($vector1); + + $vector2 = $embeddingPlatform->invoke($doc2->content)->asVectors()[0]; + $doc2->setVoyage3Vector($vector2); + + $dm->flush(); + + +Step 4: Create the Vector Search Index +-------------------------------------- + +When updating documents, the vector search index is asynchronously updated by +MongoDB Atlas. You have to wait a few seconds before the changes are reflected +in search results. + +.. code-block:: php + + $schemaManager->createDocumentSearchIndexes(Guide::class); + + +If the vector search index created after inserting documents, the index is +marked as "READY" when all existing documents are indexed. You can wait for +the index to be ready using the following code: + +.. code-block:: php + + $schemaManager->waitForSearchIndexes([Guide::class]); + +Step 5: Run a Vector Search Aggregation +--------------------------------------- + +Use the aggregation builder to run a vector search query: + +.. code-block:: php + + $results = $dm->createAggregationBuilder(Guide::class) + ->vectorSearch() + ->index('default') + ->path('voyage3Vector') + ->queryVector($vector) + ->filter($qb->expr()->field('published')->equals(true)) + ->numCandidates(10) + ->limit(10) + ->set() + ->field('score') + ->expression(['$meta' => 'vectorSearchScore']) + ->getAggregation()->execute()->toArray(); + + var_dump($results); + +Notes +----- +- Vector embeddings should be generated using a reliable embedding system +- The vector field must be of type ``float[]``, ``int[]`` or ``bool[]``, it + must match with the embedding vector type and dimensions. +- The ``#[VectorSearchIndex]`` annotation configures the index for vector search +- Use the aggregation builder's ``vectorSearch`` stage to query for similar vectors. +- Doctrine ODM 2.13+ is required for vector search support. + + +.. _`MongoDB Atlas Vector Search`: +.. _`Voyage AI`: https://www.voyageai.com/ +.. _`Symfony AI`: https://symfony.com/ai diff --git a/docs/en/reference/aggregation-stage-reference.rst b/docs/en/reference/aggregation-stage-reference.rst index a5926aec16..0c64de1dac 100644 --- a/docs/en/reference/aggregation-stage-reference.rst +++ b/docs/en/reference/aggregation-stage-reference.rst @@ -31,6 +31,7 @@ Doctrine MongoDB ODM provides integration for the following aggregation pipeline - `$skip `_ - `$sort `_ - `$sortByCount `_ +- `$vectorSearch `_ - `$unionWith `_ - `$unset `_ - `$unwind `_ @@ -43,6 +44,10 @@ Doctrine MongoDB ODM provides integration for the following aggregation pipeline documentation to ensure that the pipeline stage is available in the MongoDB version you are using. +.. note:: + + Support for ``$vectorSearch`` was added in Doctrine MongoDB ODM 2.13. + $addFields ---------- @@ -689,6 +694,11 @@ number of available operators, please refer to the `MongoDB documentation `_ for a reference of all available operators. +.. note:: + A `Search index `_ + is required for this stage. See the :doc:`#[SearchIndex] attribute <../reference/attributes-reference#search_index>` + for details on how to define it. + .. code-block:: php fields('title', 'content') ; +To combine multiple search operators, use the `compound operator `_: + +.. code-block:: php + + createAggregationBuilder(\Documents\Fruits::class); + $builder + ->compound() + ->must() + ->text()->query('varieties')->path('description') + ->should(minimumShouldMatch: 1) + ->text()->query('Fuji')->path('description') + ->text()->query('Golden Delicious')->path('description') + ; + +This aggregation will return `Fruits` documents from whose `description` field +contains the word "varieties" (must), and should also contain either "Fuji" or +"Golden Delicious" in the `description` field (at least one due to +`minimumShouldMatch: 1`). + $set ---- @@ -785,6 +816,41 @@ The example above is equivalent to the following pipeline: ->sort(['count' => -1]) ; +$vectorSearch +------------- + +The ``$vectorSearch`` stage performs a vector similarity search on the specified +field or fields which must be covered by an Atlas Vector Search index. +``$vectorSearch`` must be the first stage in the aggregation pipeline. + +.. note:: + A `Vector Search index `_ + is required for this stage. See the :doc:`#[VectorSearchIndex] attribute <../reference/attributes-reference#vector_search_index>` + for details on how to define it. + +.. code-block:: php + + createAggregationBuilder(\Documents\Products::class); + $builder + ->vectorSearch() + ->index('vectorIndexName') + ->path('vectorField') + ->filter( + $builder->matchExpr() + ->field('status') + ->notEqual('discontinued') + ) + ->queryVector([0.1, 0.2, 0.3, 0.4, 0.5]) + ->numCandidates($limit * 20) + ->limit($limit) + ->project() + ->field('_id')->expression(0) + ->field('product')->expression('$$ROOT') + ->field('score')->meta('vectorSearchScore'); + ; + $unionWith ---------- diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index b2786dfa4e..adbaea13f9 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -1,3 +1,4 @@ +:orphan: Attributes Reference ===================== diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst index 601934d6ef..f51164cfce 100644 --- a/docs/en/reference/attributes-reference.rst +++ b/docs/en/reference/attributes-reference.rst @@ -1167,6 +1167,10 @@ Optional arguments: This attribute is used to specify :ref:`search indexes ` for `MongoDB Atlas Search `__. +.. note:: + + For vector search indexes, see :ref:`vector_search_index` below. + The arguments correspond to arguments for `MongoDB\Collection::createSearchIndex() `__. Excluding ``name``, arguments are used to create the @@ -1397,6 +1401,73 @@ for the related collection. // rest of the class code... } +#[VectorSearchIndex] +-------------------- + +.. _vector_search_index: + +The ``#[VectorSearchIndex]`` attribute is used to define a vector search index +on a document class. This enables efficient similarity search on vector fields, +such as those used for machine learning embeddings. + +Optional arguments: + +- ``name``: (optional) The name of the vector search index. If omitted, a default name is used. +- ``fields`` (required): A list of field definitions. Each field definition is an associative array describing a vector or filter field. For vector fields, the following keys are supported: + + - ``type``: Must be set to ``'vector'`` for vector fields or ``'filter'`` for filter fields. + - ``path``: The name of the field in the document to index. + - ``numDimensions``: (vector fields only) The number of dimensions in the vector. + - ``similarity``: (vector fields only) The vector similarity function to use. Supported values include ``'euclidean'``, ``'cosine'``, and ``'dotProduct'``. Use the constants from ``Doctrine\ODM\MongoDB\Mapping\ClassMetadata::VECTOR_SIMILARITY_*`` for best compatibility. + - ``quantization``: (vector fields only, optional) The quantization method, e.g., ``'scalar'``. + - ``hnswOptions``: (vector fields only, optional) Options for the HNSW algorithm: ``maxEdges`` and ``numEdgeCandidates``. + + For filter fields, only ``type: 'filter'`` and ``path`` are required. + + +Example: + +.. code-block:: php + + 'vector', + 'path' => 'plotEmbeddingVoyage3Large', + 'numDimensions' => 2048, + 'similarity' => ClassMetadata::VECTOR_SIMILARITY_DOT_PRODUCT, + 'quantization' => ClassMetadata::VECTOR_QUANTIZATION_SCALAR, + ], + [ + 'type' => 'filter', + 'path' => 'category', + ], + ], + )] + class VectorEmbedding + { + #[Id] + public ?string $id = null; + + /** @var list */ + #[Field(type: Type::COLLECTION)] + public array $plotEmbeddingVoyage3Large = []; + + #[Field] + public string $category; + } + +For more details, see the MongoDB documentation on `Atlas Vector Search `_. + #[Version] ---------- diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 397ff3df91..90ba5a8d60 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -151,6 +151,10 @@ Here is a quick overview of the built-in mapping types: - ``raw`` - ``string`` - ``timestamp`` +- ``uuid`` +- ``vector_float32`` +- ``vector_int8`` +- ``vector_packed_bit`` You can read more about the available MongoDB types on `php.net `_. @@ -164,20 +168,24 @@ You can read more about the available MongoDB types on `php.net `_ to ``MongoDB\BSON\Binary`` instance with a "uuid" type +- ``vector_float32``: list of floats to ``MongoDB\BSON\Binary`` instance with vector type "Float32" +- ``vector_int8``: list of integers to ``MongoDB\BSON\Binary`` instance with vector type "Int8" +- ``vector_packed_bit``: list of booleans to ``MongoDB\BSON\Binary`` instance with vector type "PackedBit" .. note:: @@ -187,6 +195,10 @@ This list explains some of the less obvious mapping types: suitable you should either use an embedded document or use formats provided by the MongoDB driver (e.g. ``\MongoDB\BSON\UTCDateTime`` instead of ``\DateTime``). +.. note:: + + The vector types require the MongoDB PHP extension version 2.2.0 or higher. + .. _reference-php-mapping-types: PHP Types Mapping @@ -206,6 +218,7 @@ follows: - ``float``: ``float`` - ``int``: ``int`` - ``string``: ``string`` +- ``Symfony\Component\Uid\Uuid``: ``uuid`` Doctrine can also autoconfigure any backed ``enum`` it encounters: ``type`` will be set to ``string`` or ``int``, depending on the enum's backing type, @@ -269,13 +282,23 @@ Here is an example: You can configure custom ID strategies if you don't want to use the default object ID. The available strategies are: -- ``AUTO`` - Uses the native generated ObjectId. +- ``AUTO`` - Automatically generates an ObjectId or Symfony UUID depending on the identifier type. - ``ALNUM`` - Generates an alpha-numeric string (based on an incrementing value). - ``CUSTOM`` - Defers generation to an implementation of ``IdGenerator`` specified in the ``class`` option. - ``INCREMENT`` - Uses another collection to auto increment an integer identifier. -- ``UUID`` - Generates a UUID identifier. +- ``UUID`` - Generates a UUID identifier (deprecated). - ``NONE`` - Do not generate any identifier. ID must be manually set. +When using the ``AUTO`` strategy in combination with a UUID identifier, the generator can create UUIDs of type 1, type 4, +and type 7 automatically. For all other UUID types, assign the identifier manually in combination with the ``NONE`` +strategy. + +.. note:: + + The ``UUID`` generator is deprecated, as it stores UUIDs as strings. It is recommended to use the ``AUTO`` strategy + with a ``uuid`` type identifier field instead. If you need to keep generating string UUIDs, you can use the + ``CUSTOM`` strategy with your own generator. + Here is an example how to manually set a string identifier for your documents: .. configuration-block:: diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 97cd2f32df..dae01691f2 100644 --- a/docs/en/reference/transactions-and-concurrency.rst +++ b/docs/en/reference/transactions-and-concurrency.rst @@ -105,7 +105,7 @@ a ``LockException`` is thrown, which indicates that the document was already mod .. note:: Only types implementing the ``\Doctrine\ODM\MongoDB\Types\Versionable`` interface can be used for versioning. - Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, and ``date_immutable``. + Following ODM types can be used for versioning: ``int``, ``decimal128``, ``date``, ``date_immutable``, and ``object_id``. Document Configuration ^^^^^^^^^^^^^^^^^^^^^^ @@ -190,7 +190,8 @@ Choosing the Field Type """"""""""""""""""""""" When using the date-based type in a high-concurrency environment, it is still possible to create multiple documents -with the same version and cause a conflict. This can be avoided by using the ``int`` or ``decimal128`` type. +with the same version and cause a conflict. This can be avoided by using the ``int``, ``decimal128``, or ``object_id`` type. +The ``object_id`` type contains the timestamp of its creation, but also a random value to ensure uniqueness. Usage """"" diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 13e3274c32..4080829210 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -1,63 +1,56 @@ -.. toc:: - - .. tocheader:: Tutorials - - .. toctree:: - :depth: 3 - :glob: - - tutorials/* - -.. toc:: - - .. tocheader:: Reference - - .. toctree:: - :depth: 3 - - reference/introduction - reference/architecture - reference/console-commands - reference/basic-mapping - reference/custom-mapping-types - reference/reference-mapping - reference/bidirectional-references - reference/complex-references - reference/indexes - reference/search-indexes - reference/inheritance-mapping - reference/embedded-mapping - reference/trees - reference/storing-files-with-gridfs - reference/xml-mapping - reference/attributes-reference - reference/metadata-drivers - reference/working-with-objects - reference/document-repositories - reference/events - reference/migrating-schemas - reference/query-builder-api - reference/aggregation-builder - reference/aggregation-stage-reference - reference/geospatial-queries - reference/find-and-update - reference/upserting-documents - reference/filters - reference/priming-references - reference/capped-collections - reference/storage-strategies - reference/custom-collections - reference/sharding - reference/transactions-and-concurrency - reference/best-practices - reference/change-tracking-policies - -.. toc:: - - .. tocheader:: Cookbook - - .. toctree:: - :depth: 3 - :glob: - - cookbook/* +:orphan: + +.. toctree:: + :caption: Tutorials + :depth: 3 + :glob: + + tutorials/* + +.. toctree:: + :caption: Reference + :depth: 3 + + reference/introduction + reference/architecture + reference/console-commands + reference/basic-mapping + reference/custom-mapping-types + reference/reference-mapping + reference/bidirectional-references + reference/complex-references + reference/indexes + reference/search-indexes + reference/inheritance-mapping + reference/embedded-mapping + reference/trees + reference/storing-files-with-gridfs + reference/xml-mapping + reference/attributes-reference + reference/metadata-drivers + reference/working-with-objects + reference/document-repositories + reference/events + reference/migrating-schemas + reference/query-builder-api + reference/aggregation-builder + reference/aggregation-stage-reference + reference/geospatial-queries + reference/find-and-update + reference/upserting-documents + reference/filters + reference/priming-references + reference/capped-collections + reference/storage-strategies + reference/custom-collections + reference/sharding + reference/transactions-and-concurrency + reference/best-practices + reference/change-tracking-policies + +.. toctree:: + :caption: Cookbook + :depth: 3 + :glob: + + cookbook/* diff --git a/doctrine-mongo-mapping.xsd b/doctrine-mongo-mapping.xsd index 6ff939cd59..c7dda0342a 100644 --- a/doctrine-mongo-mapping.xsd +++ b/doctrine-mongo-mapping.xsd @@ -157,6 +157,7 @@ + @@ -640,6 +641,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php index 013e7e1bdd..4169f237a2 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php @@ -4,6 +4,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation; +use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Iterator\CachingIterator; use Doctrine\ODM\MongoDB\Iterator\HydratingIterator; @@ -11,10 +12,14 @@ use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\SchemaException; use MongoDB\Collection; use MongoDB\Driver\CursorInterface; use function array_merge; +use function current; +use function in_array; +use function key; /** @phpstan-import-type PipelineExpression from Builder */ final class Aggregation implements IterableResult @@ -64,6 +69,45 @@ private function prepareIterator(CursorInterface $cursor): Iterator $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->classMetadata); } - return $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor); + $iterator = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor); + + $this->assertSearchIndexExistsForEmptyResult($iterator); + + return $iterator; + } + + /** + * If the server implements a server-side error for missing search indexes, + * this assertion can be removed. + * + * @see https://jira.mongodb.org/browse/SERVER-110974 + * @see Configuration::setAssertSearchIndexExistsForEmptyResult() + * + * @param CachingIterator|UnrewindableIterator $iterator + */ + private function assertSearchIndexExistsForEmptyResult(CachingIterator|UnrewindableIterator $iterator): void + { + // The iterator is always rewinded + if ($iterator->key() !== null) { + return; // Results not empty + } + + if (! $this->dm->getConfiguration()->assertSearchIndexExistsForEmptyResult()) { + return; // Feature disabled + } + + // Search stages must be the first stage in the pipeline + $stage = $this->pipeline[0] ?? null; + if (! $stage || ! in_array(key($stage), ['$search', '$searchMeta', '$vectorSearch'], true)) { + return; // Not a search aggregation + } + + // @phpcs:ignore SlevomatCodingStandard.PHP.UselessParentheses + $indexName = ((object) current($stage))->index ?? 'default'; + if ($this->collection->listSearchIndexes(['filter' => ['name' => $indexName]])->key() !== null) { + return; // Index exists + } + + throw SchemaException::searchIndexNotFound($this->collection->getNamespace(), $indexName); } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php index 4293a9c6b9..444af0dd50 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php @@ -570,7 +570,7 @@ public function sample(int $size): Stage\Sample */ public function search(): Stage\Search { - $stage = new Stage\Search($this); + $stage = new Stage\Search($this, $this->getDocumentPersister()); return $this->addStage($stage); } @@ -652,6 +652,19 @@ public function sortByCount(string $expression): Stage\SortByCount return $this->addStage($stage); } + /** + * The $vectorSearch stage performs a vector similarity search on the specified + * field which must be covered by an Atlas Vector Search index. + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#mongodb-pipeline-pipe.-vectorSearch + */ + public function vectorSearch(): Stage\VectorSearch + { + $stage = new Stage\VectorSearch($this, $this->getDocumentPersister()); + + return $this->addStage($stage); + } + /** * Performs a union of two collections. $unionWith combines pipeline results * from two collections into a single result set. The stage outputs the diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php index 03bf828d8a..20f642d929 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php @@ -122,11 +122,10 @@ public function add($expression1, $expression2, ...$expressions): static */ public function addAnd($expression, ...$expressions): static { - if (! isset($this->expr['$and'])) { - $this->expr['$and'] = []; - } - - $this->expr['$and'] = array_merge($this->expr['$and'], array_map([$this, 'prepareArgument'], func_get_args())); + $this->expr['$and'] = array_merge( + $this->expr['$and'] ?? [], + array_map($this->prepareArgument(...), func_get_args()), + ); return $this; } @@ -141,11 +140,10 @@ public function addAnd($expression, ...$expressions): static */ public function addOr($expression, ...$expressions): static { - if (! isset($this->expr['$or'])) { - $this->expr['$or'] = []; - } - - $this->expr['$or'] = array_merge($this->expr['$or'], array_map([$this, 'prepareArgument'], func_get_args())); + $this->expr['$or'] = array_merge( + $this->expr['$or'] ?? [], + array_map($this->prepareArgument(...), func_get_args()), + ); return $this; } @@ -1116,7 +1114,7 @@ private function prepareArgument($expression) } if (is_array($expression)) { - return array_map([$this, 'prepareArgument'], $expression); + return array_map($this->prepareArgument(...), $expression); } if ($expression instanceof self) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php index 3d90d55924..8343bcb3e4 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php @@ -368,10 +368,14 @@ public function sample(int $size): Stage\Sample * The $search stage performs a full-text search on the specified field or * fields which must be covered by an Atlas Search index. * + * @deprecated Since doctrine/mongodb-odm 2.13. This $search stage must be the first of the pipeline, use Builder::search() instead. + * * @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#mongodb-pipeline-pipe.-search */ public function search(): Stage\Search { + trigger_deprecation('doctrine/mongodb-odm', '2.13', 'Using "%s" is deprecated because the $search stage must be the first of the pipeline, use "%s::search()" instead.', __METHOD__, Builder::class); + return $this->builder->search(); } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php index bf3a74fd95..478914f65c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php @@ -79,7 +79,7 @@ abstract protected function getStageName(): string; private function convertExpression($expression) { if (is_array($expression)) { - return array_map([$this, 'convertExpression'], $expression); + return array_map($this->convertExpression(...), $expression); } if (is_string($expression) && substr($expression, 0, 1) === '$') { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php index 1f39d4f95f..589ff2ae4e 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php @@ -43,7 +43,7 @@ protected function getReplaceExpression() private function convertExpression($expression) { if (is_array($expression)) { - return array_map([$this, 'convertExpression'], $expression); + return array_map($this->convertExpression(...), $expression); } if (is_string($expression) && substr($expression, 0, 1) === '$') { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AddFields.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AddFields.php index da431921ed..9c6749352e 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AddFields.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AddFields.php @@ -10,7 +10,7 @@ * Fluent interface for adding a $addFields stage to an aggregation pipeline. * * @phpstan-import-type OperatorExpression from Expr - * @phpstan-type AddFieldsStageExpression array{'$addFields': array} + * @phpstan-type AddFieldsStageExpression array{"$addFields": array} * @final */ class AddFields extends Operator diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/CollStats.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/CollStats.php index 881c067215..7848ecabf0 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/CollStats.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/CollStats.php @@ -11,7 +11,7 @@ * Fluent interface for adding a $collStats stage to an aggregation pipeline. * * @phpstan-type CollStatsStageExpression array{ - * '$collStats': array{ + * "$collStats": array{ * latencyStats?: array{histograms?: bool}, * storageStats?: array{}, * } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Count.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Count.php index 008e6a52fb..9fc3dcf2dd 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Count.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Count.php @@ -10,7 +10,7 @@ /** * Fluent interface for adding a $count stage to an aggregation pipeline. * - * @phpstan-type CountStageExpression array{'$count': string} + * @phpstan-type CountStageExpression array{"$count": string} */ class Count extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php index 50f01ac755..a6e4e2591c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php @@ -13,10 +13,10 @@ /** * Fluent interface for adding a $densify stage to an aggregation pipeline. * - * @phpstan-type BoundsType 'full'|'partition'|array{0: int|float|UTCDateTime, 1: int|float|UTCDateTime} - * @phpstan-type UnitType 'year'|'month'|'week'|'day'|'hour'|'minute'|'second'|'millisecond' + * @phpstan-type BoundsType "full"|"partition"|array{0: int|float|UTCDateTime, 1: int|float|UTCDateTime} + * @phpstan-type UnitType "year"|"month"|"week"|"day"|"hour"|"minute"|"second"|"millisecond" * @phpstan-type DensifyStageExpression array{ - * '$densify': object{ + * "$densify": object{ * field: string, * partitionByFields?: list, * range: object{ @@ -55,7 +55,7 @@ public function partitionByFields(string ...$fields): static * @param array|string $bounds * @param int|float $step * @phpstan-param BoundsType $bounds - * @phpstan-param ''|UnitType $unit + * @phpstan-param ""|UnitType $unit */ public function range($bounds, $step, string $unit = ''): static { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Facet.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Facet.php index a8a963ab0a..170d2b4af5 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Facet.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Facet.php @@ -15,7 +15,7 @@ * Fluent interface for adding a $facet stage to an aggregation pipeline. * * @phpstan-import-type PipelineExpression from Builder - * @phpstan-type FacetStageExpression array{'$facet': array} + * @phpstan-type FacetStageExpression array{"$facet": array} */ class Facet extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Fill.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Fill.php index 211a5b9a48..29482ef15f 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Fill.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Fill.php @@ -22,7 +22,7 @@ * @phpstan-type SortDirection int|SortDirectionKeywords * @phpstan-type SortShape array * @phpstan-type FillStageExpression array{ - * '$fill': array{ + * "$fill": array{ * partitionBy?: string|OperatorExpression, * partitionByFields?: list, * sortBy?: SortShape, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php index e58809f2a0..59d39e2415 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php @@ -257,7 +257,7 @@ private function fromReference(string $fieldName): static private function convertExpression($expression) { if (is_array($expression)) { - return array_map([$this, 'convertExpression'], $expression); + return array_map($this->convertExpression(...), $expression); } if (is_string($expression) && substr($expression, 0, 1) === '$') { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Group.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Group.php index b6bc71a5f5..bac1690ac4 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Group.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Group.php @@ -12,7 +12,7 @@ /** * Fluent interface for adding a $group stage to an aggregation pipeline. * - * @phpstan-type GroupStageExpression array{'$group': array} + * @phpstan-type GroupStageExpression array{"$group": array} */ class Group extends Operator implements GroupAccumulatorOperators { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/IndexStats.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/IndexStats.php index f253ee462a..3dd969528c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/IndexStats.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/IndexStats.php @@ -10,7 +10,7 @@ /** * Fluent interface for adding a $indexStats stage to an aggregation pipeline. * - * @phpstan-type IndexStatsStageExpression array{'$indexStats': object} + * @phpstan-type IndexStatsStageExpression array{"$indexStats": object} */ class IndexStats extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Limit.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Limit.php index 61df12b398..bf6c45cbb2 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Limit.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Limit.php @@ -10,7 +10,7 @@ /** * Fluent interface for adding a $limit stage to an aggregation pipeline. * - * @phpstan-type LimitStageExpression array{'$limit': int} + * @phpstan-type LimitStageExpression array{"$limit": int} */ class Limit extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php index 49a93c0fb7..67612a33f6 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php @@ -19,9 +19,9 @@ * @phpstan-import-type PipelineExpression from Builder * @phpstan-type PipelineParamType Builder|Stage|PipelineExpression * @phpstan-type LookupStageExpression array{ - * '$lookup': array{ + * "$lookup": array{ * from: string, - * 'as'?: string, + * "as"?: string, * localField?: string, * foreignField?: string, * pipeline?: PipelineExpression, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php index 36b4a0616a..c90b9534fd 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php @@ -18,11 +18,11 @@ /** * @phpstan-import-type PipelineExpression from Builder * @phpstan-type OutputCollection string|array{db: string, coll: string} - * @phpstan-type WhenMatchedType 'replace'|'keepExisting'|'merge'|'fail'|PipelineExpression + * @phpstan-type WhenMatchedType "replace"|"keepExisting"|"merge"|"fail"|PipelineExpression * @phpstan-type WhenMatchedParamType Builder|Stage|WhenMatchedType - * @phpstan-type WhenNotMatchedType 'insert'|'discard'|'fail' + * @phpstan-type WhenNotMatchedType "insert"|"discard"|"fail" * @phpstan-type MergeStageExpression array{ - * '$merge': object{ + * "$merge": object{ * into: OutputCollection, * on?: string|list, * let?: array, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php index 2021e6d1e6..ddec73fa8b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php @@ -15,7 +15,7 @@ /** * @phpstan-import-type OutputCollection from Merge - * @phpstan-type OutStageExpression array{'$out': OutputCollection} + * @phpstan-type OutStageExpression array{"$out": OutputCollection} */ class Out extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Project.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Project.php index 8b77ec68e4..ee7498c9fa 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Project.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Project.php @@ -10,7 +10,7 @@ * Fluent interface for adding a $project stage to an aggregation pipeline. * * @phpstan-import-type OperatorExpression from Expr - * @phpstan-type ProjectStageExpression array{'$project': array} + * @phpstan-type ProjectStageExpression array{"$project": array} */ class Project extends Operator { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Redact.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Redact.php index c8b0614072..0e549df000 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Redact.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Redact.php @@ -10,7 +10,7 @@ * Fluent interface for adding a $redact stage to an aggregation pipeline. * * @phpstan-import-type OperatorExpression from Expr - * @phpstan-type SetStageExpression array{'$redact': array} + * @phpstan-type SetStageExpression array{"$redact": array} */ class Redact extends Operator { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceRoot.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceRoot.php index 4ffa90f1f1..c6706c3943 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceRoot.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceRoot.php @@ -9,7 +9,7 @@ /** * @phpstan-import-type OperatorExpression from Expr * @phpstan-type ReplaceRootStageExpression array{ - * '$replaceRoot': array{ + * "$replaceRoot": array{ * newRoot: OperatorExpression|string, * } * } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceWith.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceWith.php index c1341eed40..1f58ddf235 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceWith.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/ReplaceWith.php @@ -8,7 +8,7 @@ /** * @phpstan-import-type OperatorExpression from Expr - * @phpstan-type ReplaceWithStageExpression array{'$replaceWith': OperatorExpression|string} + * @phpstan-type ReplaceWithStageExpression array{"$replaceWith": OperatorExpression|string} */ class ReplaceWith extends AbstractReplace { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sample.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sample.php index 7d87bb1eca..82c0cd280f 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sample.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sample.php @@ -10,7 +10,7 @@ /** * Fluent interface for adding a $sample stage to an aggregation pipeline. * - * @phpstan-type SampleStageExpression array{'$sample': array{size: int}} + * @phpstan-type SampleStageExpression array{"$sample": array{size: int}} */ class Sample extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php index dc38503739..a27000e76f 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SearchOperator; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function in_array; use function is_array; @@ -17,12 +18,12 @@ /** * @phpstan-import-type SortDirectionKeywords from Sort - * @phpstan-type CountType 'lowerBound'|'total' - * @phpstan-type SortMetaKeywords 'searchScore' - * @phpstan-type SortMeta array{'$meta': SortMetaKeywords} + * @phpstan-type CountType "lowerBound"|"total" + * @phpstan-type SortMetaKeywords "searchScore" + * @phpstan-type SortMeta array{"$meta": SortMetaKeywords} * @phpstan-type SortShape array * @phpstan-type SearchStageExpression array{ - * '$search': object{ + * "$search": object{ * index?: string, * count?: object{ * type: CountType, @@ -70,7 +71,7 @@ class Search extends Stage implements SupportsAllSearchOperators /** @var array */ private array $sort = []; - public function __construct(Builder $builder) + public function __construct(Builder $builder, private DocumentPersister $persister) { parent::__construct($builder); } @@ -105,7 +106,7 @@ public function getExpression(): array } if ($this->sort) { - $params->sort = (object) $this->sort; + $params->sort = (object) $this->persister->prepareSort($this->sort); } if ($this->operator !== null) { @@ -214,4 +215,9 @@ protected function getSearchStage(): static { return $this; } + + protected function getDocumentPersister(): DocumentPersister + { + return $this->persister; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php index c7dede7c4c..ffaa9dcfd3 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php @@ -7,6 +7,10 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; + +use function array_map; +use function is_array; /** * @internal @@ -18,7 +22,7 @@ */ abstract class AbstractSearchOperator extends Stage implements SearchOperator { - public function __construct(private Search $search) + public function __construct(private Search $search, private DocumentPersister $persister) { parent::__construct($search->builder); } @@ -64,4 +68,35 @@ protected function getSearchStage(): Search { return $this->search; } + + /** + * @param T $field + * + * @return T + * + * @template T of string|string[] + */ + protected function prepareFieldPath(string|array $field): string|array + { + if (is_array($field)) { + return array_map($this->persister->prepareFieldName(...), $field); + } + + return $this->persister->prepareFieldName($field); + } + + /** + * @param list|object> $documents + * + * @return list|object> + */ + protected function prepareDocuments(array $documents): array + { + return array_map($this->persister->prepareQueryOrNewObj(...), $documents); + } + + protected function getDocumentPersister(): DocumentPersister + { + return $this->persister; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php index bb0d8fcace..95f367e62b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function array_values; @@ -23,9 +24,9 @@ class Autocomplete extends AbstractSearchOperator implements ScoredSearchOperato private string $tokenOrder = ''; private ?object $fuzzy = null; - public function __construct(Search $search, string $path, string ...$query) + public function __construct(Search $search, DocumentPersister $persister, string $path, string ...$query) { - parent::__construct($search); + parent::__construct($search, $persister); $this->query(...$query); $this->path($path); @@ -79,7 +80,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->tokenOrder) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php index 8b8f604276..220b05e742 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php @@ -115,7 +115,7 @@ static function (SearchOperator $operator): object { protected function getAddOperatorClosure(): Closure { - return Closure::fromCallable([$this, 'addOperator']); + return $this->addOperator(...); } protected function getCompoundStage(): Compound diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/CompoundSearchOperatorInterface.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/CompoundSearchOperatorInterface.php index c2fe830c65..2aaa4202cd 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/CompoundSearchOperatorInterface.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/CompoundSearchOperatorInterface.php @@ -4,6 +4,13 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use GeoJson\Geometry\LineString; +use GeoJson\Geometry\MultiPolygon; +use GeoJson\Geometry\Point; +use GeoJson\Geometry\Polygon; +use MongoDB\BSON\ObjectId; +use MongoDB\BSON\UTCDateTime; + interface CompoundSearchOperatorInterface extends SupportsCompoundableSearchOperators { public function must(): Compound; @@ -13,4 +20,39 @@ public function mustNot(): Compound; public function should(?int $minimumShouldMatch = null): Compound; public function filter(): Compound; + + public function autocomplete(string $path = '', string ...$query): Autocomplete&CompoundSearchOperatorInterface; + + public function embeddedDocument(string $path = ''): EmbeddedDocument&CompoundSearchOperatorInterface; + + /** @param string|int|float|ObjectId|UTCDateTime|null $value */ + public function equals(string $path = '', $value = null): Equals&CompoundSearchOperatorInterface; + + public function exists(string $path): Exists&CompoundSearchOperatorInterface; + + /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ + public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape&CompoundSearchOperatorInterface; + + public function geoWithin(string ...$path): GeoWithin&CompoundSearchOperatorInterface; + + /** @param array|object $documents */ + public function moreLikeThis(...$documents): MoreLikeThis&CompoundSearchOperatorInterface; + + /** + * @param int|float|UTCDateTime|array|Point|null $origin + * @param int|float|null $pivot + */ + public function near($origin = null, $pivot = null, string ...$path): Near&CompoundSearchOperatorInterface; + + public function phrase(): Phrase&CompoundSearchOperatorInterface; + + public function queryString(string $query = '', string $defaultPath = ''): QueryString&CompoundSearchOperatorInterface; + + public function range(): Range&CompoundSearchOperatorInterface; + + public function regex(): Regex&CompoundSearchOperatorInterface; + + public function text(): Text&CompoundSearchOperatorInterface; + + public function wildcard(): Wildcard&CompoundSearchOperatorInterface; } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php index fe8d5a11b1..cec0677a93 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -19,9 +20,9 @@ class EmbeddedDocument extends AbstractSearchOperator implements SupportsEmbedda private string $path; private ?SearchOperator $operator = null; - public function __construct(Search $search, string $path) + public function __construct(Search $search, DocumentPersister $persister, string $path) { - parent::__construct($search); + parent::__construct($search, $persister); $this->path($path); } @@ -52,7 +53,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->operator) { $params->operator = (object) $this->operator->getExpression(); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php index fbaf401ed3..51627535e4 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use MongoDB\BSON\ObjectId; use MongoDB\BSON\UTCDateTime; @@ -22,9 +23,9 @@ class Equals extends AbstractSearchOperator implements ScoredSearchOperator private mixed $value; /** @param string|int|float|ObjectId|UTCDateTime|null $value */ - public function __construct(Search $search, string $path = '', $value = null) + public function __construct(Search $search, DocumentPersister $persister, string $path = '', $value = null) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->path($path) @@ -54,7 +55,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { $params = (object) [ - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), 'value' => $this->value, ]; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php index 8ad546384c..1f2577798c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -13,9 +14,9 @@ */ class Exists extends AbstractSearchOperator { - public function __construct(Search $search, private string $path = '') + public function __construct(Search $search, DocumentPersister $persister, private string $path = '') { - parent::__construct($search); + parent::__construct($search, $persister); } public function getOperatorName(): string @@ -25,6 +26,6 @@ public function getOperatorName(): string public function getOperatorParams(): object { - return (object) ['path' => $this->path]; + return (object) ['path' => $this->prepareFieldPath($this->path)]; } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php index c7ab3e4014..9cb35c500b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; @@ -27,9 +28,9 @@ class GeoShape extends AbstractSearchOperator implements ScoredSearchOperator private LineString|Point|Polygon|MultiPolygon|array|null $geometry = null; /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ - public function __construct(Search $search, $geometry = null, string $relation = '', string ...$path) + public function __construct(Search $search, DocumentPersister $persister, $geometry = null, string $relation = '', string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->geometry($geometry) @@ -67,7 +68,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { $params = (object) [ - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), 'relation' => $this->relation, 'geometry' => $this->geometry instanceof Geometry ? $this->geometry->jsonSerialize() diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php index c72b283e47..635d14882b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -27,9 +28,9 @@ class GeoWithin extends AbstractSearchOperator implements ScoredSearchOperator private array|MultiPolygon|Polygon|null $geometry = null; - public function __construct(Search $search, string ...$path) + public function __construct(Search $search, DocumentPersister $persister, string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this->path(...$path); } @@ -84,7 +85,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->box) { $params->box = $this->box; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php index 198a7f3413..170c791c2e 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function array_values; @@ -19,9 +20,9 @@ class MoreLikeThis extends AbstractSearchOperator private array $like = []; /** @param array|object $documents */ - public function __construct(Search $search, ...$documents) + public function __construct(Search $search, DocumentPersister $persister, ...$documents) { - parent::__construct($search); + parent::__construct($search, $persister); $this->like = array_values($documents); } @@ -33,6 +34,8 @@ public function getOperatorName(): string public function getOperatorParams(): object { - return (object) ['like' => $this->like]; + return (object) [ + 'like' => $this->prepareDocuments($this->like), + ]; } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php index f244259c94..6a1bab69d8 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\Point; use MongoDB\BSON\UTCDateTime; @@ -29,9 +30,9 @@ class Near extends AbstractSearchOperator implements ScoredSearchOperator * @param int|float|UTCDateTime|array|Point|null $origin * @param int|float|null $pivot */ - public function __construct(Search $search, $origin = null, $pivot = null, string ...$path) + public function __construct(Search $search, DocumentPersister $persister, $origin = null, $pivot = null, string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->origin($origin) @@ -74,7 +75,7 @@ public function getOperatorParams(): object ? $this->origin->jsonSerialize() : $this->origin, 'pivot' => $this->pivot, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; return $this->appendScore($params); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php index e397003bc0..e5e590a088 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php @@ -52,7 +52,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->slop !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php index 6b00ce5ed7..ea1e8cb61c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -18,9 +19,9 @@ class QueryString extends AbstractSearchOperator implements ScoredSearchOperator private string $query; private string $defaultPath; - public function __construct(Search $search, string $query = '', string $defaultPath = '') + public function __construct(Search $search, DocumentPersister $persister, string $query = '', string $defaultPath = '') { - parent::__construct($search); + parent::__construct($search, $persister); $this ->query($query) @@ -50,7 +51,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'defaultPath' => $this->defaultPath, + 'defaultPath' => $this->prepareFieldPath($this->defaultPath), ]; return $this->appendScore($params); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php index af3fc0cdd9..6253bce2aa 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php @@ -73,7 +73,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->gt !== null) { $name = $this->includeLowerBound ? 'gte' : 'gt'; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php index 90fd1f8d8d..f547eac7ee 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php @@ -50,7 +50,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->allowAnalyzedField !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php index efb1d89d79..c408999d98 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -15,6 +16,8 @@ /** @internal */ trait SupportsAllSearchOperatorsTrait { + abstract protected function getDocumentPersister(): DocumentPersister; + abstract protected function getSearchStage(): Search; /** @@ -28,83 +31,83 @@ abstract protected function addOperator(SearchOperator $operator): SearchOperato public function autocomplete(string $path = '', string ...$query): Autocomplete { - return $this->addOperator(new Autocomplete($this->getSearchStage(), $path, ...$query)); + return $this->addOperator(new Autocomplete($this->getSearchStage(), $this->getDocumentPersister(), $path, ...$query)); } public function compound(): Compound { - return $this->addOperator(new Compound($this->getSearchStage())); + return $this->addOperator(new Compound($this->getSearchStage(), $this->getDocumentPersister())); } public function embeddedDocument(string $path = ''): EmbeddedDocument { - return $this->addOperator(new EmbeddedDocument($this->getSearchStage(), $path)); + return $this->addOperator(new EmbeddedDocument($this->getSearchStage(), $this->getDocumentPersister(), $path)); } /** @param string|int|float|ObjectId|UTCDateTime|null $value */ public function equals(string $path = '', $value = null): Equals { - return $this->addOperator(new Equals($this->getSearchStage(), $path, $value)); + return $this->addOperator(new Equals($this->getSearchStage(), $this->getDocumentPersister(), $path, $value)); } public function exists(string $path): Exists { - return $this->addOperator(new Exists($this->getSearchStage(), $path)); + return $this->addOperator(new Exists($this->getSearchStage(), $this->getDocumentPersister(), $path)); } - /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ + /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape { - return $this->addOperator(new GeoShape($this->getSearchStage(), $geometry, $relation, ...$path)); + return $this->addOperator(new GeoShape($this->getSearchStage(), $this->getDocumentPersister(), $geometry, $relation, ...$path)); } public function geoWithin(string ...$path): GeoWithin { - return $this->addOperator(new GeoWithin($this->getSearchStage(), ...$path)); + return $this->addOperator(new GeoWithin($this->getSearchStage(), $this->getDocumentPersister(), ...$path)); } /** @param array|object $documents */ public function moreLikeThis(...$documents): MoreLikeThis { - return $this->addOperator(new MoreLikeThis($this->getSearchStage(), ...$documents)); + return $this->addOperator(new MoreLikeThis($this->getSearchStage(), $this->getDocumentPersister(), ...$documents)); } /** - * @param int|float|UTCDateTime|array|Point|null $origin - * @param int|float|null $pivot + * @param int|float|UTCDateTime|array|Point|null $origin + * @param int|float|null $pivot */ public function near($origin = null, $pivot = null, string ...$path): Near { - return $this->addOperator(new Near($this->getSearchStage(), $origin, $pivot, ...$path)); + return $this->addOperator(new Near($this->getSearchStage(), $this->getDocumentPersister(), $origin, $pivot, ...$path)); } public function phrase(): Phrase { - return $this->addOperator(new Phrase($this->getSearchStage())); + return $this->addOperator(new Phrase($this->getSearchStage(), $this->getDocumentPersister())); } public function queryString(string $query = '', string $defaultPath = ''): QueryString { - return $this->addOperator(new QueryString($this->getSearchStage(), $query, $defaultPath)); + return $this->addOperator(new QueryString($this->getSearchStage(), $this->getDocumentPersister(), $query, $defaultPath)); } public function range(): Range { - return $this->addOperator(new Range($this->getSearchStage())); + return $this->addOperator(new Range($this->getSearchStage(), $this->getDocumentPersister())); } public function regex(): Regex { - return $this->addOperator(new Regex($this->getSearchStage())); + return $this->addOperator(new Regex($this->getSearchStage(), $this->getDocumentPersister())); } public function text(): Text { - return $this->addOperator(new Text($this->getSearchStage())); + return $this->addOperator(new Text($this->getSearchStage(), $this->getDocumentPersister())); } public function wildcard(): Wildcard { - return $this->addOperator(new Wildcard($this->getSearchStage())); + return $this->addOperator(new Wildcard($this->getSearchStage(), $this->getDocumentPersister())); } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php index 684cc5e38d..46bfd6efa7 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php @@ -20,6 +20,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedRegex; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedText; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedWildcard; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -30,6 +31,8 @@ /** @internal */ trait SupportsCompoundableOperatorsTrait { + abstract protected function getDocumentPersister(): DocumentPersister; + abstract protected function getSearchStage(): Search; abstract protected function getCompoundStage(): Compound; @@ -45,104 +48,80 @@ abstract protected function getAddOperatorClosure(): Closure; */ abstract protected function addOperator(SearchOperator $operator): SearchOperator; - /** return Autocomplete&CompoundSearchOperatorInterface */ - public function autocomplete(string $path = '', string ...$query): Autocomplete + public function autocomplete(string $path = '', string ...$query): Autocomplete&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedAutocomplete($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path, ...$query)); + return $this->addOperator(new CompoundedAutocomplete($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path, ...$query)); } - /** @return EmbeddedDocument&CompoundSearchOperatorInterface */ - public function embeddedDocument(string $path = ''): EmbeddedDocument + public function embeddedDocument(string $path = ''): EmbeddedDocument&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedEmbeddedDocument($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path)); + return $this->addOperator(new CompoundedEmbeddedDocument($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path)); } - /** - * @param string|int|float|ObjectId|UTCDateTime|null $value - * - * @return Equals&CompoundSearchOperatorInterface - */ - public function equals(string $path = '', $value = null): Equals + /** @param string|int|float|ObjectId|UTCDateTime|null $value */ + public function equals(string $path = '', $value = null): Equals&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedEquals($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path, $value)); + return $this->addOperator(new CompoundedEquals($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path, $value)); } - /** @return Exists&CompoundSearchOperatorInterface */ - public function exists(string $path): Exists + public function exists(string $path): Exists&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedExists($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path)); + return $this->addOperator(new CompoundedExists($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path)); } - /** - * @param LineString|Point|Polygon|MultiPolygon|array|null $geometry - * - * @return GeoShape&CompoundSearchOperatorInterface - */ - public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape + /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ + public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedGeoShape($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $geometry, $relation, ...$path)); + return $this->addOperator(new CompoundedGeoShape($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $geometry, $relation, ...$path)); } - /** @return GeoWithin&CompoundSearchOperatorInterface */ - public function geoWithin(string ...$path): GeoWithin + public function geoWithin(string ...$path): GeoWithin&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedGeoWithin($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), ...$path)); + return $this->addOperator(new CompoundedGeoWithin($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), ...$path)); } - /** - * @param array|object $documents - * - * @return MoreLikeThis&CompoundSearchOperatorInterface - */ - public function moreLikeThis(...$documents): MoreLikeThis + /** @param array|object $documents */ + public function moreLikeThis(...$documents): MoreLikeThis&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedMoreLikeThis($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), ...$documents)); + return $this->addOperator(new CompoundedMoreLikeThis($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), ...$documents)); } /** - * @param int|float|UTCDateTime|array|Point|null $origin - * @param int|float|null $pivot - * - * @return Near&CompoundSearchOperatorInterface + * @param int|float|UTCDateTime|array|Point|null $origin + * @param int|float|null $pivot */ - public function near($origin = null, $pivot = null, string ...$path): Near + public function near($origin = null, $pivot = null, string ...$path): Near&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedNear($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $origin, $pivot, ...$path)); + return $this->addOperator(new CompoundedNear($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $origin, $pivot, ...$path)); } - /** @return Phrase&CompoundSearchOperatorInterface */ - public function phrase(): Phrase + public function phrase(): Phrase&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedPhrase($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedPhrase($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } - /** @return QueryString&CompoundSearchOperatorInterface */ - public function queryString(string $query = '', string $defaultPath = ''): QueryString + public function queryString(string $query = '', string $defaultPath = ''): QueryString&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedQueryString($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $query, $defaultPath)); + return $this->addOperator(new CompoundedQueryString($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $query, $defaultPath)); } - /** @return Range&CompoundSearchOperatorInterface */ - public function range(): Range + public function range(): Range&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedRange($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedRange($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } - /** @return Regex&CompoundSearchOperatorInterface */ - public function regex(): Regex + public function regex(): Regex&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedRegex($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedRegex($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } - /** @return Text&CompoundSearchOperatorInterface */ - public function text(): Text + public function text(): Text&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedText($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedText($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } - /** @return Wildcard&CompoundSearchOperatorInterface */ - public function wildcard(): Wildcard + public function wildcard(): Wildcard&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedWildcard($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedWildcard($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsGeoShapeOperator.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsGeoShapeOperator.php index 74dd3a9bf1..73988ea090 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsGeoShapeOperator.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsGeoShapeOperator.php @@ -11,6 +11,6 @@ interface SupportsGeoShapeOperator { - /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ + /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape; } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php index 31fc8b477e..ee9eca6f3b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php @@ -10,8 +10,8 @@ interface SupportsNearOperator { /** - * @param int|float|UTCDateTime|array|Point|null $origin - * @param int|float|null $pivot + * @param int|float|UTCDateTime|array|Point|null $origin + * @param int|float|null $pivot */ public function near($origin = null, $pivot = null, string ...$path): Near; } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php index b0b5d2b1c3..d645edcc21 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php @@ -71,7 +71,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->fuzzy) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php index 43257ff355..6e067ec407 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php @@ -50,7 +50,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->allowAnalyzedField !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Set.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Set.php index 45d265555f..37aa74ce8e 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Set.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Set.php @@ -10,7 +10,7 @@ * Fluent interface for adding a $set stage to an aggregation pipeline. * * @phpstan-import-type OperatorExpression from Expr - * @phpstan-type SetStageExpression array{'$set': array} + * @phpstan-type SetStageExpression array{"$set": array} * @final */ class Set extends Operator diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php index 0d36a7eaa8..accec57428 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php @@ -19,7 +19,7 @@ * @phpstan-type SortDirection int|SortDirectionKeywords * @phpstan-type SortShape array * @phpstan-type SetWindowFieldsStageExpression array{ - * '$setWindowFields': object{ + * "$setWindowFields": object{ * partitionBy?: string|OperatorExpression, * sortBy?: SortShape, * output: object, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields/Output.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields/Output.php index 640610ad1f..4ef61a985d 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields/Output.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields/Output.php @@ -22,9 +22,9 @@ * Fluent builder for output param of $setWindowFields stage * * @phpstan-import-type SortShape from SetWindowFields - * @phpstan-type WindowBound 'current'|'unbounded'|int + * @phpstan-type WindowBound "current"|"unbounded"|int * @phpstan-type WindowBounds array{0: WindowBound, 1: WindowBound} - * @phpstan-type WindowUnit 'year'|'quarter'|'month'|'week'|'day'|'hour'|'minute'|'second'|'millisecond' + * @phpstan-type WindowUnit "year"|"quarter"|"month"|"week"|"day"|"hour"|"minute"|"second"|"millisecond" * @phpstan-type Window object{ * document?: WindowBounds, * range?: WindowBounds, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Skip.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Skip.php index 440b04ed90..b85b666208 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Skip.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Skip.php @@ -10,7 +10,7 @@ /** * Fluent interface for adding a $skip stage to an aggregation pipeline. * - * @phpstan-type SkipStageExpression array{'$skip': int} + * @phpstan-type SkipStageExpression array{"$skip": int} */ class Skip extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sort.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sort.php index 952597b07b..c92775a799 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sort.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Sort.php @@ -15,12 +15,12 @@ /** * Fluent interface for adding a $sort stage to an aggregation pipeline. * - * @phpstan-type SortMetaKeywords 'textScore'|'indexKey' - * @phpstan-type SortDirectionKeywords 'asc'|'desc' - * @phpstan-type SortMeta array{'$meta': SortMetaKeywords} + * @phpstan-type SortMetaKeywords "textScore"|"indexKey" + * @phpstan-type SortDirectionKeywords "asc"|"desc" + * @phpstan-type SortMeta array{"$meta": SortMetaKeywords} * @phpstan-type SortShape array * @phpstan-type SortStageExpression array{ - * '$sort': array + * "$sort": array * } */ class Sort extends Stage diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SortByCount.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SortByCount.php index 76f1347deb..d25c0764c6 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SortByCount.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SortByCount.php @@ -11,7 +11,7 @@ use function substr; -/** @phpstan-type SortByCountStageExpression array{'$sortByCount': string} */ +/** @phpstan-type SortByCountStageExpression array{"$sortByCount": string} */ class SortByCount extends Stage { private string $fieldName; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php index 0f388a8f43..4127f7c5b3 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php @@ -16,7 +16,7 @@ * @phpstan-import-type PipelineExpression from Builder * @phpstan-type PipelineParamType array|Builder|Stage|PipelineExpression * @phpstan-type UnionWithStageExpression array{ - * '$unionWith': object{ + * "$unionWith": object{ * coll: string, * pipeline?: PipelineExpression, * } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnsetStage.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnsetStage.php index 8e02c90596..f6c0f062fc 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnsetStage.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnsetStage.php @@ -14,7 +14,7 @@ /** * Fluent interface for adding an $unset stage to an aggregation pipeline. * - * @phpstan-type UnsetStageExpression array{'$unset': list} + * @phpstan-type UnsetStageExpression array{"$unset": list} */ class UnsetStage extends Stage { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Unwind.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Unwind.php index 060ab558e6..31dcd582ab 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Unwind.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Unwind.php @@ -11,7 +11,7 @@ * Fluent interface for adding a $unwind stage to an aggregation pipeline. * * @phpstan-type UnwindStageExpression array{ - * '$unwind': string|array{ + * "$unwind": string|array{ * path: string, * includeArrayIndex?: string, * preserveNullAndEmptyArrays?: bool, diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php new file mode 100644 index 0000000000..97d1ba723f --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php @@ -0,0 +1,157 @@ +|list|list|Binary + * @phpstan-type VectorSearchStageExpression array{ + * "$vectorSearch": object{ + * exact?: bool, + * filter?: object, + * index?: string, + * limit?: int, + * numCandidates?: int, + * path?: string, + * queryVector?: Vector, + * } + * } + */ +class VectorSearch extends Stage +{ + /** @see Binary::TYPE_VECTOR introduced in ext-mongodb 2.2 */ + private const BINARY_TYPE_VECTOR = 9; + + private ?bool $exact = null; + private array|Expr|null $filter = null; + private ?string $index = null; + private ?int $limit = null; + private ?int $numCandidates = null; + private ?string $path = null; + /** @phpstan-var Vector|null */ + private array|Binary|null $queryVector = null; + + public function __construct(Builder $builder, private DocumentPersister $persister) + { + parent::__construct($builder); + } + + public function getExpression(): array + { + $params = []; + + if ($this->exact !== null) { + $params['exact'] = $this->exact; + } + + if ($this->filter instanceof Expr) { + $params['filter'] = $this->filter->getQuery(); + } elseif (is_array($this->filter)) { + $params['filter'] = $this->filter; + } + + if ($this->index !== null) { + $params['index'] = $this->index; + } + + if ($this->limit !== null) { + $params['limit'] = $this->limit; + } + + if ($this->numCandidates !== null) { + $params['numCandidates'] = $this->numCandidates; + } + + if ($this->path !== null) { + $params['path'] = $this->persister->prepareFieldName($this->path); + } + + if ($this->queryVector !== null) { + $params['queryVector'] = $this->queryVector; + } + + return [$this->getStageName() => $params]; + } + + public function exact(bool $exact): static + { + $this->exact = $exact; + + return $this; + } + + /** @phpstan-param array|Expr $filter */ + public function filter(array|Expr $filter): static + { + $this->filter = $filter; + + return $this; + } + + public function index(string $index): static + { + $this->index = $index; + + return $this; + } + + public function limit(int $limit): static + { + $this->limit = $limit; + + return $this; + } + + public function numCandidates(int $numCandidates): static + { + $this->numCandidates = $numCandidates; + + return $this; + } + + public function path(string $path): static + { + $this->path = $path; + + return $this; + } + + /** @phpstan-param Vector $queryVector */ + public function queryVector(array|Binary $queryVector): static + { + if ($queryVector === []) { + throw new InvalidArgumentException('Query vector cannot be an empty array.'); + } + + if (is_array($queryVector) && ! array_is_list($queryVector)) { + throw new InvalidArgumentException('Query vector must be a list of numbers, got an associative array.'); + } + + if ($queryVector instanceof Binary && $queryVector->getType() !== self::BINARY_TYPE_VECTOR) { + throw new InvalidArgumentException(sprintf('Binary query vector must be of type 9 (Vector), got %d.', $queryVector->getType())); + } + + $this->queryVector = $queryVector; + + return $this; + } + + protected function getStageName(): string + { + return '$vectorSearch'; + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Configuration.php b/lib/Doctrine/ODM/MongoDB/Configuration.php index 8ea5c844c6..6934bd60fe 100644 --- a/lib/Doctrine/ODM/MongoDB/Configuration.php +++ b/lib/Doctrine/ODM/MongoDB/Configuration.php @@ -808,6 +808,24 @@ private function getAutoEncryptionOptions(): array ...$this->attributes['autoEncryption'] ?? [], ]; } + + /** + * Pipelines using a search index that does not exist or is not queryable + * will return zero documents. By enabling this feature, an additional query + * is performed when the pipeline doesn't return any results to check if the + * search index exists. If the index does not exist, an exception is thrown. + * This feature is enabled by default. + * This applies to $search, $searchMeta and $vectorSearch pipelines. + */ + public function setAssertSearchIndexExistsForEmptyResult(bool $enabled): void + { + $this->attributes['assertSearchIndexExistsForEmptyResult'] = $enabled; + } + + public function assertSearchIndexExistsForEmptyResult(): bool + { + return $this->attributes['assertSearchIndexExistsForEmptyResult'] ?? true; + } } interface_exists(MappingDriver::class); diff --git a/lib/Doctrine/ODM/MongoDB/Event/OnClassMetadataNotFoundEventArgs.php b/lib/Doctrine/ODM/MongoDB/Event/OnClassMetadataNotFoundEventArgs.php index f90f2079ba..ae0112b149 100644 --- a/lib/Doctrine/ODM/MongoDB/Event/OnClassMetadataNotFoundEventArgs.php +++ b/lib/Doctrine/ODM/MongoDB/Event/OnClassMetadataNotFoundEventArgs.php @@ -15,7 +15,6 @@ */ final class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs { - /** @var ClassMetadata|null */ private ?ClassMetadata $foundMetadata = null; /** @param class-string $className */ @@ -24,13 +23,11 @@ public function __construct(private string $className, DocumentManager $dm) parent::__construct($dm); } - /** @param ClassMetadata|null $classMetadata */ public function setFoundMetadata(?ClassMetadata $classMetadata = null): void { $this->foundMetadata = $classMetadata; } - /** @return ClassMetadata|null */ public function getFoundMetadata(): ?ClassMetadata { return $this->foundMetadata; diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php index e5ac3ce7fc..e9542f5b46 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php @@ -157,7 +157,6 @@ public function generateHydratorClasses(array $classes, ?string $toDir = null): } } - /** @param ClassMetadata $class */ private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, ?string $fileName): void { $code = ''; diff --git a/lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php b/lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php index c5b3b132ee..7d0a958da5 100644 --- a/lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php +++ b/lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php @@ -9,6 +9,8 @@ /** * AutoGenerator generates a native ObjectId + * + * @deprecated use ObjectIdGenerator instead */ final class AutoGenerator extends AbstractIdGenerator { diff --git a/lib/Doctrine/ODM/MongoDB/Id/ObjectIdGenerator.php b/lib/Doctrine/ODM/MongoDB/Id/ObjectIdGenerator.php new file mode 100644 index 0000000000..d98f3088da --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Id/ObjectIdGenerator.php @@ -0,0 +1,17 @@ + UuidV1::class, + 4 => UuidV4::class, + 7 => UuidV7::class, + ]; + + public function __construct(private readonly string $class) + { + if (! in_array($this->class, self::SUPPORTED_TYPES, true)) { + throw new InvalidArgumentException(sprintf('Invalid UUID type "%s". Expected one of: %s.', $this->class, implode(', ', array_values(self::SUPPORTED_TYPES)))); + } + } + + public function generate(DocumentManager $dm, object $document): Uuid + { + return new $this->class(); + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php b/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php index 5fe5004089..ac2a188290 100644 --- a/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php +++ b/lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php @@ -18,9 +18,7 @@ use function strlen; use function substr; -/** - * Generates UUIDs. - */ +/** @deprecated without replacement. Use a custom generator or switch to binary UUIDs. */ final class UuidGenerator extends AbstractIdGenerator { /** diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php index cb5bc2a11f..77b6fa47d6 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php @@ -11,6 +11,8 @@ /** * Defines a search index on a class. * + * @see https://www.mongodb.com/docs/atlas/atlas-search/index-definitions/ + * * @Annotation * @NamedArgumentConstructor * @phpstan-import-type SearchIndexStoredSource from ClassMetadata diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/VectorSearchIndex.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/VectorSearchIndex.php new file mode 100644 index 0000000000..ed8f281504 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/VectorSearchIndex.php @@ -0,0 +1,29 @@ + $fields */ + public function __construct( + public array $fields, + public ?string $name = null, + ) { + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index 0ab69a6e1d..3088a770e4 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -34,7 +34,11 @@ use ReflectionEnum; use ReflectionNamedType; use ReflectionProperty; +use Symfony\Component\Uid\UuidV1; +use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Uid\UuidV7; +use function array_column; use function array_filter; use function array_key_exists; use function array_keys; @@ -250,18 +254,30 @@ * @phpstan-type SearchIndexDefinition array{ * mappings: array{ * dynamic?: bool, - * fields?: array, + * fields?: array>, * }, * analyzer?: string, * searchAnalyzer?: string, - * analyzers?: array, + * analyzers?: list>, * storedSource?: SearchIndexStoredSource, * synonyms?: list, * } * @phpstan-type SearchIndexMapping array{ + * type: "search"|"vectorSearch", * name: string, * definition: SearchIndexDefinition * } + * @phpstan-type VectorSearchIndexField array{ + * type: "vector"|"filter", + * path: string, + * numDimensions?: int, + * similarity?: self::VECTOR_SIMILARITY_*, + * quantization?: self::VECTOR_QUANTIZATION_*, + * hnswOptions?: array{maxEdges?: int, numEdgeCandidates?: int} + * } + * @phpstan-type VectorSearchIndexDefinition array{ + * fields: list + * } * @phpstan-type ShardKeys array * @phpstan-type ShardOptions array * @phpstan-type ShardKey array{ @@ -288,6 +304,8 @@ /** * UUID means Doctrine will generate a uuid for us. + * + * @deprecated without replacement. Use a custom generator or switch to binary UUIDs. */ public const GENERATOR_TYPE_UUID = 3; @@ -459,6 +477,13 @@ */ public const DEFAULT_SEARCH_INDEX_NAME = 'default'; + public const VECTOR_SIMILARITY_EUCLIDEAN = 'euclidean'; + public const VECTOR_SIMILARITY_COSINE = 'cosine'; + public const VECTOR_SIMILARITY_DOT_PRODUCT = 'dotProduct'; + public const VECTOR_QUANTIZATION_NONE = 'none'; + public const VECTOR_QUANTIZATION_SCALAR = 'scalar'; + public const VECTOR_QUANTIZATION_BINARY = 'binary'; + private const ALLOWED_GRIDFS_FIELDS = ['_id', 'chunkSize', 'filename', 'length', 'metadata', 'uploadDate']; /** @@ -923,6 +948,16 @@ public function getIdentifier(): array return [$this->identifier]; } + /** + * Gets the mapping of the identifier field + * + * @phpstan-return FieldMapping + */ + public function getIdentifierMapping(): array + { + return $this->fieldMappings[$this->identifier]; + } + /** * Since MongoDB only allows exactly one identifier field * this will always return an array with only one value @@ -1243,19 +1278,29 @@ public function hasIndexes(): bool /** * Add a search index for this Document. * - * @phpstan-param SearchIndexDefinition $definition + * @phpstan-param SearchIndexDefinition|VectorSearchIndexDefinition $definition + * @phpstan-param "search"|"vectorSearch" $type */ - public function addSearchIndex(array $definition, ?string $name = null): void + public function addSearchIndex(array $definition, ?string $name = null, string $type = 'search'): void { $name ??= self::DEFAULT_SEARCH_INDEX_NAME; - if (empty($definition['mappings']['dynamic']) && empty($definition['mappings']['fields'])) { + if ($type !== 'search' && $type !== 'vectorSearch') { + throw new InvalidArgumentException(sprintf('Search index type must be either "search" or "vectorSearch", "%s" given.', $type)); + } + + if ($type === 'search' && empty($definition['mappings']['dynamic']) && empty($definition['mappings']['fields'])) { throw MappingException::emptySearchIndexDefinition($this->name, $name); } + if ($type === 'vectorSearch' && ! in_array('vector', array_column($definition['fields'] ?? [], 'type'), true)) { + throw MappingException::emptyVectorSearchIndexDefinition($this->name, $name); + } + $this->searchIndexes[] = [ 'definition' => $definition, 'name' => $name, + 'type' => $type, ]; } @@ -1794,8 +1839,7 @@ public function isCollectionValuedAssociation($fieldName): bool */ public function isSingleValuedReference(string $fieldName): bool { - return isset($this->fieldMappings[$fieldName]['association']) && - $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_ONE; + return ($this->fieldMappings[$fieldName]['association'] ?? null) === self::REFERENCE_ONE; } /** @@ -1804,8 +1848,7 @@ public function isSingleValuedReference(string $fieldName): bool */ public function isCollectionValuedReference(string $fieldName): bool { - return isset($this->fieldMappings[$fieldName]['association']) && - $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_MANY; + return ($this->fieldMappings[$fieldName]['association'] ?? null) === self::REFERENCE_MANY; } /** @@ -1814,8 +1857,7 @@ public function isCollectionValuedReference(string $fieldName): bool */ public function isSingleValuedEmbed(string $fieldName): bool { - return isset($this->fieldMappings[$fieldName]['association']) && - $this->fieldMappings[$fieldName]['association'] === self::EMBED_ONE; + return ($this->fieldMappings[$fieldName]['association'] ?? null) === self::EMBED_ONE; } /** @@ -1824,8 +1866,7 @@ public function isSingleValuedEmbed(string $fieldName): bool */ public function isCollectionValuedEmbed(string $fieldName): bool { - return isset($this->fieldMappings[$fieldName]['association']) && - $this->fieldMappings[$fieldName]['association'] === self::EMBED_MANY; + return ($this->fieldMappings[$fieldName]['association'] ?? null) === self::EMBED_MANY; } /** @@ -2234,8 +2275,7 @@ public function getAssociationNames(): array /** @param string $fieldName */ public function getTypeOfField($fieldName): ?string { - return isset($this->fieldMappings[$fieldName]) ? - $this->fieldMappings[$fieldName]['type'] : null; + return $this->fieldMappings[$fieldName]['type'] ?? null; } /** @@ -2362,22 +2402,18 @@ public function mapField(array $mapping): array } $this->generatorOptions = $mapping['options'] ?? []; - switch ($this->generatorType) { - case self::GENERATOR_TYPE_AUTO: - $mapping['type'] = 'id'; - break; - default: - if (! empty($this->generatorOptions['type'])) { - $mapping['type'] = (string) $this->generatorOptions['type']; - } elseif (empty($mapping['type'])) { - $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID; - } + if ($this->generatorType !== self::GENERATOR_TYPE_AUTO) { + if (! empty($this->generatorOptions['type'])) { + $mapping['type'] = (string) $this->generatorOptions['type']; + } elseif (empty($mapping['type'])) { + $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID; + } + } elseif ($mapping['type'] !== Type::UUID) { + $mapping['type'] = Type::ID; } unset($this->generatorOptions['type']); - } - - if (! isset($mapping['type'])) { + } elseif (! isset($mapping['type'])) { // Default to string $mapping['type'] = Type::STRING; } @@ -2769,6 +2805,11 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array } switch ($type->getName()) { + case UuidV1::class: + case UuidV4::class: + case UuidV7::class: + $mapping['type'] = Type::UUID; + break; case DateTime::class: $mapping['type'] = Type::DATE; break; diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index 1b78b1e62e..87b073d753 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -12,15 +12,17 @@ use Doctrine\ODM\MongoDB\Event\OnClassMetadataNotFoundEventArgs; use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Id\AlnumGenerator; -use Doctrine\ODM\MongoDB\Id\AutoGenerator; use Doctrine\ODM\MongoDB\Id\IdGenerator; use Doctrine\ODM\MongoDB\Id\IncrementGenerator; +use Doctrine\ODM\MongoDB\Id\ObjectIdGenerator; +use Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator; use Doctrine\ODM\MongoDB\Id\UuidGenerator; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\Mapping\ReflectionService; use ReflectionException; +use ReflectionNamedType; use function assert; use function get_class_methods; @@ -186,7 +188,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS if ($parent->idGenerator) { $class->setIdGenerator($parent->idGenerator); } - } else { + } elseif ($class->identifier) { $this->completeIdGeneratorMapping($class); } @@ -230,12 +232,36 @@ protected function newClassMetadataInstance($className): ClassMetadata return new ClassMetadata($className); } + private function generateAutoIdGenerator(ClassMetadata $class): void + { + $identifierMapping = $class->getIdentifierMapping(); + switch ($identifierMapping['type']) { + case 'id': + case 'objectId': + $class->setIdGenerator(new ObjectIdGenerator()); + break; + case 'uuid': + $reflectionProperty = $class->getReflectionProperty($identifierMapping['fieldName']); + if (! $reflectionProperty->getType() instanceof ReflectionNamedType) { + throw MappingException::autoIdGeneratorNeedsType($class->name, $identifierMapping['fieldName']); + } + + $class->setIdGenerator(new SymfonyUuidGenerator($reflectionProperty->getType()->getName())); + break; + default: + throw MappingException::unsupportedTypeForAutoGenerator( + $class->name, + $identifierMapping['type'], + ); + } + } + private function completeIdGeneratorMapping(ClassMetadata $class): void { $idGenOptions = $class->generatorOptions; switch ($class->generatorType) { case ClassMetadata::GENERATOR_TYPE_AUTO: - $class->setIdGenerator(new AutoGenerator()); + $this->generateAutoIdGenerator($class); break; case ClassMetadata::GENERATOR_TYPE_INCREMENT: $incrementGenerator = new IncrementGenerator(); diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php index 99415f205f..5db0d3f6ed 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php @@ -8,7 +8,6 @@ use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\Annotations\AbstractIndex; -use Doctrine\ODM\MongoDB\Mapping\Annotations\SearchIndex; use Doctrine\ODM\MongoDB\Mapping\Annotations\ShardKey; use Doctrine\ODM\MongoDB\Mapping\Annotations\TimeSeries; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; @@ -108,6 +107,10 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad $this->addSearchIndex($metadata, $attribute); } + if ($attribute instanceof ODM\VectorSearchIndex) { + $this->addVectorSearchIndex($metadata, $attribute); + } + if ($attribute instanceof ODM\Indexes) { trigger_deprecation( 'doctrine/mongodb-odm', @@ -344,10 +347,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad } } - /** - * @param ClassMetadata $class - * @param array $keys - */ + /** @param array $keys */ private function addIndex(ClassMetadata $class, AbstractIndex $index, array $keys = []): void { $keys = array_merge($keys, $index->keys); @@ -369,8 +369,7 @@ private function addIndex(ClassMetadata $class, AbstractIndex $index, array $key $class->addIndex($keys, $options); } - /** @param ClassMetadata $class */ - private function addSearchIndex(ClassMetadata $class, SearchIndex $index): void + private function addSearchIndex(ClassMetadata $class, ODM\SearchIndex $index): void { $definition = []; @@ -386,14 +385,18 @@ private function addSearchIndex(ClassMetadata $class, SearchIndex $index): void } } - $class->addSearchIndex($definition, $index->name ?? null); + $class->addSearchIndex($definition, $index->name ?? null, 'search'); + } + + private function addVectorSearchIndex(ClassMetadata $class, ODM\VectorSearchIndex $index): void + { + $definition = [ + 'fields' => $index->fields, + ]; + + $class->addSearchIndex($definition, $index->name ?? null, 'vectorSearch'); } - /** - * @param ClassMetadata $class - * - * @throws MappingException - */ private function setShardKey(ClassMetadata $class, ODM\ShardKey $shardKey): void { $options = []; diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php index 22c02483bb..2655649fa5 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php @@ -210,6 +210,12 @@ public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\C } } + if (isset($xmlRoot->{'vector-search-indexes'})) { + foreach ($xmlRoot->{'vector-search-indexes'}->{'vector-search-index'} as $searchIndex) { + $this->addVectorSearchIndex($metadata, $searchIndex); + } + } + if (isset($xmlRoot->{'shard-key'})) { $this->setShardKey($metadata, $xmlRoot->{'shard-key'}[0]); } @@ -380,10 +386,7 @@ public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\C // phpcs:enable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed - /** - * @param ClassMetadata $class - * @phpstan-param FieldMappingConfig $mapping - */ + /** @phpstan-param FieldMappingConfig $mapping */ private function addFieldMapping(ClassMetadata $class, array $mapping): void { if (isset($mapping['name'])) { @@ -423,7 +426,6 @@ private function addFieldMapping(ClassMetadata $class, array $mapping): void $class->addIndex($keys, $options); } - /** @param ClassMetadata $class */ private function addEmbedMapping(ClassMetadata $class, SimpleXMLElement $embed, string $type): void { $attributes = $embed->attributes(); @@ -473,7 +475,6 @@ private function addEmbedMapping(ClassMetadata $class, SimpleXMLElement $embed, $this->addFieldMapping($class, $mapping); } - /** @param ClassMetadata $class */ private function addReferenceMapping(ClassMetadata $class, ?SimpleXMLElement $reference, string $type): void { $cascade = array_keys((array) $reference->cascade); @@ -555,7 +556,6 @@ private function addReferenceMapping(ClassMetadata $class, ?SimpleXMLElement $re $this->addFieldMapping($class, $mapping); } - /** @param ClassMetadata $class */ private function addIndex(ClassMetadata $class, SimpleXMLElement $xmlIndex): void { $attributes = $xmlIndex->attributes(); @@ -618,7 +618,6 @@ private function addIndex(ClassMetadata $class, SimpleXMLElement $xmlIndex): voi $class->addIndex($keys, $options); } - /** @param ClassMetadata $class */ private function addSearchIndex(ClassMetadata $class, SimpleXMLElement $searchIndex): void { $definition = []; @@ -748,6 +747,44 @@ private function getSearchIndexFieldDefinition(SimpleXMLElement $field): array return $fieldDefinition; } + private function addVectorSearchIndex(ClassMetadata $class, SimpleXMLElement $searchIndex): void + { + $definition = ['fields' => []]; + + foreach ($searchIndex->{'vector-field'} as $vectorField) { + $field = [ + 'type' => 'vector', + 'path' => (string) $vectorField['path'], + 'numDimensions' => (int) $vectorField['numDimensions'], + 'similarity' => (string) $vectorField['similarity'], + ]; + if (isset($vectorField['quantization'])) { + $field['quantization'] = (string) $vectorField['quantization']; + } + + if (isset($vectorField['hnswMaxEdges'])) { + $field['hnswOptions']['maxEdges'] = (int) $vectorField['hnswMaxEdges']; + } + + if (isset($vectorField['hnswNumEdgeCandidates'])) { + $field['hnswOptions']['numEdgeCandidates'] = (int) $vectorField['hnswNumEdgeCandidates']; + } + + $definition['fields'][] = $field; + } + + foreach ($searchIndex->{'filter-field'} as $filterField) { + $definition['fields'][] = [ + 'type' => 'filter', + 'path' => (string) $filterField['path'], + ]; + } + + $name = isset($searchIndex['name']) ? (string) $searchIndex['name'] : null; + + $class->addSearchIndex($definition, $name, 'vectorSearch'); + } + /** @return array|scalar|null> */ private function getPartialFilterExpression(SimpleXMLElement $fields): array { @@ -807,7 +844,6 @@ private function convertXMLElementValue(string $value) return preg_match('/^[-]?\d+$/', $value) ? (int) $value : (float) $value; } - /** @param ClassMetadata $class */ private function setShardKey(ClassMetadata $class, SimpleXMLElement $xmlShardkey): void { $attributes = $xmlShardkey->attributes(); @@ -906,7 +942,6 @@ private function formatErrors(array $xmlErrors): string return implode("\n", array_map(static fn (LibXMLError $error): string => sprintf('Line %d:%d: %s', $error->line, $error->column, $error->message), $xmlErrors)); } - /** @param ClassMetadata $class */ private function addGridFSMappings(ClassMetadata $class, SimpleXMLElement $xmlRoot): void { if (! $class->isFile) { diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php index 7b7d46d1ba..abd19c1535 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php @@ -297,6 +297,11 @@ public static function emptySearchIndexDefinition(string $className, string $ind return new self(sprintf('%s search index "%s" must be dynamic or specify a field mapping', $className, $indexName)); } + public static function emptyVectorSearchIndexDefinition(string $className, string $indexName): self + { + return new self(sprintf('%s vector search index "%s" must have a vector field', $className, $indexName)); + } + public static function timeSeriesFieldNotFound(string $className, string $fieldName, string $field): self { return new self(sprintf( @@ -314,4 +319,22 @@ public static function rootDocumentCannotBeEncrypted(string $className): self $className, )); } + + public static function unsupportedTypeForAutoGenerator(string $className, string $type): self + { + return new self(sprintf( + 'The type "%s" can not be used for auto ID generation in class "%s".', + $type, + $className, + )); + } + + public static function autoIdGeneratorNeedsType(string $className, string $identifierFieldName): self + { + return new self(sprintf( + 'The auto ID generator for class "%s" requires the identifier field "%s" to have a type.', + $className, + $identifierFieldName, + )); + } } diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php index cddf319b11..8b99e0ccbf 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php @@ -329,7 +329,7 @@ private function insertElements(object $parent, array $collections, array $optio } /** - * Perform collections update for 'pushAll' strategy. + * Perform collection update for 'pushAll' strategy. * * @param object $parent Parent object to which passed collections is belong. * @param string[] $collsPaths Paths of collections that is passed. diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index 44cef23070..1bf370fc02 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -62,6 +62,7 @@ use function is_string; use function spl_object_id; use function sprintf; +use function str_contains; use function strpos; use function strtolower; use function trigger_deprecation; @@ -71,7 +72,7 @@ * * @internal * - * @template T of object + * @template T of object = object * * @phpstan-type CommitOptions array{ * fsync?: bool, @@ -218,7 +219,7 @@ public function executeInserts(array $options = []): void $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); } - $data[$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion); + $data[$versionMapping['name']] = $type->convertToDatabaseValue($nextVersion); } $inserts[] = $data; @@ -296,7 +297,7 @@ private function executeUpsert(object $document, array $options): void $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); } - $data['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion); + $data['$set'][$versionMapping['name']] = $type->convertToDatabaseValue($nextVersion); } foreach (array_keys($criteria) as $field) { @@ -377,8 +378,8 @@ public function update(object $document, array $options = []): void $type = Type::getType($versionMapping['type']); assert($type instanceof Versionable); $nextVersion = $type->getNextVersion($currentVersion); - $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion); - $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion); + $update['$set'][$versionMapping['name']] = $type->convertToDatabaseValue($nextVersion); + $query[$versionMapping['name']] = $type->convertToDatabaseValue($currentVersion); } if (! empty($update)) { @@ -1041,7 +1042,7 @@ public function addFilterToPreparedQuery(array $preparedQuery): array * * PHP field names and types will be converted to those used by MongoDB. * - * @param array $query + * @param array $query * * @return array */ @@ -1049,26 +1050,29 @@ public function prepareQueryOrNewObj(array $query, bool $isNewObj = false): arra { $preparedQuery = []; - foreach ($query as $key => $value) { - $key = (string) $key; + foreach ($query as $field => $value) { + $field = (string) $field; - // Recursively prepare logical query clauses - if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) { - foreach ($value as $k2 => $v2) { - $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj); - } + // Recursively prepare logical query clauses, treating each value as a separate query element + if (in_array($field, ['$and', '$or', '$nor'], true) && is_array($value)) { + $preparedQuery[$field] = array_map( + fn ($v) => $this->prepareQueryOrNewObj($v, $isNewObj), + $value, + ); continue; } - if (isset($key[0]) && $key[0] === '$' && is_array($value)) { - $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj); + // Recursively prepare nested operators, treating the value as a single query element + if (isset($field[0]) && $field[0] === '$' && is_array($value)) { + $preparedQuery[$field] = $this->prepareQueryOrNewObj($value, $isNewObj); + continue; } - $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj); + // Prepare a single query element. This may produce multiple queries (e.g. for references) + $preparedQueryElements = $this->prepareQueryElement($field, $value, null, true, $isNewObj); foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) { - $preparedValue = $this->convertToDatabaseValue($key, $preparedValue); $preparedQuery[$preparedKey] = $preparedValue; } } @@ -1083,7 +1087,7 @@ public function prepareQueryOrNewObj(array $query, bool $isNewObj = false): arra * * @return mixed */ - private function convertToDatabaseValue(string $fieldName, $value) + private function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadata $class = null) { if (is_array($value)) { foreach ($value as $k => $v) { @@ -1091,13 +1095,13 @@ private function convertToDatabaseValue(string $fieldName, $value) continue; } - $value[$k] = $this->convertToDatabaseValue($fieldName, $v); + $value[$k] = $this->convertToDatabaseValue($fieldName, $v, $class); } return $value; } - if (! $this->class->hasField($fieldName)) { + if (! $class || ! $class->hasField($fieldName)) { if ($value instanceof BackedEnum) { $value = $value->value; } @@ -1105,7 +1109,7 @@ private function convertToDatabaseValue(string $fieldName, $value) return Type::convertPHPToDatabaseValue($value); } - $mapping = $this->class->fieldMappings[$fieldName]; + $mapping = $class->fieldMappings[$fieldName]; $typeName = $mapping['type']; if (! empty($mapping['reference']) || ! empty($mapping['embedded'])) { @@ -1132,6 +1136,22 @@ private function convertToDatabaseValue(string $fieldName, $value) return $value; } + private function prepareQueryReference(mixed $value, ClassMetadata $class): mixed + { + if ( + // Scalar values are prepared immediately + ! is_array($value) + // Objects without operators can be prepared immediately + || ! $this->hasQueryOperators($value) + // Objects with DBRef fields can be prepared immediately + || $this->hasDBRefFields($value) + ) { + return $class->getDatabaseIdentifierValue($value); + } + + return $this->prepareQueryExpression($value, $class); + } + /** * Prepares a query value and converts the PHP value to the database value * if it is an identifier. @@ -1141,18 +1161,24 @@ private function convertToDatabaseValue(string $fieldName, $value) * * @param mixed $value * - * @return array + * @return array Returns an array of tuples containing the prepared field name and value */ - private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false): array + private function prepareQueryElement(string $originalFieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false, string $fieldNamePrefix = ''): array { - $class ??= $this->class; + $class ??= $this->class; + $fieldName = $fieldNamePrefix . $originalFieldName; - // @todo Consider inlining calls to ClassMetadata methods + // Process identifier fields + if (($class->hasField($originalFieldName) && $class->isIdentifier($originalFieldName)) || $originalFieldName === '_id') { + $fieldName = $fieldNamePrefix . '_id'; + + return [[$fieldName, $prepareValue ? $this->prepareQueryReference($value, $class) : $value]]; + } // Process all non-identifier fields by translating field names - if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) { - $mapping = $class->fieldMappings[$fieldName]; - $fieldName = $mapping['name']; + if ($class->hasField($originalFieldName)) { + $mapping = $class->fieldMappings[$originalFieldName]; + $fieldName = $fieldNamePrefix . $mapping['name']; if (! $prepareValue) { return [[$fieldName, $value]]; @@ -1176,7 +1202,7 @@ private function prepareQueryElement(string $fieldName, $value = null, ?ClassMet // No further preparation unless we're dealing with a simple reference if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) { - return [[$fieldName, $value]]; + return [[$fieldName, $this->convertToDatabaseValue($originalFieldName, $value, $class)]]; } // Additional preparation for one or more simple reference values @@ -1194,29 +1220,9 @@ private function prepareQueryElement(string $fieldName, $value = null, ?ClassMet return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]]; } - // Process identifier fields - if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') { - $fieldName = '_id'; - - if (! $prepareValue) { - return [[$fieldName, $value]]; - } - - if (! is_array($value)) { - return [[$fieldName, $class->getDatabaseIdentifierValue($value)]]; - } - - // Objects without operators or with DBRef fields can be converted immediately - if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { - return [[$fieldName, $class->getDatabaseIdentifierValue($value)]]; - } - - return [[$fieldName, $this->prepareQueryExpression($value, $class)]]; - } - // No processing for unmapped, non-identifier, non-dotted field names - if (strpos($fieldName, '.') === false) { - return [[$fieldName, $value]]; + if (! str_contains($originalFieldName, '.')) { + return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($originalFieldName, $value, $class) : $value]]; } /* Process "fieldName.objectProperty" queries (on arrays or objects). @@ -1225,121 +1231,111 @@ private function prepareQueryElement(string $fieldName, $value = null, ?ClassMet * significant: "fieldName.objectProperty" with an optional index or key * for collections stored as either BSON arrays or objects. */ - $e = explode('.', $fieldName, 4); + $fieldNameParts = explode('.', $originalFieldName, 4); + $partCount = count($fieldNameParts); + assert($partCount >= 2); // No further processing for unmapped fields - if (! isset($class->fieldMappings[$e[0]])) { - return [[$fieldName, $value]]; + if (! $class->hasField($fieldNameParts[0])) { + return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($fieldNameParts[0], $value, $class) : $value]]; } - $mapping = $class->fieldMappings[$e[0]]; - $e[0] = $mapping['name']; + $mapping = $class->fieldMappings[$fieldNameParts[0]]; + $fieldName = $fieldNamePrefix . $mapping['name'] . '.' . implode('.', array_slice($fieldNameParts, 1)); // Hash and raw fields will not be prepared beyond the field name if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) { - $fieldName = implode('.', $e); - return [[$fieldName, $value]]; } - if ( - $mapping['type'] === ClassMetadata::MANY && CollectionHelper::isHash($mapping['strategy']) - && isset($e[2]) - ) { - $objectProperty = $e[2]; - $objectPropertyPrefix = $e[1] . '.'; - $nextObjectProperty = implode('.', array_slice($e, 3)); - } elseif ($e[1] !== '$') { - $fieldName = $e[0] . '.' . $e[1]; - $objectProperty = $e[1]; - $objectPropertyPrefix = ''; - $nextObjectProperty = implode('.', array_slice($e, 2)); - } elseif (isset($e[2])) { - $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2]; - $objectProperty = $e[2]; - $objectPropertyPrefix = $e[1] . '.'; - $nextObjectProperty = implode('.', array_slice($e, 3)); + if (isset($mapping['targetDocument'])) { + // For associations with a targetDocument (i.e. embedded or reference), get the class metadata for the target document + $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); + } elseif (is_object($value) && ! $this->dm->getMetadataFactory()->isTransient($value::class)) { + // For associations without a targetDocument, try to infer the class metadata from the object + $targetClass = $this->dm->getClassMetadata($value::class); } else { - $fieldName = $e[0] . '.' . $e[1]; - - return [[$fieldName, $value]]; - } - - // No further processing for fields without a targetDocument mapping - if (! isset($mapping['targetDocument'])) { - if ($nextObjectProperty) { - $fieldName .= '.' . $nextObjectProperty; - } - - return [[$fieldName, $value]]; - } - - $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); - - // No further processing for unmapped targetDocument fields - if (! $targetClass->hasField($objectProperty)) { - if ($nextObjectProperty) { - $fieldName .= '.' . $nextObjectProperty; - } - - return [[$fieldName, $value]]; - } - - $targetMapping = $targetClass->getFieldMapping($objectProperty); - $objectPropertyIsId = $targetClass->isIdentifier($objectProperty); - - // Prepare DBRef identifiers or the mapped field's property path - $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID - ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0]) - : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name']; - - // Process targetDocument identifier fields - if ($objectPropertyIsId) { - if (! $prepareValue) { - return [[$fieldName, $value]]; + // Without a target document, no further processing is possible + return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($fieldNameParts[0], $value) : $value]]; + } + + // Don't recurse for references in queries. Instead, prepare them directly + if (! $inNewObj && ! empty($mapping['reference'])) { + // First part is the name of the reference + // Second part is either a positional operator, index/key, or the name of a field + // Third part (if any) is the name of a field + // That means, we can implode all field parts except the first as the next field name + if ($fieldNameParts[1] === '$') { + assert($partCount >= 3); + $objectProperty = $fieldNameParts[2]; + $referencePrefix = $fieldNamePrefix . $mapping['name'] . '.$'; + } else { + $objectProperty = $fieldNameParts[1]; + $referencePrefix = $fieldNamePrefix . $mapping['name']; } - if (! is_array($value)) { - return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; - } + if ($targetClass->hasField($objectProperty) && $targetClass->isIdentifier($objectProperty)) { + $fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $referencePrefix); - // Objects without operators or with DBRef fields can be converted immediately - if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { - return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; + return [[$fieldName, $prepareValue ? $this->prepareQueryReference($value, $targetClass) : $value]]; } - return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]]; + return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($objectProperty, $value, $targetClass) : $value]]; } - /* The property path may include a third field segment, excluding the - * collection item pointer. If present, this next object property must - * be processed recursively. + /* + * 1 element: impossible (because of the dot) + * 2 elements: fieldName.objectProperty, fieldName., or fieldName.$. For EmbedMany and ReferenceMany, treat the second element as index if $inNewObj is true and convert the value. Otherwise, recurse. + * 3+ elements: fieldname.foo.bar, fieldName..foo, or fieldName.$.foo. For EmbedMany and ReferenceMany, treat the second element as index, and recurse into the third element. Otherwise, recurse with the second element as field name. */ - if ($nextObjectProperty) { - // Respect the targetDocument's class metadata when recursing - $nextTargetClass = isset($targetMapping['targetDocument']) - ? $this->dm->getClassMetadata($targetMapping['targetDocument']) - : null; - - if (empty($targetMapping['reference'])) { - $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue); - } else { - // No recursive processing for references as most probably somebody is querying DBRef or alike - if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) { - $nextObjectProperty = '$' . $nextObjectProperty; + if ($mapping['type'] === ClassMetadata::MANY) { + if ($inNewObj || CollectionHelper::isHash($mapping['strategy'])) { + // When there are only two segments in a hash or when serialising a new object, we seem to be replacing an entire element. Don't recurse, just convert the value. + if ($partCount === 2) { + // In order to prepare the embedded document value, we need to recurse with the original field name, then append the second segment + $prepared = $this->prepareQueryElement( + $mapping['name'], + $value, + $targetClass, + $prepareValue, + $inNewObj, + $fieldNamePrefix, + ); + + $preparedFieldName = $prepared[0][0]; + $preparedValue = $prepared[0][1]; + + return [[$preparedFieldName . '.' . $fieldNameParts[1], $preparedValue]]; } - $fieldNames = [[$nextObjectProperty, $value]]; + // When there are more than two segments, treat the second segment (index/key/positional operator) as part of the field name and recurse into the rest + $newPrefix = $fieldNamePrefix . $mapping['name'] . '.' . $fieldNameParts[1] . '.'; + $newFieldName = implode('.', array_slice($fieldNameParts, 2)); + } else { + // When serializing a query, the second segment is a positional operator ($), a numeric index for collections, or anything else for a hash. + $newPrefix = $fieldNamePrefix . $mapping['name'] . '.'; + $newFieldName = implode('.', array_slice($fieldNameParts, 1)); } - return array_map(static function ($preparedTuple) use ($fieldName) { - [$key, $value] = $preparedTuple; - - return [$fieldName . '.' . $key, $value]; - }, $fieldNames); + return $this->prepareQueryElement( + $newFieldName, + $value, + $targetClass, + $prepareValue, + $inNewObj, + $newPrefix, + ); } - return [[$fieldName, $value]]; + // For everything else, recurse with the first segment as field name and the target document class + return $this->prepareQueryElement( + implode('.', array_slice($fieldNameParts, 1)), + $value, + $targetClass, + $prepareValue, + $inNewObj, + $fieldNamePrefix . $mapping['name'] . '.', + ); } /** diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php index 80c257d66c..beb53ee47f 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php @@ -4,6 +4,8 @@ namespace Doctrine\ODM\MongoDB\Persisters; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\MappingException; @@ -392,7 +394,12 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo break; } - $value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections); + // Prepare persistent collection if it's not already one + $collection = $rawValue instanceof PersistentCollectionInterface + ? $rawValue + : $this->preparePersistentCollection($mapping, $embeddedDocument, $rawValue); + + $value = $this->prepareAssociatedCollectionValue($collection, $includeNestedCollections); break; default: @@ -507,4 +514,24 @@ public function prepareAssociatedCollectionValue(PersistentCollectionInterface $ return $setData; } + + /** @param array|Collection $rawValue */ + private function preparePersistentCollection(array $mapping, object $owner, array|Collection $rawValue): PersistentCollectionInterface + { + if ($rawValue instanceof PersistentCollectionInterface) { + return $rawValue; + } + + // If $actualData[$name] is not a Collection then use an ArrayCollection. + if (! $rawValue instanceof Collection) { + $rawValue = new ArrayCollection($rawValue); + } + + // Inject PersistentCollection + $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $rawValue); + $coll->setOwner($owner, $mapping); + $coll->setDirty(! $rawValue->isEmpty()); + + return $coll; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php index d3d6d2dc11..b9df9191dd 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php @@ -140,11 +140,7 @@ private function createInitializer( }; } - /** - * @param ClassMetadata $metadata - * - * @return array - */ + /** @return array */ private function skippedFieldsFqns(ClassMetadata $metadata): array { $skippedFieldsFqns = []; diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/CachingClassNameResolver.php b/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/CachingClassNameResolver.php index 8c2879ed34..cd0343301c 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/CachingClassNameResolver.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Resolver/CachingClassNameResolver.php @@ -26,10 +26,6 @@ public function getRealClass(string $class): string public function resolveClassName(string $className): string { - if (! isset($this->resolvedNames[$className])) { - $this->resolvedNames[$className] = $this->resolver->resolveClassName($className); - } - - return $this->resolvedNames[$className]; + return $this->resolvedNames[$className] ??= $this->resolver->resolveClassName($className); } } diff --git a/lib/Doctrine/ODM/MongoDB/Query/Builder.php b/lib/Doctrine/ODM/MongoDB/Query/Builder.php index 7a0fed310d..6578e1e8a9 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Builder.php @@ -457,11 +457,8 @@ public function equals($value): self */ public function exclude($fieldName = null): self { - if (! isset($this->query['select'])) { - $this->query['select'] = []; - } - - $fieldNames = is_array($fieldName) ? $fieldName : func_get_args(); + $this->query['select'] ??= []; + $fieldNames = is_array($fieldName) ? $fieldName : func_get_args(); foreach ($fieldNames as $fieldName) { $this->query['select'][$fieldName] = 0; @@ -1261,11 +1258,8 @@ public function returnNew(bool $bool = true): self */ public function select($fieldName = null): self { - if (! isset($this->query['select'])) { - $this->query['select'] = []; - } - - $fieldNames = is_array($fieldName) ? $fieldName : func_get_args(); + $this->query['select'] ??= []; + $fieldNames = is_array($fieldName) ? $fieldName : func_get_args(); foreach ($fieldNames as $fieldName) { $this->query['select'][$fieldName] = 1; @@ -1464,11 +1458,8 @@ public function snapshot(bool $bool = true): self */ public function sort($fieldName, $order = 1): self { - if (! isset($this->query['sort'])) { - $this->query['sort'] = []; - } - - $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order]; + $this->query['sort'] ??= []; + $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order]; foreach ($fields as $fieldName => $order) { if (is_string($order)) { diff --git a/lib/Doctrine/ODM/MongoDB/Query/Expr.php b/lib/Doctrine/ODM/MongoDB/Query/Expr.php index f2e6bbb3e3..3ff256bef2 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Expr.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Expr.php @@ -84,12 +84,8 @@ public function __construct(DocumentManager $dm) */ public function addAnd($expression, ...$expressions): self { - if (! isset($this->query['$and'])) { - $this->query['$and'] = []; - } - $this->query['$and'] = array_merge( - $this->query['$and'], + $this->query['$and'] ?? [], func_get_args(), ); @@ -107,12 +103,8 @@ public function addAnd($expression, ...$expressions): self */ public function addNor($expression, ...$expressions): self { - if (! isset($this->query['$nor'])) { - $this->query['$nor'] = []; - } - $this->query['$nor'] = array_merge( - $this->query['$nor'], + $this->query['$nor'] ?? [], func_get_args(), ); @@ -130,12 +122,8 @@ public function addNor($expression, ...$expressions): self */ public function addOr($expression, ...$expressions): self { - if (! isset($this->query['$or'])) { - $this->query['$or'] = []; - } - $this->query['$or'] = array_merge( - $this->query['$or'], + $this->query['$or'] ?? [], func_get_args(), ); diff --git a/lib/Doctrine/ODM/MongoDB/Query/FilterCollection.php b/lib/Doctrine/ODM/MongoDB/Query/FilterCollection.php index 891612d297..3d68ab2bfe 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/FilterCollection.php +++ b/lib/Doctrine/ODM/MongoDB/Query/FilterCollection.php @@ -139,8 +139,6 @@ public function isEnabled(string $name): bool /** * Gets enabled filter criteria. * - * @param ClassMetadata $class - * * @return array */ public function getFilterCriteria(ClassMetadata $class): array diff --git a/lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php b/lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php index ff3478cf8a..d038974f4d 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php +++ b/lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php @@ -136,7 +136,6 @@ public function primeReferences(ClassMetadata $class, $documents, string $fieldN * ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s) * This addresses Issue #624. * - * @param ClassMetadata $class * @param array|Traversable $documents * @param FieldMapping|null $mapping * diff --git a/lib/Doctrine/ODM/MongoDB/SchemaException.php b/lib/Doctrine/ODM/MongoDB/SchemaException.php new file mode 100644 index 0000000000..50059989f0 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/SchemaException.php @@ -0,0 +1,32 @@ + $class - * - * @phpstan-return IndexMapping[] - */ + /** @phpstan-return IndexMapping[] */ private function prepareIndexes(ClassMetadata $class): array { $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name); @@ -340,6 +340,65 @@ public function createSearchIndexes(): void } } + /** + * Wait until all search indexes are queryable for the given document classes. + * + * @param list|null $classNames List of class names to check, or null to check all mapped classes + * @param positive-int $maxTimeMs Maximum time to wait in milliseconds (default: 10,000 ms) + * @param positive-int $waitTimeMs Time to wait between checks in milliseconds (default: 100 ms) + */ + public function waitForSearchIndexes(?array $classNames = null, int $maxTimeMs = 10_000, int $waitTimeMs = 100): void + { + if ($maxTimeMs < 1) { + throw new InvalidArgumentException('$maxTimeMs must be a positive number of milliseconds.'); + } + + if ($waitTimeMs < 1) { + throw new InvalidArgumentException('$waitTimeMs must be a positive number of milliseconds.'); + } + + $classes = $classNames === null ? $this->metadataFactory->getAllMetadata() : array_map($this->metadataFactory->getMetadataFor(...), $classNames); + + /** @var array $indexesToCheck Search indexes for each class */ + $indexesToCheck = []; + foreach ($classes as $class) { + if (! $class->hasSearchIndexes()) { + continue; + } + + $indexesToCheck[$class->getName()] = array_column($class->getSearchIndexes(), 'name'); + } + + $start = hrtime(true); + while ($indexesToCheck) { + if (hrtime(true) > $start + $maxTimeMs * 1_000_000) { + throw new MongoDBException(sprintf('Timed out waiting for search indexes to become queryable after %d ms. Search indexes are not ready for the following class(es): %s', $maxTimeMs, implode(', ', array_keys($indexesToCheck)))); + } + + foreach ($indexesToCheck as $className => $indexNames) { + $collection = $this->dm->getDocumentCollection($className); + + /** @var array $indexStatus Queryable status for each index name */ + $indexStatus = array_column(iterator_to_array($collection->listSearchIndexes([ + 'filter' => ['name' => ['$in' => array_keys($indexNames)]], + 'typeMap' => ['root' => 'array'], + ])), 'queryable', 'name'); + + // Check that all indexes exist + $missingIndexes = array_diff_key($indexNames, array_keys($indexStatus)); + if ($missingIndexes) { + throw SchemaException::missingSearchIndex($className, $missingIndexes); + } + + // Remove the indexes that are ready from the list of indexes to check + $indexesToCheck[$className] = array_keys(array_filter($indexStatus, static fn ($queryable) => ! $queryable)); + } + + // Remove empty arrays and wait before checking again + ($indexesToCheck = array_filter($indexesToCheck)) && usleep($waitTimeMs * 1_000); + } + } + /** * Create search indexes for the given document class. * @@ -355,7 +414,7 @@ public function createDocumentSearchIndexes(string $documentName): void throw new InvalidArgumentException('Cannot create search indexes for mapped super classes, embedded documents, query result documents, or views.'); } - $searchIndexes = $class->getSearchIndexes(); + $searchIndexes = $this->prepareSearchIndexes($class); if (empty($searchIndexes)) { return; @@ -367,11 +426,11 @@ public function createDocumentSearchIndexes(string $documentName): void /* createSearchIndexes builds indexes asynchronously but still reports * the names of created indexes. Report an error if any defined names - * were not actually created. */ + * were not created. */ $unprocessedNames = array_diff($definedNames, $createdNames); if (! empty($unprocessedNames)) { - throw new InvalidArgumentException(sprintf('The following search indexes for %s were not created: %s', $class->name, implode(', ', $unprocessedNames))); + throw SchemaException::missingSearchIndex($class->name, $unprocessedNames); } } @@ -410,7 +469,7 @@ public function updateDocumentSearchIndexes(string $documentName): void throw new InvalidArgumentException('Cannot update search indexes for mapped super classes, embedded documents, query result documents, or views.'); } - $searchIndexes = $class->getSearchIndexes(); + $searchIndexes = $this->prepareSearchIndexes($class); $collection = $this->dm->getDocumentCollection($class->name); $definedNames = array_column($searchIndexes, 'name'); @@ -486,6 +545,59 @@ public function deleteDocumentSearchIndexes(string $documentName): void } } + /** + * @param ClassMetadata $class + * + * @phpstan-return list + */ + private function prepareSearchIndexes(ClassMetadata $class): array + { + $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name); + $indexes = $class->getSearchIndexes(); + $newIndexes = []; + + foreach ($indexes as $index) { + $definition = $index['definition']; + if (is_array($definition['fields'] ?? null)) { + // Vector Search Index, field names in 'path' parameter + $fields = []; + foreach ($definition['fields'] as $field) { + $key = $persister->prepareFieldName($field['path']); + if ($class->hasField($key)) { + $field['path'] = $class->getFieldMapping($key)['name']; + } else { + $field['path'] = $key; + } + + $fields[] = $field; + } + + $definition['fields'] = $fields; + } elseif (is_array($definition['mappings']['fields'] ?? null)) { + // Search Index with fields mappings, field names as keys + $fields = []; + foreach ($definition['mappings']['fields'] as $name => $field) { + $key = $persister->prepareFieldName($name); + if ($class->hasField($key)) { + $fields[$class->getFieldMapping($key)['name']] = $field; + } else { + $fields[$key] = $field; + } + } + + $definition['mappings']['fields'] = $fields; + } + + $newIndexes[] = [ + 'type' => $index['type'], + 'name' => $index['name'], + 'definition' => $definition, + ]; + } + + return $newIndexes; + } + /** * Ensure collection validators are up to date for all mapped document classes. */ @@ -1026,14 +1138,12 @@ private function runShardCollectionCommand(string $documentName, ?WriteConcern $ ); } - /** @param ClassMetadata $class */ private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false): void { $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern, $background); $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern, $background); } - /** @param ClassMetadata $class */ private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false): void { $chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection(); @@ -1049,7 +1159,6 @@ private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ); } - /** @param ClassMetadata $class */ private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false): void { $filesCollection = $this->dm->getDocumentCollection($class->getName()); diff --git a/lib/Doctrine/ODM/MongoDB/Tools/ResolveTargetDocumentListener.php b/lib/Doctrine/ODM/MongoDB/Tools/ResolveTargetDocumentListener.php index d2e74fcee2..1a0164ee4e 100644 --- a/lib/Doctrine/ODM/MongoDB/Tools/ResolveTargetDocumentListener.php +++ b/lib/Doctrine/ODM/MongoDB/Tools/ResolveTargetDocumentListener.php @@ -86,10 +86,7 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $args): void } } - /** - * @param ClassMetadata $classMetadata - * @phpstan-param AssociationFieldMapping $mapping - */ + /** @phpstan-param AssociationFieldMapping $mapping */ private function remapAssociation(ClassMetadata $classMetadata, array $mapping): void { $newMapping = $this->resolveTargetDocuments[$mapping['targetDocument']]; diff --git a/lib/Doctrine/ODM/MongoDB/Types/AbstractVectorType.php b/lib/Doctrine/ODM/MongoDB/Types/AbstractVectorType.php new file mode 100644 index 0000000000..aeeb62e775 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Types/AbstractVectorType.php @@ -0,0 +1,134 @@ +getVectorType()); + } + + if (! $value instanceof Binary) { + throw new InvalidArgumentException(sprintf('Invalid data type %s received for vector field, expected null, array or MongoDB\BSON\Binary', get_debug_type($value))); + } + + if ($value->getType() !== Binary::TYPE_VECTOR) { + throw new InvalidArgumentException(sprintf('Invalid binary data of type %d received for vector field, expected binary type %d', $value->getType(), Binary::TYPE_VECTOR)); + } + + if ($value->getVectorType() !== $this->getVectorType()) { + throw new InvalidArgumentException(sprintf('Invalid binary vector data of vector type %s received for vector field, expected vector type %s', $value->getVectorType()->name, $this->getVectorType()->name)); + } + + return $value; + } + + /** @return list|list|list|null */ + public function convertToPHPValue(mixed $value): ?array + { + if ($value === null) { + return null; + } + + if (is_array($value)) { + return $value; + } + + if (! $value instanceof Binary) { + throw new InvalidArgumentException(sprintf('Invalid data of type "%s" received for vector field', get_debug_type($value))); + } + + if ($value->getType() !== Binary::TYPE_VECTOR) { + throw new InvalidArgumentException(sprintf('Invalid binary data of type %d received for vector field', $value->getType())); + } + + if ($value->getVectorType() !== $this->getVectorType()) { + throw new InvalidArgumentException(sprintf('Invalid binary vector data of vector type %s received for vector field, expected vector type %s', $value->getVectorType()->name, $this->getVectorType()->name)); + } + + return $value->toArray(); + } + + public function closureToMongo(): string + { + return str_replace('%%vectorType%%', $this->getVectorType()->name, <<<'PHP' + if ($value === null) { + $return = null; + return; + } + + if (\is_array($value)) { + $return = \MongoDB\BSON\Binary::fromVector($value, \MongoDB\BSON\VectorType::%%vectorType%%); + return; + } + + if (! $value instanceof \MongoDB\BSON\Binary) { + throw new InvalidArgumentException(sprintf('Invalid data type %s received for vector field, expected null, array or MongoDB\BSON\Binary', get_debug_type($value))); + } + + if ($value->getType() !== \MongoDB\BSON\Binary::TYPE_VECTOR) { + throw new InvalidArgumentException(sprintf('Invalid binary data of type %d received for vector field, expected binary type %d', $value->getType(), \MongoDB\BSON\Binary::TYPE_VECTOR)); + } + + if ($value->getVectorType() !== \MongoDB\BSON\VectorType::%%vectorType%%) { + throw new \InvalidArgumentException(sprintf('Invalid binary vector data of vector type %s received for vector field, expected vector type %%vectorType%%', $value->getVectorType()->name)); + } + + $return = $value; +PHP); + } + + public function closureToPHP(): string + { + return str_replace('%%vectorType%%', $this->getVectorType()->name, <<<'PHP' + if ($value === null) { + $return = null; + return; + } + + if (\is_array($value)) { + $return = $value; + return; + } + + if (! $value instanceof \MongoDB\BSON\Binary) { + throw new \InvalidArgumentException(sprintf('Invalid data of type "%s" received for vector field', get_debug_type($value))); + } + + if ($value->getType() !== \MongoDB\BSON\Binary::TYPE_VECTOR) { + throw new \InvalidArgumentException(sprintf('Invalid binary data of type %d received for vector field', $value->getType())); + } + + if ($value->getVectorType() !== \MongoDB\BSON\VectorType::%%vectorType%%) { + throw new \InvalidArgumentException(sprintf('Invalid binary vector data of vector type %s received for vector field, expected vector type %%vectorType%%', $value->getVectorType()->name)); + } + + $return = $value->toArray(); +PHP); + } + + abstract protected function getVectorType(): VectorType; +} diff --git a/lib/Doctrine/ODM/MongoDB/Types/BinaryUuidType.php b/lib/Doctrine/ODM/MongoDB/Types/BinaryUuidType.php new file mode 100644 index 0000000000..0038dfc7f2 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Types/BinaryUuidType.php @@ -0,0 +1,77 @@ + null, + $value instanceof Binary => $value, + $value instanceof Uuid => new Binary($value->toBinary(), Binary::TYPE_UUID), + is_string($value) => new Binary(Uuid::fromString($value)->toBinary(), Binary::TYPE_UUID), + default => throw new InvalidArgumentException(sprintf('Invalid data type %s received for UUID', get_debug_type($value))), + }; + } + + public function convertToPHPValue(mixed $value): Uuid + { + if ($value instanceof Uuid) { + return $value; + } + + if (! $value instanceof Binary) { + throw new InvalidArgumentException(sprintf('Invalid data of type "%s" received for Uuid', get_debug_type($value))); + } + + if ($value->getType() !== Binary::TYPE_UUID) { + throw new InvalidArgumentException(sprintf('Invalid binary data of type %d received for Uuid', $value->getType())); + } + + return Uuid::fromBinary($value->getData()); + } + + public function closureToMongo(): string + { + return <<<'PHP' +$return = match (true) { + $value === null => null, + $value instanceof \MongoDB\BSON\Binary => $value, + $value instanceof \Symfony\Component\Uid\Uuid => new \MongoDB\BSON\Binary($value->toBinary(), \MongoDB\BSON\Binary::TYPE_UUID), + is_string($value) => new \MongoDB\BSON\Binary(\Symfony\Component\Uid\Uuid::fromString($value)->toBinary(), \MongoDB\BSON\Binary::TYPE_UUID), + default => throw new \InvalidArgumentException(sprintf('Invalid data type %s received for UUID', get_debug_type($value))), +}; +PHP; + } + + public function closureToPHP(): string + { + return <<<'PHP' + if ($value instanceof \Symfony\Component\Uid\Uuid) { + $return = $value; + return; + } + + if (! $value instanceof \MongoDB\BSON\Binary) { + throw new \InvalidArgumentException(sprintf('Invalid data of type "%s" received for Uuid', get_debug_type($value))); + } + + if ($value->getType() !== \MongoDB\BSON\Binary::TYPE_UUID) { + throw new \InvalidArgumentException(sprintf('Invalid binary data of type %d received for Uuid', $value->getType())); + } + + $return = \Symfony\Component\Uid\Uuid::fromBinary($value->getData()); +PHP; + } +} diff --git a/lib/Doctrine/ODM/MongoDB/Types/ObjectIdType.php b/lib/Doctrine/ODM/MongoDB/Types/ObjectIdType.php index 13f7f87ffc..9a96a8859a 100644 --- a/lib/Doctrine/ODM/MongoDB/Types/ObjectIdType.php +++ b/lib/Doctrine/ODM/MongoDB/Types/ObjectIdType.php @@ -9,7 +9,7 @@ /** * The ObjectId type. */ -class ObjectIdType extends Type +class ObjectIdType extends Type implements Versionable { public function convertToDatabaseValue($value) { @@ -38,4 +38,9 @@ public function closureToPHP(): string { return '$return = (string) $value;'; } + + public function getNextVersion($current): ObjectId + { + return new ObjectId(); + } } diff --git a/lib/Doctrine/ODM/MongoDB/Types/Type.php b/lib/Doctrine/ODM/MongoDB/Types/Type.php index a6399cd645..391d140dee 100644 --- a/lib/Doctrine/ODM/MongoDB/Types/Type.php +++ b/lib/Doctrine/ODM/MongoDB/Types/Type.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\Types; use InvalidArgumentException; use MongoDB\BSON\ObjectId; +use Symfony\Component\Uid\Uuid; use function end; use function explode; @@ -45,6 +46,10 @@ abstract class Type public const OBJECTID = 'object_id'; public const RAW = 'raw'; public const DECIMAL128 = 'decimal128'; + public const UUID = 'uuid'; + public const VECTOR_FLOAT32 = 'vector_float32'; + public const VECTOR_INT8 = 'vector_int8'; + public const VECTOR_PACKED_BIT = 'vector_packed_bit'; /** @deprecated const was deprecated in doctrine/mongodb-odm 2.1 and will be removed in 3.0. Use Type::INT instead */ public const INTID = 'int_id'; @@ -86,6 +91,10 @@ abstract class Type self::OBJECTID => Types\ObjectIdType::class, self::RAW => Types\RawType::class, self::DECIMAL128 => Types\Decimal128Type::class, + self::UUID => Types\BinaryUuidType::class, + self::VECTOR_FLOAT32 => Types\VectorFloat32Type::class, + self::VECTOR_INT8 => Types\VectorInt8Type::class, + self::VECTOR_PACKED_BIT => Types\VectorPackedBitType::class, ]; /** Prevent instantiation and force use of the factory method. */ @@ -119,11 +128,19 @@ public function convertToPHPValue($value) return $value; } + /** + * Get the PHP code equivalent to {@see convertToDatabaseValue()}, used in code generator. + * Use variables $value for input and $return for output. + */ public function closureToMongo(): string { return '$return = $value;'; } + /** + * Get the PHP code equivalent to {@see convertToPHPValue()}, used in code generator. + * Use variables $value for input and $return for output. + */ public function closureToPHP(): string { return '$return = $value;'; @@ -167,11 +184,15 @@ public static function getTypeFromPHPVariable($variable): ?Type { if (is_object($variable)) { if ($variable instanceof DateTimeInterface) { - return self::getType('date'); + return self::getType(self::DATE); } if ($variable instanceof ObjectId) { - return self::getType('id'); + return self::getType(self::ID); + } + + if ($variable instanceof Uuid) { + return self::getType(self::UUID); } } else { $type = gettype($variable); diff --git a/lib/Doctrine/ODM/MongoDB/Types/VectorFloat32Type.php b/lib/Doctrine/ODM/MongoDB/Types/VectorFloat32Type.php new file mode 100644 index 0000000000..1c4f25bb93 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Types/VectorFloat32Type.php @@ -0,0 +1,15 @@ + array(0 => mixed, 1 => mixed)) + * @return array{property: array{0: mixed, 1: mixed}} * @phpstan-return array */ public function getDocumentChangeSet(object $document): array @@ -1139,7 +1139,7 @@ private function persistNew(ClassMetadata $class, object $document): void )); } - if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) { + if ($class->getIdentifierMapping()['type'] === Type::ID && $idValue !== null && $class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) { throw new InvalidArgumentException(sprintf( '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.', $document::class, diff --git a/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php b/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php index 16efa66aae..ca7ef6691d 100644 --- a/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php +++ b/lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php @@ -293,7 +293,6 @@ private function cascadePostPersist(ClassMetadata $class, object $document, ?Ses } } - /** @param ClassMetadata $class */ private function dispatchEvent(ClassMetadata $class, string $eventName, ?EventArgs $eventArgs = null): void { if ($class->isView()) { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index e39a9c89c6..3a44845f46 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -40,6 +40,7 @@ + @@ -52,7 +53,7 @@ tests/Doctrine/ODM/MongoDB/Tests/Mapping/Documents/GlobalNamespaceDocument.php - + tests/Doctrine/ODM/MongoDB/Tests/Mapping/Documents/GlobalNamespaceDocument.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 62e3a7c837..b123bec670 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,35 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Call to an undefined method MongoDB\\Driver\\Monitoring\\CommandFailedEvent\|MongoDB\\Driver\\Monitoring\\CommandSucceededEvent\:\:getServer\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/APM/Command.php - - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 2 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php - - - - message: '#^Instanceof between MongoDB\\Driver\\CursorInterface and Iterator will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php - - - - message: '#^Instanceof between MongoDB\\Driver\\CursorInterface and MongoDB\\Driver\\CursorInterface will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Aggregation\:\:__construct\(\) has parameter \$classMetadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Aggregation\:\:execute\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics @@ -54,60 +24,18 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Builder\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php - - message: '#^PHPDoc tag @param references unknown parameter\: \$applyFilters$#' identifier: parameter.notFound count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Expr\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Expr\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\:\:execute\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\AbstractBucket\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\AbstractBucket\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractBucket.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\AbstractReplace\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\AbstractReplace\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/AbstractReplace.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Bucket\\AbstractOutput\:\:avg\(\) has parameter \$expressions with no type specified\.$#' identifier: missingType.parameter @@ -156,24 +84,6 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Fill.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\GraphLookup\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\GraphLookup\:\:getDocumentPersister\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\GraphLookup\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\GraphLookup\:\:\$from \(string\|null\) is never assigned null so it can be removed from the property type\.$#' identifier: property.unusedType @@ -192,30 +102,6 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Lookup\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Lookup\:\:getDocumentPersister\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Lookup\:\:getDocumentPersister\(\) return type with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Lookup\:\:prepareFieldName\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php - - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' identifier: argument.templateType @@ -234,12 +120,6 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Out\:\:fromDocument\(\) has parameter \$classMetadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php - - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' identifier: argument.templateType @@ -247,1666 +127,958 @@ parameters: path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Out.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedAutocomplete\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedAutocomplete.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEmbeddedDocument\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEmbeddedDocument.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEquals\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEquals.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedExists\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedExists.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedAutocomplete\:\:__construct\(\) has parameter \$args with no type specified\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoShape\:\:__construct\(\) has parameter \$args with no type specified\.$#' identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedAutocomplete.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoShape.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedAutocomplete\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoWithin\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedAutocomplete.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedAutocomplete\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedMoreLikeThis\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedAutocomplete.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedMoreLikeThis.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEmbeddedDocument\:\:__construct\(\) has parameter \$args with no type specified\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedNear\:\:__construct\(\) has parameter \$args with no type specified\.$#' identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEmbeddedDocument.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedNear.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEmbeddedDocument\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedPhrase\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEmbeddedDocument.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedPhrase.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEmbeddedDocument\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedQueryString\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEmbeddedDocument.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedQueryString.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEquals\:\:__construct\(\) has parameter \$args with no type specified\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRange\:\:__construct\(\) has parameter \$args with no type specified\.$#' identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEquals.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRange.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEquals\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRegex\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEquals.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRegex.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedEquals\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedText\:\:__construct\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedEquals.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedText.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedExists\:\:__construct\(\) has parameter \$args with no type specified\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedWildcard\:\:__construct\(\) has parameter \$args with no type specified\.$#' identifier: missingType.parameter count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedExists.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedWildcard.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedExists\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:__construct\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedExists.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedExists\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:geometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedExists.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoShape\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoShape.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoShape\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:\$geometry type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoShape.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoShape\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:box\(\) has parameter \$bottomLeft with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoShape.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoWithin\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:box\(\) has parameter \$topRight with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoWithin\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:circle\(\) has parameter \$center with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedGeoWithin\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:convertGeometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedGeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedMoreLikeThis\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:convertGeometry\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedMoreLikeThis.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedMoreLikeThis\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:geometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedMoreLikeThis.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedMoreLikeThis\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:\$geometry type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedMoreLikeThis.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedNear\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:\$relation is never read, only written\.$#' + identifier: property.onlyWritten count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedNear.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedNear\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:__construct\(\) has parameter \$origin with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedNear.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedNear\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:origin\(\) has parameter \$origin with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedNear.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedPhrase\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:\$origin type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedPhrase.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedPhrase\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Range\:\:\$includeUpperBound is never read, only written\.$#' + identifier: property.onlyWritten count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedPhrase.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedPhrase\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Class Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith has type alias PipelineParamType with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedPhrase.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedQueryString\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:pipeline\(\) has parameter \$pipeline with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedQueryString.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedQueryString\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:\$pipeline \(array\|Doctrine\\ODM\\MongoDB\\Aggregation\\Builder\|Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\|null\) is never assigned Doctrine\\ODM\\MongoDB\\Aggregation\\Stage so it can be removed from the property type\.$#' + identifier: property.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedQueryString.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedQueryString\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:\$pipeline type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedQueryString.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRange\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' + identifier: argument.templateType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRange.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRange\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\VectorSearch\:\:\$filter type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRange.php + path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRange\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Return type \(Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactoryInterface\) of method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getMetadataFactory\(\) should be compatible with return type \(Doctrine\\Persistence\\Mapping\\ClassMetadataFactory\\>\) of method Doctrine\\Persistence\\ObjectManager\:\:getMetadataFactory\(\)$#' + identifier: method.childReturnType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRange.php + path: lib/Doctrine/ODM/MongoDB/DocumentManager.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRegex\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRegex.php + path: lib/Doctrine/ODM/MongoDB/DocumentManager.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRegex\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\Persistence\\Event\\OnClearEventArgs\\:\:__construct\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRegex.php + path: lib/Doctrine/ODM/MongoDB/Event/OnClearEventArgs.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedRegex\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$initializer of method ProxyManager\\Proxy\\GhostObjectInterface\\:\:setProxyInitializer\(\) expects \(Closure\(ProxyManager\\Proxy\\GhostObjectInterface\\=, string\=, array\\=, Closure\|null\=, array\\=\)\: bool\)\|null, Closure\(ProxyManager\\Proxy\\GhostObjectInterface, string, array, mixed, array\)\: true given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedRegex.php + path: lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedText\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Property Doctrine\\ODM\\MongoDB\\Hydrator\\HydratorFactory\:\:\$hydratorNamespace \(string\|null\) is never assigned null so it can be removed from the property type\.$#' + identifier: property.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedText.php + path: lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedText\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Interface Doctrine\\ODM\\MongoDB\\Iterator\\IterableResult extends generic interface IteratorAggregate but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedText.php + path: lib/Doctrine/ODM/MongoDB/Iterator/IterableResult.php + + - + message: '#^Method Doctrine\\ODM\\MongoDB\\Iterator\\IterableResult\:\:getIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' + identifier: missingType.generics + count: 1 + path: lib/Doctrine/ODM/MongoDB/Iterator/IterableResult.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedText\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\SearchIndex\:\:__construct\(\) has parameter \$fields with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedText.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedWildcard\:\:__construct\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter + message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedWildcard.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedWildcard\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Instanceof between Doctrine\\Persistence\\Reflection\\RuntimeReflectionProperty and ReflectionProperty will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedWildcard.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Compound\\CompoundedWildcard\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:addInheritedAssociationMapping\(\) has Doctrine\\ODM\\MongoDB\\Mapping\\MappingException in PHPDoc @throws tag but it''s not thrown\.$#' + identifier: throws.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Compound/CompoundedWildcard.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\EmbeddedDocument\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:getBucketName\(\) never returns null so it can be removed from the return type\.$#' + identifier: return.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\EmbeddedDocument\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:mapField\(\) should return array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\} but returns array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}\.$#' + identifier: return.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:__construct\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:checkDuplicateMapping\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:geometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:setVersionMapping\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\: string, fieldName\: string, name\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoShape\:\:\$geometry type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:typeRequirementsAreMet\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:box\(\) has parameter \$bottomLeft with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$rootClass \(class\-string\|null\) is never assigned null so it can be removed from the property type\.$#' + identifier: property.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:box\(\) has parameter \$topRight with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$name\.$#' + identifier: generics.variance count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:circle\(\) has parameter \$center with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$reflClass\.$#' + identifier: generics.variance count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:convertGeometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Template type T is declared as covariant, but occurs in invariant position in return type of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:getReflectionClass\(\)\.$#' + identifier: generics.variance count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:convertGeometry\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\AttributeDriver\:\:getClassAttributes\(\) has parameter \$class with generic class ReflectionClass but does not specify its types\: T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:geometry\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\AttributeReader\:\:getClassAttributes\(\) has parameter \$class with generic class ReflectionClass but does not specify its types\: T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:\$geometry type has no value type specified in iterable type array\.$#' + message: '#^Match expression does not handle remaining value\: string$#' + identifier: match.unhandled + count: 1 + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php + + - + message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:getSearchIndexFieldDefinition\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\GeoWithin\:\:\$relation is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Parameter \#2 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:addFieldMapping\(\) expects array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}, array\, Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\EncryptQuery\|float\|int\|MongoDB\\BSON\\Decimal128\|MongoDB\\BSON\\Int64\|MongoDB\\BSON\\UTCDateTime\|string\|null\>\|bool\|string\> given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:__construct\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#2 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:addFieldMapping\(\) expects array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}, non\-empty\-array\\|string\|true\> given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:origin\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#2 \$options of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:addIndex\(\) expects array\{background\?\: bool, bits\?\: int, default_language\?\: string, expireAfterSeconds\?\: int, language_override\?\: string, min\?\: float, max\?\: float, name\?\: string, \.\.\.\}, array\\|string, mixed\>\|bool\|float\|int\|string\|null\>\|bool\|float\|int\|string\|null\> given\.$#' + identifier: argument.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php + path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Near\:\:\$origin type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Call to function is_object\(\) with Doctrine\\Common\\Collections\\Collection will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\Range\:\:\$includeUpperBound is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Call to function method_exists\(\) with Doctrine\\Common\\Collections\\Collection and ''findFirst'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\SupportsGeoShapeOperator\:\:geoShape\(\) has parameter \$geometry with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Call to function method_exists\(\) with Doctrine\\Common\\Collections\\Collection and ''reduce'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsGeoShapeOperator.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Search\\SupportsNearOperator\:\:near\(\) has parameter \$origin with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:add\(\) with return type void returns true but should not return anything\.$#' + identifier: return.void count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\SortByCount\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^PHPDoc tag @var for property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints with type Doctrine\\ODM\\MongoDB\\PersistentCollection\\Hints is incompatible with native type array\.$#' + identifier: property.phpDocType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SortByCount.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Class Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith has type alias PipelineParamType with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^PHPDoc tag @var for property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping with type Doctrine\\ODM\\MongoDB\\PersistentCollection\\FieldMapping\|null is not subtype of native type array\|null\.$#' + identifier: property.phpDocType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:pipeline\(\) has parameter \$pipeline with no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints has unknown class Doctrine\\ODM\\MongoDB\\PersistentCollection\\Hints as its type\.$#' + identifier: class.notFound + count: 1 + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + + - + message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:\$pipeline \(array\|Doctrine\\ODM\\MongoDB\\Aggregation\\Builder\|Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\|null\) is never assigned Doctrine\\ODM\\MongoDB\\Aggregation\\Stage so it can be removed from the property type\.$#' - identifier: property.unusedType + message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping has unknown class Doctrine\\ODM\\MongoDB\\PersistentCollection\\FieldMapping as its type\.$#' + identifier: class.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnionWith\:\:\$pipeline type has no value type specified in iterable type array\.$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' identifier: argument.templateType count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\UnsetStage\:\:__construct\(\) has parameter \$documentPersister with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\\AbstractPersistentCollectionFactory\:\:createCollectionClass\(\) return type with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnsetStage.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection/AbstractPersistentCollectionFactory.php - - message: '#^Return type \(Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactoryInterface\) of method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getMetadataFactory\(\) should be compatible with return type \(Doctrine\\Persistence\\Mapping\\ClassMetadataFactory\\>\) of method Doctrine\\Persistence\\ObjectManager\:\:getMetadataFactory\(\)$#' - identifier: method.childReturnType + message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\\DefaultPersistentCollectionFactory\:\:createCollectionClass\(\) return type with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/DocumentManager.php + path: lib/Doctrine/ODM/MongoDB/PersistentCollection/DefaultPersistentCollectionFactory.php - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:delete\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/DocumentManager.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Method Doctrine\\Persistence\\Event\\OnClearEventArgs\\:\:__construct\(\) invoked with 2 parameters, 1 required\.$#' - identifier: arguments.count + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:deleteElements\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Event/OnClearEventArgs.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Parameter \#1 \$initializer of method ProxyManager\\Proxy\\GhostObjectInterface\\:\:setProxyInitializer\(\) expects \(Closure\(ProxyManager\\Proxy\\GhostObjectInterface\\=, string\=, array\\=, Closure\|null\=, array\\=\)\: bool\)\|null, Closure\(ProxyManager\\Proxy\\GhostObjectInterface, string, array, mixed, array\)\: true given\.$#' - identifier: argument.type + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:getPathAndParent\(\) has parameter \$coll with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Hydrator\\HydratorFactory\:\:\$hydratorNamespace \(string\|null\) is never assigned null so it can be removed from the property type\.$#' - identifier: property.unusedType + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:getValuePrepareCallback\(\) has parameter \$coll with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Interface Doctrine\\ODM\\MongoDB\\Iterator\\IterableResult extends generic interface IteratorAggregate but does not specify its types\: TKey, TValue$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:insertElements\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Iterator/IterableResult.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Iterator\\IterableResult\:\:getIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:setCollections\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Iterator/IterableResult.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\SearchIndex\:\:__construct\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:update\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Annotations/SearchIndex.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^PHPDoc tag @var for variable \$addToSetColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata has type alias SearchIndexDefinition with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 2 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata has type alias SearchIndexMapping with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 2 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + message: '#^PHPDoc tag @var for variable \$pushAllColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics + count: 1 + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Instanceof between Doctrine\\Persistence\\Reflection\\RuntimeReflectionProperty and ReflectionProperty will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue + message: '#^PHPDoc tag @var for variable \$setColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:addInheritedAssociationMapping\(\) has Doctrine\\ODM\\MongoDB\\Mapping\\MappingException in PHPDoc @throws tag but it''s not thrown\.$#' - identifier: throws.unusedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:addSearchIndex\(\) has parameter \$definition with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 2 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:getBucketName\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:getSearchIndexes\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 2 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:mapField\(\) should return array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\} but returns array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}\.$#' - identifier: return.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:checkDuplicateMapping\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' - identifier: argument.type + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:setVersionMapping\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\: string, fieldName\: string, name\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' - identifier: argument.type + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyInverseSideQuery\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Parameter \#1 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:typeRequirementsAreMet\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\} given\.$#' - identifier: argument.type + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyWithRepositoryMethodCursor\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$rootClass \(class\-string\|null\) is never assigned null so it can be removed from the property type\.$#' - identifier: property.unusedType + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyWithRepositoryMethodCursor\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' + identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$searchIndexes type has no value type specified in iterable type array\.$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:isInTransaction\(\) has parameter \$options with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue - count: 2 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$name\.$#' - identifier: generics.variance - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:\$reflClass\.$#' - identifier: generics.variance - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php - - - - message: '#^Template type T is declared as covariant, but occurs in invariant position in return type of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\:\:getReflectionClass\(\)\.$#' - identifier: generics.variance count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedFields\(\) has parameter \$parentClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadAll\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedFields\(\) has parameter \$subClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadCollection\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedIndexes\(\) has parameter \$parentClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadEmbedManyCollection\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedIndexes\(\) has parameter \$subClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyCollectionInverseSide\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedRelations\(\) has parameter \$parentClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyCollectionOwningSide\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:addInheritedRelations\(\) has parameter \$subClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyWithRepositoryMethod\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:completeIdGeneratorMapping\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:wrapCursor\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:doLoadMetadata\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Parameter \#1 \$array \(non\-empty\-list\) of array_values is already a list, call has no effect\.$#' + identifier: arrayValues.list count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:doLoadMetadata\(\) has parameter \$parent with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\PersistenceBuilder\:\:preparePersistentCollection\(\) has parameter \$mapping with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:initializeReflection\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\PersistenceBuilder\:\:preparePersistentCollection\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:isEntity\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Call to an undefined static method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyGhost\(\)\.$#' + identifier: staticMethod.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:setInheritedShardKey\(\) has parameter \$parentClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Call to function is_scalar\(\) with int\\|int\<5, max\> will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:setInheritedShardKey\(\) has parameter \$subClass with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyInitializer\(\) has Doctrine\\ODM\\MongoDB\\DocumentNotFoundException in PHPDoc @throws tag but it''s not thrown\.$#' + identifier: throws.unusedType count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:validateIdentifier\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyInitializer\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Proxy\\InternalProxy does not specify its types\: T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:wakeupReflection\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Proxy\\InternalProxy does not specify its types\: T$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - message: '#^PHPDoc tag @method for method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactoryInterface\:\:getAllMetadata\(\) return type contains generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics + message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\StaticProxyFactory\:\:createInitializer\(\) should return Closure\(ProxyManager\\Proxy\\GhostObjectInterface\&TDocument\=, string\=, array\\=, Closure\|null\=, array\\=\)\: bool but returns Closure\(ProxyManager\\Proxy\\GhostObjectInterface, string, array, mixed, array\)\: true\.$#' + identifier: return.type count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php + path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php - - message: '#^PHPDoc tag @method for method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactoryInterface\:\:getLoadedMetadata\(\) return type contains generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php + message: '#^Unsafe call to private method Doctrine\\ODM\\MongoDB\\Query\\Expr\:\:convertExpression\(\) through static\:\:\.$#' + identifier: staticClassAccess.privateMethod + count: 3 + path: lib/Doctrine/ODM/MongoDB/Query/Expr.php - - message: '#^PHPDoc tag @method for method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactoryInterface\:\:getMetadataFor\(\) return type contains generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Query\:\:getIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactoryInterface.php + path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\AttributeDriver\:\:getClassAttributes\(\) has parameter \$class with generic class ReflectionClass but does not specify its types\: T$#' + message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Query\:\:makeIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php + path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\AttributeReader\:\:getClassAttributes\(\) has parameter \$class with generic class ReflectionClass but does not specify its types\: T$#' + message: '#^Property Doctrine\\ODM\\MongoDB\\Query\\Query\:\:\$iterator with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' identifier: missingType.generics count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeReader.php - - - - message: '#^Match expression does not handle remaining value\: string$#' - identifier: match.unhandled - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:getSearchIndexFieldDefinition\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - - - message: '#^Parameter \#2 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:addFieldMapping\(\) expects array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}, array\, Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\EncryptQuery\|float\|int\|MongoDB\\BSON\\Decimal128\|MongoDB\\BSON\\Int64\|MongoDB\\BSON\\UTCDateTime\|string\|null\>\|bool\|string\> given\.$#' - identifier: argument.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - - - message: '#^Parameter \#2 \$mapping of method Doctrine\\ODM\\MongoDB\\Mapping\\Driver\\XmlDriver\:\:addFieldMapping\(\) expects array\{type\?\: string, fieldName\?\: string, name\?\: string, strategy\?\: string, association\?\: int, id\?\: bool, isOwningSide\?\: bool, collectionClass\?\: class\-string, \.\.\.\}, non\-empty\-array\\|string\|true\> given\.$#' - identifier: argument.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - - - message: '#^Parameter \#2 \$options of method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata\\:\:addIndex\(\) expects array\{background\?\: bool, bits\?\: int, default_language\?\: string, expireAfterSeconds\?\: int, language_override\?\: string, min\?\: float, max\?\: float, name\?\: string, \.\.\.\}, array\\|string, mixed\>\|bool\|float\|int\|string\|null\>\|bool\|float\|int\|string\|null\> given\.$#' - identifier: argument.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php - - - - message: '#^Call to function is_object\(\) with Doctrine\\Common\\Collections\\Collection will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - - - message: '#^Call to function method_exists\(\) with Doctrine\\Common\\Collections\\Collection and ''findFirst'' will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - - - message: '#^Call to function method_exists\(\) with Doctrine\\Common\\Collections\\Collection and ''reduce'' will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:add\(\) with return type void returns true but should not return anything\.$#' - identifier: return.void - count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php - - - - message: '#^PHPDoc tag @var for property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints with type Doctrine\\ODM\\MongoDB\\PersistentCollection\\Hints is incompatible with native type array\.$#' - identifier: property.phpDocType - count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - message: '#^PHPDoc tag @var for property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping with type Doctrine\\ODM\\MongoDB\\PersistentCollection\\FieldMapping\|null is not subtype of native type array\|null\.$#' - identifier: property.phpDocType + message: '#^Strict comparison using \!\=\= between array\\|bool\|int\|MongoDB\\Driver\\ReadPreference\|string and null will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints has unknown class Doctrine\\ODM\\MongoDB\\PersistentCollection\\Hints as its type\.$#' - identifier: class.notFound + message: '#^Parameter &\$groupedIds by\-ref type of method Doctrine\\ODM\\MongoDB\\Query\\ReferencePrimer\:\:addManyReferences\(\) expects array\\>, array\\> given\.$#' + identifier: parameterByRef.type count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$hints type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/ClearCache/MetadataCommand.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping has unknown class Doctrine\\ODM\\MongoDB\\PersistentCollection\\FieldMapping as its type\.$#' - identifier: class.notFound + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GenerateHydratorsCommand.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\PersistentCollection\:\:\$mapping type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GeneratePersistentCollectionsCommand.php - - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' - identifier: argument.templateType + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GenerateProxiesCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\\AbstractPersistentCollectionFactory\:\:createCollectionClass\(\) return type with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection/AbstractPersistentCollectionFactory.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/QueryCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\PersistentCollection\\DefaultPersistentCollectionFactory\:\:createCollectionClass\(\) return type with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/PersistentCollection/DefaultPersistentCollectionFactory.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:delete\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Match expression does not handle remaining value\: string$#' + identifier: match.unhandled count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/CreateCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:deleteElements\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Match expression does not handle remaining value\: string$#' + identifier: match.unhandled count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/DropCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:getPathAndParent\(\) has parameter \$coll with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php + path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/ValidateCommand.php - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:getValuePrepareCallback\(\) has parameter \$coll with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:insertElements\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:setCollections\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:update\(\) has parameter \$collections with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^PHPDoc tag @var for variable \$addToSetColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^PHPDoc tag @var for variable \$pushAllColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^PHPDoc tag @var for variable \$setColls contains generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php - - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 2 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Instanceof between MongoDB\\Collection and MongoDB\\Collection will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Instanceof between MongoDB\\Driver\\CursorInterface and Iterator will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Instanceof between MongoDB\\Driver\\CursorInterface and MongoDB\\Driver\\CursorInterface will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyInverseSideQuery\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyWithRepositoryMethodCursor\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:createReferenceManyWithRepositoryMethodCursor\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:getClassDiscriminatorValues\(\) has parameter \$metadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:isInTransaction\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadAll\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadCollection\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadEmbedManyCollection\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyCollectionInverseSide\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyCollectionOwningSide\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:loadReferenceManyWithRepositoryMethod\(\) has parameter \$collection with generic interface Doctrine\\ODM\\MongoDB\\PersistentCollection\\PersistentCollectionInterface but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:prepareQueryElement\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:prepareQueryExpression\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister\:\:wrapCursor\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Parameter \#1 \$array \(non\-empty\-list\) of array_values is already a list, call has no effect\.$#' - identifier: arrayValues.list - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Result of && is always true\.$#' - identifier: booleanAnd.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php - - - - message: '#^Call to an undefined static method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyGhost\(\)\.$#' - identifier: staticMethod.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Call to function is_scalar\(\) with int\\|int\<5, max\> will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyInitializer\(\) has Doctrine\\ODM\\MongoDB\\DocumentNotFoundException in PHPDoc @throws tag but it''s not thrown\.$#' - identifier: throws.unusedType - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyInitializer\(\) has parameter \$persister with generic class Doctrine\\ODM\\MongoDB\\Persisters\\DocumentPersister but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:createLazyInitializer\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Proxy\\InternalProxy does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:generateProxyClass\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:generateProxyClasses\(\) has parameter \$classes with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:generateSerializeImpl\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:generateUseLazyGhostTrait\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Proxy\\InternalProxy does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:loadProxyClass\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\LazyGhostProxyFactory\:\:skipClass\(\) has parameter \$metadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/LazyGhostProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Proxy\\Factory\\StaticProxyFactory\:\:createInitializer\(\) should return Closure\(ProxyManager\\Proxy\\GhostObjectInterface\&TDocument\=, string\=, array\\=, Closure\|null\=, array\\=\)\: bool but returns Closure\(ProxyManager\\Proxy\\GhostObjectInterface, string, array, mixed, array\)\: true\.$#' - identifier: return.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Expr\:\:convertExpression\(\) has parameter \$classMetadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Expr.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Expr\:\:convertExpressions\(\) has parameter \$classMetadata with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Expr.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Expr\:\:setClassMetadata\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Expr.php - - - - message: '#^Unsafe call to private method Doctrine\\ODM\\MongoDB\\Query\\Expr\:\:convertExpression\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 3 - path: lib/Doctrine/ODM/MongoDB/Query/Expr.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Query\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Query\:\:getIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\Query\:\:makeIterator\(\) return type with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Query\\Query\:\:\$iterator with generic interface Doctrine\\ODM\\MongoDB\\Iterator\\Iterator does not specify its types\: TValue$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - - - message: '#^Strict comparison using \!\=\= between array\\|bool\|int\|MongoDB\\Driver\\ReadPreference\|string and null will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/Query.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Query\\ReferencePrimer\:\:defaultPrimer\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php - - - - message: '#^Parameter &\$groupedIds by\-ref type of method Doctrine\\ODM\\MongoDB\\Query\\ReferencePrimer\:\:addManyReferences\(\) expects array\\>, array\\> given\.$#' - identifier: parameterByRef.type - count: 1 - path: lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/ClearCache/MetadataCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GenerateHydratorsCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GeneratePersistentCollectionsCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/GenerateProxiesCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/QueryCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/AbstractCommand.php - - - - message: '#^Match expression does not handle remaining value\: string$#' - identifier: match.unhandled - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/CreateCommand.php - - - - message: '#^Match expression does not handle remaining value\: string$#' - identifier: match.unhandled - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/DropCommand.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Console\\Helper\\HelperInterface\:\:getDocumentManager\(\)\.$#' - identifier: method.notFound - count: 1 - path: lib/Doctrine/ODM/MongoDB/Tools/Console/Command/Schema/ValidateCommand.php - - - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 path: lib/Doctrine/ODM/MongoDB/Types/DateImmutableType.php - - message: '#^Unsafe call to private method Doctrine\\ODM\\MongoDB\\Types\\DateType\:\:craftDateTime\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 1 - path: lib/Doctrine/ODM/MongoDB/Types/DateType.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:getTransactionOptions\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:stripTransactionOptions\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:stripTransactionOptions\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:withTransaction\(\) has parameter \$transactionOptions with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - - - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' - identifier: argument.templateType - count: 1 - path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - - - message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' - identifier: argument.templateType - count: 3 - path: lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php - - - - message: '#^Parameter \#1 \$builder of method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Facet\:\:pipeline\(\) expects Doctrine\\ODM\\MongoDB\\Aggregation\\Builder\|Doctrine\\ODM\\MongoDB\\Aggregation\\Stage, stdClass given\.$#' - identifier: argument.type - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/FacetTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\ProjectTest\:\:testAccumulatorsWithMultipleArguments\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/ProjectTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\ProjectTest\:\:testAccumulatorsWithMultipleArguments\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/ProjectTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchCompoundOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchEmbeddedDocumentOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSearchAfter\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSearchBefore\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSort\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SetWindowFieldsTest\:\:testOperators\(\) has parameter \$args with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SetWindowFieldsTest.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SetWindowFieldsTest\:\:testOperators\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SetWindowFieldsTest.php - - - - message: '#^Constant DOCTRINE_MONGODB_SERVER not found\.$#' - identifier: constant.notFound - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\BaseTestCase\:\:assertArraySubset\(\) has parameter \$array with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - - - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\BaseTestCase\:\:assertArraySubset\(\) has parameter \$subset with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - - - message: '#^Used constant DOCTRINE_MONGODB_SERVER not found\.$#' - identifier: constant.notFound - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - - - message: '#^Parameter \#2 \$referenceMapping of method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:createReference\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{storeAs\: ''dbRef''\} given\.$#' - identifier: argument.type - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php - - - - message: '#^Parameter \#2 \$projects of class Documents\\Developer constructor expects Doctrine\\Common\\Collections\\Collection\\|null, Doctrine\\Common\\Collections\\ArrayCollection\ given\.$#' - identifier: argument.type - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/DocumentRepositoryTest.php - - - - message: '#^Parameter \#2 \$collections of method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:delete\(\) expects array\, array\\|Doctrine\\Common\\Collections\\Collection\\> given\.$#' - identifier: argument.type - count: 3 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\CustomDatabaseTest\:\:\$id is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\DefaultDatabaseTest\:\:\$id is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php - - - - message: '#^PHPDoc type Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithDiscriminator\:\:\$embeddedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$embeddedChildren\.$#' - identifier: property.phpDocType - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - - - message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithDiscriminator\:\:\$referencedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$referencedChildren\.$#' - identifier: property.phpDocType - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - - - message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithoutDiscriminator\:\:\$embeddedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$embeddedChildren\.$#' - identifier: property.phpDocType - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - - - message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithoutDiscriminator\:\:\$referencedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$referencedChildren\.$#' - identifier: property.phpDocType - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\DocumentPersisterTestDocumentWithReferenceToDocumentWithCustomId\:\:\$documentWithCustomId is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\DocumentPersisterTestDocumentWithReferenceToDocumentWithCustomId\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php - - - - message: '#^Expression "\$groups\[0\]" on a separate line does not do anything\.$#' - identifier: expr.resultUnused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ChildObject\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/LifecycleTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentObject\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/LifecycleTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Hierarchy\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/NestedDocumentsTest.php - - - - message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientId\.$#' - identifier: property.notFound - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php - - - - message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientName\.$#' - identifier: property.notFound - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php - - - - message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientRecord\.$#' - identifier: property.notFound - count: 6 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php - - - - message: '#^Parameter \$discriminatorMap of attribute class Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\ReferenceOne constructor expects array\\|null, array\ given\.$#' - identifier: argument.type - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/TargetDocumentTest.php - - - - message: '#^Parameter \$targetDocument of attribute class Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\ReferenceOne constructor expects class\-string\|null, string given\.$#' - identifier: argument.type + message: '#^Unsafe call to private method Doctrine\\ODM\\MongoDB\\Types\\DateType\:\:craftDateTime\(\) through static\:\:\.$#' + identifier: staticClassAccess.privateMethod count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/TargetDocumentTest.php + path: lib/Doctrine/ODM/MongoDB/Types/DateType.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1058PersistDocument\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:getTransactionOptions\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1058Test.php + path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1058PersistDocument\:\:\$value is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:stripTransactionOptions\(\) has parameter \$options with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1058Test.php + path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1058UpsertDocument\:\:\$value is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:stripTransactionOptions\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1058Test.php + path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1964Document\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\UnitOfWork\:\:withTransaction\(\) has parameter \$transactionOptions with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1964Test.php + path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1990Document\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' + identifier: argument.templateType count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1990Test.php + path: lib/Doctrine/ODM/MongoDB/UnitOfWork.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH1990Document\:\:\$parent is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1990Test.php + message: '#^Unable to resolve the template type T in call to method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:getClassMetadata\(\)$#' + identifier: argument.templateType + count: 3 + path: lib/Doctrine/ODM/MongoDB/Utility/LifecycleEventManager.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH921Post\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Parameter \#1 \$builder of method Doctrine\\ODM\\MongoDB\\Aggregation\\Stage\\Facet\:\:pipeline\(\) expects Doctrine\\ODM\\MongoDB\\Aggregation\\Builder\|Doctrine\\ODM\\MongoDB\\Aggregation\\Stage, stdClass given\.$#' + identifier: argument.type count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH921Test.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/FacetTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH921User\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\ProjectTest\:\:testAccumulatorsWithMultipleArguments\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH921Test.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/ProjectTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\GH999Document\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\ProjectTest\:\:testAccumulatorsWithMultipleArguments\(\) has parameter \$expected with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH999Test.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/ProjectTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\Ticket\\MODM116Parent\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchCompoundOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/MODM116Test.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\AnnotationDriverTestSuper\:\:\$private is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchEmbeddedDocumentOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractAnnotationDriverTestCase.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\DocumentSubClass2\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperators\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\DocumentSubClass2\:\:\$name is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSearchAfter\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\DocumentSubClass\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSearchBefore\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\DocumentSubClass\:\:\$name is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SearchTest\:\:testSearchOperatorsWithSort\(\) has parameter \$expectedOperator with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\GridFSChildClass\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SetWindowFieldsTest\:\:testOperators\(\) has parameter \$args with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SetWindowFieldsTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\GridFSParentClass\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Aggregation\\Stage\\SetWindowFieldsTest\:\:testOperators\(\) has parameter \$expected with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SetWindowFieldsTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassBase\:\:\$mapped1 is unused\.$#' - identifier: property.unused + message: '#^Constant DOCTRINE_MONGODB_SERVER not found\.$#' + identifier: constant.notFound count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassBase\:\:\$mapped2 is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\BaseTestCase\:\:assertArraySubset\(\) has parameter \$array with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassBase\:\:\$mappedRelated1 is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\BaseTestCase\:\:assertArraySubset\(\) has parameter \$subset with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassBase\:\:\$transient is unused\.$#' - identifier: property.unused + message: '#^Used constant DOCTRINE_MONGODB_SERVER not found\.$#' + identifier: constant.notFound count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassRelated1\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Parameter \#2 \$referenceMapping of method Doctrine\\ODM\\MongoDB\\DocumentManager\:\:createReference\(\) expects array\{type\: string, fieldName\: string, name\: string, isCascadeRemove\: bool, isCascadePersist\: bool, isCascadeRefresh\: bool, isCascadeMerge\: bool, isCascadeDetach\: bool, \.\.\.\}, array\{storeAs\: ''dbRef''\} given\.$#' + identifier: argument.type count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/DocumentManagerTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\MappedSuperclassRelated1\:\:\$name is unused\.$#' - identifier: property.unused + message: '#^Parameter \#2 \$projects of class Documents\\Developer constructor expects Doctrine\\Common\\Collections\\Collection\\|null, Doctrine\\Common\\Collections\\ArrayCollection\ given\.$#' + identifier: argument.type count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/DocumentRepositoryTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\TransientBaseClass\:\:\$transient1 is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + message: '#^Parameter \#2 \$collections of method Doctrine\\ODM\\MongoDB\\Persisters\\CollectionPersister\:\:delete\(\) expects array\, array\\|Doctrine\\Common\\Collections\\Collection\\> given\.$#' + identifier: argument.type + count: 3 + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/CollectionPersisterTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\TransientBaseClass\:\:\$transient2 is unused\.$#' - identifier: property.unused + message: '#^PHPDoc type Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithDiscriminator\:\:\$embeddedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$embeddedChildren\.$#' + identifier: property.phpDocType count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/BasicInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\LoadEventTestDocument\:\:\$about is unused\.$#' - identifier: property.unused + message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithDiscriminator\:\:\$referencedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$referencedChildren\.$#' + identifier: property.phpDocType count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataLoadEventTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\LoadEventTestDocument\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithoutDiscriminator\:\:\$embeddedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$embeddedChildren\.$#' + identifier: property.phpDocType count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataLoadEventTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\LoadEventTestDocument\:\:\$name is unused\.$#' - identifier: property.unused + message: '#^PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocumentWithoutDiscriminator\:\:\$referencedChildren is not covariant with PHPDoc type array\\|Doctrine\\Common\\Collections\\Collection\ of overridden property Doctrine\\ODM\\MongoDB\\Tests\\Functional\\ParentDocument\:\:\$referencedChildren\.$#' + identifier: property.phpDocType count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataLoadEventTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/DiscriminatorsDefaultValueTest.php - - message: '#^Property DoctrineGlobal_User\:\:\$email is unused\.$#' - identifier: property.unused + message: '#^Expression "\$groups\[0\]" on a separate line does not do anything\.$#' + identifier: expr.resultUnused count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/Documents/GlobalNamespaceDocument.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/FunctionalTest.php - - message: '#^Property DoctrineGlobal_User\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientId\.$#' + identifier: property.notFound count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/Documents/GlobalNamespaceDocument.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php - - message: '#^Property DoctrineGlobal_User\:\:\$username is unused\.$#' - identifier: property.unused + message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientName\.$#' + identifier: property.notFound count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/Documents/GlobalNamespaceDocument.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php + + - + message: '#^Access to an undefined property MongoDB\\Model\\BSONDocument\:\:\$patientRecord\.$#' + identifier: property.notFound + count: 6 + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryableEncryptionTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ShardedCollectionPerClass1\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Parameter \$discriminatorMap of attribute class Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\ReferenceOne constructor expects array\\|null, array\ given\.$#' + identifier: argument.type count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/TargetDocumentTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ShardedSingleCollInheritance1\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Parameter \$targetDocument of attribute class Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\ReferenceOne constructor expects class\-string\|null, string given\.$#' + identifier: argument.type count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Functional/TargetDocumentTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ShardedSubclass\:\:\$id is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ClassMetadataTest\:\:testEmptyVectorSearchIndexDefinition\(\) has parameter \$definition with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ShardedSuperclass\:\:\$name is unused\.$#' - identifier: property.unused + message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Mapping\\ClassMetadataTest\:\:testSearchIndexDefinition\(\) has parameter \$definition with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php + path: tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\Query\\BuilderTest\:\:testExclude\(\) has parameter \$expected with no value type specified in iterable type array\.$#' @@ -2053,289 +1225,19 @@ parameters: path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\GH297\\User\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/GH297/User.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\ResolveTargetDocument\:\:\$embedMany is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\ResolveTargetDocument\:\:\$embedOne is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\ResolveTargetDocument\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\ResolveTargetDocument\:\:\$refMany is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\ResolveTargetDocument\:\:\$refOne is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\Tools\\TargetDocument\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php - - - - message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\ArrayTest\:\:\$id is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php - - - - message: '#^Property Documents\\Account\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Account.php - - - - message: '#^Property Documents\\Address\:\:\$test is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/Address.php - - - - message: '#^Property Documents\\Album\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Album.php - - - - message: '#^Property Documents\\Article\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Article.php - - - - message: '#^Property Documents\\Bars\\Bar\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Bars/Bar.php - - - - message: '#^Property Documents\\Category\:\:\$id is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/Category.php - - - - message: '#^Property Documents\\Developer\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Developer.php - - - - message: '#^Property Documents\\Developer\:\:\$name is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with arguments MongoDB\\BSON\\Binary, null and ''Binary UUIDs are…'' will always evaluate to false\.$#' + identifier: staticMethod.impossibleType count: 1 - path: tests/Documents/Developer.php + path: tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php - - message: '#^Property Documents\\Ecommerce\\StockItem\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with arguments Symfony\\Component\\Uid\\UuidV4, null and ''Uuid objects are…'' will always evaluate to false\.$#' + identifier: staticMethod.impossibleType count: 1 - path: tests/Documents/Ecommerce/StockItem.php + path: tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php - message: '#^Method Documents\\Encryption\\Client\:\:__construct\(\) has parameter \$clientCards with generic interface Doctrine\\Common\\Collections\\Collection but does not specify its types\: TKey, T$#' identifier: missingType.generics count: 1 path: tests/Documents/Encryption/Client.php - - - - message: '#^Property Documents\\Event\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Event.php - - - - message: '#^Property Documents\\File\:\:\$chunkSize is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/File.php - - - - message: '#^Property Documents\\File\:\:\$filename is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/File.php - - - - message: '#^Property Documents\\File\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/File.php - - - - message: '#^Property Documents\\File\:\:\$length is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/File.php - - - - message: '#^Property Documents\\File\:\:\$uploadDate is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/File.php - - - - message: '#^Property Documents\\FileWithoutChunkSize\:\:\$chunkSize is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutChunkSize.php - - - - message: '#^Property Documents\\FileWithoutChunkSize\:\:\$filename is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutChunkSize.php - - - - message: '#^Property Documents\\FileWithoutChunkSize\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutChunkSize.php - - - - message: '#^Property Documents\\FileWithoutChunkSize\:\:\$length is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutChunkSize.php - - - - message: '#^Property Documents\\FileWithoutChunkSize\:\:\$uploadDate is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutChunkSize.php - - - - message: '#^Property Documents\\FileWithoutMetadata\:\:\$filename is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutMetadata.php - - - - message: '#^Property Documents\\FileWithoutMetadata\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/FileWithoutMetadata.php - - - - message: '#^Property Documents\\Functional\\FavoritesUser\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Functional/FavoritesUser.php - - - - message: '#^Property Documents\\Group\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Group.php - - - - message: '#^Property Documents\\Message\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Message.php - - - - message: '#^Property Documents\\ProfileNotify\:\:\$profileId is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/ProfileNotify.php - - - - message: '#^Property Documents\\Project\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Project.php - - - - message: '#^Property Documents\\SchemaValidated\:\:\$email is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/SchemaValidated.php - - - - message: '#^Property Documents\\SchemaValidated\:\:\$id is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/SchemaValidated.php - - - - message: '#^Property Documents\\SchemaValidated\:\:\$name is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/SchemaValidated.php - - - - message: '#^Property Documents\\SchemaValidated\:\:\$phone is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/SchemaValidated.php - - - - message: '#^Property Documents\\SchemaValidated\:\:\$status is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Documents/SchemaValidated.php - - - - message: '#^Property Documents\\Task\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Task.php - - - - message: '#^Property Documents\\Tournament\\Participant\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Tournament/Participant.php - - - - message: '#^Property Documents\\Tournament\\Tournament\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/Tournament/Tournament.php - - - - message: '#^Property Documents\\UserName\:\:\$id is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/UserName.php - - - - message: '#^Property Documents\\UserName\:\:\$username is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/UserName.php - - - - message: '#^Property Documents\\UserName\:\:\$viewReference is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/UserName.php - - - - message: '#^Property Documents\\ViewReference\:\:\$referenceOneViewMappedBy is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: tests/Documents/ViewReference.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2c2c3e1985..f5f3707fbe 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -26,13 +26,34 @@ parameters: - identifier: staticMethod.alreadyNarrowedType path: tests/Doctrine/ - - message: '#with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata does not specify its types: T$#' + - message: '#generic class Doctrine\\ODM\\MongoDB\\(Mapping\\ClassMetadata|Persisters\\DocumentPersister) (but )?does not specify its types\: T$#' identifier: missingType.generics - message: '#^(Constant|Used constant) DOCTRINE_MONGODB_DATABASE not found\.$#' identifier: constant.notFound path: tests/ + # Unused properties from test document classes + - message: '#^Property (DoctrineGlobal_|Documents\\|Doctrine\\ODM\\MongoDB\\Tests\\)[^:]+(? [ 'autocomplete' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => 'content', + 'path' => 'article_title', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete('content', 'MongoDB', 'Aggregation', 'Pipeline') - ->path('content'); + ->path('title'); }, ]; @@ -49,8 +52,7 @@ public static function provideAutocompleteBuilders(): Generator 'tokenOrder' => 'any', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -62,17 +64,16 @@ public static function provideAutocompleteBuilders(): Generator 'expectedOperator' => [ 'autocomplete' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => 'content', + 'path' => 'article_title', 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') - ->path('content') + ->path('title') ->boostScore(1.5); }, ]; @@ -87,8 +88,7 @@ public static function provideAutocompleteBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -108,8 +108,7 @@ public static function provideAutocompleteBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -134,8 +133,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -166,8 +164,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -197,8 +194,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -226,8 +222,7 @@ public static function provideEmbeddedDocumentBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->embeddedDocument('items') ->text() ->path('items.content') @@ -270,8 +265,7 @@ public static function provideEmbeddedDocumentCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search $stage) { return $stage->embeddedDocument('items') ->compound() ->must() @@ -295,8 +289,7 @@ public static function provideEqualsBuilders(): Generator 'value' => 'MongoDB Aggregation Pipeline', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals('content', 'MongoDB Aggregation Pipeline'); }, ]; @@ -311,8 +304,7 @@ public static function provideEqualsBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals() ->path('content') ->value('MongoDB Aggregation Pipeline') @@ -330,8 +322,7 @@ public static function provideEqualsBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals() ->path('content') ->value('MongoDB Aggregation Pipeline') @@ -344,11 +335,10 @@ public static function provideExistsBuilders(): Generator { yield 'Exists required only' => [ 'expectedOperator' => [ - 'exists' => (object) ['path' => 'content'], + 'exists' => (object) ['path' => 'article_title'], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->exists('content'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->exists('title'); }, ]; } @@ -358,17 +348,16 @@ public static function provideGeoShapeBuilders(): Generator yield 'CompoundedGeoShape required only' => [ 'expectedOperator' => [ 'geoShape' => (object) [ - 'path' => ['location1', 'location2'], + 'path' => ['article_title', 'location2'], 'relation' => 'contains', 'geometry' => ['coordinates' => [12.345, 23.456], 'type' => 'Point'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape( new Point([12.345, 23.456]), 'contains', - 'location1', + 'title', 'location2', ); }, @@ -385,8 +374,7 @@ public static function provideGeoShapeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape() ->path('location') ->relation('contains') @@ -406,8 +394,7 @@ public static function provideGeoShapeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape() ->path('location') ->relation('contains') @@ -422,16 +409,15 @@ public static function provideGeoWithinBuilders(): Generator yield 'GeoWithin box' => [ 'expectedOperator' => [ 'geoWithin' => (object) [ - 'path' => ['location1', 'location2'], + 'path' => ['article_title', 'location2'], 'box' => (object) [ 'bottomLeft' => ['coordinates' => [-12.345, -23.456], 'type' => 'Point'], 'topRight' => ['coordinates' => [12.345, 23.456], 'type' => 'Point'], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->geoWithin('location1', 'location2') + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->geoWithin('title', 'location2') ->box(new Point([-12.345, -23.456]), new Point([12.345, 23.456])); }, ]; @@ -446,8 +432,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14); @@ -467,8 +452,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->geometry(new Polygon([ @@ -491,8 +475,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14) @@ -513,8 +496,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14) @@ -527,10 +509,9 @@ public static function provideMoreLikeThisBuilders(): Generator { yield 'MoreLikeThis with single like' => [ 'expectedOperator' => [ - 'moreLikeThis' => (object) ['like' => [['title' => 'The Godfather']]], + 'moreLikeThis' => (object) ['like' => [['article_title' => 'The Godfather']]], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->moreLikeThis(['title' => 'The Godfather']); }, ]; @@ -539,16 +520,29 @@ public static function provideMoreLikeThisBuilders(): Generator 'expectedOperator' => [ 'moreLikeThis' => (object) [ 'like' => [ - ['title' => 'The Godfather'], - ['title' => 'The Green Mile'], + ['article_title' => 'The Godfather', 'not_mapped_field' => 'Some value'], + ['article_title' => 'The Green Mile'], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->moreLikeThis(['title' => 'The Godfather'], ['title' => 'The Green Mile']); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->moreLikeThis(['title' => 'The Godfather', 'not_mapped_field' => 'Some value'], ['title' => 'The Green Mile']); }, ]; + + yield 'MoreLikeThis with field names mapping' => [ + 'expectedOperator' => [ + 'moreLikeThis' => (object) [ + 'like' => [ + ['disable-at' => new UTCDateTime(new DateTime('2020-01-01T00:00:00Z'))], + ], + ], + ], + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->moreLikeThis(['disabledAt' => new DateTime('2020-01-01T00:00:00Z')]); + }, + 'className' => User::class, + ]; } public static function provideNearBuilders(): Generator @@ -558,12 +552,11 @@ public static function provideNearBuilders(): Generator 'near' => (object) [ 'origin' => 5, 'pivot' => 3, - 'path' => ['value1', 'value2'], + 'path' => ['article_title', 'value2'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->near(5, 3, 'value1', 'value2'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->near(5, 3, 'title', 'value2'); }, ]; @@ -577,8 +570,7 @@ public static function provideNearBuilders(): Generator 'path' => ['createdAt'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) use ($date) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) use ($date) { return $stage->near() ->path('createdAt') ->origin($date) @@ -594,8 +586,7 @@ public static function provideNearBuilders(): Generator 'path' => ['createdAt'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->near() ->path('createdAt') ->origin(new Point([12.345, 23.456])) @@ -610,11 +601,10 @@ public static function providePhraseBuilders(): Generator 'expectedOperator' => [ 'phrase' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -629,8 +619,7 @@ public static function providePhraseBuilders(): Generator 'slop' => 3, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -648,8 +637,7 @@ public static function providePhraseBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -667,8 +655,7 @@ public static function providePhraseBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -683,12 +670,11 @@ public static function provideQueryStringBuilders(): Generator 'expectedOperator' => [ 'queryString' => (object) [ 'query' => 'MongoDB Aggregation Pipeline', - 'defaultPath' => 'content', + 'defaultPath' => 'article_title', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->queryString('MongoDB Aggregation Pipeline', 'content'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->queryString('MongoDB Aggregation Pipeline', 'title'); }, ]; @@ -702,8 +688,7 @@ public static function provideQueryStringBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->queryString() ->query('content:pipeline OR title:pipeline') ->defaultPath('content') @@ -721,8 +706,7 @@ public static function provideQueryStringBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->queryString() ->query('content:pipeline OR title:pipeline') ->defaultPath('content') @@ -736,14 +720,13 @@ public static function provideRangeBuilders(): Generator yield 'Range gt only' => [ 'expectedOperator' => [ 'range' => (object) [ - 'path' => ['field1', 'field2'], + 'path' => ['article_title', 'field2'], 'gt' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() - ->path('field1', 'field2') + ->path('title', 'field2') ->gt(5); }, ]; @@ -755,8 +738,7 @@ public static function provideRangeBuilders(): Generator 'gte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->gte(5); @@ -770,8 +752,7 @@ public static function provideRangeBuilders(): Generator 'lt' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lt(5); @@ -785,8 +766,7 @@ public static function provideRangeBuilders(): Generator 'lte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(5); @@ -801,8 +781,7 @@ public static function provideRangeBuilders(): Generator 'gte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -821,8 +800,7 @@ public static function provideRangeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -842,8 +820,7 @@ public static function provideRangeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -859,11 +836,10 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -874,12 +850,11 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => true, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -891,12 +866,11 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => false, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -908,14 +882,13 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -927,14 +900,13 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'constant' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -949,11 +921,10 @@ public static function provideTextBuilders(): Generator 'expectedOperator' => [ 'text' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -968,8 +939,7 @@ public static function provideTextBuilders(): Generator 'synonyms' => 'mySynonyms', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -987,8 +957,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1006,8 +975,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1027,8 +995,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1043,11 +1010,10 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -1058,12 +1024,11 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => true, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1075,12 +1040,11 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => false, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1092,14 +1056,13 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1111,14 +1074,13 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'constant' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1143,7 +1105,7 @@ public static function provideWildcardBuilders(): Generator #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1159,7 +1121,7 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper 'returnStoredSource' => true, ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index'); @@ -1196,7 +1158,7 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1207,7 +1169,7 @@ public function testSearchOperatorsWithSort(array $expectedOperator, Closure $cr ], ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index'); @@ -1242,9 +1204,9 @@ public function testSearchOperatorsWithSort(array $expectedOperator, Closure $cr #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchCompoundOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchCompoundOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $compound = $searchStage ->index('my_search_index') ->compound(); @@ -1291,9 +1253,9 @@ public function testSearchCompoundOperators(array $expectedOperator, Closure $cr #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $embedded = $searchStage ->index('my_search_index') ->embeddedDocument('foo'); @@ -1315,7 +1277,7 @@ public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Clo } #[DataProvider('provideAutocompleteBuilders')] - public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1332,7 +1294,7 @@ public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Clo 'searchBefore' => 'marker', ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index') ->searchBefore('marker'); @@ -1356,7 +1318,7 @@ public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Clo } #[DataProvider('provideAutocompleteBuilders')] - public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1373,7 +1335,7 @@ public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Clos 'searchAfter' => 'marker', ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index') ->searchAfter('marker'); @@ -1395,4 +1357,12 @@ public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Clos $searchStage->getExpression(), ); } + + /** @param class-string $className */ + private function createSearchStage(?string $className = null): Search + { + $className ??= CmsArticle::class; + + return new Search($this->getTestAggregationBuilder($className), $this->dm->getUnitOfWork()->getDocumentPersister($className)); + } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php new file mode 100644 index 0000000000..6244c93022 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php @@ -0,0 +1,154 @@ +createVectorSearchStage(); + self::assertSame(['$vectorSearch' => []], $stage->getExpression()); + } + + public function testExact(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->exact(true); + self::assertSame(['$vectorSearch' => ['exact' => true]], $stage->getExpression()); + } + + public function testFilterArray(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->filter(['status' => ['$ne' => 'inactive']]); + self::assertSame(['$vectorSearch' => ['filter' => ['status' => ['$ne' => 'inactive']]]], $stage->getExpression()); + } + + public function testFilterExpr(): void + { + [$stage, $builder] = $this->createVectorSearchStage(); + $stage->filter($builder->matchExpr()->field('status')->notEqual('inactive')); + self::assertSame(['$vectorSearch' => ['filter' => ['status' => ['$ne' => 'inactive']]]], $stage->getExpression()); + } + + public function testIndex(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->index('myIndex'); + self::assertSame(['$vectorSearch' => ['index' => 'myIndex']], $stage->getExpression()); + } + + public function testLimit(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->limit(10); + self::assertSame(['$vectorSearch' => ['limit' => 10]], $stage->getExpression()); + } + + public function testNumCandidates(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->numCandidates(5); + self::assertSame(['$vectorSearch' => ['numCandidates' => 5]], $stage->getExpression()); + } + + public function testPath(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->path('vectorField'); + self::assertSame(['$vectorSearch' => ['path' => 'vectorField']], $stage->getExpression()); + } + + public function testPathIsPrepared(): void + { + [$stage] = $this->createVectorSearchStage(VectorEmbedding::class); + $stage->path('vectorFloat'); + self::assertSame(['$vectorSearch' => ['path' => 'db_vector_float']], $stage->getExpression()); + } + + public function testQueryVector(): void + { + [$stage] = $this->createVectorSearchStage(); + $stage->queryVector([1, 2, 3]); + self::assertSame(['$vectorSearch' => ['queryVector' => [1, 2, 3]]], $stage->getExpression()); + } + + public function testQueryVectorAcceptsBinary(): void + { + [$stage] = $this->createVectorSearchStage(); + if (enum_exists(VectorType::class)) { + $binaryVector = Binary::fromVector([1, 2, 3], VectorType::Int8); + self::assertInstanceOf(Binary::class, $binaryVector); + } else { + $binaryVector = new Binary("\x03\x00\x01\x02\x03", 9); + } + + $stage->queryVector($binaryVector); + self::assertSame(['$vectorSearch' => ['queryVector' => $binaryVector]], $stage->getExpression()); + } + + #[TestWith([new Binary("\x03\x00\x01\x02\x03", Binary::TYPE_GENERIC), 'Binary query vector must be of type 9 (Vector), got 0.'])] + #[TestWith([[1 => 1, 2 => 3], 'Query vector must be a list of numbers, got an associative array.'])] + #[TestWith([[], 'Query vector cannot be an empty array.'])] + public function testQueryVectorInvalidType(mixed $queryVector, string $message): void + { + [$stage] = $this->createVectorSearchStage(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + $stage->queryVector($queryVector); + } + + public function testChainingAllOptions(): void + { + [$stage, $builder] = $this->createVectorSearchStage(); + $stage + ->exact(false) + ->filter($builder->matchExpr()->field('status')->notEqual('inactive')) + ->index('idx') + ->limit(7) + ->numCandidates(3) + ->path('vec') + ->queryVector([0.1, 0.2]); + self::assertSame([ + '$vectorSearch' => [ + 'exact' => false, + 'filter' => ['status' => ['$ne' => 'inactive']], + 'index' => 'idx', + 'limit' => 7, + 'numCandidates' => 3, + 'path' => 'vec', + 'queryVector' => [0.1, 0.2], + ], + ], $stage->getExpression()); + } + + /** + * @param class-string $className + * + * @return array{0: VectorSearch, 1: Builder} + */ + private function createVectorSearchStage(string $className = User::class): array + { + return [ + new VectorSearch($builder = $this->getTestAggregationBuilder($className), $this->dm->getUnitOfWork()->getDocumentPersister($className)), + $builder, + ]; + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php b/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php index e1c347eff9..665f4ef964 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php @@ -14,6 +14,7 @@ use Doctrine\Persistence\Mapping\Driver\MappingDriver; use MongoDB\Client; use MongoDB\Driver\Command; +use MongoDB\Driver\Exception\CommandException; use MongoDB\Driver\Manager; use MongoDB\Driver\Server; use MongoDB\Model\DatabaseInfo; @@ -45,23 +46,34 @@ abstract class BaseTestCase extends TestCase protected static bool $allowsTransactions = true; protected ?DocumentManager $dm; protected UnitOfWork $uow; + private bool $disableFailPoints = false; - public function setUp(): void + protected function setUp(): void { $this->dm = static::createTestDocumentManager(); $this->uow = $this->dm->getUnitOfWork(); } - public function tearDown(): void + protected function tearDown(): void { if (! $this->dm) { return; } + $client = $this->dm->getClient(); + + // Remove any fail points that may have been set + if ($this->disableFailPoints) { + $client->getDatabase('admin')->command([ + 'configureFailPoint' => 'failCommand', + 'mode' => 'off', + ]); + $this->disableFailPoints = false; + } + // Check if the database exists. Calling listCollections on a non-existing // database in a sharded setup will cause an invalid command cursor to be // returned - $client = $this->dm->getClient(); $databases = iterator_to_array($client->listDatabases()); $databaseNames = array_map(static fn (DatabaseInfo $database) => $database->getName(), $databases); if (! in_array(DOCTRINE_MONGODB_DATABASE, $databaseNames)) { @@ -294,4 +306,28 @@ private static function detectTransactionSupport(): bool return $manager->selectServer()->getType() !== Server::TYPE_STANDALONE; } + + protected function createFailPoint(string $failCommand, bool $transient = false, int $times = 1): void + { + try { + $this->dm->getClient()->getManager()->executeCommand( + 'admin', + new Command([ + 'configureFailPoint' => 'failCommand', + 'mode' => ['times' => $times], + 'data' => [ + 'errorCode' => 192, // FailPointEnabled + 'errorLabels' => $transient ? ['TransientTransactionError'] : [], + 'failCommands' => [$failCommand], + ], + ]), + ); + $this->disableFailPoints = true; + } catch (CommandException $exception) { + // no such command: 'configureFailPoint' + if ($exception->getCode() === 59) { + self::markTestSkipped('Test skipped because the server does not support fail points'); + } + } + } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Events/TransactionalLifecycleEventsTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Events/TransactionalLifecycleEventsTest.php index 7d2cd2bc02..d0567a57ea 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Events/TransactionalLifecycleEventsTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Events/TransactionalLifecycleEventsTest.php @@ -23,16 +23,6 @@ public function setUp(): void $this->skipTestIfTransactionalFlushDisabled(); } - public function tearDown(): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => 'off', - ]); - - parent::tearDown(); - } - public function testPersistEvents(): void { $root = new RootEventDocument(); @@ -41,7 +31,7 @@ public function testPersistEvents(): void $root->embedded = new EmbeddedEventDocument(); $root->embedded->name = 'embedded'; - $this->createFailPoint('insert'); + $this->createFailPoint('insert', transient: true); $this->dm->persist($root); $this->dm->flush(); @@ -61,7 +51,7 @@ public function testUpdateEvents(): void $this->dm->persist($root); $this->dm->flush(); - $this->createFailPoint('update'); + $this->createFailPoint('update', transient: true); $root->name = 'updated'; $root->embedded->name = 'updated'; @@ -85,7 +75,7 @@ public function testUpdateEventsRootOnly(): void $this->dm->persist($root); $this->dm->flush(); - $this->createFailPoint('update'); + $this->createFailPoint('update', transient: true); $root->name = 'updated'; @@ -108,7 +98,7 @@ public function testUpdateEventsEmbeddedOnly(): void $this->dm->persist($root); $this->dm->flush(); - $this->createFailPoint('update'); + $this->createFailPoint('update', transient: true); $root->embedded->name = 'updated'; @@ -136,7 +126,7 @@ public function testUpdateEventsWithNewEmbeddedDocument(): void $this->dm->persist($root); $this->dm->flush(); - $this->createFailPoint('update'); + $this->createFailPoint('update', transient: true); $root->name = 'updated'; $root->embedded = $secondEmbedded; @@ -168,7 +158,7 @@ public function testRemoveEvents(): void $this->dm->persist($root); $this->dm->flush(); - $this->createFailPoint('delete'); + $this->createFailPoint('delete', transient: true); $this->dm->remove($root); $this->dm->flush(); @@ -185,19 +175,6 @@ protected static function createTestDocumentManager(): DocumentManager return DocumentManager::create($client, $config); } - - private function createFailPoint(string $failCommand): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => ['times' => 1], - 'data' => [ - 'errorCode' => 192, // FailPointEnabled - 'errorLabels' => ['TransientTransactionError'], - 'failCommands' => [$failCommand], - ], - ]); - } } #[ODM\MappedSuperclass] diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php new file mode 100644 index 0000000000..e1cafe14fe --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php @@ -0,0 +1,168 @@ +dm->getSchemaManager(); + $schemaManager->createDocumentCollection(CmsArticle::class); + + $user = new CmsUser(); + $user->status = 'active'; + $this->dm->persist($user); + + $article1 = new CmsArticle(); + $article1->topic = 'Technology'; + $article1->title = 'Introduction to MongoDB Atlas Search'; + $article1->text = 'MongoDB Atlas Search provides full-text search capabilities with advanced features like autocomplete and fuzzy matching.'; + $article1->setAuthor($user); + + $article2 = new CmsArticle(); + $article2->topic = 'Database'; + $article2->title = 'Working with Document Databases'; + $article2->text = 'Document databases like MongoDB offer flexible schema design and powerful query capabilities for modern applications.'; + $article2->setAuthor($user); + + $article3 = new CmsArticle(); + $article3->topic = 'Programming'; + $article3->title = 'PHP and MongoDB Integration'; + $article3->text = 'The MongoDB ODM for PHP provides an easy way to work with MongoDB documents using object-oriented programming.'; + $article3->setAuthor($user); + + $this->dm->persist($article1); + $this->dm->persist($article2); + $this->dm->persist($article3); + + // Write with majority concern to ensure data is visible for search + $this->dm->flush(['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]); + + // Index must be created after data insertion, so the index status is not immediately "READY" + $schemaManager->createDocumentSearchIndexes(CmsArticle::class); + + // Wait for the search index to be ready (Atlas Local needs time to build the index) + $schemaManager->waitForSearchIndexes([CmsArticle::class, CmsUser::class]); + + $results = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->autocomplete() + ->query('Mongo') + ->path('title') + ->fuzzy(2, 2) + ->limit(5) + ->getAggregation()->execute()->toArray(); + + $this->assertNotEmpty($results, 'Autocomplete search should return results'); + + $results = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->compound() + ->must() + ->text() + ->query('database') + ->path('text') + ->should() + ->text() + ->query('MongoDB') + ->path('title') + ->addFields() + ->field('score') + ->expression(['$meta' => 'searchScore']) + ->sort(['score' => 'searchScore']) + ->getAggregation()->execute()->toArray(); + + foreach ($results as $result) { + $this->assertIsArray($result); + $this->assertStringContainsStringIgnoringCase('database', $result['text']); + } + + $results = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->text() + ->query('Atlas Search') + ->path('text') + ->highlight('text', 100, 1) + ->addFields() + ->field('highlights') + ->expression(['$meta' => 'searchHighlights']) + ->getAggregation()->execute()->toArray(); + + foreach ($results as $result) { + $this->assertIsArray($result); + $this->assertIsArray($result['highlights']); + } + + $results = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->text() + ->query('MongoDB') + ->path('title', 'text') + ->countDocuments('total') + ->getAggregation()->execute()->toArray(); + + $this->assertNotEmpty($results, 'Count search should return results'); + } + + public function testIndexNotCreated(): void + { + $aggregation = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->text() + ->query('Atlas Search') + ->path('text') + ->getAggregation(); + + $this->expectException(SchemaException::class); + $this->expectExceptionMessageMatches('#^The search index "search_articles" of the collection "[^."]+\.CmsArticle" is not found\.$#'); + + $aggregation->execute(); + } + + public function testIndexNotCreatedWithoutException(): void + { + $this->dm->getConfiguration()->setAssertSearchIndexExistsForEmptyResult(false); + + $results = $this->dm->createAggregationBuilder(CmsArticle::class) + ->search() + ->index('search_articles') + ->text() + ->query('Atlas Search') + ->path('text') + ->getAggregation()->execute(); + + $this->assertCount(0, $results->toArray()); + } + + public function testIndexNotCreatedWithCustomStage(): void + { + $aggregation = ($builder = $this->dm->createAggregationBuilder(CmsArticle::class)) + ->addStage(new class ($builder) extends Stage { + public function getExpression(): array + { + return ['$search' => ['text' => ['query' => 'Atlas Search', 'path' => 'text']]]; + } + })->getAggregation(); + + $this->expectException(SchemaException::class); + $this->expectExceptionMessageMatches('#^The search index "default" of the collection "[^."]+\.CmsArticle" is not found\.$#'); + + $aggregation->execute(); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php index 624cb515c9..757eaca4aa 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/DatabasesTest.php @@ -12,12 +12,12 @@ class DatabasesTest extends BaseTestCase { public function testCustomDatabase(): void { - self::assertEquals('test_custom', $this->dm->getDocumentDatabase(CustomDatabaseTest::class)->getDatabaseName()); + self::assertEquals('test_custom', $this->dm->getDocumentDatabase(TestCustomDatabase::class)->getDatabaseName()); } public function testDefaultDatabase(): void { - self::assertEquals('test_default', $this->dm->getDocumentDatabase(DefaultDatabaseTest::class)->getDatabaseName()); + self::assertEquals('test_default', $this->dm->getDocumentDatabase(TestDefaultDatabase::class)->getDatabaseName()); } protected static function getConfiguration(): Configuration @@ -31,7 +31,7 @@ protected static function getConfiguration(): Configuration } #[ODM\Document(db: 'test_custom')] -class CustomDatabaseTest +class TestCustomDatabase { /** @var string|null */ #[ODM\Id] @@ -39,7 +39,7 @@ class CustomDatabaseTest } #[ODM\Document] -class DefaultDatabaseTest +class TestDefaultDatabase { /** @var string|null */ #[ODM\Id] diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php index 69592e719b..885d63f7d4 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php @@ -134,10 +134,10 @@ public static function getTestPrepareFieldNameData(): array ['association.nested', 'associationName.nestedName'], ['association.nested.$id', 'associationName.nestedName.$id'], ['association.nested._id', 'associationName.nestedName._id'], - ['association.nested.id', 'associationName.nestedName._id'], - ['association.nested.association.nested.$id', 'associationName.nestedName.associationName.nestedName.$id'], - ['association.nested.association.nested.id', 'associationName.nestedName.associationName.nestedName._id'], - ['association.nested.association.nested.firstName', 'associationName.nestedName.associationName.nestedName.firstName'], + ['association.nested.id', 'associationName.nestedName.id'], + ['association.nested.association.nested.$id', 'associationName.nestedName.association.nested.$id'], + ['association.nested.association.nested.id', 'associationName.nestedName.association.nested.id'], + ['association.nested.association.nested.firstName', 'associationName.nestedName.association.nested.firstName'], ]; } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdTest.php index b764a2de84..298f0c38af 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/IdTest.php @@ -364,19 +364,22 @@ private function createIdTestClass(string $type, string $strategy): string if (! class_exists($className)) { $code = sprintf( - 'namespace Doctrine\ODM\MongoDB\Tests\Functional; - -use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; - -#[ODM\Document] -class %s -{ - #[ODM\Id(strategy: "%s", options: ["type" => "%s"])] - public $id; - - #[ODM\Field(type: "string")] - public $test = "test"; -}', + <<<'PHP' + namespace %s; + + use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + + #[ODM\Document] + class %s + { + #[ODM\Id(strategy: "%s", options: ["type" => "%s"])] + public $id; + + #[ODM\Field(type: "string")] + public $test = "test"; + } + PHP, + __NAMESPACE__, $shortClassName, $strategy, $type, diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/SchemaManagerWaitForSearchIndexesTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SchemaManagerWaitForSearchIndexesTest.php new file mode 100644 index 0000000000..00b6cd7a96 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SchemaManagerWaitForSearchIndexesTest.php @@ -0,0 +1,120 @@ +dm->getClassMetadata(CmsArticle::class) + ->setCollection('articles_' . bin2hex(random_bytes(4))); + } + + #[TestWith([0])] + #[TestWith([50_000])] + public function testWait(int $nbDocuments): void + { + $schemaManager = $this->dm->getSchemaManager(); + $collection = $this->dm->getDocumentCollection(CmsArticle::class); + + $schemaManager->createDocumentCollection(CmsArticle::class); + + if ($nbDocuments) { + $bulk = new BulkWrite(); + for ($i = 0; $i < $nbDocuments; $i++) { + $bulk->insert(['topic' => 'topic ' . $i, 'title' => 'title ' . $i, 'text' => 'text ' . $i]); + } + + $collection->getManager()->executeBulkWrite( + $collection->getNamespace(), + $bulk, + ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)], + ); + } + + // The index must be created after data insertion, so the index status is not immediately "READY" + $schemaManager->createDocumentSearchIndexes(CmsArticle::class); + + $this->assertNotSame('READY', $collection->listSearchIndexes(['name' => 'search_articles'])->current()['status']); + + $start = hrtime(true); + $schemaManager->waitForSearchIndexes([CmsArticle::class]); + $timeMs = (hrtime(true) - $start) / 1_000_000; + + $this->assertSame($nbDocuments, $collection->aggregate([ + [ + '$searchMeta' => [ + 'index' => 'search_articles', + 'exists' => ['path' => '_id'], + 'count' => ['type' => 'total'], + ], + ], + ])->toArray()[0]['count']['total'], 'All documents are indexed'); + + $this->assertSame('READY', $collection->listSearchIndexes(['name' => 'search_articles'])->current()['status'], 'Ready after ' . $timeMs . ' ms'); + } + + public function testErrors(): void + { + $schemaManager = $this->dm->getSchemaManager(); + + // Search index missing + try { + $schemaManager->waitForSearchIndexes([CmsArticle::class]); + $this->fail('Expected SchemaException not thrown'); + } catch (SchemaException $exception) { + $this->assertSame('The document class "Documents\CmsArticle" is missing the following search index(es): "search_articles"', $exception->getMessage()); + } + + $schemaManager->createDocumentCollection(CmsArticle::class); + $schemaManager->createDocumentSearchIndexes(CmsArticle::class); + + // Timeout too short + try { + $schemaManager->waitForSearchIndexes([CmsArticle::class], 1); + $this->fail('Expected SchemaException not thrown'); + } catch (MongoDBException $exception) { + $this->assertSame('Timed out waiting for search indexes to become queryable after 1 ms. Search indexes are not ready for the following class(es): Documents\CmsArticle', $exception->getMessage()); + } + + // Not specifying classes waits for all + try { + $schemaManager->waitForSearchIndexes(); + $this->fail('Expected SchemaException not thrown'); + } catch (SchemaException $exception) { + // The missing class varies depending on the test execution order, + // classes are added to the ClassMetadataFactory in the order they are used + $this->assertMatchesRegularExpression('#The document class "Documents\\\\(CmsAddress|VectorEmbedding)" is missing the following search index\(es\): "default"#', $exception->getMessage()); + } + + // Remove the collection + $schemaManager->dropDocumentCollection(CmsArticle::class); + + try { + $schemaManager->waitForSearchIndexes([CmsArticle::class]); + $this->fail('Expected SchemaException not thrown'); + } catch (SchemaException $exception) { + $this->assertSame('The document class "Documents\CmsArticle" is missing the following search index(es): "search_articles"', $exception->getMessage()); + } + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2789Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2789Test.php new file mode 100644 index 0000000000..bae9c72e30 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2789Test.php @@ -0,0 +1,88 @@ +dm->persist($doc); + $this->dm->flush(); + + $documents = $this->dm->getDocumentCollection(GH2789VersionedUuid::class)->find()->toArray(); + self::assertCount(1, $documents); + self::assertEquals(new Binary('1', 142), $documents[0]['version'], 'The version field should be stored using the custom type'); + + $doc->message = 'new message'; + $this->dm->persist($doc); + $this->dm->flush(); + + $documents = $this->dm->getDocumentCollection(GH2789VersionedUuid::class)->find()->toArray(); + self::assertCount(1, $documents); + self::assertEquals(new Binary('2', 142), $documents[0]['version'], 'The version field should be incremented and stored using the custom type'); + } +} + +#[ODM\Document(collection: 'gh2789_versioned_uuid')] +class GH2789VersionedUuid +{ + #[ODM\Id] + public string $id; + + #[ODM\Version] + #[ODM\Field(type: GH2789CustomType::class)] + public int $version; + + public function __construct( + #[ODM\Field(type: 'string')] + public string $message, + ) { + } +} + +/** + * Custom type that stores an integer as a MongoDB Binary subtype 142. + */ +class GH2789CustomType extends Type implements Versionable +{ + public function convertToPHPValue($value): int + { + assert($value instanceof Binary); + + return (int) $value->getData(); + } + + public function convertToDatabaseValue($value): Binary + { + assert(is_int($value)); + + return new Binary((string) $value, 142); + } + + public function getNextVersion($current): int + { + if ($current === null) { + return 1; + } + + assert(is_int($current)); + + return $current + 1; + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2825Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2825Test.php new file mode 100644 index 0000000000..1405a47b04 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2825Test.php @@ -0,0 +1,295 @@ +embedded = new GH2825Embedded('level 1'); + + $this->dm->persist($document); + $this->dm->flush(); + + $embedded = new GH2825Embedded('level 2'); + + $this->dm->persist($embedded); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('embedded.embedded')->set($embedded) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)]); + + self::assertSame('level 1', $result['embedded']['renamed']); + self::assertSame('level 2', $result['embedded']['embedded']['renamed']); + } + + public function testQueryBuilderUpdatesEmbedManyCorrectly(): void + { + $document = new GH2825Document('foo'); + $document->embeddedDocuments[] = new GH2825Embedded('level 1'); + + $this->dm->persist($document); + $this->dm->flush(); + + $embedded = new GH2825Embedded('level 2'); + + $this->dm->persist($embedded); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('embeddedDocuments.property')->equals('level 1') + ->field('embeddedDocuments.$.embedded')->set($embedded) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)]); + + self::assertIsArray($result['embeddedDocuments']); + self::assertSame('level 1', $result['embeddedDocuments'][0]['renamed']); + self::assertSame('level 2', $result['embeddedDocuments'][0]['embedded']['renamed']); + } + + public function testQueryBuilderReplacesEmbedManyCorrectly(): void + { + $document = new GH2825Document('foo'); + $document->embeddedDocuments[] = new GH2825Embedded('original'); + + $this->dm->persist($document); + $this->dm->flush(); + + $embedded = new GH2825Embedded('replaced'); + + $this->dm->persist($embedded); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('embeddedDocuments.property')->equals('original') + ->field('embeddedDocuments.$')->set($embedded) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)]); + + self::assertIsArray($result['embeddedDocuments']); + self::assertSame('replaced', $result['embeddedDocuments'][0]['renamed']); + } + + public function testQueryBuilderUpdatesReferenceOneCorrectly(): void + { + $document = new GH2825Document('document'); + $document->embedded = new GH2825Embedded('embedded'); + $reference = new GH2825Document('referenced'); + + $this->dm->persist($document); + $this->dm->persist($reference); + + $this->dm->flush(); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('referenceStoreAsId')->set($reference) + ->field('referenceStoreAsRef')->set($reference) + ->field('referenceStoreAsDbRef')->set($reference) + ->field('embedded.referenceStoreAsId')->set($reference) + ->field('embedded.referenceStoreAsRef')->set($reference) + ->field('embedded.referenceStoreAsDbRef')->set($reference) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)], ['typeMap' => ['root' => 'array', 'document' => 'array']]); + + $referenceId = new ObjectId($reference->id); + + self::assertEquals($referenceId, $result['referenceStoreAsId']); + self::assertEquals(['id' => $referenceId], $result['referenceStoreAsRef']); + self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['referenceStoreAsDbRef']); + + self::assertEquals($referenceId, $result['embedded']['referenceStoreAsId']); + self::assertEquals(['id' => $referenceId], $result['embedded']['referenceStoreAsRef']); + self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['embedded']['referenceStoreAsDbRef']); + } + + public function testQueryBuilderUpdatesReferenceManyCorrectly(): void + { + $document = new GH2825Document('document'); + $document->embeddedDocuments[] = new GH2825Embedded('embedded'); + $reference = new GH2825Document('referenced'); + + $this->dm->persist($document); + $this->dm->persist($reference); + + $this->dm->flush(); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('embeddedDocuments.property')->equals('embedded') + ->field('embeddedDocuments.$.referenceStoreAsId')->set($reference) + ->field('embeddedDocuments.$.referenceStoreAsRef')->set($reference) + ->field('embeddedDocuments.$.referenceStoreAsDbRef')->set($reference) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)], ['typeMap' => ['root' => 'array', 'document' => 'array']]); + + $referenceId = new ObjectId($reference->id); + + self::assertIsArray($result['embeddedDocuments']); + + self::assertEquals($referenceId, $result['embeddedDocuments'][0]['referenceStoreAsId']); + self::assertEquals(['id' => $referenceId], $result['embeddedDocuments'][0]['referenceStoreAsRef']); + self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['embeddedDocuments'][0]['referenceStoreAsDbRef']); + } + + public function testQueryBuilderReplacesReferenceManyCorrectly(): void + { + $document = new GH2825Document('document'); + $reference = new GH2825Document('original'); + $otherReference = new GH2825Document('original'); + $document->referencedDocumentsStoreAsId[] = $reference; + $document->referencedDocumentsStoreAsRef[] = $reference; + $document->referencedDocumentsStoreAsDbRef[] = $reference; + + $this->dm->persist($document); + $this->dm->persist($reference); + $this->dm->persist($otherReference); + + $this->dm->flush(); + + $this->dm->createQueryBuilder(GH2825Document::class) + ->updateOne() + ->field('id')->equals($document->id) + ->field('referencedDocumentsStoreAsId.0')->set($otherReference) + ->field('referencedDocumentsStoreAsRef.0')->set($otherReference) + ->field('referencedDocumentsStoreAsDbRef.0')->set($otherReference) + ->getQuery() + ->execute(); + + $result = $this->dm->getDocumentCollection(GH2825Document::class) + ->findOne(['_id' => new ObjectId($document->id)], ['typeMap' => ['root' => 'array', 'document' => 'array']]); + + $referenceId = new ObjectId($otherReference->id); + + self::assertIsArray($result['referencedDocumentsStoreAsId']); + self::assertIsArray($result['referencedDocumentsStoreAsRef']); + self::assertIsArray($result['referencedDocumentsStoreAsDbRef']); + + self::assertEquals($referenceId, $result['referencedDocumentsStoreAsId'][0]); + self::assertEquals(['id' => $referenceId], $result['referencedDocumentsStoreAsRef'][0]); + self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['referencedDocumentsStoreAsDbRef'][0]); + } +} + +#[ODM\Document] +class GH2825Document +{ + #[ODM\Id] + public string|null $id; + + #[ODM\Field] + public string $name; + + #[ODM\EmbedOne(targetDocument: GH2825Embedded::class)] + public GH2825Embedded|null $embedded = null; + + #[ODM\ReferenceOne(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)] + public GH2825Document|null $referenceStoreAsId = null; + + #[ODM\ReferenceOne(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)] + public GH2825Document|null $referenceStoreAsRef = null; + + #[ODM\ReferenceOne(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)] + public GH2825Document|null $referenceStoreAsDbRef = null; + + /** @var Collection */ + #[ODM\EmbedMany(targetDocument: GH2825Embedded::class)] + public Collection $embeddedDocuments; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)] + public Collection $referencedDocumentsStoreAsId; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)] + public Collection $referencedDocumentsStoreAsRef; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)] + public Collection $referencedDocumentsStoreAsDbRef; + + public function __construct(string $name) + { + $this->name = $name; + $this->embeddedDocuments = new ArrayCollection(); + $this->referencedDocumentsStoreAsId = new ArrayCollection(); + $this->referencedDocumentsStoreAsRef = new ArrayCollection(); + $this->referencedDocumentsStoreAsDbRef = new ArrayCollection(); + } +} + +#[ODM\EmbeddedDocument] +class GH2825Embedded +{ + #[ODM\Field(name: 'renamed')] + public string $property; + + #[ODM\EmbedOne(targetDocument: self::class)] + public GH2825Embedded $embedded; + + #[ODM\ReferenceOne(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)] + public GH2825Document|null $referenceStoreAsId = null; + + #[ODM\ReferenceOne(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)] + public GH2825Document|null $referenceStoreAsRef = null; + + #[ODM\ReferenceOne(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)] + public GH2825Document|null $referenceStoreAsDbRef = null; + + /** @var Collection */ + #[ODM\EmbedMany(targetDocument: self::class)] + public Collection $embeddedDocuments; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)] + public Collection $referencedDocumentsStoreAsId; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)] + public Collection $referencedDocumentsStoreAsRef; + + /** @var Collection */ + #[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)] + public Collection $referencedDocumentsStoreAsDbRef; + + public function __construct(string $property) + { + $this->property = $property; + $this->embeddedDocuments = new ArrayCollection(); + $this->referencedDocumentsStoreAsId = new ArrayCollection(); + $this->referencedDocumentsStoreAsRef = new ArrayCollection(); + $this->referencedDocumentsStoreAsDbRef = new ArrayCollection(); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/UuidMappingTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/UuidMappingTest.php new file mode 100644 index 0000000000..ad737f94e9 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/UuidMappingTest.php @@ -0,0 +1,64 @@ +dm->getClassMetadata(UuidTestDocument::class); + $idMapping = $metadata->getIdentifierMapping(); + self::assertSame(Type::UUID, $idMapping['type'], 'Id field should have UUID type'); + } + + public function testExplicitValue(): void + { + $uuid = new UuidV4(); + $document = new UuidTestDocument(); + + $document->id = $uuid; + $document->explicitlyTypedUuid = $uuid; + $document->untypedUuid = $uuid; + + $this->dm->persist($document); + $this->dm->flush(); + + $check = $this->dm->find(UuidTestDocument::class, $document->id); + self::assertInstanceOf(UuidTestDocument::class, $check); + self::assertEquals($uuid, $check->id); + self::assertEquals($uuid, $check->explicitlyTypedUuid); + self::assertEquals($uuid, $check->untypedUuid); + } + + public function testAutoGenerateIdV4(): void + { + $document = new UuidTestDocument(); + + $this->dm->persist($document); + $this->dm->flush(); + + $check = $this->dm->find(UuidTestDocument::class, $document->id); + self::assertInstanceOf(UuidTestDocument::class, $check); + self::assertInstanceOf(UuidV4::class, $check->id); + } +} + +#[ODM\Document] +class UuidTestDocument +{ + #[ODM\Id] + public UuidV4 $id; + + #[ODM\Field(type: Type::UUID)] + public ?UuidV4 $explicitlyTypedUuid = null; + + #[ODM\Field] + public ?UuidV4 $untypedUuid = null; +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/VectorSearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/VectorSearchTest.php new file mode 100644 index 0000000000..05db805fc0 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/VectorSearchTest.php @@ -0,0 +1,86 @@ +dm->getSchemaManager(); + + // Create the collection and vector search indexes + $schemaManager->createDocumentCollection(VectorEmbedding::class); + + // Insert some test documents with vector embeddings + $doc1 = new VectorEmbedding(); + $doc1->vectorFloat = [1.0, 2.0, 3.0]; + $doc1->vectorInt = [1, 2, 3]; + $doc1->filterField = 'active'; + + $doc2 = new VectorEmbedding(); + $doc2->vectorFloat = [4.0, 5.0, 6.0]; + $doc2->vectorInt = [4, 5, 6]; + $doc2->filterField = 'inactive'; + + $doc3 = new VectorEmbedding(); + $doc3->vectorFloat = [1.5, 2.5, 3.5]; + $doc3->vectorInt = [2, 3, 4]; + $doc3->filterField = 'active'; + + $this->dm->persist($doc1); + $this->dm->persist($doc2); + $this->dm->persist($doc3); + // Write with majority concern to ensure data is visible for search + $this->dm->flush(['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]); + + // Index must be created after data insertion, so the index status is not immediately "READY" + $schemaManager->createDocumentSearchIndexes(VectorEmbedding::class); + + // Wait for search index to be ready (Atlas Local needs time to build the index) + $schemaManager->waitForSearchIndexes([VectorEmbedding::class]); + + $results = $this->dm->createAggregationBuilder(VectorEmbedding::class) + ->vectorSearch() + ->index('default') + ->queryVector([1.1, 2.1, 3.1]) + ->path('vectorFloat') + ->numCandidates(10) + ->limit(5) + ->set() + ->field('score') + ->expression(['$meta' => 'vectorSearchScore']) + ->getAggregation()->execute()->toArray(); + + $this->assertCount(3, $results); + foreach ($results as $result) { + $this->assertIsArray($result); + $this->assertIsFloat($result['score'], 'Result should have a score'); + } + + // Test with filter + $results = ($builder = $this->dm->createAggregationBuilder(VectorEmbedding::class)) + ->vectorSearch() + ->index('vector_int') + ->queryVector([1, 1, 3]) + ->path('vectorInt') + ->numCandidates(10) + ->limit(5) + ->filter($builder->matchExpr()->field('filterField')->equals('active')) + ->getAggregation()->execute()->toArray(); + + $this->assertCount(2, $results); + foreach ($results as $result) { + $this->assertIsArray($result); + $this->assertEquals('active', $result['filterField'], 'Filtered results should only contain active documents'); + } + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTestCase.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTestCase.php index 4799fd9ce9..c4a8be3546 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTestCase.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTestCase.php @@ -115,13 +115,14 @@ public function testDocumentLevelWriteConcern(ClassMetadata $class): ClassMetada #[Depends('testDocumentLevelWriteConcern')] public function testFieldMappings(ClassMetadata $class): ClassMetadata { - self::assertCount(14, $class->fieldMappings); + self::assertCount(15, $class->fieldMappings); self::assertTrue(isset($class->fieldMappings['identifier'])); self::assertTrue(isset($class->fieldMappings['version'])); self::assertTrue(isset($class->fieldMappings['lock'])); self::assertTrue(isset($class->fieldMappings['name'])); self::assertTrue(isset($class->fieldMappings['email'])); self::assertTrue(isset($class->fieldMappings['roles'])); + self::assertTrue(isset($class->fieldMappings['embedding'])); return $class; } @@ -237,7 +238,7 @@ public function testLockFieldMappings(ClassMetadata $class): ClassMetadata #[Depends('testIdentifier')] public function testAssocations(ClassMetadata $class): ClassMetadata { - self::assertCount(14, $class->fieldMappings); + self::assertCount(15, $class->fieldMappings); return $class; } @@ -433,6 +434,7 @@ public function testSearchIndexes(ClassMetadata $class): void $expectedIndexes = [ [ 'name' => 'default', + 'type' => 'search', 'definition' => [ 'mappings' => ['dynamic' => true], 'analyzer' => 'lucene.standard', @@ -442,6 +444,7 @@ public function testSearchIndexes(ClassMetadata $class): void ], [ 'name' => 'usernameAndPhoneNumbers', + 'type' => 'search', 'definition' => [ 'mappings' => [ 'fields' => [ @@ -473,6 +476,30 @@ public function testSearchIndexes(ClassMetadata $class): void ], ], ], + [ + 'type' => 'vectorSearch', + 'name' => 'embeddingIndex', + 'definition' => [ + 'fields' => [ + [ + 'type' => 'vector', + 'path' => 'embedding', + 'numDimensions' => 1536, + 'similarity' => 'euclidean', + 'quantization' => 'scalar', + 'hnswOptions' => ['maxEdges' => 16, 'numEdgeCandidates' => 200], + ], + [ + 'type' => 'filter', + 'path' => 'name', + ], + [ + 'type' => 'filter', + 'path' => 'email', + ], + ], + ], + ], ]; self::assertEquals($expectedIndexes, $class->getSearchIndexes()); @@ -729,6 +756,21 @@ public function testTimeSeriesDocumentWithBucket(): void * {"name"="mySynonyms", "analyzer"="lucene.english", "source"={"collection"="synonyms"}}, * }, * ) + * @ODM\VectorSearchIndex( + * fields={ + * { + * "type"="vector", + * "path"="embedding", + * "numDimensions"=1536, + * "similarity"="euclidean", + * "quantization"="scalar", + * "hnswOptions"={"maxEdges"=16, "numEdgeCandidates"=200, }, + * }, + * {"type"="filter", "path"="name"}, + * {"type"="filter", "path"="email"}, + * }, + * name="embeddingIndex", + * ) * @ODM\ShardKey(keys={"name"="asc"},unique=true,numInitialChunks=4096) * @ODM\ReadPreference("primaryPreferred", tags={ * { "dc"="east" }, @@ -764,6 +806,27 @@ public function testTimeSeriesDocumentWithBucket(): void ['name' => 'mySynonyms', 'analyzer' => 'lucene.english', 'source' => ['collection' => 'synonyms']], ], )] +#[ODM\VectorSearchIndex( + fields: [ + [ + 'type' => 'vector', + 'path' => 'embedding', + 'numDimensions' => 1536, + 'similarity' => ClassMetadata::VECTOR_SIMILARITY_EUCLIDEAN, + 'quantization' => ClassMetadata::VECTOR_QUANTIZATION_SCALAR, + 'hnswOptions' => ['maxEdges' => 16, 'numEdgeCandidates' => 200], + ], + [ + 'type' => 'filter', + 'path' => 'name', + ], + [ + 'type' => 'filter', + 'path' => 'email', + ], + ], + name: 'embeddingIndex', +)] #[ODM\ShardKey(keys: ['name' => 'asc'], unique: true, numInitialChunks: 4096)] #[ODM\ReadPreference('primaryPreferred', tags: [['dc' => 'east'], ['dc' => 'west'], []])] class AbstractMappingDriverUser @@ -890,6 +953,14 @@ class AbstractMappingDriverUser #[ODM\Field(type: 'collection')] public $roles = []; + /** + * @ODM\Field(type="collection") + * + * @var int[] + */ + #[ODM\Field(type: 'collection')] + public $embedding = []; + /** @ODM\PrePersist */ #[ODM\PrePersist] public function doStuffOnPrePersist(): void diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php index cb69ce98cb..5c8a780206 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php @@ -39,6 +39,7 @@ use InvalidArgumentException; use MongoDB\BSON\Document; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; use ReflectionClass; use ReflectionException; use stdClass; @@ -982,6 +983,62 @@ public function testEmptySearchIndexDefinition(): void $cm->addSearchIndex(['mappings' => []]); } + #[TestWith([null, ClassMetadata::DEFAULT_SEARCH_INDEX_NAME, ['mappings' => ['dynamic' => true]]])] + #[TestWith(['custom_name', 'custom_name', ['mappings' => ['fields' => ['title' => ['type' => 'string']]]]])] + public function testSearchIndexDefinition(?string $name, string $expectedName, array $definition): void + { + $cm = new ClassMetadata('stdClass'); + $cm->addSearchIndex($definition, $name); + + self::assertSame([ + [ + 'definition' => $definition, + 'name' => $expectedName, + 'type' => 'search', + ], + ], $cm->getSearchIndexes()); + } + + #[TestWith([[]])] + #[TestWith([['fields' => []]])] + #[TestWith([['fields' => [['type' => 'filter', 'path' => 'foo']]]])] + public function testEmptyVectorSearchIndexDefinition(array $definition): void + { + $cm = new ClassMetadata('stdClass'); + + $this->expectException(MappingException::class); + $this->expectExceptionMessage('stdClass vector search index "default" must have a vector field'); + $cm->addSearchIndex($definition, 'default', 'vectorSearch'); + } + + public function testVectorSearchIndexDefinition(): void + { + $definition = [ + 'fields' => [ + [ + 'type' => 'vector', + 'path' => 'embedding', + 'numDimensions' => 85, + 'similarity' => ClassMetadata::VECTOR_SIMILARITY_COSINE, + ], + [ + 'type' => 'filter', + 'path' => 'category', + ], + ], + ]; + $cm = new ClassMetadata('stdClass'); + $cm->addSearchIndex($definition, 'embeddings_index', 'vectorSearch'); + + self::assertSame([ + [ + 'definition' => $definition, + 'name' => 'embeddings_index', + 'type' => 'vectorSearch', + ], + ], $cm->getSearchIndexes()); + } + public function testTimeSeriesMappingOnlyWithTimeField(): void { $metadata = $this->dm->getClassMetadata(TimeSeriesTestDocument::class); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml index 96c4493b15..300450d1e6 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml @@ -28,6 +28,7 @@ + @@ -59,6 +60,13 @@ + + + + + + + diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php index f3915f7993..5efcdbbd33 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php @@ -837,6 +837,35 @@ public function testNonRewindable(): void self::assertInstanceOf(UnrewindableIterator::class, $query->execute()); } + public function testQueryPreparesNestedValues(): void + { + $objectId = new ObjectId(); + $oidString = (string) $objectId; + + $embedded = new EmbeddedForNestedFieldPreparation(); + $embedded->objectId = $oidString; + + $builder = new Builder($this->dm, DocumentForNestedFieldPreparation::class); + $builder + ->updateOne() + ->field('embedded.embedded.objectId') + ->equals($oidString) + ->field('embedded.embedded.embedded') + ->set($embedded); + + $query = $builder->getQuery()->getQuery(); + $filter = $query['query']; + $newObj = $query['newObj']; + + self::assertArrayHasKey('embedded.embedded.test', $filter); + self::assertInstanceOf(ObjectId::class, $filter['embedded.embedded.test']); + + self::assertArrayHasKey('embedded.embedded.embedded', $newObj['$set']); + self::assertIsArray($newObj['$set']['embedded.embedded.embedded']); + self::assertArrayHasKey('test', $newObj['$set']['embedded.embedded.embedded']); + self::assertInstanceOf(ObjectId::class, $newObj['$set']['embedded.embedded.embedded']['test']); + } + private function getTestQueryBuilder(): Builder { return new Builder($this->dm, User::class); @@ -922,3 +951,23 @@ class ChildC extends ParentClass #[ODM\ReferenceMany(storeAs: 'dbRef')] public $featurePartialMany; } + +#[ODM\Document] +class DocumentForNestedFieldPreparation +{ + #[ODM\Id] + public string $id; + + #[ODM\EmbedOne(targetDocument: EmbeddedForNestedFieldPreparation::class)] + public EmbeddedForNestedFieldPreparation $embedded; +} + +#[ODM\EmbeddedDocument] +class EmbeddedForNestedFieldPreparation +{ + #[ODM\Field(name: 'test', type: Type::OBJECTID)] + public string $objectId; + + #[ODM\EmbedOne(targetDocument: self::class)] + public ?EmbeddedForNestedFieldPreparation $embedded = null; +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php index d1544b7319..c14c76c049 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\TimeSeries\Granularity; +use Doctrine\ODM\MongoDB\SchemaException; use Doctrine\ODM\MongoDB\SchemaManager; use Documents\BaseDocument; use Documents\CmsAddress; @@ -25,6 +26,7 @@ use Documents\TimeSeries\TimeSeriesDocument; use Documents\Tournament\Tournament; use Documents\UserName; +use Documents\VectorEmbedding; use InvalidArgumentException; use Iterator; use MongoDB\BSON\Document; @@ -48,6 +50,7 @@ use PHPUnit\Framework\MockObject\MockObject; use function array_count_values; +use function array_key_exists; use function array_map; use function assert; use function in_array; @@ -72,10 +75,11 @@ class SchemaManagerTest extends BaseTestCase ShardedOneWithDifferentKey::class, ]; - /** @var list */ + /** @var array> */ private array $searchIndexedClasses = [ - CmsAddress::class, - CmsArticle::class, + CmsAddress::class => ['default'], + CmsArticle::class => ['search_articles'], + VectorEmbedding::class => ['default', 'vector_int'], ]; /** @var list */ @@ -112,12 +116,7 @@ public function setUp(): void $this->documentCollections[$cm->getCollection()] = $this->getMockCollection($cm->getCollection()); } - $db = $this->getDatabaseName($cm); - if (isset($this->documentDatabases[$db])) { - continue; - } - - $this->documentDatabases[$db] = $this->getMockDatabase(); + $this->documentDatabases[$this->getDatabaseName($cm)] ??= $this->getMockDatabase(); } $client->method('getDatabase')->willReturnCallback(fn (string $db) => $this->documentDatabases[$db]); @@ -392,17 +391,18 @@ public function testDeleteDocumentIndexes(array $expectedWriteOptions, ?int $max public function testCreateSearchIndexes(): void { - $searchIndexedCollections = array_map( - fn (string $fqcn) => $this->dm->getClassMetadata($fqcn)->getCollection(), - $this->searchIndexedClasses, - ); + $searchIndexesPerCollectionName = []; + foreach ($this->searchIndexedClasses as $fqcn => $indexes) { + $searchIndexesPerCollectionName[$this->dm->getClassMetadata($fqcn)->getCollection()] = $indexes; + } + foreach ($this->documentCollections as $collectionName => $collection) { - if (in_array($collectionName, $searchIndexedCollections)) { + if (array_key_exists($collectionName, $searchIndexesPerCollectionName)) { $collection ->expects($this->once()) ->method('createSearchIndexes') ->with($this->anything()) - ->willReturn(['default']); + ->willReturn($searchIndexesPerCollectionName[$collectionName]); } else { $collection->expects($this->never())->method('createSearchIndexes'); } @@ -411,16 +411,49 @@ public function testCreateSearchIndexes(): void $this->schemaManager->createSearchIndexes(); } + public function testCreateDocumentSearchIndexesNotCreatedError(): void + { + $this->documentCollections['CmsArticle'] + ->expects($this->once()) + ->method('createSearchIndexes') + ->with($this->anything()) + ->willReturn(['foo']); + + $this->expectException(SchemaException::class); + $this->expectExceptionMessage('The document class "Documents\CmsArticle" is missing the following search index(es): "search_articles"'); + + $this->schemaManager->createDocumentSearchIndexes(CmsArticle::class); + } + public function testCreateDocumentSearchIndexes(): void { - $cmsArticleCollectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection(); + $expectedCollectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection(); foreach ($this->documentCollections as $collectionName => $collection) { - if ($collectionName === $cmsArticleCollectionName) { - $collection + if ($collectionName === $expectedCollectionName) { + $this->documentCollections['CmsArticle'] ->expects($this->once()) ->method('createSearchIndexes') ->with($this->anything()) - ->willReturn(['default']); + ->willReturnCallback(function (array $indexes) { + $this->assertSame([ + [ + 'type' => 'search', + 'name' => 'search_articles', + 'definition' => [ + 'mappings' => [ + 'dynamic' => true, + 'fields' => [ + 'article_title' => ['type' => 'autocomplete'], + 'text' => ['type' => 'string'], + 'not_mapped_field' => ['type' => 'token'], + ], + ], + ], + ], + ], $indexes); + + return ['search_articles']; + }); } else { $collection->expects($this->never())->method('createSearchIndexes'); } @@ -429,6 +462,49 @@ public function testCreateDocumentSearchIndexes(): void $this->schemaManager->createDocumentSearchIndexes(CmsArticle::class); } + public function testCreateVectorSearchIndex(): void + { + $expectedCollectionName = $this->dm->getClassMetadata(VectorEmbedding::class)->getCollection(); + foreach ($this->documentCollections as $collectionName => $collection) { + if ($collectionName === $expectedCollectionName) { + $this->documentCollections['vector_embeddings'] + ->expects($this->once()) + ->method('createSearchIndexes') + ->with($this->anything()) + ->willReturnCallback(function (array $indexes) { + $this->assertSame([ + [ + 'type' => 'vectorSearch', + 'name' => 'default', + 'definition' => [ + 'fields' => [ + ['type' => 'vector', 'path' => 'db_vector_float', 'numDimensions' => 3, 'similarity' => 'dotProduct'], + ], + ], + ], + [ + 'type' => 'vectorSearch', + 'name' => 'vector_int', + 'definition' => [ + 'fields' => [ + ['type' => 'filter', 'path' => 'filterField'], + ['type' => 'filter', 'path' => 'not_mapped_filter'], + ['type' => 'vector', 'path' => 'vectorInt', 'numDimensions' => 3, 'similarity' => 'cosine'], + ], + ], + ], + ], $indexes); + + return ['default', 'vector_int']; + }); + } else { + $collection->expects($this->never())->method('createSearchIndexes'); + } + } + + $this->schemaManager->createDocumentSearchIndexes(VectorEmbedding::class); + } + public function testCreateDocumentSearchIndexesNotSupported(): void { $exception = $this->createSearchIndexCommandException(); @@ -458,7 +534,7 @@ public function testUpdateDocumentSearchIndexes(): void ->expects($this->once()) ->method('listSearchIndexes') ->willReturn(new ArrayIterator([ - ['name' => 'default'], + ['name' => 'search_articles'], ['name' => 'foo'], ])); $collection @@ -468,7 +544,7 @@ public function testUpdateDocumentSearchIndexes(): void $collection ->expects($this->once()) ->method('updateSearchIndex') - ->with('default', $this->anything()); + ->with('search_articles', $this->anything()); $this->schemaManager->updateDocumentSearchIndexes(CmsArticle::class); } @@ -1328,7 +1404,6 @@ public static function dataIsMongoTextIndexEquivalentToDocumentIndex(): array ]; } - /** @param ClassMetadata $cm */ private function getDatabaseName(ClassMetadata $cm): string { return ($cm->getDatabase() ?: $this->dm->getConfiguration()->getDefaultDB()) ?: 'doctrine'; diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php new file mode 100644 index 0000000000..407ce66ab1 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php @@ -0,0 +1,99 @@ +toRfc4122(); + $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); + + self::assertNull($type->convertToDatabaseValue(null), 'null is not converted'); + self::assertEquals($binaryUuid, $type->convertToDatabaseValue($uuid), 'Uuid objects are converted to Binary objects'); + self::assertEquals($binaryUuid, $type->convertToDatabaseValue($stringUuid), 'String UUIDs are converted to Binary objects'); + self::assertSame($binaryUuid, $type->convertToDatabaseValue($binaryUuid), 'Binary UUIDs are returned as is'); + } + + public function testConvertInvalidUuid(): void + { + $type = Type::getType(Type::UUID); + + $this->expectException(InvalidArgumentException::class); + $type->convertToDatabaseValue('invalid'); + } + + public function testConvertToPHPValue(): void + { + $type = Type::getType(Type::UUID); + $uuid = new UuidV4(); + $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); + + self::assertEquals($uuid, $type->convertToPHPValue($binaryUuid), 'Binary UUIDs are converted to Uuid objects'); + self::assertSame($uuid, $type->convertToPHPValue($uuid), 'Uuid objects are returned as is'); + } + + public function testConvertInvalidBinaryUuid(): void + { + $type = Type::getType(Type::UUID); + + $this->expectException(InvalidArgumentException::class); + $type->convertToPHPValue(new Binary('invalid', Binary::TYPE_UUID)); + } + + public function testConvertInvalidBinary(): void + { + $type = Type::getType(Type::UUID); + + $this->expectException(Throwable::class); + $type->convertToPHPValue(new Binary('invalid', Binary::TYPE_GENERIC)); + } + + public function testClosureToMongo(): void + { + $type = Type::getType(Type::UUID); + $uuid = new UuidV4(); + $stringUuid = $uuid->toRfc4122(); + $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); + + $convertToDatabaseValue = static function ($value) use ($type) { + $return = null; + eval($type->closureToMongo()); + + return $return; + }; + + self::assertNull($convertToDatabaseValue(null), 'null is not converted'); + self::assertEquals($binaryUuid, $convertToDatabaseValue($uuid), 'Uuid objects are converted to Binary objects'); + self::assertEquals($binaryUuid, $convertToDatabaseValue($stringUuid), 'String UUIDs are converted to Binary objects'); + self::assertSame($binaryUuid, $convertToDatabaseValue($binaryUuid), 'Binary UUIDs are returned as is'); + } + + public function testClosureToPhp(): void + { + $type = Type::getType(Type::UUID); + $uuid = new UuidV4(); + $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); + + $convertToPHPValue = static function ($value) use ($type) { + $return = null; + eval($type->closureToPHP()); + + return $return; + }; + + self::assertEquals($uuid, $convertToPHPValue($binaryUuid), 'Binary UUIDs are converted to Uuid objects'); + self::assertSame($uuid, $convertToPHPValue($uuid), 'Uuid objects are returned as is'); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Types/TypeTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Types/TypeTest.php index 4013e7e8b7..25fa434917 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Types/TypeTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Types/TypeTest.php @@ -17,8 +17,10 @@ use MongoDB\BSON\Timestamp; use MongoDB\BSON\UTCDateTime; use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\Uid\UuidV4; use function get_debug_type; +use function hex2bin; use function md5; use function str_pad; use function str_repeat; @@ -66,6 +68,7 @@ public static function provideTypes(): array 'objectId' => [Type::OBJECTID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')], 'raw' => [Type::RAW, (object) ['foo' => 'bar']], 'decimal128' => [Type::DECIMAL128, '4.20', new Decimal128('4.20')], + 'uuid' => [Type::UUID, new UuidV4('550e8400-e29b-41d4-a716-446655440000'), new Binary(hex2bin('550e8400e29b41d4a716446655440000'), Binary::TYPE_UUID)], ]; } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Types/VectorTypeTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Types/VectorTypeTest.php new file mode 100644 index 0000000000..77018933d8 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Types/VectorTypeTest.php @@ -0,0 +1,152 @@ += 2.2')] +class VectorTypeTest extends TestCase +{ + #[DataProvider('providePhpVectors')] + public function testConvertToDatabaseValue(string $name, mixed $value, mixed $expectedValue): void + { + $this->assertEquals($expectedValue, Type::getType($name)->convertToDatabaseValue($value)); + } + + #[DataProvider('providePhpVectors')] + public function testClosureToDatabase(string $name, mixed $value, mixed $expectedValue): void + { + $return = $this; + eval(Type::getType($name)->closureToMongo()); + + $this->assertEquals($expectedValue, $return); + } + + /** @return iterable */ + public static function providePhpVectors(): iterable + { + $array = [1.0, 2.0, 3.0, 4.0]; + $binary = Binary::fromVector($array, VectorType::Float32); + + yield [Type::VECTOR_FLOAT32, null, null]; + yield [Type::VECTOR_FLOAT32, $array, $binary]; + yield [Type::VECTOR_FLOAT32, $binary, $binary]; + + $array = [1, 2, 3, 4]; + $binary = Binary::fromVector($array, VectorType::Int8); + + yield [Type::VECTOR_INT8, null, null]; + yield [Type::VECTOR_INT8, $array, $binary]; + yield [Type::VECTOR_INT8, $binary, $binary]; + + $array = [true, false, 0, 1]; + $binary = Binary::fromVector($array, VectorType::PackedBit); + + yield [Type::VECTOR_PACKED_BIT, null, null]; + yield [Type::VECTOR_PACKED_BIT, $array, $binary]; + yield [Type::VECTOR_PACKED_BIT, $binary, $binary]; + } + + #[DataProvider('provideDatabaseVectors')] + public function testConvertToPHPValue(string $name, mixed $value, mixed $expectedValue): void + { + $this->assertEquals($expectedValue, Type::getType($name)->convertToPHPValue($value)); + } + + #[DataProvider('provideDatabaseVectors')] + public function testClosureToPHP(string $name, mixed $value, mixed $expectedValue): void + { + $return = $this; + eval(Type::getType($name)->closureToPHP()); + + $this->assertEquals($expectedValue, $return); + } + + /** @return iterable */ + public static function provideDatabaseVectors(): iterable + { + $array = [1.0, 2.0, 3.0, 4.0]; + $binary = Binary::fromVector($array, VectorType::Float32); + + yield [Type::VECTOR_FLOAT32, null, null]; + yield [Type::VECTOR_FLOAT32, $array, $array]; + yield [Type::VECTOR_FLOAT32, $binary, $array]; + + $array = [1, 2, 3, 4]; + $binary = Binary::fromVector($array, VectorType::Int8); + + yield [Type::VECTOR_INT8, null, null]; + yield [Type::VECTOR_INT8, $array, $array]; + yield [Type::VECTOR_INT8, $binary, $array]; + + $array = [true, false, 0, 1]; + $binary = Binary::fromVector($array, VectorType::PackedBit); + + yield [Type::VECTOR_PACKED_BIT, null, null]; + yield [Type::VECTOR_PACKED_BIT, $array, $array]; + yield [Type::VECTOR_PACKED_BIT, $binary, $array]; + } + + #[DataProvider('provideDatabaseValueException')] + public function testConvertToPHPValueException(mixed $value, string $message): void + { + $type = Type::getType(Type::VECTOR_FLOAT32); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + $type->convertToDatabaseValue($value); + } + + #[DataProvider('provideDatabaseValueException')] + public function testClosureToPHPValueException(mixed $value, string $message): void + { + $type = Type::getType(Type::VECTOR_FLOAT32); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + eval($type->closureToMongo()); + } + + /** @return iterable */ + public static function provideDatabaseValueException(): iterable + { + yield ['invalid', 'Invalid data type string received for vector field, expected null, array or MongoDB\BSON\Binary']; + yield [new Binary("\x03\x00\x01\x02\x03", Binary::TYPE_GENERIC), 'Invalid binary data of type 0 received for vector field']; + yield [Binary::fromVector([1, 2, 3], VectorType::Int8), 'Invalid binary vector data of vector type Int8 received for vector field, expected vector type Float32']; + } + + #[DataProvider('providePHPValueException')] + public function testConvertToDatabaseValueException(mixed $value, string $message): void + { + $type = Type::getType(Type::VECTOR_FLOAT32); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + $type->convertToPHPValue($value); + } + + #[DataProvider('providePHPValueException')] + public function testClosureToDatabaseException(mixed $value, string $message): void + { + $type = Type::getType(Type::VECTOR_FLOAT32); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + eval($type->closureToPHP()); + } + + public static function providePHPValueException(): iterable + { + yield ['invalid', 'Invalid data of type "string" received for vector field']; + yield [new Binary("\x03\x00\x01\x02\x03", Binary::TYPE_GENERIC), 'Invalid binary data of type 0 received for vector field']; + yield [Binary::fromVector([1, 2, 3], VectorType::Int8), 'Invalid binary vector data of vector type Int8 received for vector field, expected vector type Float32']; + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Types/VersionableTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Types/VersionableTest.php new file mode 100644 index 0000000000..ba48765092 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Types/VersionableTest.php @@ -0,0 +1,83 @@ +getType(Type::INT); + self::assertSame(1, $type->getNextVersion(null)); + self::assertSame(2, $type->getNextVersion(1)); + } + + public function testDecimal128NextVersion(): void + { + $type = $this->getType(Type::DECIMAL128); + self::assertSame('1', $type->getNextVersion(null)); + self::assertSame('2', $type->getNextVersion('1')); + } + + public function testDateTimeNextVersion(): void + { + $type = $this->getType(Type::DATE); + $current = new DateTime(); + $next = $type->getNextVersion(null); + self::assertInstanceOf(DateTime::class, $next); + self::assertGreaterThanOrEqual($current, $next); + self::assertLessThanOrEqual(new DateTime(), $next); + + $next = $type->getNextVersion(new DateTime('2000-01-01')); + self::assertInstanceOf(DateTime::class, $next); + self::assertGreaterThanOrEqual($current, $next); + self::assertLessThanOrEqual(new DateTime(), $next); + } + + public function testDateTimeImmutableNextVersion(): void + { + $type = $this->getType(Type::DATE_IMMUTABLE); + $current = new DateTime(); + $next = $type->getNextVersion(null); + self::assertInstanceOf(DateTimeImmutable::class, $next); + self::assertGreaterThanOrEqual($current, $next); + self::assertLessThanOrEqual(new DateTimeImmutable(), $next); + + $next = $type->getNextVersion(new DateTimeImmutable('2000-01-01')); + self::assertInstanceOf(DateTimeImmutable::class, $next); + self::assertGreaterThanOrEqual($current, $next); + self::assertLessThanOrEqual(new DateTimeImmutable(), $next); + } + + public function testObjectIdNextVersion(): void + { + $type = $this->getType(Type::OBJECTID); + $current = new ObjectId(); + $next = $type->getNextVersion(null); + self::assertInstanceOf(ObjectId::class, $next); + self::assertGreaterThan($current, $next); + self::assertLessThan(new ObjectId(), $next); + + $next = $type->getNextVersion($current); + self::assertInstanceOf(ObjectId::class, $next); + self::assertGreaterThan($current, $next); + self::assertLessThan(new ObjectId(), $next); + } + + private function getType(string $name): Versionable + { + $type = Type::getType($name); + + self::assertInstanceOf(Versionable::class, $type); + + return $type; + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkCommitConsistencyTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkCommitConsistencyTest.php index 07376b591a..d75f8c17f3 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkCommitConsistencyTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkCommitConsistencyTest.php @@ -18,16 +18,6 @@ class UnitOfWorkCommitConsistencyTest extends BaseTestCase // This test requires transactions to be disabled protected static bool $allowsTransactions = false; - public function tearDown(): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => 'off', - ]); - - parent::tearDown(); - } - public function testInsertErrorKeepsFailingInsertions(): void { $firstUser = new ForumUser(); @@ -446,16 +436,4 @@ protected static function createTestDocumentManager(): DocumentManager return DocumentManager::create($client, $config); } - - private function createFailpoint(string $commandName): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => ['times' => 1], - 'data' => [ - 'errorCode' => 192, // FailPointEnabled - 'failCommands' => [$commandName], - ], - ]); - } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php index ac065a8ded..55d61fedba 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php @@ -257,7 +257,7 @@ public function testComputingChangesetForFileWithoutMetadataThrowsNoError(): voi #[DataProvider('getScheduleForUpdateWithArraysTests')] public function testScheduleForUpdateWithArrays(?array $origData, ?array $updateData, bool $shouldInUpdate): void { - $arrayTest = new ArrayTest($origData); + $arrayTest = new ArrayDocument($origData); $this->uow->persist($arrayTest); $this->uow->computeChangeSets(); $this->uow->commit(); @@ -701,7 +701,7 @@ public function setOwner(NotifyChangedDocument $owner): void } #[ODM\Document] -class ArrayTest +class ArrayDocument { /** @var string|null */ #[ODM\Id] diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTransactionalCommitConsistencyTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTransactionalCommitConsistencyTest.php index da80267c1c..9c69b0b44e 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTransactionalCommitConsistencyTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTransactionalCommitConsistencyTest.php @@ -25,16 +25,6 @@ public function setUp(): void $this->skipTestIfNoTransactionSupport(); } - public function tearDown(): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => 'off', - ]); - - parent::tearDown(); - } - public function testFatalInsertError(): void { $firstUser = new ForumUser(); @@ -48,7 +38,7 @@ public function testFatalInsertError(): void $friendUser = new FriendUser('GromNaN'); $this->uow->persist($friendUser); - $this->createFatalFailPoint('insert'); + $this->createFailPoint('insert'); try { $this->uow->commit(); @@ -92,7 +82,7 @@ public function testTransientInsertError(): void $this->uow->persist($friendUser); // Add a failpoint that triggers a transient error. The transaction will be retried and succeeds - $this->createTransientFailPoint('insert'); + $this->createFailPoint('insert', transient: true); $this->uow->commit(); @@ -130,7 +120,7 @@ public function testMultipleTransientErrors(): void $this->uow->persist($friendUser); // Add a failpoint that triggers multiple transient errors. The transaction is expected to fail - $this->createTransientFailPoint('insert', 2); + $this->createFailPoint('insert', transient: true, times: 2); try { $this->uow->commit(); @@ -251,7 +241,7 @@ public function testFatalUpsertError(): void $user->username = 'alcaeus'; $this->uow->persist($user); - $this->createFatalFailPoint('update'); + $this->createFailPoint('update'); try { $this->uow->commit(); @@ -278,7 +268,7 @@ public function testTransientUpsertError(): void $user->username = 'alcaeus'; $this->uow->persist($user); - $this->createTransientFailPoint('update'); + $this->createFailPoint('update', transient: true); $this->uow->commit(); @@ -300,7 +290,7 @@ public function testFatalUpdateError(): void $user->username = 'jmikola'; - $this->createFatalFailPoint('update'); + $this->createFailPoint('update'); try { $this->uow->commit(); @@ -328,7 +318,7 @@ public function testTransientUpdateError(): void $user->username = 'jmikola'; - $this->createTransientFailPoint('update'); + $this->createFailPoint('update', transient: true); $this->uow->commit(); @@ -353,7 +343,7 @@ public function testFatalUpdateErrorWithNewEmbeddedDocument(): void $address->setCity('Olching'); $user->setAddress($address); - $this->createFatalFailPoint('update'); + $this->createFailPoint('update'); try { $this->uow->commit(); @@ -381,7 +371,7 @@ public function testTransientUpdateErrorWithNewEmbeddedDocument(): void $address->setCity('Olching'); $user->setAddress($address); - $this->createTransientFailPoint('update'); + $this->createFailPoint('update', transient: true); $this->uow->commit(); @@ -405,7 +395,7 @@ public function testFatalUpdateErrorOfEmbeddedDocument(): void $address->setCity('Munich'); - $this->createFatalFailPoint('update'); + $this->createFailPoint('update'); try { $this->uow->commit(); @@ -435,7 +425,7 @@ public function testTransientUpdateErrorOfEmbeddedDocument(): void $address->setCity('Munich'); - $this->createTransientFailPoint('update'); + $this->createFailPoint('update', transient: true); $this->uow->commit(); @@ -459,7 +449,7 @@ public function testFatalUpdateErrorWithRemovedEmbeddedDocument(): void $user->removeAddress(); - $this->createFatalFailPoint('update'); + $this->createFailPoint('update'); try { $this->uow->commit(); @@ -491,7 +481,7 @@ public function testTransientUpdateErrorWithRemovedEmbeddedDocument(): void $user->removeAddress(); - $this->createTransientFailPoint('update'); + $this->createFailPoint('update', transient: true); $this->uow->commit(); @@ -515,7 +505,7 @@ public function testFatalDeleteErrorWithEmbeddedDocument(): void $this->uow->remove($user); - $this->createFatalFailPoint('delete'); + $this->createFailPoint('delete'); try { $this->uow->commit(); @@ -549,7 +539,7 @@ public function testTransientDeleteErrorWithEmbeddedDocument(): void $this->uow->remove($user); - $this->createTransientFailPoint('delete'); + $this->createFailPoint('delete', transient: true); $this->uow->commit(); @@ -582,7 +572,7 @@ public function testTransientErrorPreservesCollectionChangesets(): void // Remove fooUser and create a transient failpoint to force the deletion // to fail. This exposes the issue with collections $this->uow->remove($fooUser); - $this->createTransientFailPoint('delete'); + $this->createFailPoint('delete', transient: true); $this->uow->commit(); @@ -608,29 +598,4 @@ protected static function getConfiguration(): Configuration return $configuration; } - - private function createTransientFailPoint(string $failCommand, int $times = 1): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => ['times' => $times], - 'data' => [ - 'errorCode' => 192, // FailPointEnabled - 'errorLabels' => ['TransientTransactionError'], - 'failCommands' => [$failCommand], - ], - ]); - } - - private function createFatalFailPoint(string $failCommand): void - { - $this->dm->getClient()->getDatabase('admin')->command([ - 'configureFailPoint' => 'failCommand', - 'mode' => ['times' => 1], - 'data' => [ - 'errorCode' => 192, // FailPointEnabled - 'failCommands' => [$failCommand], - ], - ]); - } } diff --git a/tests/Documents/CmsArticle.php b/tests/Documents/CmsArticle.php index 15accb4aa8..cd059c9543 100644 --- a/tests/Documents/CmsArticle.php +++ b/tests/Documents/CmsArticle.php @@ -8,7 +8,15 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ODM\Index(keys: ['topic' => 'asc'])] -#[ODM\SearchIndex(dynamic: true)] +#[ODM\SearchIndex( + name: 'search_articles', + dynamic: true, + fields: [ + 'title' => ['type' => 'autocomplete'], + 'text' => ['type' => 'string'], + 'not_mapped_field' => ['type' => 'token'], + ], +)] #[ODM\Document] class CmsArticle { @@ -19,7 +27,7 @@ class CmsArticle #[ODM\Field(type: 'string')] public $topic; /** @var string|null */ - #[ODM\Field(type: 'string')] + #[ODM\Field(type: 'string', name: 'article_title')] public $title; /** @var string|null */ #[ODM\Field(type: 'string')] diff --git a/tests/Documents/GH2310Container.php b/tests/Documents/GH2310Container.php index 0802b5ff58..b0323a94a1 100644 --- a/tests/Documents/GH2310Container.php +++ b/tests/Documents/GH2310Container.php @@ -25,6 +25,6 @@ public function __construct(?string $id, ?GH2310Embedded $embedded) #[ODM\EmbeddedDocument] class GH2310Embedded { - #[ODM\Field(type: 'integer')] + #[ODM\Field(type: 'int')] public int $value; } diff --git a/tests/Documents/VectorEmbedding.php b/tests/Documents/VectorEmbedding.php new file mode 100644 index 0000000000..2a8cfbd7dc --- /dev/null +++ b/tests/Documents/VectorEmbedding.php @@ -0,0 +1,43 @@ + 'vector', 'path' => 'vectorFloat', 'numDimensions' => 3, 'similarity' => ClassMetadata::VECTOR_SIMILARITY_DOT_PRODUCT], + ], +)] +#[VectorSearchIndex( + name: 'vector_int', + fields: [ + ['type' => 'filter', 'path' => 'filterField'], + ['type' => 'filter', 'path' => 'not_mapped_filter'], + ['type' => 'vector', 'path' => 'vectorInt', 'numDimensions' => 3, 'similarity' => ClassMetadata::VECTOR_SIMILARITY_COSINE], + ], +)] +class VectorEmbedding +{ + #[Id] + public ?string $id = null; + + /** @var list */ + #[Field(type: Type::COLLECTION, name: 'db_vector_float')] + public array $vectorFloat = []; + + /** @var list */ + #[Field(type: Type::COLLECTION)] + public array $vectorInt = []; + + #[Field(type: Type::STRING)] + public string $filterField; +}