Skip to content

Commit 55581e3

Browse files
committed
Type the result-set accessors of ResultInterface
1 parent a5ae7f0 commit 55581e3

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

‎docs/type-inference.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ Each method then wraps that row type:
116116
- `first()`: a single row or `null`.
117117
- `findColumn()`: a list of the selected column's values, or `null`.
118118
119+
### ResultMethodReturnTypeExtension
120+
121+
**Class:** `CodeIgniter\PHPStan\Type\ResultMethodReturnTypeExtension`
122+
123+
This extension sharpens the row-set accessors of `CodeIgniter\Database\ResultInterface`, which the
124+
framework declares as a bare `array` or a loose object union at the interface level:
125+
126+
- `getResultArray()`: `list<array<string, mixed>>`
127+
- `getResultObject()`: `list<stdClass>`
128+
- `getRowArray()`: `array<string, mixed>|null`
129+
- `getRowObject()`: `stdClass|null`
130+
- `getCustomResultObject($className)`: `list<T>` when `$className` is a constant `class-string`, or a
131+
list of unknown objects otherwise.
132+
119133
## Dynamic Static Method Return Type Extensions
120134
121135
### ReflectionHelperMethodInvokerStaticReturnTypeExtension

‎extension.neon‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ services:
138138
tags:
139139
- phpstan.broker.dynamicMethodReturnTypeExtension
140140

141+
-
142+
class: CodeIgniter\PHPStan\Type\ResultMethodReturnTypeExtension
143+
tags:
144+
- phpstan.broker.dynamicMethodReturnTypeExtension
145+
141146
-
142147
class: CodeIgniter\PHPStan\Type\ServicesGetSharedInstanceReturnTypeExtension
143148
tags:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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\Database\ResultInterface;
17+
use PhpParser\Node\Expr\MethodCall;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\Reflection\MethodReflection;
20+
use PHPStan\Reflection\ReflectionProvider;
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\MixedType;
26+
use PHPStan\Type\ObjectType;
27+
use PHPStan\Type\ObjectWithoutClassType;
28+
use PHPStan\Type\StringType;
29+
use PHPStan\Type\Type;
30+
use PHPStan\Type\TypeCombinator;
31+
use stdClass;
32+
33+
/**
34+
* Sharpens the row-set accessors of `CodeIgniter\Database\ResultInterface`, which the framework declares
35+
* as a bare `array` or a loose object union. `getCustomResultObject()` is typed from its class-string argument.
36+
*/
37+
final class ResultMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
38+
{
39+
public function __construct(
40+
private readonly ReflectionProvider $reflectionProvider,
41+
) {}
42+
43+
public function getClass(): string
44+
{
45+
return ResultInterface::class;
46+
}
47+
48+
public function isMethodSupported(MethodReflection $methodReflection): bool
49+
{
50+
return in_array($methodReflection->getName(), [
51+
'getResultArray',
52+
'getResultObject',
53+
'getRowArray',
54+
'getRowObject',
55+
'getCustomResultObject',
56+
], true);
57+
}
58+
59+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
60+
{
61+
return match ($methodReflection->getName()) {
62+
'getResultArray' => $this->listOf(new ArrayType(new StringType(), new MixedType())),
63+
'getResultObject' => $this->listOf(new ObjectType(stdClass::class)),
64+
'getRowArray' => TypeCombinator::addNull(new ArrayType(new StringType(), new MixedType())),
65+
'getRowObject' => TypeCombinator::addNull(new ObjectType(stdClass::class)),
66+
'getCustomResultObject' => $this->listOf($this->customObjectType($methodCall, $scope)),
67+
default => null,
68+
};
69+
}
70+
71+
private function customObjectType(MethodCall $methodCall, Scope $scope): Type
72+
{
73+
$args = $methodCall->getArgs();
74+
75+
if (! isset($args[0])) {
76+
return new ObjectWithoutClassType();
77+
}
78+
79+
foreach ($scope->getType($args[0]->value)->getConstantStrings() as $className) {
80+
if ($this->reflectionProvider->hasClass($className->getValue())) {
81+
return new ObjectType($className->getValue());
82+
}
83+
}
84+
85+
return new ObjectWithoutClassType();
86+
}
87+
88+
private function listOf(Type $itemType): Type
89+
{
90+
return TypeCombinator::intersect(
91+
new ArrayType(new IntegerType(), $itemType),
92+
new AccessoryArrayListType(),
93+
);
94+
}
95+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Database\ResultInterface;
17+
use CodeIgniter\PHPStan\Tests\Fixtures\Entity\BlogComment;
18+
19+
use function PHPStan\Testing\assertType;
20+
21+
function resultAccessors(ResultInterface $result, string $dynamic): void
22+
{
23+
// The framework annotates these only on BaseResult, not on the interface, so the interface type is bare.
24+
assertType('list<array<string, mixed>>', $result->getResultArray());
25+
assertType('list<stdClass>', $result->getResultObject());
26+
assertType('array<string, mixed>|null', $result->getRowArray());
27+
assertType('stdClass|null', $result->getRowObject());
28+
29+
// getCustomResultObject() is typed from its class-string argument.
30+
assertType('list<CodeIgniter\PHPStan\Tests\Fixtures\Entity\BlogComment>', $result->getCustomResultObject(BlogComment::class));
31+
32+
// A non-constant class name degrades to a list of unknown objects rather than the framework's bare array.
33+
assertType('list<object>', $result->getCustomResultObject($dynamic));
34+
35+
// Already precise on the interface via a conditional @return, so the extension leaves it untouched.
36+
assertType('stdClass|null', $result->getRow(0));
37+
}

0 commit comments

Comments
 (0)