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/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index f6ef2b9689..0df1185043 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -95,7 +95,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"
@@ -103,24 +102,24 @@ 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' }}"
+ - name: "Remove optional dependencies"
+ if: "${{ matrix.remove-optional-dependencies }}"
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 }}
+ 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:
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/.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"
diff --git a/composer.json b/composer.json
index e4418af3e1..6cee2b40a1 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,9 +37,9 @@
"friendsofphp/proxy-manager-lts": "^1.0",
"mongodb/mongodb": "^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-exporter": "^6.2 || ^7.0"
+ "symfony/var-exporter": "^6.4 || ^7.0 || ^8.0"
},
"require-dev": {
"ext-bcmath": "*",
@@ -49,11 +51,11 @@
"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",
- "symfony/uid": "^5.4 || ^6.0 || ^7.0",
- "symfony/var-dumper": "^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",
+ "symfony/var-dumper": "^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/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();
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/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
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/phpstan-baseline.neon b/phpstan-baseline.neon
index 03a54bafca..0ac80e1f49 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/phpunit.xml.dist b/phpunit.xml.dist
index 4451e15bc1..e3bc2085ba 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
= 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.');
}
@@ -706,13 +712,13 @@ 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
{
$this->nativeLazyObject = $nativeLazyObject;
- $this->lazyGhostObject = ! $nativeLazyObject || $this->lazyGhostObject;
}
public function isNativeLazyObjectEnabled(): bool
diff --git a/src/DocumentManager.php b/src/DocumentManager.php
index d7c919d32f..62085a936b 100644
--- a/src/DocumentManager.php
+++ b/src/DocumentManager.php
@@ -41,6 +41,7 @@
use function is_object;
use function ltrim;
use function sprintf;
+use function trigger_deprecation;
/**
* The DocumentManager class is the central access point for managing the
@@ -153,15 +154,31 @@ 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();
$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) {
@@ -295,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/Hydrator/HydratorFactory.php b/src/Hydrator/HydratorFactory.php
index 0611c6c261..5172bc94a5 100644
--- a/src/Hydrator/HydratorFactory.php
+++ b/src/Hydrator/HydratorFactory.php
@@ -453,24 +453,12 @@ public function hydrate(object $document, array $data, array $hints = []): array
$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/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/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/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) {
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/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 c53a514b34..25e721f7f7 100644
--- a/src/Tools/Console/Command/QueryCommand.php
+++ b/src/Tools/Console/Command/QueryCommand.php
@@ -28,8 +28,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')
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/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/BaseTestCase.php b/tests/Tests/BaseTestCase.php
index 43fec12308..835c6c0e9e 100644
--- a/tests/Tests/BaseTestCase.php
+++ b/tests/Tests/BaseTestCase.php
@@ -105,8 +105,12 @@ 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_NATIVE_LAZY_OBJECT']) {
+ $config->setUseNativeLazyObject(true);
+ } elseif ($_ENV['USE_LAZY_GHOST_OBJECT']) {
+ $config->setUseLazyGhostObject(true);
+ }
if ($config->isNativeLazyObjectEnabled()) {
NativeLazyObjectFactory::enableTracking();
diff --git a/tests/Tests/ConfigurationTest.php b/tests/Tests/ConfigurationTest.php
index c07d62c8f9..87c6757411 100644
--- a/tests/Tests/ConfigurationTest.php
+++ b/tests/Tests/ConfigurationTest.php
@@ -4,19 +4,26 @@
namespace Doctrine\ODM\MongoDB\Tests;
+use Composer\InstalledVersions;
use Doctrine\ODM\MongoDB\Configuration;
use Doctrine\ODM\MongoDB\ConfigurationException;
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionFactory;
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;
+use ProxyManager\Configuration as ProxyManagerConfiguration;
use stdClass;
+use Symfony\Component\VarExporter\LazyGhostTrait;
use function base64_encode;
+use function class_exists;
use function str_repeat;
+use function trait_exists;
+use function version_compare;
class ConfigurationTest extends TestCase
{
@@ -31,17 +38,45 @@ public function testUseNativeLazyObjectBeforePHP84(): void
$c->setUseNativeLazyObject(true);
}
+ #[IgnoreDeprecations]
public function testUseLazyGhostObject(): void
{
$c = new Configuration();
+ if (! trait_exists(LazyGhostTrait::class)) {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Package "symfony/var-exporter" >= 8.0 does not provide lazy ghost objects, use native lazy objects instead.');
+ }
+
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());
}
+ #[RequiresPhp('>= 8.4')]
+ public function testUseLazyGhostObjectWithSymfony8(): void
+ {
+ if (InstalledVersions::isInstalled('symfony/var-exporter') && 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);
+ }
+
+ #[IgnoreDeprecations]
public function testNativeLazyObjectDeprecatedByDefault(): void
{
$c = new Configuration();
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]
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/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/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/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/AnnotationDriverTest.php b/tests/Tests/Mapping/AnnotationDriverTest.php
index e9c37a10a1..650bcabe72 100644
--- a/tests/Tests/Mapping/AnnotationDriverTest.php
+++ b/tests/Tests/Mapping/AnnotationDriverTest.php
@@ -4,12 +4,15 @@
namespace Doctrine\ODM\MongoDB\Tests\Mapping;
+use Doctrine\Common\Annotations\AnnotationReader;
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 class_exists;
+#[RequiresMethod(AnnotationReader::class, '__construct')]
class AnnotationDriverTest extends AbstractAnnotationDriverTestCase
{
protected static function loadDriver(array $paths = []): MappingDriver
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/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,
+ ) {
+ }
+}
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/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());
+ }
+}
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 = [
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