Skip to content
Merged
16 changes: 16 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions docs/en/reference/migrating-schemas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ 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

<?php

$schemaManager->createDocumentCollection(Person::class);

Once the collection is created, you can also set up indexes with ``ensureIndexes``,
and search indexes with ``createSearchIndexes``:

.. code-block:: php

<?php

$schemaManager->ensureIndexes();
Expand Down
4 changes: 2 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -726,7 +727,6 @@ public function setUseNativeLazyObject(bool $nativeLazyObject): void
}

$this->nativeLazyObject = $nativeLazyObject;
$this->lazyGhostObject = ! $nativeLazyObject || $this->lazyGhostObject;
}

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;
});
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));
$skippedFieldsFqns[] = $this->propertyFqcn($metadata->getPropertyAccessor($idField)->getUnderlyingReflector());
}

foreach ($metadata->getReflectionClass()->getProperties() as $property) {
Expand Down
1 change: 1 addition & 0 deletions tests/Documents/FileWithoutMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FileWithoutMetadata
*
* @var string|null
*/
#[ODM\File\Filename]
private $filename;

public function getId(): ?string
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;
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
21 changes: 16 additions & 5 deletions tests/Tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,23 +39,30 @@ 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());
}

#[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.');
}

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;
}
}
4 changes: 2 additions & 2 deletions tests/Tests/Mapping/AbstractMappingDriverTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -1254,15 +1254,15 @@ class AbstractMappingDriverDuplicateDatabaseNameNotSaved extends AbstractMapping
*
* @var string|null
*/
#[ODM\Field(type: 'int', name: 'baz')]
#[ODM\Field(type: 'string', name: 'baz')]
public $foo;

/**
* @ODM\Field(type="string", name="baz", notSaved=true)
*
* @var string|null
*/
#[ODM\Field(type: 'int', name: 'baz', notSaved: true)]
#[ODM\Field(type: 'string', name: 'baz', notSaved: true)]
public $bar;
}

Expand Down
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
10 changes: 10 additions & 0 deletions tests/Tests/Mapping/Documents/GlobalNamespaceDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,75 @@
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/** @ODM\Document */
#[ODM\Document]
class DoctrineGlobal_Article
{
/**
* @ODM\Id
*
* @var string|null
*/
#[ODM\Id]
protected $id;

/**
* @ODM\Field(type="string")
*
* @var string|null
*/
#[ODM\Field(type: 'string')]
protected $headline;

/**
* @ODM\Field(type="string")
*
* @var string|null
*/
#[ODM\Field(type: 'string')]
protected $text;

/**
* @ODM\ReferenceMany(targetDocument=DoctrineGlobal_User::class)
*
* @var DoctrineGlobal_User|null
*/
#[ODM\ReferenceMany(targetDocument: DoctrineGlobal_User::class)]
protected $author;

/**
* @ODM\ReferenceMany(targetDocument=DoctrineGlobal_User::class)
*
* @var Collection<int, DoctrineGlobal_User>
*/
#[ODM\ReferenceMany(targetDocument: DoctrineGlobal_User::class)]
protected $editor;
}

/** @ODM\Document */
#[ODM\Document]
class DoctrineGlobal_User
{
/**
* @ODM\Id
*
* @var string|null
*/
#[ODM\Id]
private $id;

/**
* @ODM\Field(type="string")
*
* @var string
*/
#[ODM\Field(type: 'string')]
private $username;

/**
* @ODM\Field(type="string")
*
* @var string
*/
#[ODM\Field(type: 'string')]
private $email;
}
Loading