Skip to content

Commit 1d3e671

Browse files
authored
Recognise missing models in ClassRegistry::init() (#26)
Recognise model-less tables in ClassRegistry::init() * Uses schema to understand database structure #16
1 parent 1885887 commit 1d3e671

35 files changed

+274
-33
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"ARiddlestone\\PHPStanCakePHP2\\Test\\": "tests/"
3030
},
3131
"classmap": [
32-
"tests/classes",
32+
"tests/Feature/classes",
3333
"vendor/cakephp/cakephp/lib/Cake/Cache",
3434
"vendor/cakephp/cakephp/lib/Cake/Config",
3535
"vendor/cakephp/cakephp/lib/Cake/Console/Shell.php",

extension.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ parameters:
55
- lib/Cake/Model/Behavior/*.php
66
- app/Plugin/*/Model/Behavior/*.php
77
- app/Model/Behavior/*.php
8+
SchemaService:
9+
schemaPaths:
10+
- app/Config/Schema/*.php
811
stubFiles:
912
- stubs/Utility.php
1013
services:
@@ -14,6 +17,9 @@ services:
1417
- class: ARiddlestone\PHPStanCakePHP2\ClassModelsExtension
1518
tags:
1619
- phpstan.broker.propertiesClassReflectionExtension
20+
- class: ARiddlestone\PHPStanCakePHP2\ClassRegistryInitExtension
21+
tags:
22+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
1723
- class: ARiddlestone\PHPStanCakePHP2\ClassTasksExtension
1824
tags:
1925
- phpstan.broker.propertiesClassReflectionExtension
@@ -22,7 +28,13 @@ services:
2228
behaviorPaths: %ModelBehaviorsExtension.behaviorPaths%
2329
tags:
2430
- phpstan.broker.methodsClassReflectionExtension
31+
- class: ARiddlestone\PHPStanCakePHP2\Service\SchemaService
32+
arguments:
33+
schemaPaths: %SchemaService.schemaPaths%
2534
parametersSchema:
2635
ModelBehaviorsExtension: structure([
2736
behaviorPaths: listOf(string())
2837
])
38+
SchemaService: structure([
39+
schemaPaths: listOf(string())
40+
])

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ parameters:
44
- src
55
- tests
66
excludePaths:
7-
- tests/data/*
7+
- tests/Feature/data/*

src/ClassReflectionFinder.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,10 @@
1111
/**
1212
* Finds class reflections for all classes at specified glob paths, optionally
1313
* restricted to children of certain classes.
14-
*
15-
* Assumes classes have no namespace, and are named the same as their files, eg.
16-
* "MyClass" is in a file called "MyClass.php".
1714
*/
1815
final class ClassReflectionFinder
1916
{
20-
/**
21-
* @var ReflectionProvider
22-
*/
23-
private $reflectionProvider;
17+
private ReflectionProvider $reflectionProvider;
2418

2519
public function __construct(ReflectionProvider $reflectionProvider)
2620
{
@@ -36,11 +30,12 @@ public function __construct(ReflectionProvider $reflectionProvider)
3630
*/
3731
public function getClassReflections(
3832
array $paths,
39-
string $isA = 'stdClass'
33+
string $isA = 'stdClass',
34+
?callable $pathToClassName = null
4035
): array {
4136
$classReflections = array_map(
4237
[$this->reflectionProvider, 'getClass'],
43-
$this->getClassNamesFromPaths($paths)
38+
$this->getClassNamesFromPaths($paths, $pathToClassName)
4439
);
4540
return array_filter(
4641
$classReflections,
@@ -59,8 +54,10 @@ static function (
5954
*
6055
* @throws Exception
6156
*/
62-
private function getClassNamesFromPaths(array $paths): array
63-
{
57+
private function getClassNamesFromPaths(
58+
array $paths,
59+
?callable $pathToClassName
60+
): array {
6461
$classPaths = [];
6562
foreach ($paths as $path) {
6663
$filePaths = glob($path);
@@ -69,12 +66,15 @@ private function getClassNamesFromPaths(array $paths): array
6966
}
7067
$classPaths = array_merge($classPaths, $filePaths);
7168
}
72-
$classNames = array_map(static function ($classPath) {
73-
return basename($classPath, '.php');
74-
}, $classPaths);
69+
$classNames = array_map($pathToClassName ?? [$this, 'getClassNameFromFileName'], $classPaths);
7570
return array_filter(
7671
$classNames,
7772
[$this->reflectionProvider, 'hasClass']
7873
);
7974
}
75+
76+
private function getClassNameFromFileName(string $fileName): string
77+
{
78+
return basename($fileName, '.php');
79+
}
8080
}

src/ClassRegistryInitExtension.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace ARiddlestone\PHPStanCakePHP2;
4+
5+
use ARiddlestone\PHPStanCakePHP2\Service\SchemaService;
6+
use Inflector;
7+
use PhpParser\ConstExprEvaluator;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Type\BooleanType;
13+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\ObjectWithoutClassType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\UnionType;
18+
19+
class ClassRegistryInitExtension implements DynamicStaticMethodReturnTypeExtension
20+
{
21+
private ReflectionProvider $reflectionProvider;
22+
23+
private SchemaService $schemaService;
24+
25+
public function __construct(
26+
ReflectionProvider $reflectionProvider,
27+
SchemaService $schemaService)
28+
{
29+
$this->reflectionProvider = $reflectionProvider;
30+
$this->schemaService = $schemaService;
31+
}
32+
33+
public function getClass(): string
34+
{
35+
return 'ClassRegistry';
36+
}
37+
38+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
39+
{
40+
return $methodReflection->getName() === 'init';
41+
}
42+
43+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
44+
{
45+
$arg1 = $methodCall->getArgs()[0]->value;
46+
$evaluator = new ConstExprEvaluator();
47+
$arg1 = $evaluator->evaluateSilently($arg1);
48+
if (! is_string($arg1)) {
49+
return $this->getDefaultType();
50+
}
51+
if ($this->reflectionProvider->hasClass($arg1)) {
52+
return new ObjectType($arg1);
53+
}
54+
if ($this->schemaService->hasTable(Inflector::tableize($arg1))) {
55+
return new ObjectType('Model');
56+
}
57+
return $this->getDefaultType();
58+
}
59+
60+
private function getDefaultType(): Type
61+
{
62+
return new UnionType([
63+
new BooleanType(),
64+
new ObjectWithoutClassType()
65+
]);
66+
}
67+
}

src/Service/SchemaService.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace ARiddlestone\PHPStanCakePHP2\Service;
4+
5+
use ARiddlestone\PHPStanCakePHP2\ClassReflectionFinder;
6+
use Exception;
7+
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use ReflectionProperty as CoreReflectionProperty;
10+
11+
/**
12+
* Identifies schema files, and uses them to provide information about tables
13+
* and columns in the database.
14+
*
15+
* @phpstan-type table_schema mixed
16+
* @phpstan-type column_schema mixed
17+
*/
18+
final class SchemaService
19+
{
20+
private ReflectionProvider $reflectionProvider;
21+
22+
/**
23+
* @var array<string>
24+
*/
25+
private array $schemaPaths;
26+
27+
/**
28+
* @var array<string, table_schema>
29+
*/
30+
private ?array $tableSchemas = null;
31+
32+
/**
33+
* @param ReflectionProvider $reflectionProvider
34+
* @param array<string> $schemaPaths
35+
*/
36+
public function __construct(
37+
ReflectionProvider $reflectionProvider,
38+
array $schemaPaths
39+
) {
40+
$this->reflectionProvider = $reflectionProvider;
41+
$this->schemaPaths = $schemaPaths;
42+
}
43+
44+
/**
45+
* @throws Exception
46+
*/
47+
public function hasTable(string $table): bool
48+
{
49+
return array_key_exists($table, $this->getTableSchemas());
50+
}
51+
52+
/**
53+
* @param string $table
54+
* @return table_schema|null
55+
* @throws Exception
56+
*/
57+
public function getTableSchema(string $table)
58+
{
59+
$tableSchemas = $this->getTableSchemas();
60+
return array_key_exists($table, $tableSchemas)
61+
? $tableSchemas[$table]
62+
: null;
63+
}
64+
65+
/**
66+
* @return array<string, table_schema>
67+
*
68+
* @throws Exception
69+
*/
70+
private function getTableSchemas(): array
71+
{
72+
if (is_array($this->tableSchemas)) {
73+
return $this->tableSchemas;
74+
}
75+
$cakeSchemaPropertyNames = array_map(
76+
function (ReflectionProperty $reflectionProperty) {
77+
return $reflectionProperty->getName();
78+
},
79+
$this->reflectionProvider->getClass('CakeSchema')->getNativeReflection()->getProperties()
80+
);
81+
$this->tableSchemas = [];
82+
$classReflectionFinder = new ClassReflectionFinder(
83+
$this->reflectionProvider
84+
);
85+
$schemaReflections = $classReflectionFinder->getClassReflections(
86+
$this->schemaPaths,
87+
'CakeSchema',
88+
function (string $fileName) {
89+
return $this->fileNameToClassName($fileName);
90+
}
91+
);
92+
foreach ($schemaReflections as $schemaReflection) {
93+
$propertyNames = array_map(
94+
function (ReflectionProperty $reflectionProperty) {
95+
return $reflectionProperty->getName();
96+
},
97+
$schemaReflection->getNativeReflection()
98+
->getProperties(CoreReflectionProperty::IS_PUBLIC)
99+
);
100+
$tableProperties = array_diff($propertyNames, $cakeSchemaPropertyNames);
101+
$this->tableSchemas += array_intersect_key(
102+
$schemaReflection->getNativeReflection()->getDefaultProperties(),
103+
array_fill_keys($tableProperties, null)
104+
);
105+
}
106+
107+
return $this->tableSchemas;
108+
}
109+
110+
private function fileNameToClassName(string $fileName): string
111+
{
112+
return str_replace(
113+
' ',
114+
'',
115+
ucwords(
116+
str_replace(
117+
['_', '-'],
118+
' ',
119+
basename($fileName, '.php')
120+
)
121+
)
122+
) . 'Schema';
123+
}
124+
}

tests/Stubs/UtilityTest.php renamed to tests/Feature/ClassRegistryInitTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?php
22

3-
namespace ARiddlestone\PHPStanCakePHP2\Test\Stubs;
3+
namespace ARiddlestone\PHPStanCakePHP2\Test\Feature;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

7-
class UtilityTest extends TypeInferenceTestCase
7+
class ClassRegistryInitTest extends TypeInferenceTestCase
88
{
99
/**
1010
* @return mixed[]
1111
*/
1212
public function dataFileAsserts(): iterable
1313
{
14-
yield from $this->gatherAssertTypes(__DIR__ . '/../data/stubs/class_registry_init.php');
14+
yield from $this->gatherAssertTypes(__DIR__ . '/data/class_registry_init.php');
1515
}
1616

1717
/**
@@ -26,7 +26,7 @@ public function testControllerExtensions(string $assertType, string $file, ...$a
2626
public static function getAdditionalConfigFiles(): array
2727
{
2828
return [
29-
__DIR__ . '/../data/phpstan.neon',
29+
__DIR__ . '/data/phpstan.neon',
3030
];
3131
}
3232
}

tests/ComponentExtensionsTest.php renamed to tests/Feature/ComponentExtensionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace ARiddlestone\PHPStanCakePHP2\Test;
3+
namespace ARiddlestone\PHPStanCakePHP2\Test\Feature;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

tests/ControllerExtensionsTest.php renamed to tests/Feature/ControllerExtensionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace ARiddlestone\PHPStanCakePHP2\Test;
3+
namespace ARiddlestone\PHPStanCakePHP2\Test\Feature;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

tests/ModelExtensionsTest.php renamed to tests/Feature/ModelExtensionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace ARiddlestone\PHPStanCakePHP2\Test;
3+
namespace ARiddlestone\PHPStanCakePHP2\Test\Feature;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

tests/ShellExtensionsTest.php renamed to tests/Feature/ShellExtensionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace ARiddlestone\PHPStanCakePHP2\Test;
3+
namespace ARiddlestone\PHPStanCakePHP2\Test\Feature;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

0 commit comments

Comments
 (0)