diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 56cd905e18..ab64f52002 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -111,13 +111,6 @@ jobs: composer require --no-update symfony/var-dumper:^7@dev composer require --no-update --dev symfony/cache:^7@dev - - name: "Configure PHP 8.4" - if: "${{ matrix.php-version == '8.4' }}" - run: | - # psalm is not compatible with PHP 8.4 - # https://github.com/vimeo/psalm/pull/10928 - composer remove --no-update --dev vimeo/psalm - - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v3" with: diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index fbbe373de1..82000d9c31 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -151,7 +151,7 @@ Here is a quick overview of the built-in mapping types: - ``string`` - ``timestamp`` -You can read more about the available MongoDB types on `php.net `_. +You can read more about the available MongoDB types on `php.net `_. .. note:: diff --git a/lib/Doctrine/ODM/MongoDB/Iterator/CachingIterator.php b/lib/Doctrine/ODM/MongoDB/Iterator/CachingIterator.php index d726712013..8d80284fa2 100644 --- a/lib/Doctrine/ODM/MongoDB/Iterator/CachingIterator.php +++ b/lib/Doctrine/ODM/MongoDB/Iterator/CachingIterator.php @@ -5,7 +5,8 @@ namespace Doctrine\ODM\MongoDB\Iterator; use Countable; -use Generator; +use Iterator as SPLIterator; +use IteratorIterator; use ReturnTypeWillChange; use RuntimeException; use Traversable; @@ -33,13 +34,11 @@ final class CachingIterator implements Countable, Iterator /** @var array */ private array $items = []; - /** @var Generator|null */ - private ?Generator $iterator; + /** @var SPLIterator|null */ + private ?SPLIterator $iterator; private bool $iteratorAdvanced = false; - private bool $iteratorExhausted = false; - /** * Initialize the iterator and stores the first item in the cache. This * effectively rewinds the Traversable and the wrapping Generator, which @@ -51,7 +50,8 @@ final class CachingIterator implements Countable, Iterator */ public function __construct(Traversable $iterator) { - $this->iterator = $this->wrapTraversable($iterator); + $this->iterator = new IteratorIterator($iterator); + $this->iterator->rewind(); $this->storeCurrentItem(); } @@ -94,9 +94,10 @@ public function key() /** @see http://php.net/iterator.next */ public function next(): void { - if (! $this->iteratorExhausted) { - $this->getIterator()->next(); + if ($this->iterator !== null) { + $this->iterator->next(); $this->storeCurrentItem(); + $this->iteratorAdvanced = true; } next($this->items); @@ -126,15 +127,13 @@ public function valid(): bool */ private function exhaustIterator(): void { - while (! $this->iteratorExhausted) { + while ($this->iterator !== null) { $this->next(); } - - $this->iterator = null; } - /** @return Generator */ - private function getIterator(): Generator + /** @return SPLIterator */ + private function getIterator(): SPLIterator { if ($this->iterator === null) { throw new RuntimeException('Iterator has already been destroyed'); @@ -148,28 +147,12 @@ private function getIterator(): Generator */ private function storeCurrentItem(): void { - $key = $this->getIterator()->key(); + $key = $this->iterator->key(); if ($key === null) { - return; + $this->iterator = null; + } else { + $this->items[$key] = $this->getIterator()->current(); } - - $this->items[$key] = $this->getIterator()->current(); - } - - /** - * @param Traversable $traversable - * - * @return Generator - */ - private function wrapTraversable(Traversable $traversable): Generator - { - foreach ($traversable as $key => $value) { - yield $key => $value; - - $this->iteratorAdvanced = true; - } - - $this->iteratorExhausted = true; } } diff --git a/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php b/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php index 827b4cf4bf..7e3fc075c2 100644 --- a/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php +++ b/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php @@ -6,8 +6,8 @@ use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\UnitOfWork; -use Generator; use Iterator; +use IteratorIterator; use ReturnTypeWillChange; use RuntimeException; use Traversable; @@ -24,8 +24,8 @@ */ final class HydratingIterator implements Iterator { - /** @var Generator>|null */ - private ?Generator $iterator; + /** @var Iterator>|null */ + private ?Iterator $iterator; /** * @param Traversable> $traversable @@ -34,7 +34,8 @@ final class HydratingIterator implements Iterator */ public function __construct(Traversable $traversable, private UnitOfWork $unitOfWork, private ClassMetadata $class, private array $unitOfWorkHints = []) { - $this->iterator = $this->wrapTraversable($traversable); + $this->iterator = new IteratorIterator($traversable); + $this->iterator->rewind(); } public function __destruct() @@ -74,8 +75,8 @@ public function valid(): bool return $this->key() !== null; } - /** @return Generator> */ - private function getIterator(): Generator + /** @return Iterator> */ + private function getIterator(): Iterator { if ($this->iterator === null) { throw new RuntimeException('Iterator has already been destroyed'); @@ -93,16 +94,4 @@ private function hydrate(?array $document): ?object { return $document !== null ? $this->unitOfWork->getOrCreateDocument($this->class->name, $document, $this->unitOfWorkHints) : null; } - - /** - * @param Traversable> $traversable - * - * @return Generator> - */ - private function wrapTraversable(Traversable $traversable): Generator - { - foreach ($traversable as $key => $value) { - yield $key => $value; - } - } } diff --git a/lib/Doctrine/ODM/MongoDB/Iterator/UnrewindableIterator.php b/lib/Doctrine/ODM/MongoDB/Iterator/UnrewindableIterator.php index f13baed355..c0352c050e 100644 --- a/lib/Doctrine/ODM/MongoDB/Iterator/UnrewindableIterator.php +++ b/lib/Doctrine/ODM/MongoDB/Iterator/UnrewindableIterator.php @@ -4,7 +4,8 @@ namespace Doctrine\ODM\MongoDB\Iterator; -use Generator; +use Iterator as SPLIterator; +use IteratorIterator; use LogicException; use ReturnTypeWillChange; use RuntimeException; @@ -23,39 +24,34 @@ */ final class UnrewindableIterator implements Iterator { - /** @var Generator|null */ - private ?Generator $iterator; + /** @var SPLIterator|null */ + private ?SPLIterator $iterator; private bool $iteratorAdvanced = false; /** - * Initialize the iterator. This effectively rewinds the Traversable and - * the wrapping Generator, which will execute up to its first yield statement. - * Additionally, this mimics behavior of the SPL iterators and allows users - * to omit an explicit call to rewind() before using the other methods. + * Initialize the iterator. This effectively rewinds the Traversable. + * This mimics behavior of the SPL iterators and allows users to omit an + * explicit call to rewind() before using the other methods. * * @param Traversable $iterator */ public function __construct(Traversable $iterator) { - $this->iterator = $this->wrapTraversable($iterator); - $this->iterator->key(); + $this->iterator = new IteratorIterator($iterator); + $this->iterator->rewind(); } public function toArray(): array { $this->preventRewinding(__METHOD__); - $toArray = function () { - if (! $this->valid()) { - return; - } - - yield $this->key() => $this->current(); - yield from $this->getIterator(); - }; - - return iterator_to_array($toArray()); + try { + return iterator_to_array($this->getIterator()); + } finally { + $this->iteratorAdvanced = true; + $this->iterator = null; + } } /** @return TValue|null */ @@ -84,6 +80,13 @@ public function next(): void } $this->iterator->next(); + $this->iteratorAdvanced = true; + + if ($this->iterator->valid()) { + return; + } + + $this->iterator = null; } /** @see http://php.net/iterator.rewind */ @@ -108,8 +111,8 @@ private function preventRewinding(string $method): void } } - /** @return Generator */ - private function getIterator(): Generator + /** @return SPLIterator */ + private function getIterator(): SPLIterator { if ($this->iterator === null) { throw new RuntimeException('Iterator has already been destroyed'); @@ -117,20 +120,4 @@ private function getIterator(): Generator return $this->iterator; } - - /** - * @param Traversable $traversable - * - * @return Generator - */ - private function wrapTraversable(Traversable $traversable): Generator - { - foreach ($traversable as $key => $value) { - yield $key => $value; - - $this->iteratorAdvanced = true; - } - - $this->iterator = null; - } } diff --git a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php index 13fd4bd3ea..e13bbfb14f 100644 --- a/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Proxy/Factory/StaticProxyFactory.php @@ -145,13 +145,21 @@ private function createInitializer( */ private function skippedFieldsFqns(ClassMetadata $metadata): array { - $idFieldFqcns = []; + $skippedFieldsFqns = []; foreach ($metadata->getIdentifierFieldNames() as $idField) { - $idFieldFqcns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField)); + $skippedFieldsFqns[] = $this->propertyFqcn($metadata->getReflectionProperty($idField)); } - return $idFieldFqcns; + foreach ($metadata->getReflectionClass()->getProperties() as $property) { + if ($metadata->hasField($property->getName())) { + continue; + } + + $skippedFieldsFqns[] = $this->propertyFqcn($property); + } + + return $skippedFieldsFqns; } private function propertyFqcn(ReflectionProperty $property): string diff --git a/lib/Doctrine/ODM/MongoDB/SchemaManager.php b/lib/Doctrine/ODM/MongoDB/SchemaManager.php index ae6cd510e3..f7b6c2ad84 100644 --- a/lib/Doctrine/ODM/MongoDB/SchemaManager.php +++ b/lib/Doctrine/ODM/MongoDB/SchemaManager.php @@ -413,7 +413,9 @@ public function updateDocumentSearchIndexes(string $documentName): void $definedNames = array_column($searchIndexes, 'name'); try { - $existingNames = array_column(iterator_to_array($collection->listSearchIndexes()), 'name'); + /* The typeMap option can be removed when bug is fixed in the minimum required version. + * https://jira.mongodb.org/browse/PHPLIB-1548 */ + $existingNames = array_column(iterator_to_array($collection->listSearchIndexes(['typeMap' => ['root' => 'array']])), 'name'); } catch (CommandException $e) { /* If $listSearchIndexes doesn't exist, only throw if search indexes have been defined. * If no search indexes are defined and the server doesn't support search indexes, there's @@ -465,7 +467,9 @@ public function deleteDocumentSearchIndexes(string $documentName): void $collection = $this->dm->getDocumentCollection($class->name); try { - $searchIndexes = $collection->listSearchIndexes(); + /* The typeMap option can be removed when bug is fixed in the minimum required version. + * https://jira.mongodb.org/browse/PHPLIB-1548 */ + $searchIndexes = $collection->listSearchIndexes(['typeMap' => ['root' => 'array']]); } catch (CommandException $e) { // If the server does not support search indexes, there are no indexes to remove in any case if ($this->isSearchIndexCommandException($e)) { diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/CachingIteratorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/CachingIteratorTest.php index 52b90c1dec..dc8373f115 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/CachingIteratorTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/CachingIteratorTest.php @@ -59,6 +59,19 @@ public function testIterationWithEmptySet(): void self::assertFalse($iterator->valid()); } + public function testIterationWithInvalidIterator(): void + { + $mock = $this->createMock(Iterator::class); + // The method next() should not be called on a dead cursor. + $mock->expects(self::never())->method('next'); + // The method valid() return false on a dead cursor. + $mock->expects(self::once())->method('valid')->willReturn(false); + + $iterator = new CachingIterator($mock); + + $this->assertEquals([], $iterator->toArray()); + } + public function testPartialIterationDoesNotExhaust(): void { $traversable = $this->getTraversableThatThrows([1, 2, new Exception()]); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/UnrewindableIteratorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/UnrewindableIteratorTest.php index 304724c2ac..1284204256 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/UnrewindableIteratorTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/UnrewindableIteratorTest.php @@ -100,6 +100,15 @@ public function testRewindAfterPartialIteration(): void iterator_to_array($iterator); } + public function testRewindAfterToArray(): void + { + $iterator = new UnrewindableIterator($this->getTraversable([1, 2, 3])); + + $iterator->toArray(); + $this->expectException(LogicException::class); + $iterator->rewind(); + } + public function testToArray(): void { $iterator = new UnrewindableIterator($this->getTraversable([1, 2, 3])); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php index 2be5e3fc1a..5a1db88dc5 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Proxy/Factory/StaticProxyFactoryTest.php @@ -11,6 +11,7 @@ use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Cart; +use Documents\DocumentWithUnmappedProperties; use MongoDB\Client; use MongoDB\Collection; use MongoDB\Database; @@ -22,15 +23,10 @@ class StaticProxyFactoryTest extends BaseTestCase /** @var Client|MockObject */ private Client $client; - public function setUp(): void + public function testProxyInitializeWithException(): void { - parent::setUp(); - $this->dm = $this->createMockedDocumentManager(); - } - public function testProxyInitializeWithException(): void - { $collection = $this->createMock(Collection::class); $database = $this->createMock(Database::class); @@ -81,6 +77,17 @@ private function createMockedDocumentManager(): DocumentManager return DocumentManager::create($this->client, $config); } + + public function testCreateProxyForDocumentWithUnmappedProperties(): void + { + $proxy = $this->dm->getReference(DocumentWithUnmappedProperties::class, '123'); + self::assertInstanceOf(GhostObjectInterface::class, $proxy); + + // Disable initialiser so we can access properties without initialising the object + $proxy->setProxyInitializer(null); + + self::assertSame('bar', $proxy->foo); + } } class DocumentNotFoundListener diff --git a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php index c532a8bd84..e3f1174445 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php @@ -789,7 +789,7 @@ public function testCreateTimeSeriesCollection(array $expectedWriteOptions, ?int $this->schemaManager->createDocumentCollection(TimeSeriesDocument::class, $maxTimeMs, $writeConcern); } - /** @psalm-param IndexOptions $expectedWriteOptions */ + /** @phpstan-param IndexOptions $expectedWriteOptions */ #[DataProvider('getWriteOptions')] public function testCreateCollections(array $expectedWriteOptions, ?int $maxTimeMs, ?WriteConcern $writeConcern): void { diff --git a/tests/Documents/DocumentWithUnmappedProperties.php b/tests/Documents/DocumentWithUnmappedProperties.php new file mode 100644 index 0000000000..06339e79b2 --- /dev/null +++ b/tests/Documents/DocumentWithUnmappedProperties.php @@ -0,0 +1,17 @@ +