Skip to content

Commit 1b86945

Browse files
authored
Merge pull request #53 from tlueder/SiteDynamicReturnTypeExtension
Make attributes of Site Api configurable
2 parents 80dce07 + f574bb0 commit 1b86945

8 files changed

+260
-6
lines changed

README.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ TYPO3 CMS class reflection extension for PHPStan & framework-specific rules
88

99
To use this extension, require it in [Composer](https://getcomposer.org/):
1010

11-
```
11+
```Shell
1212
composer require --dev saschaegerer/phpstan-typo3
1313
```
1414

@@ -19,25 +19,26 @@ If you also install [phpstan/extension-installer](https://github.com/phpstan/ext
1919

2020
If you don't want to use `phpstan/extension-installer`, put this into your phpstan.neon config:
2121

22-
```
22+
```NEON
2323
includes:
2424
- vendor/saschaegerer/phpstan-typo3/extension.neon
2525
```
26+
2627
</details>
2728

2829
### Custom Context API Aspects
2930

3031
If you use custom aspects for the TYPO3 Context API you can add a mapping so PHPStan knows
3132
what type of aspect class is returned by the context API
3233

33-
```
34+
```NEON
3435
parameters:
3536
typo3:
3637
contextApiGetAspectMapping:
3738
myCustomAspect: FlowdGmbh\MyProject\Context\MyCustomAspect
3839
```
3940

40-
```
41+
```PHP
4142
// PHPStan will now know that $myCustomAspect is of type FlowdGmbh\MyProject\Context\MyCustomAspect
4243
$myCustomAspect = GeneralUtility::makeInstance(Context::class)->getAspect('myCustomAspect');
4344
```
@@ -47,15 +48,42 @@ $myCustomAspect = GeneralUtility::makeInstance(Context::class)->getAspect('myCus
4748
If you use custom PSR-7 request attribute you can add a mapping so PHPStan knows
4849
what type of class is returned by Request::getAttribute()
4950

50-
```
51+
```NEON
5152
parameters:
5253
typo3:
5354
requestGetAttributeMapping:
5455
myAttribute: FlowdGmbh\MyProject\Http\MyAttribute
5556
myNullableAttribute: FlowdGmbh\MyProject\Http\MyAttribute|null
5657
```
5758

58-
```
59+
```PHP
5960
// PHPStan will now know that $myAttribute is of type FlowdGmbh\MyProject\Http\MyAttribute
6061
$myAttribute = $request->getAttribute('myAttribute');
6162
```
63+
64+
### Custom Site Attribute
65+
66+
If you use custom attributes for the TYPO3 Site API you can add a mapping so PHPStan knows
67+
what type is returned by the site API
68+
69+
```NEON
70+
parameters:
71+
typo3:
72+
siteGetAttributeMapping:
73+
myArrayAttribute: array
74+
myIntAttribute: int
75+
myStringAttribute: string
76+
```
77+
78+
```PHP
79+
$site = $this->request->getAttribute('site');
80+
81+
// PHPStan will now know that $myArrayAttribute is of type array<mixed, mixed>
82+
$myArrayAttribute = $site->getAttribute('myArrayAttribute');
83+
84+
// PHPStan will now know that $myIntAttribute is of type int
85+
$myIntAttribute = $site->getAttribute('myIntAttribute');
86+
87+
// PHPStan will now know that $myStringAttribute is of type string
88+
$myStringAttribute = $site->getAttribute('myStringAttribute');
89+
```

extension.neon

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ services:
4545
requestGetAttributeMapping: %typo3.requestGetAttributeMapping%
4646
tags:
4747
- phpstan.rules.rule
48+
-
49+
class: SaschaEgerer\PhpstanTypo3\Rule\SiteAttributeValidationRule
50+
arguments:
51+
siteGetAttributeMapping: %typo3.siteGetAttributeMapping%
52+
tags:
53+
- phpstan.rules.rule
4854
-
4955
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryQueryDynamicReturnTypeExtension
5056
tags:
@@ -59,6 +65,12 @@ services:
5965
requestGetAttributeMapping: %typo3.requestGetAttributeMapping%
6066
tags:
6167
- phpstan.broker.dynamicMethodReturnTypeExtension
68+
-
69+
class: SaschaEgerer\PhpstanTypo3\Type\SiteDynamicReturnTypeExtension
70+
arguments:
71+
siteGetAttributeMapping: %typo3.siteGetAttributeMapping%
72+
tags:
73+
- phpstan.broker.dynamicMethodReturnTypeExtension
6274
parameters:
6375
bootstrapFiles:
6476
- phpstan.bootstrap.php
@@ -81,6 +93,14 @@ parameters:
8193
site: TYPO3\CMS\Core\Site\Entity\Site
8294
applicationType: TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_*
8395
routing: TYPO3\CMS\Core\Routing\SiteRouteResult|TYPO3\CMS\Core\Routing\PageArguments
96+
siteGetAttributeMapping:
97+
base: string
98+
baseVariants: list
99+
errorHandling: list
100+
languages: list
101+
rootPageId: int
102+
routeEnhancers: array
103+
websiteTitle: string
84104
stubFiles:
85105
- stubs/DomainObjectInterface.stub
86106
- stubs/GeneralUtility.stub
@@ -135,4 +155,5 @@ parametersSchema:
135155
typo3: structure([
136156
contextApiGetAspectMapping: arrayOf(string())
137157
requestGetAttributeMapping: arrayOf(string())
158+
siteGetAttributeMapping: arrayOf(string())
138159
])

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ includes:
33
- extension.neon
44
- tests/Unit/Type/data/context-get-aspect-return-types.neon
55
- tests/Unit/Type/data/request-get-attribute-return-types.neon
6+
- tests/Unit/Type/data/site-get-attribute-return-types.neon
67

78
parameters:
89
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 TYPO3\CMS\Core\Site\Entity\Site;
9+
10+
/**
11+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall>
12+
*/
13+
class SiteAttributeValidationRule implements \PHPStan\Rules\Rule
14+
{
15+
16+
/** @var array<string, string> */
17+
private $siteGetAttributeMapping;
18+
19+
/**
20+
* @param array<string, string> $siteGetAttributeMapping
21+
*/
22+
public function __construct(array $siteGetAttributeMapping)
23+
{
24+
$this->siteGetAttributeMapping = $siteGetAttributeMapping;
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(Site::class) && $declaringClass->getName() !== Site::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->siteGetAttributeMapping[$argument->value->value])) {
56+
return [];
57+
}
58+
59+
$ruleError = RuleErrorBuilder::message(sprintf(
60+
'There is no site 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 site attribute to the typo3.siteGetAttributeMapping setting.')->build();
65+
66+
return [$ruleError];
67+
}
68+
69+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\TypeStringResolver;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\Type;
12+
13+
class SiteDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
14+
{
15+
16+
/** @var array<string, string> */
17+
private $siteGetAttributeMapping;
18+
19+
/** @var TypeStringResolver */
20+
private $typeStringResolver;
21+
22+
/**
23+
* @param array<string, string> $siteGetAttributeMapping
24+
*/
25+
public function __construct(array $siteGetAttributeMapping, TypeStringResolver $typeStringResolver)
26+
{
27+
$this->siteGetAttributeMapping = $siteGetAttributeMapping;
28+
$this->typeStringResolver = $typeStringResolver;
29+
}
30+
31+
public function getClass(): string
32+
{
33+
return \TYPO3\CMS\Core\Site\Entity\Site::class;
34+
}
35+
36+
public function getTypeFromMethodCall(
37+
MethodReflection $methodReflection,
38+
MethodCall $methodCall,
39+
Scope $scope
40+
): Type
41+
{
42+
$argument = $methodCall->getArgs()[0] ?? null;
43+
44+
if ($argument === null || !($argument->value instanceof \PhpParser\Node\Scalar\String_)) {
45+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
46+
}
47+
48+
if (isset($this->siteGetAttributeMapping[$argument->value->value])) {
49+
return $this->typeStringResolver->resolve($this->siteGetAttributeMapping[$argument->value->value]);
50+
}
51+
52+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
53+
}
54+
55+
public function isMethodSupported(
56+
MethodReflection $methodReflection
57+
): bool
58+
{
59+
return $methodReflection->getName() === 'getAttribute';
60+
}
61+
62+
}
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 SiteGetAttributeDynamicReturnTypeExtensionTest 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/site-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/site-get-attribute-return-types.neon',
39+
];
40+
}
41+
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
typo3:
3+
siteGetAttributeMapping:
4+
myCustomIntAttribute: int
5+
myCustomStringAttribute: string
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SiteGetAttributeReturnTypes;
4+
5+
use TYPO3\CMS\Core\Site\Entity\Site;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
// phpcs:ignore Squiz.Classes.ClassFileName.NoMatch
10+
class MySite
11+
{
12+
13+
public function getAttributeTests(Site $site): void
14+
{
15+
assertType('string', $site->getAttribute('base'));
16+
assertType('array<int, mixed>', $site->getAttribute('baseVariants'));
17+
assertType('array<int, mixed>', $site->getAttribute('errorHandling'));
18+
assertType('array<int, mixed>', $site->getAttribute('languages'));
19+
assertType('int', $site->getAttribute('rootPageId'));
20+
assertType('array', $site->getAttribute('routeEnhancers'));
21+
assertType('string', $site->getAttribute('websiteTitle'));
22+
assertType('int', $site->getAttribute('myCustomIntAttribute'));
23+
assertType('string', $site->getAttribute('myCustomStringAttribute'));
24+
}
25+
26+
}

0 commit comments

Comments
 (0)