Skip to content

Commit 2823e8d

Browse files
authored
ENGCOM-7566: PHPStan add support of magic methods of Data Object. #27905
2 parents dab4a1a + 0606e39 commit 2823e8d

File tree

10 files changed

+667
-2
lines changed

10 files changed

+667
-2
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\PhpStan\Reflection\Php;
9+
10+
use Magento\Framework\DataObject;
11+
use Magento\Framework\Session\SessionManager;
12+
use PHPStan\DependencyInjection\Container;
13+
use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension;
14+
use PHPStan\Reflection\ClassReflection;
15+
use PHPStan\Reflection\MethodReflection;
16+
use PHPStan\Reflection\MethodsClassReflectionExtension;
17+
18+
/**
19+
* Extension to add support of magic methods (get/set/uns/has) based on @see DataObject
20+
*/
21+
class DataObjectClassReflectionExtension implements MethodsClassReflectionExtension
22+
{
23+
private const MAGIC_METHODS_PREFIXES = [
24+
'get',
25+
'set',
26+
'uns',
27+
'has'
28+
];
29+
30+
private const PREFIX_LENGTH = 3;
31+
32+
/**
33+
* @var Container
34+
*/
35+
private $container;
36+
37+
/**
38+
* @var AnnotationsMethodsClassReflectionExtension
39+
*/
40+
private $annotationsMethodsClassReflectionExtension;
41+
42+
/**
43+
* @param Container $container
44+
*/
45+
public function __construct(Container $container)
46+
{
47+
$this->container = $container;
48+
$this->annotationsMethodsClassReflectionExtension = $this->container
49+
->getByType(AnnotationsMethodsClassReflectionExtension::class);
50+
}
51+
52+
/**
53+
* Check if class has relations with DataObject and requested method can be considered as a magic method.
54+
*
55+
* @param ClassReflection $classReflection
56+
* @param string $methodName
57+
*
58+
* @return bool
59+
*/
60+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
61+
{
62+
// Workaround due to annotation extension always loads last.
63+
if ($this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodName)) {
64+
// In case when annotation already available for the method, we will not use 'magic methods' approach.
65+
return false;
66+
}
67+
if ($classReflection->isSubclassOf(DataObject::class) || $classReflection->getName() == DataObject::class) {
68+
return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES);
69+
}
70+
/** SessionManager redirects all calls to `__call` to container which extends DataObject */
71+
if ($classReflection->isSubclassOf(SessionManager::class)
72+
|| $classReflection->getName() === SessionManager::class
73+
) {
74+
/** @see \Magento\Framework\Session\SessionManager::__call */
75+
return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES);
76+
}
77+
78+
return false;
79+
}
80+
81+
/**
82+
* Get prefix from method name.
83+
*
84+
* @param string $methodName
85+
*
86+
* @return string
87+
*/
88+
private function getPrefix(string $methodName): string
89+
{
90+
return (string)substr($methodName, 0, self::PREFIX_LENGTH);
91+
}
92+
93+
/**
94+
* Get method reflection instance.
95+
*
96+
* @param ClassReflection $classReflection
97+
* @param string $methodName
98+
*
99+
* @return MethodReflection
100+
*/
101+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
102+
{
103+
return new DataObjectMethodReflection($classReflection, $methodName);
104+
}
105+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\PhpStan\Reflection\Php;
9+
10+
use PHPStan\Reflection\ClassMemberReflection;
11+
use PHPStan\Reflection\ClassReflection;
12+
use PHPStan\Reflection\FunctionVariant;
13+
use PHPStan\Reflection\MethodReflection;
14+
use PHPStan\Reflection\ParameterReflection;
15+
use PHPStan\Reflection\ParametersAcceptor;
16+
use PHPStan\Reflection\Php\DummyParameter;
17+
use PHPStan\TrinaryLogic;
18+
use PHPStan\Type\BooleanType;
19+
use PHPStan\Type\Generic\TemplateTypeMap;
20+
use PHPStan\Type\IntegerType;
21+
use PHPStan\Type\MixedType;
22+
use PHPStan\Type\ObjectType;
23+
use PHPStan\Type\StringType;
24+
use PHPStan\Type\Type;
25+
use PHPStan\Type\UnionType;
26+
use PHPStan\Type\VoidType;
27+
28+
/**
29+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
30+
*/
31+
class DataObjectMethodReflection implements MethodReflection
32+
{
33+
private const PREFIX_LENGTH = 3;
34+
35+
/**
36+
* @var ClassReflection
37+
*/
38+
private $classReflection;
39+
40+
/**
41+
* @var string
42+
*/
43+
private $methodName;
44+
45+
/**
46+
* @param ClassReflection $classReflection
47+
* @param string $methodName
48+
*/
49+
public function __construct(ClassReflection $classReflection, string $methodName)
50+
{
51+
$this->classReflection = $classReflection;
52+
$this->methodName = $methodName;
53+
}
54+
55+
/**
56+
* Get methods class reflection.
57+
*
58+
* @return ClassReflection
59+
*/
60+
public function getDeclaringClass(): ClassReflection
61+
{
62+
return $this->classReflection;
63+
}
64+
65+
/**
66+
* Get if method is static.
67+
*
68+
* @return bool
69+
*/
70+
public function isStatic(): bool
71+
{
72+
return false;
73+
}
74+
75+
/**
76+
* Get if method is private.
77+
*
78+
* @return bool
79+
*/
80+
public function isPrivate(): bool
81+
{
82+
return false;
83+
}
84+
85+
/**
86+
* Get if method is public.
87+
*
88+
* @return bool
89+
*/
90+
public function isPublic(): bool
91+
{
92+
return true;
93+
}
94+
95+
/**
96+
* Get Method PHP Doc comment message.
97+
*
98+
* @return string|null
99+
*/
100+
public function getDocComment(): ?string
101+
{
102+
return null;
103+
}
104+
105+
/**
106+
* Get Method Name.
107+
*
108+
* @return string
109+
*/
110+
public function getName(): string
111+
{
112+
return $this->methodName;
113+
}
114+
115+
/**
116+
* Get Prototype.
117+
*
118+
* @return ClassMemberReflection
119+
*/
120+
public function getPrototype(): ClassMemberReflection
121+
{
122+
return $this;
123+
}
124+
125+
/**
126+
* Get Magic Methods variant based on type (get/set/has/uns).
127+
*
128+
* @return ParametersAcceptor[]
129+
*/
130+
public function getVariants(): array
131+
{
132+
return [
133+
new FunctionVariant(
134+
TemplateTypeMap::createEmpty(),
135+
TemplateTypeMap::createEmpty(),
136+
$this->getMethodParameters(),
137+
false,
138+
$this->getReturnType()
139+
)
140+
];
141+
}
142+
143+
/**
144+
* Get prefix from method name.
145+
*
146+
* @return string
147+
*/
148+
private function getMethodNamePrefix(): string
149+
{
150+
return (string)substr($this->methodName, 0, self::PREFIX_LENGTH);
151+
}
152+
153+
/**
154+
* Get Magic Methods parameters.
155+
*
156+
* @return ParameterReflection[]
157+
*/
158+
private function getMethodParameters(): array
159+
{
160+
$params = [];
161+
switch ($this->getMethodNamePrefix()) {
162+
case 'set':
163+
$params[] = new DummyParameter(
164+
'value',
165+
new MixedType(),
166+
false,
167+
null,
168+
false,
169+
null
170+
);
171+
break;
172+
case 'get':
173+
$params[] = new DummyParameter(
174+
'index',
175+
new UnionType([new StringType(), new IntegerType()]),
176+
true,
177+
null,
178+
false,
179+
null
180+
);
181+
break;
182+
}
183+
184+
return $params;
185+
}
186+
187+
/**
188+
* Get Magic Methods return type.
189+
*
190+
* @return Type
191+
*/
192+
private function getReturnType(): Type
193+
{
194+
switch ($this->getMethodNamePrefix()) {
195+
case 'set':
196+
case 'uns':
197+
$returnType = new ObjectType($this->classReflection->getName());
198+
break;
199+
case 'has':
200+
$returnType = new BooleanType();
201+
break;
202+
default:
203+
$returnType = new MixedType();
204+
break;
205+
}
206+
207+
return $returnType;
208+
}
209+
210+
/**
211+
* Get if method is deprecated.
212+
*
213+
* @return TrinaryLogic
214+
*/
215+
public function isDeprecated(): TrinaryLogic
216+
{
217+
return TrinaryLogic::createNo();
218+
}
219+
220+
/**
221+
* Get deprecated description.
222+
*
223+
* @return string|null
224+
*/
225+
public function getDeprecatedDescription(): ?string
226+
{
227+
return null;
228+
}
229+
230+
/**
231+
* Get if method is final.
232+
*
233+
* @return TrinaryLogic
234+
*/
235+
public function isFinal(): TrinaryLogic
236+
{
237+
return TrinaryLogic::createNo();
238+
}
239+
240+
/**
241+
* Get if method is internal.
242+
*
243+
* @return TrinaryLogic
244+
*/
245+
public function isInternal(): TrinaryLogic
246+
{
247+
return TrinaryLogic::createNo();
248+
}
249+
250+
/**
251+
* Get method throw type.
252+
*
253+
* @return Type|null
254+
*/
255+
public function getThrowType(): ?Type
256+
{
257+
return new VoidType();
258+
}
259+
260+
/**
261+
* Get if method has side effect.
262+
*
263+
* @return TrinaryLogic
264+
*/
265+
public function hasSideEffects(): TrinaryLogic
266+
{
267+
return TrinaryLogic::createYes();
268+
}
269+
}

dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/PhpStan.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class PhpStan implements ToolInterface
1919
*
2020
* @see https://github.com/phpstan/phpstan#rule-levels
2121
*/
22-
private const RULE_LEVEL = 0;
22+
private const RULE_LEVEL = 1;
2323

2424
/**
2525
* Memory limit required by PHPStan for full Magento project scan.

0 commit comments

Comments
 (0)