diff --git a/composer.json b/composer.json index 81ac5e9..71f6d20 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,11 @@ "require-dev": { "squizlabs/php_codesniffer": "^3.9", "stefna/codestyle": "^1.15", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.1", "bnf/phpstan-psr-container": "^1.0", "phpunit/phpunit": "^10.5", - "phpstan/phpstan-phpunit": "^1.1", - "tomasvotruba/type-coverage": "^0.3 || ^1.0", + "phpstan/phpstan-phpunit": "^2.0", + "tomasvotruba/type-coverage": "^2.0", "phpstan/extension-installer": "^1.3" }, "autoload": { diff --git a/phpstan.neon b/phpstan.neon index 30ec84e..a837925 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ parameters: level: max paths: - src + - tests type_coverage: return_type: 90 param_type: 100 diff --git a/src/AbstractListCollection.php b/src/AbstractListCollection.php index 4a245f6..b784e86 100644 --- a/src/AbstractListCollection.php +++ b/src/AbstractListCollection.php @@ -191,8 +191,12 @@ public function filter(callable $filter): static public function map(callable $callback): ListCollection { + if ($this->data->isEmpty()) { + return new GenericListCollection($this->collectionType); // @phpstan-ignore return.type + } $newData = $this->data->map($callback); - $collection = new GenericListCollection(get_class($newData->first())); + $collectionType = $newData->first()::class; + $collection = new GenericListCollection($collectionType); $collection->data = $newData; return $collection; } @@ -211,7 +215,7 @@ public function indexBy(callable $callback): MapCollection public function column(callable $callback): array { - return $this->data->map($callback)->toArray(); + return array_values($this->data->map($callback)->toArray()); } public function merge(Collection ...$collections): static diff --git a/src/AbstractMapCollection.php b/src/AbstractMapCollection.php index df1f130..72fecfe 100644 --- a/src/AbstractMapCollection.php +++ b/src/AbstractMapCollection.php @@ -31,10 +31,11 @@ public function __construct( if (!isset($this->collectionType) && isset(static::$defaultCollectionType)) { $this->collectionType = static::$defaultCollectionType; } - $this->data = new Map(); + $this->data = new Map(); // @phpstan-ignore assign.propertyType // Invoke offsetSet() for each value added; in this way, sub-classes // may provide additional logic about values added to the array object. foreach ($data as $key => $value) { + // @phpstan-ignore function.alreadyNarrowedType if (!is_string($key)) { throw new \BadMethodCallException('Must specify key'); } @@ -83,6 +84,7 @@ public function offsetSet(mixed $offset, mixed $value): void if (!$value instanceof $this->collectionType) { throw new \TypeError('Invalid type for collection. Expected: ' . $this->collectionType); } + // @phpstan-ignore function.alreadyNarrowedType if (!is_string($offset)) { throw new \InvalidArgumentException('Offset must be of type string'); } @@ -108,7 +110,7 @@ public function count(): int public function clear(): void { - $this->data = new Map(); + $this->data = new Map(); // @phpstan-ignore assign.propertyType } /** @@ -204,6 +206,9 @@ public function filter(callable $filter): static */ public function map(callable $callback): MapCollection { + if ($this->data->isEmpty()) { + return new GenericMapCollection($this->collectionType); // @phpstan-ignore return.type + } $newData = $this->data->map($callback); $collection = new GenericMapCollection(get_class($newData->first()->value)); $collection->data = $newData; diff --git a/tests/GenericListCollectionTest.php b/tests/GenericListCollectionTest.php index 78837e5..3e270b6 100644 --- a/tests/GenericListCollectionTest.php +++ b/tests/GenericListCollectionTest.php @@ -178,6 +178,18 @@ public function testMap(): void $this->assertSame($value, $newCollection->first()?->value); } + public function testMapOnEmptyCollection(): void + { + $collection1 = new GenericListCollection(RandomEntity::class, []); + + $newCollection = $collection1->map(fn (RandomEntity $entity): ExtraEntity => new ExtraEntity((string)$entity->value)); + + $this->assertNotSame($collection1, $newCollection); + $this->assertCount(0, $newCollection); + // the collection type stays the same since the collection is empty and hence can't change + $this->assertSame($collection1->getType(), $newCollection->getType()); + } + public function testFilter(): void { $collection1 = new GenericListCollection(RandomEntity::class, [ @@ -226,7 +238,7 @@ public function testIndexBy(): void $index = 1; foreach ($mapCollection as $key => $value) { $this->assertSame('index' . $index, $key); - $this->assertInstanceOf(ExtraEntity::class, $value); + $this->assertInstanceOf(ExtraEntity::class, $value); // @phpstan-ignore method.alreadyNarrowedType $index++; } } @@ -274,7 +286,7 @@ public function testColumn(): void $values = $collection1->column(fn (RandomEntity $r) => $r->value); $this->assertCount(5, $values); - $this->assertIsList($values); + $this->assertIsList($values); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame([1, 2, 3, 4, 5], $values); } diff --git a/tests/GenericMapCollectionTest.php b/tests/GenericMapCollectionTest.php index 8319792..831093c 100644 --- a/tests/GenericMapCollectionTest.php +++ b/tests/GenericMapCollectionTest.php @@ -201,6 +201,18 @@ public function testMap(): void $this->assertSame($value, $newCollection->first()?->value); } + public function testMapOnEmptyCollection(): void + { + $collection1 = new GenericMapCollection(RandomEntity::class, []); + + $newCollection = $collection1->map(fn (string $key, RandomEntity $entity): ExtraEntity => new ExtraEntity((string)$entity->value)); + + $this->assertNotSame($collection1, $newCollection); + $this->assertCount(0, $newCollection); + // the collection type stays the same since the collection is empty and hence can't change + $this->assertSame($collection1->getType(), $newCollection->getType()); + } + public function testFilter(): void { $collection1 = new GenericMapCollection(RandomEntity::class, [ @@ -297,7 +309,7 @@ public function testIndexBy(): void $index = 1; foreach ($mapCollection as $key => $value) { $this->assertSame('index' . $index, $key); - $this->assertInstanceOf(ExtraEntity::class, $value); + $this->assertInstanceOf(ExtraEntity::class, $value); // @phpstan-ignore method.alreadyNarrowedType $index++; } } @@ -324,7 +336,7 @@ public function testColumn(): void $values = $collection1->column(fn (RandomEntity $r) => $r->value); $this->assertCount(5, $values); - $this->assertIsList($values); + $this->assertIsList($values); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(['1', '2', '3', '4', '5'], $values); } diff --git a/tests/ScalarMapTest.php b/tests/ScalarMapTest.php index 43f2d5d..9548fee 100644 --- a/tests/ScalarMapTest.php +++ b/tests/ScalarMapTest.php @@ -20,7 +20,7 @@ public function testGetStringWithDefaultValue(): void { $map = $this->createMap(); $value = $map->getString('testStringNotFound', 'random'); - $this->assertIsString($value); + $this->assertIsString($value); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame('random', $value); } @@ -55,7 +55,7 @@ public function testGetIntWithDefaultValue(): void { $map = $this->createMap(); $value = $map->getInt('testIntNotFound', 42); - $this->assertIsInt($value); + $this->assertIsInt($value); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(42, $value); } @@ -76,7 +76,7 @@ public function testGetFloatWithDefaultValue(): void { $map = $this->createMap(); $value = $map->getFloat('testFloatNotFound', 42.1); - $this->assertIsFloat($value); + $this->assertIsFloat($value); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(42.1, $value); } @@ -100,7 +100,7 @@ public function testGetBoolWithDefaultValue(): void { $map = $this->createMap(); $value = $map->getBool('testBoolNotFound', true); - $this->assertIsBool($value); + $this->assertIsBool($value); // @phpstan-ignore method.alreadyNarrowedType $this->assertTrue($value); }