Skip to content

Commit 03e0af6

Browse files
authored
Merge pull request #200 from clue-labs/phpstan
Add PHPStan to test environment
2 parents 8d60e48 + 510c038 commit 03e0af6

24 files changed

+116
-49
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# exclude dev files from export to reduce archive download size
22
/.gitattributes export-ignore
3+
/.github/ISSUE_TEMPLATE/ export-ignore
34
/.github/workflows/ export-ignore
45
/.gitignore export-ignore
56
/docs/ export-ignore
67
/examples/ export-ignore
78
/mkdocs.yml export-ignore
9+
/phpstan.neon.dist export-ignore
810
/phpunit.xml.dist export-ignore
911
/phpunit.xml.legacy export-ignore
1012
/tests/ export-ignore

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ jobs:
3434
- run: vendor/bin/phpunit --coverage-text --stderr -c phpunit.xml.legacy
3535
if: ${{ matrix.php < 7.3 }}
3636

37+
PHPStan:
38+
name: PHPStan (PHP ${{ matrix.php }})
39+
runs-on: ubuntu-22.04
40+
strategy:
41+
matrix:
42+
php:
43+
- 8.2
44+
- 8.1
45+
- 8.0
46+
- 7.4
47+
- 7.3
48+
- 7.2
49+
- 7.1
50+
steps:
51+
- uses: actions/checkout@v3
52+
- uses: shivammathur/setup-php@v2
53+
with:
54+
php-version: ${{ matrix.php }}
55+
coverage: none
56+
- run: composer install
57+
- run: vendor/bin/phpstan
58+
3759
Built-in-webserver:
3860
name: Built-in webserver (PHP ${{ matrix.php }})
3961
runs-on: ubuntu-22.04

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"react/promise": "^3 || ^2.7"
1919
},
2020
"require-dev": {
21+
"phpstan/phpstan": "1.8.10 || 1.4.10",
2122
"phpunit/phpunit": "^9.5 || ^7.5",
2223
"psr/container": "^2 || ^1"
2324
},

examples/index.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
return new React\Http\Message\Response(
119119
React\Http\Message\Response::STATUS_OK,
120120
[
121-
'Content-Length' => 5,
121+
'Content-Length' => '5',
122122
'Content-Type' => 'text/plain; charset=utf-8',
123123
'X-Is-Head' => 'true'
124124
]
@@ -161,7 +161,7 @@
161161
React\Http\Message\Response::STATUS_NOT_MODIFIED,
162162
[
163163
'ETag' => $etag,
164-
'Content-Length' => strlen($etag) - 1
164+
'Content-Length' => (string) (strlen($etag) - 1)
165165
]
166166
);
167167
}
@@ -193,7 +193,7 @@
193193
$app->get('/error/yield', function () {
194194
yield null;
195195
});
196-
$app->get('/error/class', 'Acme\Http\UnknownDeleteUserController');
196+
$app->get('/error/class', 'Acme\Http\UnknownDeleteUserController'); // @phpstan-ignore-line
197197

198198
// OPTIONS *
199199
$app->options('', function () {

phpstan.neon.dist

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
parameters:
2+
level: 5
3+
4+
paths:
5+
- examples/
6+
- src/
7+
- tests/
8+
9+
reportUnmatchedIgnoredErrors: false
10+
ignoreErrors:
11+
# ignore generic usage like `PromiseInterface<ResponseInterface>` until fixed upstream
12+
- '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<Psr\\Http\\Message\\ResponseInterface> but interface React\\Promise\\PromiseInterface is not generic\.$/'
13+
# ignore unknown `Fiber` class (PHP 8.1+)
14+
- '/^Instantiated class Fiber not found\.$/'
15+
- '/^Call to method (start|isTerminated|getReturn)\(\) on an unknown class Fiber\.$/'
16+
# ignore incomplete type information for mocks in legacy PHPUnit 7.5
17+
- '/^Parameter #\d+ \$.+ of class .+ constructor expects .+, PHPUnit\\Framework\\MockObject\\MockObject given\.$/'

src/App.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ private function runLoop()
286286
} while (true);
287287

288288
// remove signal handlers when loop stops (if registered)
289-
Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1);
289+
Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 ?? 'printf');
290290
Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf');
291291
}
292292

@@ -309,11 +309,12 @@ private function runOnce()
309309

310310
/**
311311
* @param ServerRequestInterface $request
312-
* @return ResponseInterface|PromiseInterface<ResponseInterface,void>
312+
* @return ResponseInterface|PromiseInterface<ResponseInterface>
313313
* Returns a response or a Promise which eventually fulfills with a
314314
* response. This method never throws or resolves a rejected promise.
315315
* If the request can not be routed or the handler fails, it will be
316316
* turned into a valid error response before returning.
317+
* @throws void
317318
*/
318319
private function handleRequest(ServerRequestInterface $request)
319320
{

src/Container.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ class Container
1313
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
1414
private $container;
1515

16-
/** @var array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
16+
/** @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
1717
public function __construct($loader = [])
1818
{
19+
/** @var mixed $loader explicit type check for mixed if user ignores parameter type */
1920
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
2021
throw new \TypeError(
2122
'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . (\is_object($loader) ? get_class($loader) : gettype($loader)) . ' given'
@@ -233,6 +234,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
233234
$hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull());
234235

235236
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
237+
// @phpstan-ignore-next-line for PHP < 8
236238
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
237239
if ($hasDefault) {
238240
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;

src/ErrorHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct()
2222
}
2323

2424
/**
25-
* @return ResponseInterface|PromiseInterface<ResponseInterface,void>|\Generator
25+
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
2626
* Returns a response, a Promise which eventually fulfills with a
2727
* response or a Generator which eventually returns a response. This
2828
* method never throws or resolves a rejected promise. If the next

src/Io/FiberHandler.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class FiberHandler
2222
{
2323
/**
24-
* @return ResponseInterface|PromiseInterface<ResponseInterface,void>|\Generator
24+
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
2525
* Returns a `ResponseInterface` from the next request handler in the
2626
* chain. If the next request handler returns immediately, this method
2727
* will return immediately. If the next request handler suspends the
@@ -42,15 +42,20 @@ public function __invoke(ServerRequestInterface $request, callable $next): mixed
4242
$response = $next($request);
4343
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
4444

45+
// if the next request handler returns immediately, the fiber can terminate immediately without using a Deferred
46+
// if the next request handler suspends the fiber, we only reach this point after resuming the fiber, so the code below will have assigned a Deferred
47+
/** @var ?Deferred $deferred */
4548
if ($deferred !== null) {
4649
$deferred->resolve($response);
4750
}
4851

4952
return $response;
5053
});
5154

55+
/** @throws void because the next handler will always be an `ErrorHandler` */
5256
$fiber->start();
5357
if ($fiber->isTerminated()) {
58+
/** @throws void because fiber is known to have terminated successfully */
5459
return $fiber->getReturn();
5560
}
5661

src/Io/RouteHandler.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,28 +76,37 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
7676
public function __invoke(ServerRequestInterface $request)
7777
{
7878
if ($request->getRequestTarget()[0] !== '/' && $request->getRequestTarget() !== '*') {
79-
return $this->errorHandler->requestProxyUnsupported($request);
79+
return $this->errorHandler->requestProxyUnsupported();
8080
}
8181

8282
if ($this->routeDispatcher === null) {
8383
$this->routeDispatcher = new RouteDispatcher($this->routeCollector->getData());
8484
}
8585

8686
$routeInfo = $this->routeDispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
87-
switch ($routeInfo[0]) {
88-
case \FastRoute\Dispatcher::NOT_FOUND:
89-
return $this->errorHandler->requestNotFound($request);
90-
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
91-
return $this->errorHandler->requestMethodNotAllowed($routeInfo[1]);
92-
case \FastRoute\Dispatcher::FOUND:
93-
$handler = $routeInfo[1];
94-
$vars = $routeInfo[2];
95-
96-
foreach ($vars as $key => $value) {
97-
$request = $request->withAttribute($key, rawurldecode($value));
98-
}
99-
100-
return $handler($request);
87+
assert(\is_array($routeInfo) && isset($routeInfo[0]));
88+
89+
// happy path: matching route found, assign route attributes and invoke request handler
90+
if ($routeInfo[0] === \FastRoute\Dispatcher::FOUND) {
91+
$handler = $routeInfo[1];
92+
$vars = $routeInfo[2];
93+
94+
foreach ($vars as $key => $value) {
95+
$request = $request->withAttribute($key, rawurldecode($value));
96+
}
97+
98+
return $handler($request);
10199
}
102-
} // @codeCoverageIgnore
100+
101+
// no matching route found: report error `404 Not Found`
102+
if ($routeInfo[0] === \FastRoute\Dispatcher::NOT_FOUND) {
103+
return $this->errorHandler->requestNotFound();
104+
}
105+
106+
// unexpected request method for route: report error `405 Method Not Allowed`
107+
assert($routeInfo[0] === \FastRoute\Dispatcher::METHOD_NOT_ALLOWED);
108+
assert(\is_array($routeInfo[1]) && \count($routeInfo[1]) > 0);
109+
110+
return $this->errorHandler->requestMethodNotAllowed($routeInfo[1]);
111+
}
103112
}

0 commit comments

Comments
 (0)