Skip to content

Commit 5b89457

Browse files
authored
Merge pull request Weebly#5 from Ascendens/master
Support of @mixin annotation
2 parents 440c84b + 5f51879 commit 5b89457

File tree

6 files changed

+365
-3
lines changed

6 files changed

+365
-3
lines changed

src/BuilderMethodExtension.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Reflection\ClassReflection;
1111
use PHPStan\Reflection\MethodsClassReflectionExtension;
1212
use PHPStan\Reflection\MethodReflection;
13+
use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper;
1314

1415
final class BuilderMethodExtension implements MethodsClassReflectionExtension, BrokerAwareExtension
1516
{
@@ -28,14 +29,21 @@ final class BuilderMethodExtension implements MethodsClassReflectionExtension, B
2829
*/
2930
private $methodReflectionFactory;
3031

32+
/**
33+
* @var AnnotationsHelper
34+
*/
35+
private $annotationsHelper;
36+
3137
/**
3238
* BuilderMethodExtension constructor.
3339
*
3440
* @param \Weebly\PHPStan\Laravel\MethodReflectionFactory $methodReflectionFactory
41+
* @param AnnotationsHelper $annotationsHelper
3542
*/
36-
public function __construct(MethodReflectionFactory $methodReflectionFactory)
43+
public function __construct(MethodReflectionFactory $methodReflectionFactory, AnnotationsHelper $annotationsHelper)
3744
{
3845
$this->methodReflectionFactory = $methodReflectionFactory;
46+
$this->annotationsHelper = $annotationsHelper;
3947
}
4048

4149
/**
@@ -51,7 +59,10 @@ public function setBroker(Broker $broker)
5159
*/
5260
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
5361
{
54-
if ($classReflection->isSubclassOf(Model::class) && !isset($this->methods[$classReflection->getName()])) {
62+
if (!isset($this->methods[$classReflection->getName()]) && (
63+
$classReflection->isSubclassOf(Model::class)
64+
|| in_array(Builder::class, $this->annotationsHelper->getMixins($classReflection))
65+
)) {
5566
$builder = $this->broker->getClass(Builder::class);
5667
$this->methods[$classReflection->getName()] = $this->createWrappedMethods($classReflection, $builder);
5768

src/FacadeMethodExtension.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use PHPStan\Reflection\ClassReflection;
1111
use PHPStan\Reflection\MethodsClassReflectionExtension;
1212
use PHPStan\Reflection\MethodReflection;
13+
use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper;
14+
use PHPStan\Broker\ClassNotFoundException;
1315

1416
final class FacadeMethodExtension implements MethodsClassReflectionExtension, BrokerAwareExtension
1517
{
@@ -36,14 +38,21 @@ final class FacadeMethodExtension implements MethodsClassReflectionExtension, Br
3638
*/
3739
private $methodReflectionFactory;
3840

41+
/**
42+
* @var AnnotationsHelper
43+
*/
44+
private $annotationsHelper;
45+
3946
/**
4047
* FacadeMethodExtension constructor.
4148
*
4249
* @param \Weebly\PHPStan\Laravel\MethodReflectionFactory $methodReflectionFactory
50+
* @param AnnotationsHelper $annotationsHelper
4351
*/
44-
public function __construct(MethodReflectionFactory $methodReflectionFactory)
52+
public function __construct(MethodReflectionFactory $methodReflectionFactory, AnnotationsHelper $annotationsHelper)
4553
{
4654
$this->methodReflectionFactory = $methodReflectionFactory;
55+
$this->annotationsHelper = $annotationsHelper;
4756
}
4857

4958
/**
@@ -69,6 +78,15 @@ public function hasMethod(ClassReflection $classReflection, string $methodName):
6978
$instanceReflection = $this->broker->getClass(get_class($instance));
7079
$this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $instanceReflection);
7180

81+
foreach ($this->annotationsHelper->getMixins($instanceReflection) as $mixin) {
82+
try {
83+
$mixinInstanceReflection = $this->broker->getClass($mixin);
84+
} catch (ClassNotFoundException $e) {
85+
continue;
86+
}
87+
$this->methods[$classReflection->getName()] += $this->createMethods($classReflection, $mixinInstanceReflection);
88+
}
89+
7290
if (isset($this->extensions[$instanceReflection->getName()])) {
7391
$extensionMethod = $this->extensions[$instanceReflection->getName()];
7492
$extensionReflection = $this->broker->getClass(get_class($instance->$extensionMethod()));

src/Utils/AnnotationsHelper.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Weebly\PHPStan\Laravel\Utils;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
7+
class AnnotationsHelper
8+
{
9+
/**
10+
* Resolve class mixins from doc block
11+
*
12+
* @param ClassReflection $reflection
13+
* @return array
14+
*/
15+
public function getMixins(ClassReflection $reflection) : array
16+
{
17+
preg_match_all(
18+
'/@mixin\s+([\w\\\\]+)/',
19+
(string) $reflection->getNativeReflection()->getDocComment(),
20+
$mixins
21+
);
22+
23+
return array_map(function ($mixin) {
24+
return preg_replace('#^\\\\#', '', $mixin);
25+
}, $mixins[1]);
26+
}
27+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Weebly\PHPStan\Laravel;
4+
5+
use PHPStan\Testing\TestCase;
6+
use PHPStan\Broker\Broker;
7+
use Weebly\PHPStan\Laravel\MethodReflectionFactory;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
10+
use PHPStan\Reflection\Php\PhpMethodReflection;
11+
use PHPStan\Type\FileTypeMapper;
12+
use Illuminate\Database\Eloquent\Model;
13+
use Weebly\PHPStan\Laravel\BuilderMethodExtension;
14+
use stdClass;
15+
use Illuminate\Database\Eloquent\Builder;
16+
use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper;
17+
use PHPStan\Broker\ClassNotFoundException;
18+
19+
/**
20+
* @package Tests\Weebly\PHPStan\Laravel
21+
*/
22+
class BuilderMethodExtensionTest extends TestCase
23+
{
24+
/**
25+
* @var Broker
26+
*/
27+
private $broker;
28+
29+
/**
30+
* @var string
31+
*/
32+
private $childOfModelClassName;
33+
34+
public function testHasMethodInSubclassOfModel()
35+
{
36+
try {
37+
$this->assertFalse($this->hasMethod(stdClass::class, 'find'));
38+
$this->assertTrue($this->hasMethod($this->childOfModelClassName, 'find'));
39+
$this->assertFalse($this->hasMethod(stdClass::class, 'select'));
40+
$this->assertTrue($this->hasMethod($this->childOfModelClassName, 'select'));
41+
} catch (ClassNotFoundException $e) {
42+
$this->markTestIncomplete($e->getMessage());
43+
}
44+
}
45+
46+
public function testHasMethodInClassWithMixinAnnotation()
47+
{
48+
try {
49+
$this->assertFalse($this->hasMethod(stdClass::class, 'find'));
50+
$this->assertTrue($this->hasMethod(stdClass::class, 'find', true));
51+
$this->assertFalse($this->hasMethod(stdClass::class, 'select'));
52+
$this->assertTrue($this->hasMethod(stdClass::class, 'select', true));
53+
} catch (ClassNotFoundException $e) {
54+
$this->markTestIncomplete($e->getMessage());
55+
}
56+
}
57+
58+
public function testHasMethodInBuilder()
59+
{
60+
try {
61+
$this->assertFalse($this->hasMethod(stdClass::class, 'find'));
62+
$this->assertTrue($this->hasMethod(Builder::class, 'find'));
63+
$this->assertFalse($this->hasMethod(stdClass::class, 'select'));
64+
$this->assertTrue($this->hasMethod(Builder::class, 'select'));
65+
} catch (ClassNotFoundException $e) {
66+
$this->markTestIncomplete($e->getMessage());
67+
}
68+
}
69+
70+
/**
71+
* @inheritdoc
72+
*/
73+
protected function setUp()
74+
{
75+
parent::setUp();
76+
$this->broker = $this->createBroker();
77+
$this->childOfModelClassName = get_class(new class() extends Model {});
78+
}
79+
80+
/**
81+
* @return MethodReflectionFactory
82+
*/
83+
private function makeMethodReflectionFactoryMock()
84+
{
85+
/** @var MockObject|PhpMethodReflectionFactory $phpMethodReflectionFactory */
86+
$phpMethodReflectionFactory = $this
87+
->getMockBuilder(PhpMethodReflectionFactory::class)
88+
->getMockForAbstractClass();
89+
$methodReflectionMock = $this
90+
->getMockBuilder(PhpMethodReflection::class)
91+
->disableOriginalConstructor()
92+
->getMock();
93+
$phpMethodReflectionFactory->method('create')->willReturn($methodReflectionMock);
94+
/** @var FileTypeMapper $fileTypeMapper */
95+
$fileTypeMapper = $this->getContainer()->createInstance(FileTypeMapper::class);
96+
97+
return new MethodReflectionFactory($phpMethodReflectionFactory, $fileTypeMapper);
98+
}
99+
100+
/**
101+
* Check existence of the method in given class
102+
*
103+
* @param string $className
104+
* @param string $methodName
105+
* @param bool $addBuilderMixin
106+
* @return bool
107+
* @throws ClassNotFoundException
108+
*/
109+
private function hasMethod(string $className, string $methodName, bool $addBuilderMixin = false): bool
110+
{
111+
$extension = new BuilderMethodExtension(
112+
$this->makeMethodReflectionFactoryMock(),
113+
$this->makeAnnotationsHelperMock($addBuilderMixin)
114+
);
115+
$extension->setBroker($this->broker);
116+
117+
return $extension->hasMethod($this->broker->getClass($className), $methodName);
118+
}
119+
120+
/**
121+
* @param bool $withBuilder
122+
* @return AnnotationsHelper|MockObject
123+
*/
124+
private function makeAnnotationsHelperMock(bool $withBuilder = false)
125+
{
126+
$annotationsHelper = $this
127+
->getMockBuilder(AnnotationsHelper::class)
128+
->getMock();
129+
$annotationsHelper->method('getMixins')->willReturn($withBuilder ? [Builder::class] : []);
130+
131+
return $annotationsHelper;
132+
}
133+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Tests\Weebly\PHPStan\Laravel;
4+
5+
use PHPStan\Testing\TestCase;
6+
use PHPStan\Broker\Broker;
7+
use Weebly\PHPStan\Laravel\MethodReflectionFactory;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
10+
use PHPStan\Reflection\Php\PhpMethodReflection;
11+
use PHPStan\Type\FileTypeMapper;
12+
use Weebly\PHPStan\Laravel\FacadeMethodExtension;
13+
use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper;
14+
use PHPStan\Broker\ClassNotFoundException;
15+
use Illuminate\Database\Connection;
16+
use Illuminate\Auth\AuthManager;
17+
use Illuminate\Support\Facades\Facade;
18+
19+
/**
20+
* @package Tests\Weebly\PHPStan\Laravel
21+
*/
22+
class FacadeMethodExtensionTest extends TestCase
23+
{
24+
/**
25+
* @var Broker
26+
*/
27+
private $broker;
28+
29+
public function testHasMethod()
30+
{
31+
$testFacade = new class() extends Facade{
32+
/**
33+
* @inheritdoc
34+
*/
35+
protected static function getFacadeAccessor()
36+
{
37+
return new class() {
38+
public function someMethod() {
39+
return true;
40+
}
41+
};
42+
}
43+
};
44+
45+
try {
46+
// Native accessor method
47+
$this->assertTrue($this->hasMethod(get_class($testFacade), 'someMethod'));
48+
$this->assertFalse($this->hasMethod(get_class($testFacade), 'fakeMethod'));
49+
// Method from accessor mixin
50+
$this->assertTrue($this->hasMethod(get_class($testFacade), 'table'));
51+
$this->assertTrue($this->hasMethod(get_class($testFacade), 'shouldUse'));
52+
} catch (ClassNotFoundException $e) {
53+
$this->markTestIncomplete($e->getMessage());
54+
}
55+
}
56+
57+
/**
58+
* @inheritdoc
59+
*/
60+
protected function setUp()
61+
{
62+
parent::setUp();
63+
$this->broker = $this->createBroker();
64+
}
65+
66+
/**
67+
* @return MethodReflectionFactory
68+
*/
69+
private function makeMethodReflectionFactoryMock()
70+
{
71+
/** @var MockObject|PhpMethodReflectionFactory $phpMethodReflectionFactory */
72+
$phpMethodReflectionFactory = $this
73+
->getMockBuilder(PhpMethodReflectionFactory::class)
74+
->getMockForAbstractClass();
75+
$methodReflectionMock = $this
76+
->getMockBuilder(PhpMethodReflection::class)
77+
->disableOriginalConstructor()
78+
->getMock();
79+
$phpMethodReflectionFactory->method('create')->willReturn($methodReflectionMock);
80+
/** @var FileTypeMapper $fileTypeMapper */
81+
$fileTypeMapper = $this->getContainer()->createInstance(FileTypeMapper::class);
82+
83+
return new MethodReflectionFactory($phpMethodReflectionFactory, $fileTypeMapper);
84+
}
85+
86+
/**
87+
* @return AnnotationsHelper|MockObject
88+
*/
89+
private function makeAnnotationsHelperMock()
90+
{
91+
$annotationsHelper = $this
92+
->getMockBuilder(AnnotationsHelper::class)
93+
->getMock();
94+
$annotationsHelper->method('getMixins')->willReturn([Connection::class, AuthManager::class, 'Fake']);
95+
96+
return $annotationsHelper;
97+
}
98+
99+
/**
100+
* Check existence of the method in given class
101+
*
102+
* @param string $className
103+
* @param string $methodName
104+
* @return bool
105+
* @throws ClassNotFoundException
106+
*/
107+
private function hasMethod(string $className, string $methodName): bool
108+
{
109+
$extension = new FacadeMethodExtension(
110+
$this->makeMethodReflectionFactoryMock(),
111+
$this->makeAnnotationsHelperMock()
112+
);
113+
$extension->setBroker($this->broker);
114+
115+
return $extension->hasMethod($this->broker->getClass($className), $methodName);
116+
}
117+
}

0 commit comments

Comments
 (0)