From 257a0a2ab0ba140b36b7be7e3f3eecb39a7c5059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 28 Oct 2025 12:48:23 +0100 Subject: [PATCH 01/15] Symfony 8.0 support (#2791) * Symfony 8.0 support * Use symfony/flex for version constraint in CI --- .github/workflows/atlas-ci.yml | 7 ++- .github/workflows/continuous-integration.yml | 46 +++++++++++-------- composer.json | 12 +++-- docs/en/reference/console-commands.rst | 2 +- phpcs.xml.dist | 1 + .../Command/ClearCache/MetadataCommand.php | 3 +- .../Console/Command/CommandCompatibility.php | 30 +++++++++++- .../Command/GenerateHydratorsCommand.php | 3 +- .../GeneratePersistentCollectionsCommand.php | 3 +- .../Command/GenerateProxiesCommand.php | 3 +- src/Tools/Console/Command/QueryCommand.php | 3 +- .../Command/Schema/AbstractCommand.php | 7 ++- .../Schema/AbstractCommandCompatibility.php | 30 ++++++++++++ .../Console/Command/Schema/CreateCommand.php | 3 +- .../Console/Command/Schema/DropCommand.php | 3 +- .../Console/Command/Schema/ShardCommand.php | 3 +- .../Console/Command/Schema/UpdateCommand.php | 3 +- .../Command/Schema/ValidateCommand.php | 3 +- 18 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 src/Tools/Console/Command/Schema/AbstractCommandCompatibility.php diff --git a/.github/workflows/atlas-ci.yml b/.github/workflows/atlas-ci.yml index ff9225f9a8..b4b4cf99d9 100644 --- a/.github/workflows/atlas-ci.yml +++ b/.github/workflows/atlas-ci.yml @@ -18,8 +18,13 @@ jobs: symfony: - "stable" proxy: - - "lazy-ghost" + - "native" include: + # Test with LazyGhostObject + - php-version: "8.2" + symfony: "7.4" + proxy: "lazy-ghost" + os: "ubuntu-latest" # Test with ProxyManager - php-version: "8.1" symfony: "6.4" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c837bf2cb7..b30736856a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,7 +34,7 @@ jobs: dependencies: - "highest" symfony-version: - - "stable" + - "locked" proxy: - "lazy-ghost" include: @@ -44,7 +44,7 @@ jobs: mongodb-version: "6.0" driver-version: "1.21.0" topology: "server" - symfony-version: "stable" + symfony-version: "locked" proxy: "lazy-ghost" # Test with Symfony 6.4 - topology: "server" @@ -60,7 +60,7 @@ jobs: mongodb-version: "6.0" driver-version: "stable" dependencies: "highest" - symfony-version: "stable" + symfony-version: "locked" proxy: "lazy-ghost" # Test with a 8.0 replica set - topology: "replica_set" @@ -68,21 +68,21 @@ jobs: mongodb-version: "8.0" driver-version: "stable" dependencies: "highest" - symfony-version: "stable" + symfony-version: "locked" proxy: "lazy-ghost" # Test with ProxyManager - php-version: "8.2" mongodb-version: "6.0" driver-version: "stable" dependencies: "highest" - symfony-version: "stable" + symfony-version: "locked" proxy: "proxy-manager" # Test with Native Lazy Objects - php-version: "8.4" mongodb-version: "8.0" driver-version: "stable" dependencies: "highest" - symfony-version: "stable" + symfony-version: "locked" proxy: "native" # Test with extension 1.21 - topology: "server" @@ -90,8 +90,24 @@ jobs: mongodb-version: "8.0" driver-version: "1.21.0" dependencies: "highest" - symfony-version: "stable" + symfony-version: "locked" + proxy: "lazy-ghost" + # Test with Symfony 7.4 LTS + - topology: "server" + php-version: "8.2" + mongodb-version: "8.0" + driver-version: "stable" + dependencies: "highest" + symfony-version: "7.4" proxy: "lazy-ghost" + # Test with Symfony 8 + - topology: "server" + php-version: "8.4" + mongodb-version: "8.0" + driver-version: "stable" + dependencies: "highest" + symfony-version: false + proxy: "native" # Test with a sharded cluster # Currently disabled due to a bug where MongoDB reports "sharding status unknown" # - topology: "sharded_cluster" @@ -99,7 +115,7 @@ jobs: # mongodb-version: "6.0" # driver-version: "stable" # dependencies: "highest" -# symfony-version: "stable" +# symfony-version: "locked" # proxy: "lazy-ghost" steps: @@ -127,7 +143,6 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - tools: "pecl" extensions: "mongodb-${{ matrix.driver-version }}, bcmath" coverage: "none" ini-values: "zend.assertions=1" @@ -135,24 +150,17 @@ jobs: - name: "Show driver information" run: "php --ri mongodb" - # Not used, skip transient dependencies + # Not used for tests, skip transient dependencies - name: "Remove phpbench/phpbench" run: composer remove --no-update --dev phpbench/phpbench - - name: "Configure Symfony ${{ matrix.symfony-version }}" - if: "${{ matrix.symfony-version != 'stable' }}" - run: | - composer config minimum-stability dev - # update symfony deps - composer require --no-update symfony/console:^${{ matrix.symfony-version }} - composer require --no-update symfony/var-dumper:^${{ matrix.symfony-version }} - composer require --no-update --dev symfony/cache:^${{ matrix.symfony-version }} - - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" composer-options: "--prefer-dist" + env: + SYMFONY_REQUIRE: ${{ matrix.symfony-version }} - name: "Install latest Python version" uses: actions/setup-python@v6 diff --git a/composer.json b/composer.json index 9a52ae7f4f..5e5e849fde 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,8 @@ "mapping", "object" ], + "minimum-stability": "dev", + "prefer-stable": true, "homepage": "https://www.doctrine-project.org/projects/mongodb-odm.html", "license": "MIT", "authors": [ @@ -35,10 +37,10 @@ "jean85/pretty-package-versions": "^1.3.0 || ^2.0.1", "mongodb/mongodb": "^1.21.2 || ^2.1.1", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2.2 || ^3.0", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^6.2 || ^7.0" + "symfony/var-dumper": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.4 || ^7.0" }, "require-dev": { "ext-bcmath": "*", @@ -52,8 +54,8 @@ "phpstan/phpstan-phpunit": "^2.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" + "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "conflict": { "doctrine/annotations": "<1.12 || >=3.0" diff --git a/docs/en/reference/console-commands.rst b/docs/en/reference/console-commands.rst index 9290eebd5c..1deb489c62 100644 --- a/docs/en/reference/console-commands.rst +++ b/docs/en/reference/console-commands.rst @@ -1,7 +1,7 @@ Console Commands ================ -Doctrine MongoDB ODM offers some console commands, which utilize Symfony2's +Doctrine MongoDB ODM offers some console commands, which utilize Symfony's Console component, to ease your development process: - ``odm:clear-cache:metadata`` - Clear all metadata cache of the various cache drivers. diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 4b7df00be3..859ceb7590 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -68,6 +68,7 @@ src/Mapping/Driver/CompatibilityAnnotationDriver.php src/Tools/Console/Command/CommandCompatibility.php + src/Tools/Console/Command/Schema/AbstractCommandCompatibility.php src/Tools/Console/Helper/DocumentManagerHelper.php tests/* diff --git a/src/Tools/Console/Command/ClearCache/MetadataCommand.php b/src/Tools/Console/Command/ClearCache/MetadataCommand.php index d76aa4f5a0..e07e50a574 100644 --- a/src/Tools/Console/Command/ClearCache/MetadataCommand.php +++ b/src/Tools/Console/Command/ClearCache/MetadataCommand.php @@ -22,8 +22,7 @@ class MetadataCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:clear-cache:metadata') diff --git a/src/Tools/Console/Command/CommandCompatibility.php b/src/Tools/Console/Command/CommandCompatibility.php index 5e7fb7b9fe..dd686d4792 100644 --- a/src/Tools/Console/Command/CommandCompatibility.php +++ b/src/Tools/Console/Command/CommandCompatibility.php @@ -9,10 +9,32 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -if ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) { +// Symfony 8 +if ((new ReflectionMethod(Command::class, 'configure'))->hasReturnType()) { /** @internal */ trait CommandCompatibility { + protected function configure(): void + { + $this->doConfigure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + return $this->doExecute($input, $output); + } + } +// Symfony 7 +} elseif ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) { + /** @internal */ + trait CommandCompatibility + { + /** @return void */ + protected function configure() + { + $this->doConfigure(); + } + protected function execute(InputInterface $input, OutputInterface $output): int { return $this->doExecute($input, $output); @@ -22,6 +44,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @internal */ trait CommandCompatibility { + /** @return void */ + protected function configure() + { + $this->doConfigure(); + } + /** * {@inheritDoc} * diff --git a/src/Tools/Console/Command/GenerateHydratorsCommand.php b/src/Tools/Console/Command/GenerateHydratorsCommand.php index cdcc4977b8..811797dffe 100644 --- a/src/Tools/Console/Command/GenerateHydratorsCommand.php +++ b/src/Tools/Console/Command/GenerateHydratorsCommand.php @@ -31,8 +31,7 @@ class GenerateHydratorsCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:generate:hydrators') diff --git a/src/Tools/Console/Command/GeneratePersistentCollectionsCommand.php b/src/Tools/Console/Command/GeneratePersistentCollectionsCommand.php index fb71f3ae05..f0c5c3a3ad 100644 --- a/src/Tools/Console/Command/GeneratePersistentCollectionsCommand.php +++ b/src/Tools/Console/Command/GeneratePersistentCollectionsCommand.php @@ -31,8 +31,7 @@ class GeneratePersistentCollectionsCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:generate:persistent-collections') diff --git a/src/Tools/Console/Command/GenerateProxiesCommand.php b/src/Tools/Console/Command/GenerateProxiesCommand.php index 63707a9e4b..06a502729f 100644 --- a/src/Tools/Console/Command/GenerateProxiesCommand.php +++ b/src/Tools/Console/Command/GenerateProxiesCommand.php @@ -35,8 +35,7 @@ class GenerateProxiesCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:generate:proxies') diff --git a/src/Tools/Console/Command/QueryCommand.php b/src/Tools/Console/Command/QueryCommand.php index 0ee91ca25d..d479cb931b 100644 --- a/src/Tools/Console/Command/QueryCommand.php +++ b/src/Tools/Console/Command/QueryCommand.php @@ -27,8 +27,7 @@ class QueryCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:query') diff --git a/src/Tools/Console/Command/Schema/AbstractCommand.php b/src/Tools/Console/Command/Schema/AbstractCommand.php index 8b6481b803..22e8b43d82 100644 --- a/src/Tools/Console/Command/Schema/AbstractCommand.php +++ b/src/Tools/Console/Command/Schema/AbstractCommand.php @@ -19,16 +19,15 @@ abstract class AbstractCommand extends Command { + use AbstractCommandCompatibility; + public const DB = 'db'; public const COLLECTION = 'collection'; public const INDEX = 'index'; public const SEARCH_INDEX = 'search-index'; - /** @return void */ - protected function configure() + private function configureCommonOptions(): void { - parent::configure(); - $this ->addOption('maxTimeMs', null, InputOption::VALUE_REQUIRED, 'An optional maxTimeMs that will be used for all schema operations.') ->addOption('w', null, InputOption::VALUE_REQUIRED, 'An optional w option for the write concern that will be used for all schema operations.') diff --git a/src/Tools/Console/Command/Schema/AbstractCommandCompatibility.php b/src/Tools/Console/Command/Schema/AbstractCommandCompatibility.php new file mode 100644 index 0000000000..b1b29033c2 --- /dev/null +++ b/src/Tools/Console/Command/Schema/AbstractCommandCompatibility.php @@ -0,0 +1,30 @@ +hasReturnType()) { + /** @internal */ + trait AbstractCommandCompatibility + { + protected function configure(): void + { + $this->configureCommonOptions(); + } + } +} else { + /** @internal */ + trait AbstractCommandCompatibility + { + /** @return void */ + protected function configure() + { + $this->configureCommonOptions(); + } + } +} diff --git a/src/Tools/Console/Command/Schema/CreateCommand.php b/src/Tools/Console/Command/Schema/CreateCommand.php index f1c4162343..443c236756 100644 --- a/src/Tools/Console/Command/Schema/CreateCommand.php +++ b/src/Tools/Console/Command/Schema/CreateCommand.php @@ -30,8 +30,7 @@ class CreateCommand extends AbstractCommand self::SEARCH_INDEX => ['search index(es)', 'search indexes'], ]; - /** @return void */ - protected function configure() + private function doConfigure(): void { parent::configure(); diff --git a/src/Tools/Console/Command/Schema/DropCommand.php b/src/Tools/Console/Command/Schema/DropCommand.php index 1ae039fa83..7c85ee127d 100644 --- a/src/Tools/Console/Command/Schema/DropCommand.php +++ b/src/Tools/Console/Command/Schema/DropCommand.php @@ -31,8 +31,7 @@ class DropCommand extends AbstractCommand self::SEARCH_INDEX => ['search index(es)', 'search indexes'], ]; - /** @return void */ - protected function configure() + protected function doConfigure(): void { parent::configure(); diff --git a/src/Tools/Console/Command/Schema/ShardCommand.php b/src/Tools/Console/Command/Schema/ShardCommand.php index 25bc6ba07f..db84746937 100644 --- a/src/Tools/Console/Command/Schema/ShardCommand.php +++ b/src/Tools/Console/Command/Schema/ShardCommand.php @@ -19,8 +19,7 @@ class ShardCommand extends AbstractCommand { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { parent::configure(); diff --git a/src/Tools/Console/Command/Schema/UpdateCommand.php b/src/Tools/Console/Command/Schema/UpdateCommand.php index a4d0fc313b..83805f70d5 100644 --- a/src/Tools/Console/Command/Schema/UpdateCommand.php +++ b/src/Tools/Console/Command/Schema/UpdateCommand.php @@ -19,8 +19,7 @@ class UpdateCommand extends AbstractCommand { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { parent::configure(); diff --git a/src/Tools/Console/Command/Schema/ValidateCommand.php b/src/Tools/Console/Command/Schema/ValidateCommand.php index 046c8a90a5..8678f714a2 100644 --- a/src/Tools/Console/Command/Schema/ValidateCommand.php +++ b/src/Tools/Console/Command/Schema/ValidateCommand.php @@ -19,8 +19,7 @@ class ValidateCommand extends Command { use CommandCompatibility; - /** @return void */ - protected function configure() + private function doConfigure(): void { $this ->setName('odm:schema:validate') From c11783a472733b672a9bf66f1ee956182a3b92dd Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 30 Oct 2025 08:14:48 +0100 Subject: [PATCH 02/15] Support update pipelines in query builder (#2881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support pipeline updates in Query Builder * Add pipeline updates to query builder documentation * Fix phpstan errors * Fix changed exception message * Update tests/Tests/Query/PipelineUpdateTest.php Co-authored-by: Jérôme Tamarelle --------- Co-authored-by: Jérôme Tamarelle --- docs/en/reference/query-builder-api.rst | 46 ++++- phpstan-baseline.neon | 10 +- src/Query/Builder.php | 21 +++ src/Query/Query.php | 32 ++-- tests/Tests/Query/PipelineUpdateTest.php | 230 +++++++++++++++++++++++ 5 files changed, 320 insertions(+), 19 deletions(-) create mode 100644 tests/Tests/Query/PipelineUpdateTest.php diff --git a/docs/en/reference/query-builder-api.rst b/docs/en/reference/query-builder-api.rst index 471ff0157d..88d9f0543d 100644 --- a/docs/en/reference/query-builder-api.rst +++ b/docs/en/reference/query-builder-api.rst @@ -641,8 +641,7 @@ change document field values atomically. Additionally if you are modifying a fie that is a reference you can pass managed document to the Builder and let ODM build ``DBRef`` object for you. -You have several modifier operations -available to you that make it easy to update documents in Mongo: +The following atomic update operators are available through the builder API: * ``set($name, $value, $atomic = true)`` * ``setNewObj($newObj)`` @@ -655,6 +654,49 @@ available to you that make it easy to update documents in Mongo: * ``pull($field, $value)`` * ``pullAll($field, array $valueArray)`` +You can also run `updates with Aggregation Pipeline `_ +by using the ``pipeline()`` method. You can pass an aggregation builder instance, a ``Pipeline`` instance from the +MongoDB PHP library, or an array of pipeline stages: + +.. code-block:: php + + createAggregationBuilder(User::class) + ->set() + ->field('totalScore') + ->add('$score1', '$score2'), + ); + + $pipeline = new Pipeline( + Stage::set( + totalScore: Expression::add( + Expression::fieldPath('score1'), + Expression::fieldPath('score2'), + ), + ) + ); + + $pipeline = [ + ['$set' => [ + 'totalScore' => ['$add' => ['$score1', '$score2']], + ]], + ] + + $dm->createQueryBuilder(User::class) + ->updateOne() + ->field('username')->equals('jwage') + ->pipeline($pipeline) + ->getQuery() + ->execute(); + +.. note:: + + Pipeline updates are only available for ``updateOne``, ``updateMany``, and ``findAndUpdate`` operations. + + Updating multiple documents --------------------------- diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d2f4c2db11..cb4ef20fde 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -787,7 +787,7 @@ parameters: path: src/Query/Query.php - - message: '#^Strict comparison using \!\=\= between array\\|bool\|int\|MongoDB\\Driver\\ReadPreference\|string and null will always evaluate to true\.$#' + message: '#^Strict comparison using \!\=\= between array\\|string, mixed\>\|bool\|int\|MongoDB\\Builder\\Pipeline\|MongoDB\\Driver\\ReadPreference\|string and null will always evaluate to true\.$#' identifier: notIdentical.alwaysTrue count: 1 path: src/Query/Query.php @@ -1098,6 +1098,12 @@ parameters: count: 1 path: tests/Tests/Query/BuilderTest.php + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: tests/Tests/Query/PipelineUpdateTest.php + - message: '#^Method Doctrine\\ODM\\MongoDB\\Tests\\QueryTest\:\:createCursorMock\(\) return type has no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue @@ -1105,7 +1111,7 @@ parameters: path: tests/Tests/QueryTest.php - - message: '#^Parameter \#4 \$query of class Doctrine\\ODM\\MongoDB\\Query\\Query constructor expects array\{distinct\?\: string, hint\?\: array\\|string, limit\?\: int, maxTimeMS\?\: int, multiple\?\: bool, new\?\: bool, newObj\?\: array\, query\?\: array\, \.\.\.\}, array\{type\: \-1\} given\.$#' + message: '#^Parameter \#4 \$query of class Doctrine\\ODM\\MongoDB\\Query\\Query constructor expects array\{distinct\?\: string, hint\?\: array\\|string, limit\?\: int, maxTimeMS\?\: int, multiple\?\: bool, new\?\: bool, newObj\?\: array\, pipeline\?\: list\\>\|MongoDB\\Builder\\Pipeline, \.\.\.\}, array\{type\: \-1\} given\.$#' identifier: argument.type count: 1 path: tests/Tests/QueryTest.php diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 6578e1e8a9..43e8f7cc73 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Query; use BadMethodCallException; +use Doctrine\ODM\MongoDB\Aggregation\Builder as AggregationBuilder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Iterator\IterableResult; @@ -14,6 +15,7 @@ use InvalidArgumentException; use MongoDB\BSON\Binary; use MongoDB\BSON\Javascript; +use MongoDB\Builder\Pipeline; use MongoDB\Collection; use MongoDB\Driver\ReadPreference; @@ -1064,6 +1066,25 @@ public function notIn(array $values): self return $this; } + /** + * Specifies a pipeline to be used for updates. The pipeline can be an aggregation builder, MongoDB pipeline + * instance, or an array of pipeline stages. + * + * @param AggregationBuilder|Pipeline|list> $pipeline + */ + public function pipeline(AggregationBuilder|array|Pipeline $pipeline): self + { + if ($this->query['type'] !== Query::TYPE_UPDATE && $this->query['type'] !== Query::TYPE_FIND_AND_UPDATE) { + throw new BadMethodCallException('The pipeline() method can only be used with update or findAndUpdate queries.'); + } + + $this->query['pipeline'] = $pipeline instanceof AggregationBuilder + ? $pipeline->getPipeline() + : $pipeline; + + return $this; + } + /** * Remove the first element from the current array field. * diff --git a/src/Query/Query.php b/src/Query/Query.php index d33961f6d6..f5dad99f97 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -17,6 +17,7 @@ use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\UnitOfWork; use InvalidArgumentException; +use MongoDB\Builder\Pipeline; use MongoDB\Collection; use MongoDB\DeleteResult; use MongoDB\Driver\ReadPreference; @@ -51,6 +52,7 @@ * multiple?: bool, * new?: bool, * newObj?: array, + * pipeline?: Pipeline|list>, * query?: array, * readPreference?: ReadPreference, * select?: array>, @@ -459,11 +461,17 @@ private function runQuery() $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']); $queryOptions['returnDocument'] = $this->query['new'] ?? false ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE; - $operation = $this->isFirstKeyUpdateOperator() ? 'findOneAndUpdate' : 'findOneAndReplace'; + if (isset($this->query['pipeline'])) { + $operation = 'findOneAndUpdate'; + $update = $this->query['pipeline']; + } else { + $operation = $this->isFirstKeyUpdateOperator() ? 'findOneAndUpdate' : 'findOneAndReplace'; + $update = $this->query['newObj']; + } return $this->collection->{$operation}( $this->query['query'], - $this->query['newObj'], + $update, array_merge($options, $queryOptions) ); @@ -480,11 +488,13 @@ private function runQuery() return $this->collection->insertOne($this->query['newObj'], $options); case self::TYPE_UPDATE: - $multiple = $this->query['multiple'] ?? false; + $multiple = $this->query['multiple'] ?? false; + $operation = $multiple ? 'updateMany' : 'updateOne'; + $update = $this->query['newObj']; - if ($this->isFirstKeyUpdateOperator()) { - $operation = 'updateOne'; - } else { + if (isset($this->query['pipeline'])) { + $update = $this->query['pipeline']; + } elseif (! $this->isFirstKeyUpdateOperator()) { if ($multiple) { throw new InvalidArgumentException('Combining the "multiple" option without using an update operator as first operation in a query is not supported.'); } @@ -492,17 +502,9 @@ private function runQuery() $operation = 'replaceOne'; } - if ($multiple) { - return $this->collection->updateMany( - $this->query['query'], - $this->query['newObj'], - array_merge($options, $this->getQueryOptions('upsert')), - ); - } - return $this->collection->{$operation}( $this->query['query'], - $this->query['newObj'], + $update, array_merge($options, $this->getQueryOptions('upsert')) ); diff --git a/tests/Tests/Query/PipelineUpdateTest.php b/tests/Tests/Query/PipelineUpdateTest.php new file mode 100644 index 0000000000..c9abaceb1f --- /dev/null +++ b/tests/Tests/Query/PipelineUpdateTest.php @@ -0,0 +1,230 @@ +user1 = new User(); + $this->user1->setUsername('foo'); + $this->user1->setHits(1); + + $this->user2 = new User(); + $this->user2->setUsername('bar'); + $this->user2->setHits(2); + + $this->dm->persist($this->user1); + $this->dm->persist($this->user2); + + $this->dm->flush(); + } + + public function testUpdateManyWithWrongType(): void + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The pipeline() method can only be used with update or findAndUpdate queries.'); + + $this->dm->createQueryBuilder(User::class) + ->pipeline([]); + } + + public function testUpdateOneWithAggregationBuilder(): void + { + $builder = $this->dm->createAggregationBuilder(User::class); + $builder + ->set() + ->field('hits') + ->add('$hits', 1); + + $this->dm->createQueryBuilder(User::class) + ->updateOne() + ->field('username')->equals('foo') + ->pipeline($builder) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(2, $user2->getHits()); + } + + public function testUpdateOneWithDriverPipeline(): void + { + $pipeline = new Pipeline( + Stage::set( + hits: Expression::add(Expression::fieldPath('hits'), 1), + ), + ); + + $this->dm->createQueryBuilder(User::class) + ->updateOne() + ->field('username')->equals('foo') + ->pipeline($pipeline) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(2, $user2->getHits()); + } + + public function testUpdateOneWithPipelineArray(): void + { + $this->dm->createQueryBuilder(User::class) + ->updateOne() + ->field('username')->equals('foo') + ->pipeline([['$set' => ['hits' => ['$sum' => ['$hits', 1]]]]]) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(2, $user2->getHits()); + } + + public function testUpdateManyWithAggregationBuilder(): void + { + $builder = $this->dm->createAggregationBuilder(User::class); + $builder + ->set() + ->field('hits') + ->multiply('$hits', 2); + + $this->dm->createQueryBuilder(User::class) + ->updateMany() + ->pipeline($builder) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(4, $user2->getHits()); + } + + public function testUpdateManyWithDriverPipeline(): void + { + $pipeline = new Pipeline( + Stage::set( + hits: Expression::multiply(Expression::fieldPath('hits'), 2), + ), + ); + + $this->dm->createQueryBuilder(User::class) + ->updateMany() + ->pipeline($pipeline) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(4, $user2->getHits()); + } + + public function testUpdateManyWithPipelineArray(): void + { + $this->dm->createQueryBuilder(User::class) + ->updateMany() + ->pipeline([['$set' => ['hits' => ['$multiply' => ['$hits', 2]]]]]) + ->getQuery() + ->execute(); + + $this->dm->clear(); + + $user1 = $this->dm->getRepository(User::class)->find($this->user1->getId()); + self::assertSame(2, $user1->getHits()); + + $user2 = $this->dm->getRepository(User::class)->find($this->user2->getId()); + self::assertSame(4, $user2->getHits()); + } + + public function testFindOneAndUpdateWithAggregationBuilder(): void + { + $builder = $this->dm->createAggregationBuilder(User::class); + $builder + ->set() + ->field('hits') + ->add('$hits', 1); + + $user = $this->dm->createQueryBuilder(User::class) + ->findAndUpdate() + ->returnNew() + ->field('username')->equals('foo') + ->pipeline($builder) + ->getQuery() + ->execute(); + + self::assertInstanceOf(User::class, $user); + self::assertSame(2, $user->getHits()); + } + + public function testFindOneAndUpdateWithDriverPipeline(): void + { + $this->markTestSkipped('Collection::findAndUpdate does not support pipeline updates (PHPLIB-1699)'); + + $pipeline = new Pipeline( + Stage::set( + hits: Expression::add(Expression::fieldPath('hits'), 1), + ), + ); + + $user = $this->dm->createQueryBuilder(User::class) + ->findAndUpdate() + ->returnNew() + ->field('username')->equals('foo') + ->pipeline($pipeline) + ->getQuery() + ->execute(); + + self::assertInstanceOf(User::class, $user); + self::assertSame(2, $user->getHits()); + } + + public function testFindOneAndUpdateWithPipelineArray(): void + { + $user = $this->dm->createQueryBuilder(User::class) + ->findAndUpdate() + ->returnNew() + ->field('username')->equals('foo') + ->pipeline([['$set' => ['hits' => ['$sum' => ['$hits', 1]]]]]) + ->getQuery() + ->execute(); + + self::assertInstanceOf(User::class, $user); + self::assertSame(2, $user->getHits()); + } +} From 2287b063815f1d4e2b0f3e5cee1cff98859b2235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 Oct 2025 08:18:57 +0100 Subject: [PATCH 03/15] Enable symfony/var-exporter 8.0, without lazy ghost object support (#2882) --- composer.json | 2 +- src/Configuration.php | 12 ++++++++++-- tests/Tests/ConfigurationTest.php | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5e5e849fde..2a55ca33f5 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2.2 || ^3.0", "symfony/var-dumper": "^5.4 || ^6.4 || ^7.0 || ^8.0", - "symfony/var-exporter": "^6.4 || ^7.0" + "symfony/var-exporter": "^6.4 || ^7.0 || ^8.0" }, "require-dev": { "ext-bcmath": "*", diff --git a/src/Configuration.php b/src/Configuration.php index 15b3703b6b..fa1e7de5a0 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -36,6 +36,7 @@ use Psr\Cache\CacheItemPoolInterface; use ReflectionClass; use stdClass; +use Symfony\Component\VarExporter\LazyGhostTrait; use Throwable; use function array_diff_key; @@ -44,6 +45,7 @@ use function class_exists; use function interface_exists; use function is_string; +use function trait_exists; use function trigger_deprecation; use function trim; @@ -695,12 +697,18 @@ public function setUseLazyGhostObject(bool $flag): void throw new LogicException('Cannot enable or disable LazyGhostObject when native lazy objects are enabled.'); } - if ($flag === false) { + if ($flag && ! trait_exists(LazyGhostTrait::class)) { + throw new LogicException('Package "symfony/var-exporter" >= 8.0 does not provide lazy ghost objects, use native lazy objects instead.'); + } + + if (! $flag) { if (! class_exists(ProxyManagerConfiguration::class)) { throw new LogicException('Package "friendsofphp/proxy-manager-lts" is required to disable LazyGhostObject.'); } - trigger_deprecation('doctrine/mongodb-odm', '2.10', 'Using "friendsofphp/proxy-manager-lts" is deprecated. Use "symfony/var-exporter" LazyGhostObjects instead.'); + if (PHP_VERSION_ID < 80400) { + trigger_deprecation('doctrine/mongodb-odm', '2.10', 'Using "friendsofphp/proxy-manager-lts" is deprecated. Use "symfony/var-exporter" LazyGhostObjects instead.'); + } } $this->lazyGhostObject = $flag; diff --git a/tests/Tests/ConfigurationTest.php b/tests/Tests/ConfigurationTest.php index c07d62c8f9..e9b0bc8a84 100644 --- a/tests/Tests/ConfigurationTest.php +++ b/tests/Tests/ConfigurationTest.php @@ -4,6 +4,7 @@ namespace Doctrine\ODM\MongoDB\Tests; +use Composer\InstalledVersions; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\ConfigurationException; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionFactory; @@ -17,6 +18,7 @@ use function base64_encode; use function str_repeat; +use function version_compare; class ConfigurationTest extends TestCase { @@ -33,6 +35,10 @@ public function testUseNativeLazyObjectBeforePHP84(): void public function testUseLazyGhostObject(): void { + if (! version_compare(InstalledVersions::getVersion('symfony/var-exporter'), '8', '<')) { + $this->markTestSkipped('Symfony VarExporter 8 or higher is not installed.'); + } + $c = new Configuration(); self::assertFalse($c->isLazyGhostObjectEnabled()); @@ -42,6 +48,21 @@ public function testUseLazyGhostObject(): void self::assertFalse($c->isLazyGhostObjectEnabled()); } + #[RequiresPhp('>= 8.4')] + public function testUseLazyGhostObjectWithSymfony8(): void + { + if (version_compare(InstalledVersions::getVersion('symfony/var-exporter'), '8', '<')) { + $this->markTestSkipped('Symfony VarExporter 8 or higher is not installed.'); + } + + $c = new Configuration(); + + self::expectException(LogicException::class); + self::expectExceptionMessage('Package "symfony/var-exporter" >= 8.0 does not provide lazy ghost objects, use native lazy objects instead.'); + + $c->setUseLazyGhostObject(true); + } + public function testNativeLazyObjectDeprecatedByDefault(): void { $c = new Configuration(); From ed1ea87fed4e351565ac0fcd067f8181e482aaad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 05:44:28 +0100 Subject: [PATCH 04/15] Bump doctrine/.github from 12.1.0 to 12.2.0 (#2887) Bumps [doctrine/.github](https://github.com/doctrine/.github) from 12.1.0 to 12.2.0. - [Release notes](https://github.com/doctrine/.github/releases) - [Commits](https://github.com/doctrine/.github/compare/12.1.0...v12.2.0) --- updated-dependencies: - dependency-name: doctrine/.github dependency-version: 12.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/release-on-milestone-closed.yml | 2 +- .github/workflows/website-schema.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 987911bd87..fa5904b2aa 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@12.1.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@v12.2.0" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 374fde8ed0..34d79603f7 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,4 +17,4 @@ on: jobs: documentation: name: "Generate documentation" - uses: "doctrine/.github/.github/workflows/documentation.yml@12.1.0" + uses: "doctrine/.github/.github/workflows/documentation.yml@v12.2.0" diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index f89a2bb26c..d24f421e0f 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -8,7 +8,7 @@ on: jobs: release: name: "Git tag, release & create merge-up PR" - uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.1.0" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@v12.2.0" with: use-next-minor-as-default-branch: true secrets: diff --git a/.github/workflows/website-schema.yml b/.github/workflows/website-schema.yml index f56b7241a2..80312010a8 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@12.1.0" + uses: "doctrine/.github/.github/workflows/website-schema.yml@v12.2.0" From 2d65c008790407cafaf85439cfe13f3d4cdbd53f Mon Sep 17 00:00:00 2001 From: Benjamin Courtel Date: Wed, 5 Nov 2025 04:05:07 +0100 Subject: [PATCH 05/15] Fix missing PHP syntax highlighting on "Migrating Schemas" (#2899) --- docs/en/reference/migrating-schemas.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/reference/migrating-schemas.rst b/docs/en/reference/migrating-schemas.rst index 0b979572b0..e59c343278 100644 --- a/docs/en/reference/migrating-schemas.rst +++ b/docs/en/reference/migrating-schemas.rst @@ -41,6 +41,8 @@ To create the collections for all the document classes, you can use the For a specific document class, you can use the `createDocumentCollection()` method with the class name as an argument: +.. code-block:: php + createDocumentCollection(Person::class); @@ -48,6 +50,8 @@ method with the class name as an argument: Once the collection is created, you can also set up indexes with ``ensureIndexes``, and search indexes with ``createSearchIndexes``: +.. code-block:: php + ensureIndexes(); From 134756f5df3dd517ba5b2a05f37165de4d305012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 6 Nov 2025 07:06:17 -0500 Subject: [PATCH 06/15] Add attributes for every test classes with annotations (#2893) --- tests/Documents/FileWithoutMetadata.php | 1 + tests/Tests/Mapping/AbstractMappingDriverTestCase.php | 4 ++-- .../Mapping/Documents/GlobalNamespaceDocument.php | 10 ++++++++++ tests/Tests/Tools/GH1299/BaseUser.php | 3 +++ tests/Tests/Tools/GH1299/GH1299User.php | 2 ++ tests/Tests/Tools/GH297/Address.php | 2 ++ tests/Tests/Tools/GH297/AddressTrait.php | 3 +++ tests/Tests/Tools/GH297/Admin.php | 1 + tests/Tests/Tools/GH297/User.php | 3 +++ 9 files changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/Documents/FileWithoutMetadata.php b/tests/Documents/FileWithoutMetadata.php index be82621488..806276b2bf 100644 --- a/tests/Documents/FileWithoutMetadata.php +++ b/tests/Documents/FileWithoutMetadata.php @@ -18,6 +18,7 @@ class FileWithoutMetadata * * @var string|null */ + #[ODM\File\Filename] private $filename; public function getId(): ?string diff --git a/tests/Tests/Mapping/AbstractMappingDriverTestCase.php b/tests/Tests/Mapping/AbstractMappingDriverTestCase.php index 2b60e8d1f9..5d790898f9 100644 --- a/tests/Tests/Mapping/AbstractMappingDriverTestCase.php +++ b/tests/Tests/Mapping/AbstractMappingDriverTestCase.php @@ -1254,7 +1254,7 @@ class AbstractMappingDriverDuplicateDatabaseNameNotSaved extends AbstractMapping * * @var string|null */ - #[ODM\Field(type: 'int', name: 'baz')] + #[ODM\Field(type: 'string', name: 'baz')] public $foo; /** @@ -1262,7 +1262,7 @@ class AbstractMappingDriverDuplicateDatabaseNameNotSaved extends AbstractMapping * * @var string|null */ - #[ODM\Field(type: 'int', name: 'baz', notSaved: true)] + #[ODM\Field(type: 'string', name: 'baz', notSaved: true)] public $bar; } diff --git a/tests/Tests/Mapping/Documents/GlobalNamespaceDocument.php b/tests/Tests/Mapping/Documents/GlobalNamespaceDocument.php index 5e28402940..ef5a740d26 100644 --- a/tests/Tests/Mapping/Documents/GlobalNamespaceDocument.php +++ b/tests/Tests/Mapping/Documents/GlobalNamespaceDocument.php @@ -6,6 +6,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ +#[ODM\Document] class DoctrineGlobal_Article { /** @@ -13,6 +14,7 @@ class DoctrineGlobal_Article * * @var string|null */ + #[ODM\Id] protected $id; /** @@ -20,6 +22,7 @@ class DoctrineGlobal_Article * * @var string|null */ + #[ODM\Field(type: 'string')] protected $headline; /** @@ -27,6 +30,7 @@ class DoctrineGlobal_Article * * @var string|null */ + #[ODM\Field(type: 'string')] protected $text; /** @@ -34,6 +38,7 @@ class DoctrineGlobal_Article * * @var DoctrineGlobal_User|null */ + #[ODM\ReferenceMany(targetDocument: DoctrineGlobal_User::class)] protected $author; /** @@ -41,10 +46,12 @@ class DoctrineGlobal_Article * * @var Collection */ + #[ODM\ReferenceMany(targetDocument: DoctrineGlobal_User::class)] protected $editor; } /** @ODM\Document */ +#[ODM\Document] class DoctrineGlobal_User { /** @@ -52,6 +59,7 @@ class DoctrineGlobal_User * * @var string|null */ + #[ODM\Id] private $id; /** @@ -59,6 +67,7 @@ class DoctrineGlobal_User * * @var string */ + #[ODM\Field(type: 'string')] private $username; /** @@ -66,5 +75,6 @@ class DoctrineGlobal_User * * @var string */ + #[ODM\Field(type: 'string')] private $email; } diff --git a/tests/Tests/Tools/GH1299/BaseUser.php b/tests/Tests/Tools/GH1299/BaseUser.php index 9b1473d0cb..277c84c176 100644 --- a/tests/Tests/Tools/GH1299/BaseUser.php +++ b/tests/Tests/Tools/GH1299/BaseUser.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ +#[ODM\Document] class BaseUser { /** @@ -14,6 +15,7 @@ class BaseUser * * @var string|null */ + #[ODM\Id] protected $id; /** @@ -21,6 +23,7 @@ class BaseUser * * @var string|null */ + #[ODM\Field(type: 'string')] protected $name; public function getId(): ?string diff --git a/tests/Tests/Tools/GH1299/GH1299User.php b/tests/Tests/Tools/GH1299/GH1299User.php index 4d205b1612..64ed574e58 100644 --- a/tests/Tests/Tools/GH1299/GH1299User.php +++ b/tests/Tests/Tools/GH1299/GH1299User.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ +#[ODM\Document] class GH1299User extends BaseUser { /** @@ -14,5 +15,6 @@ class GH1299User extends BaseUser * * @var string|null */ + #[ODM\Field(type: 'string')] protected $lastname; } diff --git a/tests/Tests/Tools/GH297/Address.php b/tests/Tests/Tools/GH297/Address.php index 6b921ea0a6..5a0c11f266 100644 --- a/tests/Tests/Tools/GH297/Address.php +++ b/tests/Tests/Tools/GH297/Address.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\EmbeddedDocument */ +#[ODM\EmbeddedDocument] class Address { /** @@ -14,6 +15,7 @@ class Address * * @var string|null */ + #[ODM\Field(type: 'string')] private $street; public function getStreet(): ?string diff --git a/tests/Tests/Tools/GH297/AddressTrait.php b/tests/Tests/Tools/GH297/AddressTrait.php index 9c0ba02168..eea24926f8 100644 --- a/tests/Tests/Tools/GH297/AddressTrait.php +++ b/tests/Tests/Tools/GH297/AddressTrait.php @@ -4,6 +4,8 @@ namespace Doctrine\ODM\MongoDB\Tests\Tools\GH297; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + trait AddressTrait { /** @@ -11,6 +13,7 @@ trait AddressTrait * * @var Address|null */ + #[ODM\EmbedOne] private $address; public function getAddress(): ?Address diff --git a/tests/Tests/Tools/GH297/Admin.php b/tests/Tests/Tools/GH297/Admin.php index fedea6f7ac..70011f5fa3 100644 --- a/tests/Tests/Tools/GH297/Admin.php +++ b/tests/Tests/Tools/GH297/Admin.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ +#[ODM\Document] class Admin extends User { } diff --git a/tests/Tests/Tools/GH297/User.php b/tests/Tests/Tools/GH297/User.php index d0f19462cd..9fd9bca8bf 100644 --- a/tests/Tests/Tools/GH297/User.php +++ b/tests/Tests/Tools/GH297/User.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @ODM\Document */ +#[ODM\Document] class User { use AddressTrait; @@ -16,6 +17,7 @@ class User * * @var string|null */ + #[ODM\Id] private $id; /** @@ -23,6 +25,7 @@ class User * * @var string|null */ + #[ODM\Field(type: 'string')] private $name; public function getId(): ?string From 55d2cfdb312aefe3ac447fe133de98ae4fd83850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 Oct 2025 10:28:55 +0100 Subject: [PATCH 07/15] Ensure proxy-manager is not used when native lazy are enabled --- src/DocumentManager.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/DocumentManager.php b/src/DocumentManager.php index 7ed9e3e73b..f73be994d8 100644 --- a/src/DocumentManager.php +++ b/src/DocumentManager.php @@ -154,9 +154,23 @@ protected function __construct(?Client $client = null, ?Configuration $config = $this->config->getDriverOptions(), ); - $this->classNameResolver = $this->config->isLazyGhostObjectEnabled() - ? new CachingClassNameResolver(new LazyGhostProxyClassNameResolver()) - : new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); + if ($this->config->isNativeLazyObjectEnabled()) { + $this->classNameResolver = new class implements ClassNameResolver, ProxyClassNameResolver { + public function getRealClass(string $class): string + { + return $class; + } + + public function resolveClassName(string $className): string + { + return $className; + } + }; + } elseif ($this->config->isLazyGhostObjectEnabled()) { + $this->classNameResolver = new CachingClassNameResolver(new LazyGhostProxyClassNameResolver()); + } else { + $this->classNameResolver = new CachingClassNameResolver(new ProxyManagerClassNameResolver($this->config)); + } $metadataFactoryClassName = $this->config->getClassMetadataFactoryName(); $this->metadataFactory = new $metadataFactoryClassName(); From 5b7f4346b25d75d9acc71c5ba7be385bfddb3ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 Oct 2025 11:16:01 +0100 Subject: [PATCH 08/15] Add a test without optional dependencies --- .github/workflows/continuous-integration.yml | 16 ++++++++++++++++ tests/Tests/BaseTestCase.php | 9 +++++++-- tests/Tests/ConfigurationTest.php | 8 ++++++++ tests/Tests/Mapping/AnnotationDriverTest.php | 3 +++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c837bf2cb7..c996e5dec2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -92,6 +92,15 @@ jobs: dependencies: "highest" symfony-version: "stable" proxy: "lazy-ghost" + # Test removing optional dependencies + - topology: "server" + php-version: "8.4" + mongodb-version: "8.0" + driver-version: "stable" + dependencies: "highest" + symfony-version: "stable" + proxy: "native" + remove-optional-dependencies: true # Test with a sharded cluster # Currently disabled due to a bug where MongoDB reports "sharding status unknown" # - topology: "sharded_cluster" @@ -148,6 +157,13 @@ jobs: composer require --no-update symfony/var-dumper:^${{ matrix.symfony-version }} composer require --no-update --dev symfony/cache:^${{ matrix.symfony-version }} + - name: "Remove optional dependencies" + if: "${{ matrix.remove-optional-dependencies }}" + run: | + composer remove --no-update friendsofphp/proxy-manager-lts symfony/var-exporter + composer remove --no-update --dev symfony/cache doctrine/orm doctrine/annotations + composer remove --no-update --dev doctrine/coding-standard phpstan/phpstan phpstan/phpstan-deprecation-rule phpstan/phpstan-phpunit + - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v3" with: diff --git a/tests/Tests/BaseTestCase.php b/tests/Tests/BaseTestCase.php index 43fec12308..b6a7fafc21 100644 --- a/tests/Tests/BaseTestCase.php +++ b/tests/Tests/BaseTestCase.php @@ -105,8 +105,13 @@ protected static function getConfiguration(): Configuration $config->setPersistentCollectionNamespace('PersistentCollections'); $config->setDefaultDB(DOCTRINE_MONGODB_DATABASE); $config->setMetadataDriverImpl(static::createMetadataDriverImpl()); - $config->setUseLazyGhostObject((bool) $_ENV['USE_LAZY_GHOST_OBJECT']); - $config->setUseNativeLazyObject((bool) $_ENV['USE_NATIVE_LAZY_OBJECT']); + if ($_ENV['USE_LAZY_GHOST_OBJECT']) { + $config->setUseLazyGhostObject(true); + } + + if ($_ENV['USE_NATIVE_LAZY_OBJECT']) { + $config->setUseNativeLazyObject(true); + } if ($config->isNativeLazyObjectEnabled()) { NativeLazyObjectFactory::enableTracking(); diff --git a/tests/Tests/ConfigurationTest.php b/tests/Tests/ConfigurationTest.php index c07d62c8f9..c39a738a3b 100644 --- a/tests/Tests/ConfigurationTest.php +++ b/tests/Tests/ConfigurationTest.php @@ -13,9 +13,11 @@ use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; +use ProxyManager\Configuration as ProxyManagerConfiguration; use stdClass; use function base64_encode; +use function class_exists; use function str_repeat; class ConfigurationTest extends TestCase @@ -38,6 +40,12 @@ public function testUseLazyGhostObject(): void self::assertFalse($c->isLazyGhostObjectEnabled()); $c->setUseLazyGhostObject(true); self::assertTrue($c->isLazyGhostObjectEnabled()); + + if (! class_exists(ProxyManagerConfiguration::class)) { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Package "friendsofphp/proxy-manager-lts" is required to disable LazyGhostObject.'); + } + $c->setUseLazyGhostObject(false); self::assertFalse($c->isLazyGhostObjectEnabled()); } diff --git a/tests/Tests/Mapping/AnnotationDriverTest.php b/tests/Tests/Mapping/AnnotationDriverTest.php index 55064a4ffa..3bad2754af 100644 --- a/tests/Tests/Mapping/AnnotationDriverTest.php +++ b/tests/Tests/Mapping/AnnotationDriverTest.php @@ -4,12 +4,14 @@ namespace Doctrine\ODM\MongoDB\Tests\Mapping; +use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; use Doctrine\Persistence\Mapping\Driver\FileClassLocator; use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use PHPUnit\Framework\Attributes\RequiresMethod; use function call_user_func; use function class_exists; @@ -19,6 +21,7 @@ use const E_USER_DEPRECATED; +#[RequiresMethod(AnnotationReader::class, '__construct')] class AnnotationDriverTest extends AbstractAnnotationDriverTestCase { protected static function loadDriver(array $paths = []): MappingDriver From 211488a492f5092833b8e5867f9ceb67072f17fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 Oct 2025 13:33:01 +0100 Subject: [PATCH 09/15] Don't change lazyGhostObject config when setting nativeLazyObject --- src/Configuration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index 15b3703b6b..2af5df46e8 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -708,7 +708,8 @@ public function setUseLazyGhostObject(bool $flag): void public function isLazyGhostObjectEnabled(): bool { - return $this->lazyGhostObject; + // Always false if native lazy objects are enabled + return $this->lazyGhostObject && ! $this->nativeLazyObject; } public function setUseNativeLazyObject(bool $nativeLazyObject): void @@ -718,7 +719,6 @@ public function setUseNativeLazyObject(bool $nativeLazyObject): void } $this->nativeLazyObject = $nativeLazyObject; - $this->lazyGhostObject = ! $nativeLazyObject || $this->lazyGhostObject; } public function isNativeLazyObjectEnabled(): bool From a311fe12c54dd49ec75e76e2a8f38443a106f785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 Oct 2025 13:33:16 +0100 Subject: [PATCH 10/15] Don't use deprecated ClassMetadata::getPropertyAccessor() --- src/Proxy/Factory/StaticProxyFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Proxy/Factory/StaticProxyFactory.php b/src/Proxy/Factory/StaticProxyFactory.php index b9df9191dd..fdf7b47963 100644 --- a/src/Proxy/Factory/StaticProxyFactory.php +++ b/src/Proxy/Factory/StaticProxyFactory.php @@ -146,7 +146,7 @@ private function skippedFieldsFqns(ClassMetadata $metadata): array $skippedFieldsFqns = []; foreach ($metadata->getIdentifierFieldNames() as $idField) { - $skippedFieldsFqns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField)); + $skippedFieldsFqns[] = $this->propertyFqcn($metadata->getPropertyAccessor($idField)->getUnderlyingReflector()); } foreach ($metadata->getReflectionClass()->getProperties() as $property) { From 673e6dc388e290b1b75b609e4adc4bbe2549613e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 Oct 2025 21:57:33 +0100 Subject: [PATCH 11/15] Fix initialisation of ProxyManager objects when hydrated from a collection Fix test NestedCollectionsTest with ProxyManager Add a test for read-only properties, not supported by ProxyManager https://github.com/FriendsOfPHP/proxy-manager-lts/issues/26 --- src/Hydrator/HydratorFactory.php | 17 ++----- .../ObjectCastPropertyAccessor.php | 2 +- tests/Documents/Tag.php | 2 +- .../Functional/ReadOnlyPropertiesTest.php | 51 +++++++++++++++++++ .../Mapping/LegacyReflectionFieldsTest.php | 27 ++++++++-- 5 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 tests/Tests/Functional/ReadOnlyPropertiesTest.php diff --git a/src/Hydrator/HydratorFactory.php b/src/Hydrator/HydratorFactory.php index 5a821677fb..37a8644cee 100644 --- a/src/Hydrator/HydratorFactory.php +++ b/src/Hydrator/HydratorFactory.php @@ -451,29 +451,18 @@ public function hydrate(object $document, array $data, array $hints = []): array } } + // Skip initialization to not load any object data if (PHP_VERSION_ID >= 80400) { $metadata->reflClass->markLazyObjectAsInitialized($document); } if ($document instanceof InternalProxy) { - // Skip initialization to not load any object data $document->__setInitialized(true); } // Support for legacy proxy-manager-lts - if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) { - // Inject an empty initialiser to not load any object data - $document->setProxyInitializer(static function ( - GhostObjectInterface $ghostObject, - string $method, // we don't care - array $parameters, // we don't care - &$initializer, - array $properties, // we currently do not use this - ): bool { - $initializer = null; - - return true; - }); + if ($document instanceof GhostObjectInterface) { + $document->setProxyInitializer(null); } $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints); diff --git a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php index dced04aed5..34ac25cd26 100644 --- a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php @@ -52,7 +52,7 @@ public function setValue(object $object, mixed $value): void $object->__setInitialized(false); } elseif ($object instanceof GhostObjectInterface && ! $object->isProxyInitialized()) { $initializer = $object->getProxyInitializer(); - $object->setProxyInitializer(); + $object->setProxyInitializer(null); $this->reflectionProperty->setValue($object, $value); $object->setProxyInitializer($initializer); } else { diff --git a/tests/Documents/Tag.php b/tests/Documents/Tag.php index 5461d1d7ea..70471bb678 100644 --- a/tests/Documents/Tag.php +++ b/tests/Documents/Tag.php @@ -14,7 +14,7 @@ class Tag public ?string $id; #[ODM\Field] - public readonly string $name; + public string $name; /** @var Collection */ #[ODM\ReferenceMany(targetDocument: BlogPost::class, mappedBy: 'tags')] diff --git a/tests/Tests/Functional/ReadOnlyPropertiesTest.php b/tests/Tests/Functional/ReadOnlyPropertiesTest.php new file mode 100644 index 0000000000..ff0dfd28d8 --- /dev/null +++ b/tests/Tests/Functional/ReadOnlyPropertiesTest.php @@ -0,0 +1,51 @@ +dm->getConfiguration(); + if (! $configuration->isNativeLazyObjectEnabled() && ! $configuration->isLazyGhostObjectEnabled()) { + $this->markTestSkipped('Read-only properties are not supported by the legacy Proxy Manager. https://github.com/FriendsOfPHP/proxy-manager-lts/issues/26'); + } + + $document = new ReadOnlyProperties('Test Name'); + $document->onlyRead = new ReadOnlyProperties('Nested Name'); + $this->dm->persist($document); + $this->dm->persist($document->onlyRead); + $this->dm->flush(); + $this->dm->clear(); + + $document = $this->dm->getRepository(ReadOnlyProperties::class)->find($document->id); + $this->assertEquals('Test Name', $document->name); + $this->assertEquals('Nested Name', $document->onlyRead->name); + } +} + +#[Document] +class ReadOnlyProperties +{ + #[Id] + public readonly string $id; // @phpstan-ignore property.uninitializedReadonly (initialized by reflection) + + #[Field] + public readonly string $name; + + #[ReferenceOne(targetDocument: self::class)] + public ?self $onlyRead; + + public function __construct(string $name) + { + $this->name = $name; + } +} diff --git a/tests/Tests/Mapping/LegacyReflectionFieldsTest.php b/tests/Tests/Mapping/LegacyReflectionFieldsTest.php index 860f953130..9d8c201f12 100644 --- a/tests/Tests/Mapping/LegacyReflectionFieldsTest.php +++ b/tests/Tests/Mapping/LegacyReflectionFieldsTest.php @@ -4,14 +4,18 @@ namespace Doctrine\ODM\MongoDB\Tests\Mapping; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Field; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Id; use Doctrine\ODM\MongoDB\Mapping\LegacyReflectionFields; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Address; -use Documents\Tag; use Documents\User; use LogicException; use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use function sprintf; + #[IgnoreDeprecations] class LegacyReflectionFieldsTest extends BaseTestCase { @@ -56,20 +60,33 @@ public function testGetSet(): void public function testGetSetReadonly(): void { - $class = $this->dm->getClassMetadata(Tag::class); + $class = $this->dm->getClassMetadata(ReadOnlyProperty::class); self::assertInstanceOf(LegacyReflectionFields::class, $class->reflFields); - $tag = new Tag('Important'); + $tag = new ReadOnlyProperty('Important'); $this->dm->persist($tag); $this->dm->flush(); - $tag = $this->dm->find(Tag::class, $tag->id); + $tag = $this->dm->find(ReadOnlyProperty::class, $tag->id); // Accessing the readonly property through reflection self::assertEquals('Important', $class->getReflectionProperty('name')->getValue($tag)); self::expectException(LogicException::class); - self::expectExceptionMessage('Attempting to change readonly property Documents\Tag::$name'); + self::expectExceptionMessage(sprintf('Attempting to change readonly property %s::$name', ReadOnlyProperty::class)); $class->getReflectionProperty('name')->setValue($tag, 'Very Important'); } } + +#[Document] +class ReadOnlyProperty +{ + #[Id] + public string $id; + + public function __construct( + #[Field] + public readonly string $name, + ) { + } +} From 4655b357d2f996bbf5a48212435213dcb844dd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 10 Nov 2025 15:12:45 +0100 Subject: [PATCH 12/15] Deprecate `ClassMetadataFactoryInterface::getProxyClassNameResolver()` (#2883) --- src/DocumentManager.php | 10 ++++++++-- src/DocumentNotFoundException.php | 2 +- src/Events.php | 2 +- src/Mapping/ClassMetadataFactoryInterface.php | 2 ++ tests/Tests/DocumentManagerTest.php | 9 +++++++++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/DocumentManager.php b/src/DocumentManager.php index f73be994d8..2dfb678741 100644 --- a/src/DocumentManager.php +++ b/src/DocumentManager.php @@ -176,7 +176,9 @@ public function resolveClassName(string $className): string $this->metadataFactory = new $metadataFactoryClassName(); $this->metadataFactory->setDocumentManager($this); $this->metadataFactory->setConfiguration($this->config); - $this->metadataFactory->setProxyClassNameResolver($this->classNameResolver); + if (! $this->config->isNativeLazyObjectEnabled()) { + $this->metadataFactory->setProxyClassNameResolver($this->classNameResolver); + } $cacheDriver = $this->config->getMetadataCache(); if ($cacheDriver) { @@ -310,10 +312,14 @@ public function getSchemaManager(): SchemaManager /** * Returns the class name resolver which is used to resolve real class names for proxy objects. * - * @deprecated Fetch metadata for any class string (e.g. proxy object class) and read the class name from the metadata object + * @deprecated Since 2.15, the use of proxy classes is deprecated and will be removed in Doctrine ODM 3.0. */ public function getClassNameResolver(): ClassNameResolver { + if ($this->getConfiguration()->isNativeLazyObjectEnabled()) { + trigger_deprecation('doctrine/mongodb-odm', '2.15', 'The %s() method is deprecated and will be removed in Doctrine ODM 3.0. There are no proxy classes when using native lazy objects', __METHOD__); + } + return $this->classNameResolver; } diff --git a/src/DocumentNotFoundException.php b/src/DocumentNotFoundException.php index 310cac6de9..a32c9965f6 100644 --- a/src/DocumentNotFoundException.php +++ b/src/DocumentNotFoundException.php @@ -12,7 +12,7 @@ use const JSON_THROW_ON_ERROR; /** - * Class for exception when encountering proxy object that has + * Class for exception when encountering a lazy object that has * an identifier that does not exist in the database. */ final class DocumentNotFoundException extends MongoDBException diff --git a/src/Events.php b/src/Events.php index bbeb6e4d84..b19c7f3afa 100644 --- a/src/Events.php +++ b/src/Events.php @@ -130,7 +130,7 @@ private function __construct() public const onClear = 'onClear'; /** - * The documentNotFound event occurs if a proxy object could not be found in + * The documentNotFound event occurs if a lazy object could not be found in * the database. */ public const documentNotFound = 'documentNotFound'; diff --git a/src/Mapping/ClassMetadataFactoryInterface.php b/src/Mapping/ClassMetadataFactoryInterface.php index 3c543e3cc0..bacd47ec3d 100644 --- a/src/Mapping/ClassMetadataFactoryInterface.php +++ b/src/Mapping/ClassMetadataFactoryInterface.php @@ -35,6 +35,8 @@ public function setDocumentManager(DocumentManager $dm): void; /** * Sets a resolver for real class names of a proxy. + * + * @deprecated This method is deprecated and will be removed in Doctrine ODM 3.0. */ public function setProxyClassNameResolver(ProxyClassNameResolver $resolver): void; } diff --git a/tests/Tests/DocumentManagerTest.php b/tests/Tests/DocumentManagerTest.php index 832037e53c..a23f5612d8 100644 --- a/tests/Tests/DocumentManagerTest.php +++ b/tests/Tests/DocumentManagerTest.php @@ -14,6 +14,7 @@ use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\Proxy\Factory\ProxyFactory; +use Doctrine\ODM\MongoDB\Proxy\Resolver\ClassNameResolver; use Doctrine\ODM\MongoDB\Query\Builder as QueryBuilder; use Doctrine\ODM\MongoDB\Query\FilterCollection; use Doctrine\ODM\MongoDB\SchemaManager; @@ -34,6 +35,7 @@ use MongoDB\BSON\ObjectId; use MongoDB\Client; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use RuntimeException; use stdClass; @@ -261,6 +263,13 @@ public function testGetClassNameForAssociationReturnsTargetDocumentWithNullData( $mapping = ClassMetadataTestUtil::getFieldMapping(['targetDocument' => User::class]); self::assertEquals(User::class, $this->dm->getClassNameForAssociation($mapping, null)); } + + #[IgnoreDeprecations] + public function testGetClassNameResolver(): void + { + $resolver = $this->dm->getClassNameResolver(); + self::assertInstanceOf(ClassNameResolver::class, $resolver); + } } #[ODM\Document] From 92a9f3be9a4928db98f3addb455cec22617baca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 10 Nov 2025 22:25:58 +0100 Subject: [PATCH 13/15] Fix documentation sidebar (#2901) The links need to be added explicitly, the /* notation does not work anymore. I think the issue was introduced by #2849 https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/cookbook/queryable-encryption.html --- docs/en/sidebar.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 4080829210..5f65189916 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -5,7 +5,7 @@ :depth: 3 :glob: - tutorials/* + tutorials/getting-started .. toctree:: :caption: Reference @@ -53,4 +53,14 @@ :depth: 3 :glob: - cookbook/* + cookbook/blending-orm-and-mongodb-odm + cookbook/implementing-array-access-for-domain-objects + cookbook/implementing-the-notify-changetracking-policy + cookbook/lookup-reference + cookbook/mapping-classes-to-orm-and-odm + cookbook/queryable-encryption + cookbook/resolve-target-document-listener + cookbook/simple-search-engine + cookbook/time-series-data + cookbook/validation-of-documents + cookbook/vector-search From 48a7de799bab4a814b4067f2b7b1ffb6d82c526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 12 Nov 2025 10:36:47 +0100 Subject: [PATCH 14/15] Upgrade to PHPUnit 11 for PHP >= 8.2 and check deprecation messages (#2902) * Upgrade to PHPUnit 11 for PHP >= 8.2 and check deprecation messages * Remove usage of deprecated Assert::assertContainsOnly() --- composer.json | 2 +- phpunit.xml.dist | 2 +- src/Proxy/Factory/LazyGhostProxyFactory.php | 4 +--- tests/Tests/BaseTestCase.php | 5 ++--- tests/Tests/ConfigurationTest.php | 3 +++ tests/Tests/Functional/CustomTypeTest.php | 2 +- tests/Tests/Functional/ShardKeyTest.php | 4 +++- tests/Tests/Functional/TargetDocumentTest.php | 3 ++- tests/Tests/Query/FilterCollectionTest.php | 2 +- tests/Tests/QueryTest.php | 2 +- 10 files changed, 16 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 2a55ca33f5..159d0c0883 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58", + "phpunit/phpunit": "^10.5.58|^11.5.43", "squizlabs/php_codesniffer": "^4", "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4451e15bc1..e3bc2085ba 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ 4)) { - throw new InvalidArgumentException(sprintf('Invalid auto generate mode "%s" given.', is_scalar($autoGenerate) ? (string) $autoGenerate : get_debug_type($autoGenerate))); + throw new InvalidArgumentException(sprintf('Invalid auto generate mode "%d" given.', $autoGenerate)); } $this->uow = $dm->getUnitOfWork(); diff --git a/tests/Tests/BaseTestCase.php b/tests/Tests/BaseTestCase.php index b6a7fafc21..835c6c0e9e 100644 --- a/tests/Tests/BaseTestCase.php +++ b/tests/Tests/BaseTestCase.php @@ -105,12 +105,11 @@ protected static function getConfiguration(): Configuration $config->setPersistentCollectionNamespace('PersistentCollections'); $config->setDefaultDB(DOCTRINE_MONGODB_DATABASE); $config->setMetadataDriverImpl(static::createMetadataDriverImpl()); - if ($_ENV['USE_LAZY_GHOST_OBJECT']) { - $config->setUseLazyGhostObject(true); - } if ($_ENV['USE_NATIVE_LAZY_OBJECT']) { $config->setUseNativeLazyObject(true); + } elseif ($_ENV['USE_LAZY_GHOST_OBJECT']) { + $config->setUseLazyGhostObject(true); } if ($config->isNativeLazyObjectEnabled()) { diff --git a/tests/Tests/ConfigurationTest.php b/tests/Tests/ConfigurationTest.php index a43ff4c1c2..87c6757411 100644 --- a/tests/Tests/ConfigurationTest.php +++ b/tests/Tests/ConfigurationTest.php @@ -11,6 +11,7 @@ use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionGenerator; use LogicException; use MongoDB\Driver\Manager; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; @@ -37,6 +38,7 @@ public function testUseNativeLazyObjectBeforePHP84(): void $c->setUseNativeLazyObject(true); } + #[IgnoreDeprecations] public function testUseLazyGhostObject(): void { $c = new Configuration(); @@ -74,6 +76,7 @@ public function testUseLazyGhostObjectWithSymfony8(): void $c->setUseLazyGhostObject(true); } + #[IgnoreDeprecations] public function testNativeLazyObjectDeprecatedByDefault(): void { $c = new Configuration(); diff --git a/tests/Tests/Functional/CustomTypeTest.php b/tests/Tests/Functional/CustomTypeTest.php index 77fa019261..b09ea5728e 100644 --- a/tests/Tests/Functional/CustomTypeTest.php +++ b/tests/Tests/Functional/CustomTypeTest.php @@ -47,7 +47,7 @@ public function testCustomTypeValueConversions(): void $country = $this->dm->find(Country::class, $country->id); - self::assertContainsOnly('DateTime', $country->nationalHolidays); + self::assertContainsOnlyInstancesOf(DateTime::class, $country->nationalHolidays); } public function testConvertToDatabaseValueExpectsArray(): void diff --git a/tests/Tests/Functional/ShardKeyTest.php b/tests/Tests/Functional/ShardKeyTest.php index c4eafc9f96..8d95857cf5 100644 --- a/tests/Tests/Functional/ShardKeyTest.php +++ b/tests/Tests/Functional/ShardKeyTest.php @@ -35,7 +35,9 @@ public function setUp(): void public function tearDown(): void { - $this->logger->unregister(); + if (isset($this->logger)) { + $this->logger->unregister(); + } parent::tearDown(); } diff --git a/tests/Tests/Functional/TargetDocumentTest.php b/tests/Tests/Functional/TargetDocumentTest.php index 863f5a55cc..8b50453b3d 100644 --- a/tests/Tests/Functional/TargetDocumentTest.php +++ b/tests/Tests/Functional/TargetDocumentTest.php @@ -7,11 +7,12 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use stdClass; class TargetDocumentTest extends BaseTestCase { - /** @doesNotPerformAssertions */ + #[DoesNotPerformAssertions] public function testMappedSuperClassAsTargetDocument(): void { $test = new TargetDocumentTestDocument(); diff --git a/tests/Tests/Query/FilterCollectionTest.php b/tests/Tests/Query/FilterCollectionTest.php index 558a467ee3..c5abc6d66c 100644 --- a/tests/Tests/Query/FilterCollectionTest.php +++ b/tests/Tests/Query/FilterCollectionTest.php @@ -22,7 +22,7 @@ public function testEnable(): void $enabledFilters = $filterCollection->getEnabledFilters(); self::assertCount(1, $enabledFilters); - self::assertContainsOnly(BsonFilter::class, $enabledFilters); + self::assertContainsOnlyInstancesOf(BsonFilter::class, $enabledFilters); $filterCollection->disable('testFilter'); self::assertEmpty($filterCollection->getEnabledFilters()); diff --git a/tests/Tests/QueryTest.php b/tests/Tests/QueryTest.php index a9560914f6..b643dfff02 100644 --- a/tests/Tests/QueryTest.php +++ b/tests/Tests/QueryTest.php @@ -508,7 +508,7 @@ public function testFindWithHint(): void $collection->expects($this->once()) ->method('find') ->with(['foo' => 'bar'], ['hint' => 'foo']) - ->will($this->returnValue($cursor)); + ->willReturn($cursor); // Using QueryBuilder->find adds hint to the query array $queryArray = [ From 718826c98a740b8f40092f421af8cd8598d0ef67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 12 Nov 2025 17:42:49 +0100 Subject: [PATCH 15/15] Deprecate ClassMetadata::isIdGeneratorUuid() (#2896) --- src/Mapping/ClassMetadata.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 763d70cfa9..d3e5c5ed05 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -2170,9 +2170,13 @@ public function isIdGeneratorIncrement(): bool /** * Checks whether the class will generate a uuid id. + * + * @deprecated Since 2.15, the UUID id generator is deprecated. Use GENERATOR_TYPE_AUTO with the UUID type instead. */ public function isIdGeneratorUuid(): bool { + trigger_deprecation('doctrine/mongodb-odm', '2.15', 'The method %s() is deprecated. Use GENERATOR_TYPE_AUTO with the UUID type instead.', __FUNCTION__); + return $this->generatorType === self::GENERATOR_TYPE_UUID; }