Skip to content

Commit 80dce07

Browse files
Merge pull request #60 from sascha-egerer/feature/allow-phpdoctypes-in-mappings
Allow more complex phpdoc types in mappings and add more tests
2 parents 991e85d + 3edeb0f commit 80dce07

12 files changed

+187
-15
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ what type of class is returned by Request::getAttribute()
5050
```
5151
parameters:
5252
typo3:
53-
requestApiGetAttributeMapping:
53+
requestGetAttributeMapping:
5454
myAttribute: FlowdGmbh\MyProject\Http\MyAttribute
55+
myNullableAttribute: FlowdGmbh\MyProject\Http\MyAttribute|null
5556
```
5657

5758
```

extension.neon

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ services:
3939
contextApiGetAspectMapping: %typo3.contextApiGetAspectMapping%
4040
tags:
4141
- phpstan.rules.rule
42+
-
43+
class: SaschaEgerer\PhpstanTypo3\Rule\RequestAttributeValidationRule
44+
arguments:
45+
requestGetAttributeMapping: %typo3.requestGetAttributeMapping%
46+
tags:
47+
- phpstan.rules.rule
4248
-
4349
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryQueryDynamicReturnTypeExtension
4450
tags:
@@ -50,7 +56,7 @@ services:
5056
-
5157
class: SaschaEgerer\PhpstanTypo3\Type\RequestDynamicReturnTypeExtension
5258
arguments:
53-
requestApiGetAttributeMapping: %typo3.requestApiGetAttributeMapping%
59+
requestGetAttributeMapping: %typo3.requestGetAttributeMapping%
5460
tags:
5561
- phpstan.broker.dynamicMethodReturnTypeExtension
5662
parameters:
@@ -65,14 +71,16 @@ parameters:
6571
workspace: TYPO3\CMS\Core\Context\WorkspaceAspect
6672
language: TYPO3\CMS\Core\Context\LanguageAspect
6773
typoscript: TYPO3\CMS\Core\Context\TypoScriptAspect
68-
requestApiGetAttributeMapping:
74+
requestGetAttributeMapping:
6975
backend.user: TYPO3\CMS\Backend\FrontendBackendUserAuthentication
7076
frontend.user: TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication
7177
language: TYPO3\CMS\Core\Site\Entity\SiteLanguage
7278
module: TYPO3\CMS\Backend\Module\ModuleInterface
7379
moduleData: TYPO3\CMS\Backend\Module\ModuleData
7480
normalizedParams: TYPO3\CMS\Core\Http\NormalizedParams
7581
site: TYPO3\CMS\Core\Site\Entity\Site
82+
applicationType: TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_*
83+
routing: TYPO3\CMS\Core\Routing\SiteRouteResult|TYPO3\CMS\Core\Routing\PageArguments
7684
stubFiles:
7785
- stubs/DomainObjectInterface.stub
7886
- stubs/GeneralUtility.stub
@@ -126,5 +134,5 @@ parameters:
126134
parametersSchema:
127135
typo3: structure([
128136
contextApiGetAspectMapping: arrayOf(string())
129-
requestApiGetAttributeMapping: arrayOf(string())
137+
requestGetAttributeMapping: arrayOf(string())
130138
])

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
includes:
22
- vendor/phpstan/phpstan-strict-rules/rules.neon
33
- extension.neon
4+
- tests/Unit/Type/data/context-get-aspect-return-types.neon
5+
- tests/Unit/Type/data/request-get-attribute-return-types.neon
46

57
parameters:
68
level: 8
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Rule;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\RuleErrorBuilder;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
10+
/**
11+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall>
12+
*/
13+
class RequestAttributeValidationRule implements \PHPStan\Rules\Rule
14+
{
15+
16+
/** @var array<string, string> */
17+
private $requestGetAttributeMapping;
18+
19+
/**
20+
* @param array<string, string> $requestGetAttributeMapping
21+
*/
22+
public function __construct(array $requestGetAttributeMapping)
23+
{
24+
$this->requestGetAttributeMapping = $requestGetAttributeMapping;
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return Node\Expr\MethodCall::class;
30+
}
31+
32+
public function processNode(Node $node, Scope $scope): array
33+
{
34+
if (!$node->name instanceof Node\Identifier) {
35+
return [];
36+
}
37+
38+
$methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString());
39+
if ($methodReflection === null || $methodReflection->getName() !== 'getAttribute') {
40+
return [];
41+
}
42+
43+
$declaringClass = $methodReflection->getDeclaringClass();
44+
45+
if (!$declaringClass->implementsInterface(ServerRequestInterface::class) && $declaringClass->getName() !== ServerRequestInterface::class) {
46+
return [];
47+
}
48+
49+
$argument = $node->getArgs()[0] ?? null;
50+
51+
if (!($argument instanceof Node\Arg) || !($argument->value instanceof Node\Scalar\String_)) {
52+
return [];
53+
}
54+
55+
if (isset($this->requestGetAttributeMapping[$argument->value->value])) {
56+
return [];
57+
}
58+
59+
$ruleError = RuleErrorBuilder::message(sprintf(
60+
'There is no request attribute "%s" configured so we can\'t figure out the exact type to return when calling %s::%s',
61+
$argument->value->value,
62+
$declaringClass->getDisplayName(),
63+
$methodReflection->getName()
64+
))->tip('You should add custom request attribute to the typo3.requestGetAttributeMapping setting.')->build();
65+
66+
return [$ruleError];
67+
}
68+
69+
}

src/Type/ContextDynamicReturnTypeExtension.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
use PhpParser\Node\Expr\MethodCall;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\TypeStringResolver;
78
use PHPStan\Reflection\MethodReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10-
use PHPStan\Type\ObjectType;
1111
use PHPStan\Type\Type;
1212

1313
class ContextDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
@@ -16,12 +16,16 @@ class ContextDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtens
1616
/** @var array<string, string> */
1717
private $contextApiGetAspectMapping;
1818

19+
/** @var TypeStringResolver */
20+
private $typeStringResolver;
21+
1922
/**
2023
* @param array<string, string> $contextApiGetAspectMapping
2124
*/
22-
public function __construct(array $contextApiGetAspectMapping)
25+
public function __construct(array $contextApiGetAspectMapping, TypeStringResolver $typeStringResolver)
2326
{
2427
$this->contextApiGetAspectMapping = $contextApiGetAspectMapping;
28+
$this->typeStringResolver = $typeStringResolver;
2529
}
2630

2731
public function getClass(): string
@@ -49,7 +53,7 @@ public function getTypeFromMethodCall(
4953
}
5054

5155
if (isset($this->contextApiGetAspectMapping[$argument->value->value])) {
52-
return new ObjectType($this->contextApiGetAspectMapping[$argument->value->value]);
56+
return $this->typeStringResolver->resolve($this->contextApiGetAspectMapping[$argument->value->value]);
5357
}
5458

5559
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

src/Type/RequestDynamicReturnTypeExtension.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,29 @@
44

55
use PhpParser\Node\Expr\MethodCall;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\TypeStringResolver;
78
use PHPStan\Reflection\MethodReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10-
use PHPStan\Type\ObjectType;
1111
use PHPStan\Type\Type;
12+
use PHPStan\Type\TypeCombinator;
1213

1314
class RequestDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
1415
{
1516

1617
/** @var array<string, string> */
17-
private $requestApiGetAttributeMapping;
18+
private $requestGetAttributeMapping;
19+
20+
/** @var TypeStringResolver */
21+
private $typeStringResolver;
1822

1923
/**
20-
* @param array<string, string> $requestApiGetAttributeMapping
24+
* @param array<string, string> $requestGetAttributeMapping
2125
*/
22-
public function __construct(array $requestApiGetAttributeMapping)
26+
public function __construct(array $requestGetAttributeMapping, TypeStringResolver $typeStringResolver)
2327
{
24-
$this->requestApiGetAttributeMapping = $requestApiGetAttributeMapping;
28+
$this->requestGetAttributeMapping = $requestGetAttributeMapping;
29+
$this->typeStringResolver = $typeStringResolver;
2530
}
2631

2732
public function getClass(): string
@@ -41,8 +46,8 @@ public function getTypeFromMethodCall(
4146
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
4247
}
4348

44-
if (isset($this->requestApiGetAttributeMapping[$argument->value->value])) {
45-
return new ObjectType($this->requestApiGetAttributeMapping[$argument->value->value]);
49+
if (isset($this->requestGetAttributeMapping[$argument->value->value])) {
50+
return TypeCombinator::addNull($this->typeStringResolver->resolve($this->requestGetAttributeMapping[$argument->value->value]));
4651
}
4752

4853
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

tests/Unit/Type/ContextDynamicReturnTypeExtensionTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ public function testFileAsserts(
3333

3434
public static function getAdditionalConfigFiles(): array
3535
{
36-
return [__DIR__ . '/../../../extension.neon'];
36+
return [
37+
__DIR__ . '/../../../extension.neon',
38+
__DIR__ . '/data/context-get-aspect-return-types.neon',
39+
];
3740
}
3841

3942
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Tests\Unit\Type;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class RequestGetAttributeDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<mixed>
12+
*/
13+
public function dataFileAsserts(): iterable
14+
{
15+
// path to a file with actual asserts of expected types:
16+
yield from $this->gatherAssertTypes(__DIR__ . '/data/request-get-attribute-return-types.php');
17+
}
18+
19+
/**
20+
* @dataProvider dataFileAsserts
21+
* @param string $assertType
22+
* @param string $file
23+
* @param mixed ...$args
24+
*/
25+
public function testFileAsserts(
26+
string $assertType,
27+
string $file,
28+
...$args
29+
): void
30+
{
31+
$this->assertFileAsserts($assertType, $file, ...$args);
32+
}
33+
34+
public static function getAdditionalConfigFiles(): array
35+
{
36+
return [
37+
__DIR__ . '/../../../extension.neon',
38+
__DIR__ . '/data/request-get-attribute-return-types.neon',
39+
];
40+
}
41+
42+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
typo3:
3+
contextApiGetAspectMapping:
4+
myCustomAspect: Foo\Bar|TYPO3\CMS\Core\Context\DateTimeAspect

tests/Unit/Type/data/context-get-aspect-return-types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function getAspectTests(Context $context): void
2727
assertType(UserAspect::class, $context->getAspect('frontend.user'));
2828
assertType(WorkspaceAspect::class, $context->getAspect('workspace'));
2929
assertType(LanguageAspect::class, $context->getAspect('language'));
30+
assertType('Foo\Bar|TYPO3\CMS\Core\Context\DateTimeAspect', $context->getAspect('myCustomAspect'));
3031
}
3132

3233
}

0 commit comments

Comments
 (0)