diff --git a/extension.neon b/extension.neon index 63365d37..35108264 100644 --- a/extension.neon +++ b/extension.neon @@ -43,7 +43,6 @@ parameters: - stubs/Persistence/ObjectRepository.stub - stubs/RepositoryFactory.stub - stubs/Collections/ArrayCollection.stub - - stubs/Collections/ReadableCollection.stub - stubs/Collections/Selectable.stub - stubs/ORM/AbstractQuery.stub - stubs/ORM/Exception/ORMException.stub diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 0b3a69d8..390e35a5 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -64,8 +64,10 @@ public function getFiles(): array $collectionVersion = null; } if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { + $files[] = $stubsDir . '/Collections/ReadableCollection1.stub'; $files[] = $stubsDir . '/Collections/Collection1.stub'; } else { + $files[] = $stubsDir . '/Collections/ReadableCollection.stub'; $files[] = $stubsDir . '/Collections/Collection.stub'; } diff --git a/stubs/Collections/ArrayCollection.stub b/stubs/Collections/ArrayCollection.stub index 2ecb9ea6..34d07450 100644 --- a/stubs/Collections/ArrayCollection.stub +++ b/stubs/Collections/ArrayCollection.stub @@ -2,6 +2,8 @@ namespace Doctrine\Common\Collections; +use Closure; + /** * @template TKey of array-key * @template T @@ -11,4 +13,33 @@ namespace Doctrine\Common\Collections; class ArrayCollection implements Collection, Selectable { + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return static + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return static + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: static, 1: static} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index be162ef6..05139edd 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection1.stub b/stubs/Collections/Collection1.stub index 455733c8..0f5ad708 100644 --- a/stubs/Collections/Collection1.stub +++ b/stubs/Collections/Collection1.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/ReadableCollection1.stub b/stubs/Collections/ReadableCollection1.stub new file mode 100644 index 00000000..dec73af0 --- /dev/null +++ b/stubs/Collections/ReadableCollection1.stub @@ -0,0 +1,86 @@ + + */ +interface ReadableCollection extends Countable, IteratorAggregate +{ + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool + */ + public function exists(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return ReadableCollection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return T|null + */ + public function findFirst(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(TReturn|TInitial, T):TReturn $func + * @param TInitial $initial + * + * @return TReturn|TInitial + * + * @template TReturn + * @template TInitial + */ + public function reduce(Closure $func, mixed $initial = null); + +} diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index 82a4c236..ba2c4fd6 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -14,6 +14,7 @@ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/getRepository.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/isEmpty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/Collection.php'); } /** diff --git a/tests/DoctrineIntegration/data/Collection.php b/tests/DoctrineIntegration/data/Collection.php new file mode 100644 index 00000000..f56e69d7 --- /dev/null +++ b/tests/DoctrineIntegration/data/Collection.php @@ -0,0 +1,56 @@ + */ + private $items; + + public function __construct() + { + /** @var ArrayCollection $numbers */ + $numbers = new ArrayCollection([1, 2, 3]); + + $filteredNumbers = $numbers->filter(function (int $number): bool { + return $number % 2 === 1; + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $filteredNumbers); + + $items = $filteredNumbers->map(static function (int $number): Item { + return new Item(); + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $items); + + $this->items = $items; + } + + public function removeOdd(): void + { + $this->items = $this->items->filter(function (Item $item, int $idx): bool { + return $idx % 2 === 1; + }); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + + public function __clone() + { + $this->items = $this->items->map( + static function (Item $item): Item { + return clone $item; + } + ); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + +} + +class Item +{ + +}