Skip to content

Commit 354af56

Browse files
committed
Type IncomingRequest and CLIRequest input accessors
1 parent 8708fdb commit 354af56

5 files changed

Lines changed: 270 additions & 0 deletions

File tree

‎docs/type-inference.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,32 @@ or a loose JSON union:
147147
from the declared `array|bool|float|int|stdClass|null`. A `false`, absent, or non-constant `$assoc`
148148
leaves the union in place.
149149
150+
### IncomingRequestMethodReturnTypeExtension
151+
152+
**Class:** `CodeIgniter\PHPStan\Type\IncomingRequestMethodReturnTypeExtension`
153+
154+
This extension sharpens the `CodeIgniter\HTTP\IncomingRequest` input accessors built on
155+
`RequestTrait::fetchGlobal()` (`getGet`, `getPost`, `getCookie`, `getPostGet`, `getGetPost`, and
156+
`getRawInputVar`), all declared as the loose union `array|bool|float|int|object|string|null`. The
157+
`$index` argument shape drives the result:
158+
159+
- a null or absent index returns the whole global as `array<string, mixed>`,
160+
- an array of keys returns `array<string, mixed>`,
161+
- a constant string index returns `array<mixed>|string|null` (the value can be a nested array, or null
162+
when the key is absent). `getRawInputVar` defers here, as its dotted-path leaf does not narrow usefully.
163+
164+
A filter argument (the second parameter) or a non-constant index leaves the declared union in place.
165+
`getVar()` is not handled: it falls back to the JSON input stream, so its result cannot be narrowed
166+
soundly.
167+
168+
### CLIRequestMethodReturnTypeExtension
169+
170+
**Class:** `CodeIgniter\PHPStan\Type\CLIRequestMethodReturnTypeExtension`
171+
172+
On the CLI these same accessors (`getGet`, `getPost`, `getCookie`, `getPostGet`, `getGetPost`) never read
173+
a real superglobal. They return an empty array for a null or array index and `null` for a string index,
174+
narrowing the declared `array|null` accordingly.
175+
150176
## Dynamic Static Method Return Type Extensions
151177
152178
### ReflectionHelperMethodInvokerStaticReturnTypeExtension

‎extension.neon‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ services:
148148
tags:
149149
- phpstan.broker.dynamicMethodReturnTypeExtension
150150

151+
-
152+
class: CodeIgniter\PHPStan\Type\IncomingRequestMethodReturnTypeExtension
153+
tags:
154+
- phpstan.broker.dynamicMethodReturnTypeExtension
155+
156+
-
157+
class: CodeIgniter\PHPStan\Type\CLIRequestMethodReturnTypeExtension
158+
tags:
159+
- phpstan.broker.dynamicMethodReturnTypeExtension
160+
151161
-
152162
class: CodeIgniter\PHPStan\Type\ServicesGetSharedInstanceReturnTypeExtension
153163
tags:
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 <admin@codeigniter.com>
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\HTTP\CLIRequest;
17+
use PhpParser\Node\Expr\MethodCall;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\PhpDoc\TypeStringResolver;
20+
use PHPStan\Reflection\MethodReflection;
21+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
22+
use PHPStan\Type\NullType;
23+
use PHPStan\Type\Type;
24+
25+
/**
26+
* Sharpens the `CLIRequest` input accessors, which never read a real superglobal: a null or array index
27+
* yields an empty array and a string index yields null, mirroring `CLIRequest::returnNullOrEmptyArray()`.
28+
*/
29+
final class CLIRequestMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
30+
{
31+
private const SUPPORTED_METHODS = [
32+
'getGet',
33+
'getPost',
34+
'getCookie',
35+
'getPostGet',
36+
'getGetPost',
37+
];
38+
39+
public function __construct(
40+
private readonly TypeStringResolver $typeStringResolver,
41+
) {}
42+
43+
public function getClass(): string
44+
{
45+
return CLIRequest::class;
46+
}
47+
48+
public function isMethodSupported(MethodReflection $methodReflection): bool
49+
{
50+
return in_array($methodReflection->getName(), self::SUPPORTED_METHODS, true);
51+
}
52+
53+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
54+
{
55+
$args = $methodCall->getArgs();
56+
57+
if (! isset($args[0])) {
58+
return $this->typeStringResolver->resolve('array{}');
59+
}
60+
61+
$indexType = $scope->getType($args[0]->value);
62+
63+
if ($indexType->isNull()->yes() || $indexType->isArray()->yes()) {
64+
return $this->typeStringResolver->resolve('array{}');
65+
}
66+
67+
if ($indexType->isString()->yes()) {
68+
return new NullType();
69+
}
70+
71+
return null;
72+
}
73+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 <admin@codeigniter.com>
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\HTTP\IncomingRequest;
17+
use PhpParser\Node\Expr\MethodCall;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\PhpDoc\TypeStringResolver;
20+
use PHPStan\Reflection\MethodReflection;
21+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
22+
use PHPStan\Type\Type;
23+
24+
/**
25+
* Sharpens the `IncomingRequest` input accessors built on `RequestTrait::fetchGlobal()`, declared as the
26+
* loose union `array|bool|float|int|object|string|null`. The `$index` argument shape decides the result.
27+
*/
28+
final class IncomingRequestMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
29+
{
30+
private const SUPPORTED_METHODS = [
31+
'getGet',
32+
'getPost',
33+
'getCookie',
34+
'getPostGet',
35+
'getGetPost',
36+
'getRawInputVar',
37+
];
38+
39+
public function __construct(
40+
private readonly TypeStringResolver $typeStringResolver,
41+
) {}
42+
43+
public function getClass(): string
44+
{
45+
return IncomingRequest::class;
46+
}
47+
48+
public function isMethodSupported(MethodReflection $methodReflection): bool
49+
{
50+
return in_array($methodReflection->getName(), self::SUPPORTED_METHODS, true);
51+
}
52+
53+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
54+
{
55+
$args = $methodCall->getArgs();
56+
57+
// A filter argument re-types the value through filter_var, so defer to the declared type.
58+
if (isset($args[1]) && ! $scope->getType($args[1]->value)->isNull()->yes()) {
59+
return null;
60+
}
61+
62+
// A null or absent index returns the whole global as a keyed array.
63+
if (! isset($args[0]) || $scope->getType($args[0]->value)->isNull()->yes()) {
64+
return $this->typeStringResolver->resolve('array<string, mixed>');
65+
}
66+
67+
$indexType = $scope->getType($args[0]->value);
68+
69+
// An array of keys returns an array keyed by those names.
70+
if ($indexType->isArray()->yes()) {
71+
return $this->typeStringResolver->resolve('array<string, mixed>');
72+
}
73+
74+
// A string index returns the scalar value, a nested array, or null when the key is absent.
75+
if ($indexType->isString()->yes()) {
76+
// getRawInputVar's leaf passes through dot_array_search, which does not narrow usefully.
77+
if ($methodReflection->getName() === 'getRawInputVar') {
78+
return null;
79+
}
80+
81+
return $this->typeStringResolver->resolve('array<mixed>|string|null');
82+
}
83+
84+
return null;
85+
}
86+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 <admin@codeigniter.com>
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\HTTP\CLIRequest;
17+
use CodeIgniter\HTTP\IncomingRequest;
18+
19+
use function PHPStan\Testing\assertType;
20+
21+
/**
22+
* @param list<string> $keys
23+
*/
24+
function test_incoming_request(IncomingRequest $request, string $key, array $keys, ?string $maybe): void
25+
{
26+
// A null or absent index returns the whole global as a keyed array.
27+
assertType('array<string, mixed>', $request->getGet());
28+
assertType('array<string, mixed>', $request->getPost(null));
29+
assertType('array<string, mixed>', $request->getCookie());
30+
assertType('array<string, mixed>', $request->getPostGet());
31+
assertType('array<string, mixed>', $request->getGetPost());
32+
assertType('array<string, mixed>', $request->getRawInputVar());
33+
34+
// An array of keys returns an array keyed by those names.
35+
assertType('array<string, mixed>', $request->getGet(['a', 'b']));
36+
assertType('array<string, mixed>', $request->getPost($keys));
37+
assertType('array<string, mixed>', $request->getRawInputVar(['x']));
38+
39+
// A string index returns the scalar value, a nested array, or null.
40+
assertType('array<mixed>|string|null', $request->getGet('q'));
41+
assertType('array<mixed>|string|null', $request->getPost($key));
42+
assertType('array<mixed>|string|null', $request->getCookie('session'));
43+
assertType('array<mixed>|string|null', $request->getPostGet('id'));
44+
assertType('array<mixed>|string|null', $request->getGetPost('id'));
45+
46+
// getRawInputVar's string leaf is left as the declared union.
47+
assertType('array|bool|float|int|object|string|null', $request->getRawInputVar('field'));
48+
49+
// A filter argument re-types the value, so the declared union is left in place.
50+
assertType('array|bool|float|int|object|string|null', $request->getGet('q', FILTER_VALIDATE_INT));
51+
52+
// An ambiguous index (string or null) cannot be resolved.
53+
assertType('array|bool|float|int|object|string|null', $request->getGet($maybe));
54+
}
55+
56+
/**
57+
* @param list<string> $keys
58+
*/
59+
function test_cli_request(CLIRequest $request, string $key, array $keys, ?string $maybe): void
60+
{
61+
// A null or array index yields an empty array.
62+
assertType('array{}', $request->getGet());
63+
assertType('array{}', $request->getPost(null));
64+
assertType('array{}', $request->getCookie());
65+
assertType('array{}', $request->getGet(['a']));
66+
assertType('array{}', $request->getPostGet($keys));
67+
68+
// A string index always returns null on the CLI.
69+
assertType('null', $request->getGet('q'));
70+
assertType('null', $request->getPost($key));
71+
assertType('null', $request->getCookie('session'));
72+
73+
// An ambiguous index is left as the declared union.
74+
assertType('array|null', $request->getGet($maybe));
75+
}

0 commit comments

Comments
 (0)