Skip to content

Commit 519f6ed

Browse files
committed
Add precise return types for Model's find, findAll, and first
1 parent 2bb53ee commit 519f6ed

File tree

6 files changed

+183
-0
lines changed

6 files changed

+183
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This extension provides the following features:
1414
* Provides precise return types for `config()` and `model()` functions.
1515
* Provides precise return types for `service()` and `single_service()` functions.
1616
* Provides precise return types for `fake()` helper function.
17+
* Provides precise return types for `CodeIgniter\Model`'s `find()`, `findAll()`, and `first()` methods.
1718

1819
### Rules
1920

extension.neon

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ parametersSchema:
2424
])
2525

2626
services:
27+
# helpers
2728
factoriesReturnTypeHelper:
2829
class: CodeIgniter\PHPStan\Type\FactoriesReturnTypeHelper
2930
arguments:
@@ -43,6 +44,7 @@ services:
4344
superglobalRuleHelper:
4445
class: CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalRuleHelper
4546

47+
# DynamicFunctionReturnTypeExtension
4648
-
4749
class: CodeIgniter\PHPStan\Type\FactoriesFunctionReturnTypeExtension
4850
tags:
@@ -58,6 +60,13 @@ services:
5860
tags:
5961
- phpstan.broker.dynamicFunctionReturnTypeExtension
6062

63+
# DynamicMethodReturnTypeExtension
64+
-
65+
class: CodeIgniter\PHPStan\Type\ModelFindReturnTypeExtension
66+
tags:
67+
- phpstan.broker.dynamicMethodReturnTypeExtension
68+
69+
# conditional rules
6170
-
6271
class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule
6372

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Type;
15+
16+
use CodeIgniter\Model;
17+
use PhpParser\Node\Expr\MethodCall;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\Reflection\ClassReflection;
20+
use PHPStan\Reflection\MethodReflection;
21+
use PHPStan\Type\Accessory\AccessoryArrayListType;
22+
use PHPStan\Type\ArrayType;
23+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
24+
use PHPStan\Type\IntegerType;
25+
use PHPStan\Type\Type;
26+
use PHPStan\Type\TypeCombinator;
27+
28+
final class ModelFindReturnTypeExtension implements DynamicMethodReturnTypeExtension
29+
{
30+
public function __construct(
31+
private readonly ModelFetchedReturnTypeHelper $modelFetchedReturnTypeHelper
32+
) {}
33+
34+
public function getClass(): string
35+
{
36+
return Model::class;
37+
}
38+
39+
public function isMethodSupported(MethodReflection $methodReflection): bool
40+
{
41+
return in_array($methodReflection->getName(), ['find', 'findAll', 'first'], true);
42+
}
43+
44+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
45+
{
46+
$methodName = $methodReflection->getName();
47+
48+
if ($methodName === 'find') {
49+
return $this->getTypeFromFind($methodReflection, $methodCall, $scope);
50+
}
51+
52+
if ($methodName === 'findAll') {
53+
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
54+
}
55+
56+
$classReflection = $this->getClassReflection($methodCall, $scope);
57+
58+
return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $scope));
59+
}
60+
61+
private function getClassReflection(MethodCall $methodCall, Scope $scope): ClassReflection
62+
{
63+
$classTypes = $scope->getType($methodCall->var)->getObjectClassReflections();
64+
assert(count($classTypes) === 1);
65+
66+
return current($classTypes);
67+
}
68+
69+
private function getTypeFromFind(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
70+
{
71+
$args = $methodCall->getArgs();
72+
73+
if (! isset($args[0])) {
74+
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
75+
}
76+
77+
$idType = $scope->getType($args[0]->value);
78+
79+
if ($idType->isNull()->yes()) {
80+
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
81+
}
82+
83+
if ($idType->isInteger()->yes() || $idType->isString()->yes()) {
84+
$classReflection = $this->getClassReflection($methodCall, $scope);
85+
86+
return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $scope));
87+
}
88+
89+
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
90+
}
91+
92+
private function getTypeFromFindAll(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
93+
{
94+
$classReflection = $this->getClassReflection($methodCall, $scope);
95+
96+
return AccessoryArrayListType::intersectWith(
97+
new ArrayType(
98+
new IntegerType(),
99+
$this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $scope)
100+
)
101+
);
102+
}
103+
}

tests/Fixtures/Type/model-find.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
use CodeIgniter\Shield\Models\GroupModel;
15+
use CodeIgniter\Shield\Models\UserModel;
16+
17+
use function PHPStan\Testing\assertType;
18+
19+
$users = model(UserModel::class);
20+
assertType('CodeIgniter\Shield\Entities\User|null', $users->find(1));
21+
assertType('list<CodeIgniter\Shield\Entities\User>', $users->find());
22+
assertType('list<CodeIgniter\Shield\Entities\User>', $users->find(null));
23+
assertType('list<CodeIgniter\Shield\Entities\User>', $users->find([1, 2, 3]));
24+
25+
$groups = model(GroupModel::class);
26+
assertType('array{user_id: int, group: string, created_at: string}|null', $groups->find(1));
27+
assertType('list<array{user_id: int, group: string, created_at: string}>', $groups->find());
28+
assertType('list<array{user_id: int, group: string, created_at: string}>', $groups->find(null));
29+
assertType('list<array{user_id: int, group: string, created_at: string}>', $groups->find([1, 2, 3]));
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Tests\Type;
15+
16+
use CodeIgniter\PHPStan\Tests\AdditionalConfigFilesTrait;
17+
use PHPStan\Testing\TypeInferenceTestCase;
18+
use PHPUnit\Framework\Attributes\DataProvider;
19+
use PHPUnit\Framework\Attributes\Group;
20+
21+
/**
22+
* @internal
23+
*/
24+
#[Group('Integration')]
25+
final class DynamicMethodReturnTypeExtensionTest extends TypeInferenceTestCase
26+
{
27+
use AdditionalConfigFilesTrait;
28+
29+
#[DataProvider('provideFileAssertsCases')]
30+
public function testFileAsserts(string $assertType, string $file, mixed ...$args): void
31+
{
32+
$this->assertFileAsserts($assertType, $file, ...$args);
33+
}
34+
35+
public static function provideFileAssertsCases(): iterable
36+
{
37+
yield from self::gatherAssertTypes(__DIR__ . '/../Fixtures/Type/model-find.php');
38+
}
39+
}

tests/extension-test.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
parameters:
2+
featureToggles:
3+
listType: true
24
bootstrapFiles:
35
- %rootDir%/../../../tests/bootstrap.php
46
codeigniter:

0 commit comments

Comments
 (0)