Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -718,7 +719,6 @@ public function setUseNativeLazyObject(bool $nativeLazyObject): void
}

$this->nativeLazyObject = $nativeLazyObject;
$this->lazyGhostObject = ! $nativeLazyObject || $this->lazyGhostObject;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was setting lazyGhostObject = true when calling setUseNativeLazyObject(false), which is not expected.

}

public function isNativeLazyObjectEnabled(): bool
Expand Down
20 changes: 17 additions & 3 deletions src/DocumentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
17 changes: 3 additions & 14 deletions src/Hydrator/HydratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Comment on lines -464 to -476
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to set the initializer to null, so that the proxy is marked as initialized. Otherwise, updating a value that is already set doesn't initialize the proxy, and the UOW ignore the document when computing changeset.

if ($document instanceof GhostObjectInterface) {
$document->setProxyInitializer(null);
}

$data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/Proxy/Factory/StaticProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private function skippedFieldsFqns(ClassMetadata $metadata): array
$skippedFieldsFqns = [];

foreach ($metadata->getIdentifierFieldNames() as $idField) {
$skippedFieldsFqns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove usage of the deprecated ClassMetadata::getReflectionProperty()

$skippedFieldsFqns[] = $this->propertyFqcn($metadata->getPropertyAccessor($idField)->getUnderlyingReflector());
}

foreach ($metadata->getReflectionClass()->getProperties() as $property) {
Expand Down
2 changes: 1 addition & 1 deletion tests/Documents/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Tag
public ?string $id;

#[ODM\Field]
public readonly string $name;
Copy link
Member Author

@GromNaN GromNaN Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the reaonly property that was added in #2840. That makes Doctrine\ODM\MongoDB\Tests\Aggregation\BuilderTest::testAggregationBuilder fail with proxy manager.

public string $name;

/** @var Collection<int, BlogPost> */
#[ODM\ReferenceMany(targetDocument: BlogPost::class, mappedBy: 'tags')]
Expand Down
9 changes: 7 additions & 2 deletions tests/Tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions tests/Tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
}
Expand Down
51 changes: 51 additions & 0 deletions tests/Tests/Functional/ReadOnlyPropertiesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Tests\Functional;

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\Annotations\ReferenceOne;
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;

class ReadOnlyPropertiesTest extends BaseTestCase
{
public function testReadOnlyDocument(): void
{
$configuration = $this->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;
}
}
3 changes: 3 additions & 0 deletions tests/Tests/Mapping/AnnotationDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +21,7 @@

use const E_USER_DEPRECATED;

#[RequiresMethod(AnnotationReader::class, '__construct')]
class AnnotationDriverTest extends AbstractAnnotationDriverTestCase
{
protected static function loadDriver(array $paths = []): MappingDriver
Expand Down
27 changes: 22 additions & 5 deletions tests/Tests/Mapping/LegacyReflectionFieldsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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,
) {
}
}