Skip to content

Commit bab4932

Browse files
committed
TASK: Add PropertyMapperReturnTypeExtension
Resolves: #69
1 parent 8e532dd commit bab4932

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ services:
7777
- phpstan.broker.dynamicMethodReturnTypeExtension
7878
-
7979
class: SaschaEgerer\PhpstanTypo3\Service\ValidatorClassNameResolver
80+
-
81+
class: SaschaEgerer\PhpstanTypo3\Type\PropertyMapperReturnTypeExtension
82+
tags:
83+
- phpstan.broker.dynamicMethodReturnTypeExtension
8084

8185
parameters:
8286
bootstrapFiles:
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Expr\ClassConstFetch;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\Scalar\String_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\BooleanType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\ObjectType;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
use TYPO3\CMS\Extbase\Property\PropertyMapper;
22+
23+
final class PropertyMapperReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
26+
/** @var ReflectionProvider */
27+
private $reflectionProvider;
28+
29+
public function __construct(ReflectionProvider $reflectionProvider)
30+
{
31+
$this->reflectionProvider = $reflectionProvider;
32+
}
33+
34+
public function getClass(): string
35+
{
36+
return PropertyMapper::class;
37+
}
38+
39+
public function isMethodSupported(MethodReflection $methodReflection): bool
40+
{
41+
return $methodReflection->getName() === 'convert';
42+
}
43+
44+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
45+
{
46+
$targetTypeArgument = $methodCall->getArgs()[1] ?? null;
47+
48+
if ($targetTypeArgument === null) {
49+
return null;
50+
}
51+
52+
$argumentValue = $targetTypeArgument->value;
53+
54+
if ($argumentValue instanceof ClassConstFetch) {
55+
/** @var Name $class */
56+
$class = $argumentValue->class;
57+
return TypeCombinator::addNull(new ObjectType((string) $class));
58+
}
59+
60+
if ($argumentValue instanceof String_) {
61+
return $this->createTypeFromString($argumentValue);
62+
}
63+
64+
return null;
65+
}
66+
67+
private function createTypeFromString(String_ $node): ?Type
68+
{
69+
if ($node->value === 'array') {
70+
return TypeCombinator::addNull(new ArrayType(new MixedType(), new MixedType()));
71+
}
72+
73+
if ($node->value === 'string') {
74+
return TypeCombinator::addNull(new StringType());
75+
}
76+
77+
if ($node->value === 'boolean') {
78+
return TypeCombinator::addNull(new BooleanType());
79+
}
80+
81+
if ($node->value === 'integer') {
82+
return TypeCombinator::addNull(new IntegerType());
83+
}
84+
85+
return $this->createTypeFromClassNameString($node);
86+
}
87+
88+
private function createTypeFromClassNameString(String_ $node): ?Type
89+
{
90+
$classLikeName = $node->value;
91+
92+
// remove leading slash
93+
$classLikeName = ltrim($classLikeName, '\\');
94+
if ($classLikeName === '') {
95+
return null;
96+
}
97+
98+
if (! $this->reflectionProvider->hasClass($classLikeName)) {
99+
return null;
100+
}
101+
102+
$classReflection = $this->reflectionProvider->getClass($classLikeName);
103+
if ($classReflection->getName() !== $classLikeName) {
104+
return null;
105+
}
106+
107+
// possibly string
108+
if (ctype_lower($classLikeName[0])) {
109+
return null;
110+
}
111+
112+
return TypeCombinator::addNull(new ObjectType($classLikeName));
113+
}
114+
115+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\PropertyMapperReturnTypeExtension;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
final class PropertyMapperReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<mixed>
12+
*/
13+
public function dataFileAsserts(): iterable
14+
{
15+
yield from $this->gatherAssertTypes(__DIR__ . '/data/property-converter-types.php');
16+
}
17+
18+
/**
19+
* @dataProvider dataFileAsserts
20+
* @param string $assertType
21+
* @param string $file
22+
* @param mixed ...$args
23+
*/
24+
public function testFileAsserts(
25+
string $assertType,
26+
string $file,
27+
...$args
28+
): void
29+
{
30+
$this->assertFileAsserts($assertType, $file, ...$args);
31+
}
32+
33+
public static function getAdditionalConfigFiles(): array
34+
{
35+
return [__DIR__ . '/../../../../extension.neon'];
36+
}
37+
38+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
// phpcs:disable SlevomatCodingStandard.Namespaces.RequireOneNamespaceInFile.MoreNamespacesInFile
4+
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch
5+
// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
6+
7+
namespace SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\PropertyMapperReturnTypeExtension;
8+
9+
use TYPO3\CMS\Core\Utility\GeneralUtility;
10+
use TYPO3\CMS\Extbase\Domain\Model\File;
11+
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
12+
use TYPO3\CMS\Extbase\Property\PropertyMapper;
13+
use function PHPStan\Testing\assertType;
14+
15+
class MyController extends ActionController
16+
{
17+
18+
public function convertProperties(): void
19+
{
20+
$propertyMapper = GeneralUtility::makeInstance(PropertyMapper::class);
21+
22+
$frontendUser = $propertyMapper->convert(['username' => 'username'], File::class);
23+
assertType(File::class . '|null', $frontendUser);
24+
25+
$user = $propertyMapper->convert(['username' => 'username'], 'TYPO3\CMS\Extbase\Domain\Model\File');
26+
assertType(File::class . '|null', $user);
27+
28+
$array = $propertyMapper->convert('1,2,3', 'array');
29+
assertType('array|null', $array);
30+
31+
$string = $propertyMapper->convert(1, 'string');
32+
assertType('string|null', $string);
33+
34+
$integer = $propertyMapper->convert('1', 'integer');
35+
assertType('int|null', $integer);
36+
37+
$boolean = $propertyMapper->convert('1', 'boolean');
38+
assertType('bool|null', $boolean);
39+
40+
$mixed = $propertyMapper->convert('1', 'whatever');
41+
assertType('mixed', $mixed);
42+
}
43+
44+
}

0 commit comments

Comments
 (0)