Skip to content

Commit b1c8f18

Browse files
Merge pull request #178 from sascha-egerer/bugfix/fix-object-storage-template
Fix invalid interface implementation by dynamic return type
2 parents 0246c63 + 45f4b20 commit b1c8f18

File tree

7 files changed

+91
-30
lines changed

7 files changed

+91
-30
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ services:
88
class: SaschaEgerer\PhpstanTypo3\Type\ValidatorResolverDynamicReturnTypeExtension
99
tags:
1010
- phpstan.broker.dynamicMethodReturnTypeExtension
11+
-
12+
class: SaschaEgerer\PhpstanTypo3\Type\ObjectStorageDynamicReturnTypeExtension
13+
tags:
14+
- phpstan.broker.dynamicMethodReturnTypeExtension
1115
-
1216
class: SaschaEgerer\PhpstanTypo3\Type\ContextDynamicReturnTypeExtension
1317
arguments:

phpstan.neon

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,10 @@ parameters:
1818
- '*tests/*/Source/*'
1919
- '*tests/*/data/*'
2020
ignoreErrors:
21-
-
22-
message: '#^Class TYPO3\\CMS\\Core\\Context\\[a-zA-Z]* not found\.#'
23-
path: src/Type/ContextDynamicReturnTypeExtension.php
2421
-
2522
message: "#^Calling PHPStan\\\\Reflection\\\\InitializerExprTypeResolver\\:\\:getClassConstFetchType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#"
2623
count: 1
2724
path: src/Rule/ValidatorResolverOptionsRule.php
28-
-
29-
message: "#^Method SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroupCustomFindAllWithoutModelTypeRepository\\:\\:findAll\\(\\) return type with generic interface TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface does not specify its types\\: ModelType$#"
30-
count: 1
31-
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php
32-
-
33-
message: "#^PHPDoc tag @var for variable \\$queryResult contains generic class TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Generic\\\\QueryResult but does not specify its types\\: ModelType$#"
34-
count: 2
35-
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php
36-
-
37-
message: "#^Return type \\(TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface\\) of method SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroupCustomFindAllWithoutModelTypeRepository\\:\\:findAll\\(\\) should be covariant with return type \\(array\\<int, SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\|TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface\\<SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\) of method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\<SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\:\\:findAll\\(\\)$#"
38-
count: 1
39-
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php
4025
-
4126
message: '#^Although PHPStan\\Reflection\\Php\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it''s not guaranteed to always stay the same\.$#'
4227
identifier: phpstanApi.instanceofAssumption
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10+
use PHPStan\Type\IntegerType;
11+
use PHPStan\Type\MixedType;
12+
use PHPStan\Type\StringType;
13+
use PHPStan\Type\Type;
14+
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
15+
16+
class ObjectStorageDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
17+
{
18+
19+
public function getClass(): string
20+
{
21+
return ObjectStorage::class;
22+
}
23+
24+
public function isMethodSupported(
25+
MethodReflection $methodReflection
26+
): bool
27+
{
28+
return $methodReflection->getName() === 'offsetGet';
29+
}
30+
31+
public function getTypeFromMethodCall(
32+
MethodReflection $methodReflection,
33+
MethodCall $methodCall,
34+
Scope $scope
35+
): ?Type
36+
{
37+
$firstArgument = $methodCall->args[0] ?? null;
38+
39+
if (!$firstArgument instanceof Arg) {
40+
return null;
41+
}
42+
43+
$argumentType = $scope->getType($firstArgument->value);
44+
45+
if ((new StringType())->isSuperTypeOf($argumentType)->yes()) {
46+
return $methodReflection->getVariants()[0]->getReturnType();
47+
}
48+
49+
if ((new IntegerType())->isSuperTypeOf($argumentType)->yes()) {
50+
return $methodReflection->getVariants()[0]->getReturnType();
51+
}
52+
53+
return new MixedType();
54+
}
55+
56+
}

stubs/ObjectStorage.stub

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
namespace TYPO3\CMS\Extbase\Persistence;
33

44
/**
5-
* @template TEntity
5+
* @template TEntity of object
66
* @implements \ArrayAccess<string, TEntity>
77
* @implements \Iterator<string, TEntity>
88
* @phpstan-type ObjectStorageInternal array{obj: TEntity, inf: mixed}
99
*/
1010
class ObjectStorage implements \Iterator, \ArrayAccess
1111
{
1212
/**
13-
* @param TEntity|int|string $value
14-
* @return ($value is int ? TEntity|null : mixed)
13+
* @param TEntity|string|int $value
14+
* @phpstan-return TEntity|null
1515
*/
16-
public function offsetGet($value);
16+
public function offsetGet(mixed $value);
1717
}

stubs/QueryResultInterface.stub

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
namespace TYPO3\CMS\Extbase\Persistence;
33

44
/**
5-
* @template TKey
5+
* @template TKey of int
66
* @template TValue of object
77
* @extends \Iterator<TKey, TValue>
88
* @extends \ArrayAccess<TKey, TValue>

tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ public function showAction(): void
135135
'list<SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\QueryResultToArrayDynamicReturnTypeExtension\FrontendUserGroup>',
136136
$myObjects
137137
);
138+
139+
$key = $queryResult->key();
140+
assertType(
141+
'int',
142+
$key
143+
);
138144
}
139145

140146
}

tests/Unit/Type/data/object-storage-stub-files.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,40 @@
1313
class MyModel extends AbstractEntity
1414
{
1515

16-
public function foo(): void
16+
/**
17+
* @var ObjectStorage<self>
18+
*/
19+
protected ObjectStorage $testStorage;
20+
21+
public function checkObjectStorageType(): void
1722
{
1823
$myModel = new self();
1924
/** @var ObjectStorage<MyModel> $objectStorage */
2025
$objectStorage = new ObjectStorage();
2126
$objectStorage->attach($myModel);
2227

2328
assertType('TYPO3\CMS\Extbase\Persistence\ObjectStorage<' . self::class . '>', $objectStorage);
29+
}
2430

25-
foreach ($objectStorage as $key => $value) {
26-
31+
public function checkIteration(): void
32+
{
33+
foreach ($this->testStorage as $key => $value) {
2734
assertType('string', $key);
2835
assertType(self::class, $value);
2936
}
37+
}
3038

31-
assertType(self::class . '|null', $objectStorage->offsetGet(0));
39+
public function checkArrayAccess(): void
40+
{
41+
assertType(self::class . '|null', $this->testStorage->offsetGet(0));
42+
assertType(self::class . '|null', $this->testStorage->offsetGet('0'));
43+
assertType(self::class . '|null', $this->testStorage->current());
44+
assertType(self::class . '|null', $this->testStorage[0]);
3245

33-
// We ignore errors in the next line as this will produce an
34-
// "Offset 0 does not exist on TYPO3\CMS\Extbase\Persistence\ObjectStorage<ObjectStorage\My\Test\Extension\Domain\Model\MyModel>
35-
// due to the weird implementation of ArrayAccess in ObjectStorage::offsetGet()
36-
// @phpstan-ignore-next-line
37-
assertType(self::class . '|null', $objectStorage[0]);
46+
$myModel = new self();
3847

39-
assertType('mixed', $objectStorage->offsetGet($myModel));
48+
assertType('mixed', $this->testStorage->offsetGet($this->testStorage->current()));
49+
assertType('mixed', $this->testStorage->offsetGet($myModel));
4050
}
4151

4252
}

0 commit comments

Comments
 (0)