diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b30736856a..aef3448235 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -108,6 +108,15 @@ jobs: dependencies: "highest" symfony-version: false proxy: "native" + # Test removing optional dependencies + - topology: "server" + php-version: "8.4" + mongodb-version: "8.0" + driver-version: "stable" + dependencies: "highest" + symfony-version: "locked" + 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" @@ -154,6 +163,13 @@ jobs: - name: "Remove phpbench/phpbench" run: composer remove --no-update --dev phpbench/phpbench + - 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/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/src/Configuration.php b/src/Configuration.php index fa1e7de5a0..8e1faf4881 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -716,7 +716,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 @@ -726,7 +727,6 @@ 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 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(); 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/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/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..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 e9b0bc8a84..a43ff4c1c2 100644 --- a/tests/Tests/ConfigurationTest.php +++ b/tests/Tests/ConfigurationTest.php @@ -14,10 +14,14 @@ 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 @@ -35,15 +39,22 @@ 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(); + 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()); } @@ -51,7 +62,7 @@ public function testUseLazyGhostObject(): void #[RequiresPhp('>= 8.4')] public function testUseLazyGhostObjectWithSymfony8(): void { - if (version_compare(InstalledVersions::getVersion('symfony/var-exporter'), '8', '<')) { + if (InstalledVersions::isInstalled('symfony/var-exporter') && version_compare(InstalledVersions::getVersion('symfony/var-exporter'), '8', '<')) { $this->markTestSkipped('Symfony VarExporter 8 or higher is not installed.'); } 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/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 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 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/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