Skip to content

Commit 1188670

Browse files
authored
Implement friendly exception for InvalidMiddlewareDefinitionException (#50)
1 parent 1f8125c commit 1188670

4 files changed

+143
-10
lines changed

CHANGELOG.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
## 2.0.2 under development
44

5-
- no changes in this release.
6-
5+
- Enh #45: Implement friendly exception for `InvalidMiddlewareDefinitionException` (vjik)
76

87
## 2.0.1 February 14, 2022
98

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
},
1919
"require": {
2020
"php": "^7.4|^8.0",
21+
"psr/container": "^1.0|^2.0",
2122
"psr/event-dispatcher": "^1.0",
2223
"psr/http-message": "^1.0",
2324
"psr/http-server-handler": "^1.0",
2425
"psr/http-server-middleware": "^1.0",
25-
"psr/container": "^1.0|^2.0",
26+
"yiisoft/friendly-exception": "^1.1",
2627
"yiisoft/injector": "^1.0"
2728
},
2829
"require-dev": {

src/InvalidMiddlewareDefinitionException.php

+116-5
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,140 @@
55
namespace Yiisoft\Middleware\Dispatcher;
66

77
use InvalidArgumentException;
8+
use Psr\Http\Server\MiddlewareInterface;
9+
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
810

911
use function get_class;
1012
use function is_array;
1113
use function is_object;
1214
use function is_string;
1315

14-
final class InvalidMiddlewareDefinitionException extends InvalidArgumentException
16+
final class InvalidMiddlewareDefinitionException extends InvalidArgumentException implements FriendlyExceptionInterface
1517
{
1618
/**
17-
* @param array|callable|string $middlewareDefinition
19+
* @var mixed
20+
*/
21+
private $definition;
22+
private ?string $definitionString;
23+
24+
/**
25+
* @param mixed $middlewareDefinition
1826
*/
1927
public function __construct($middlewareDefinition)
2028
{
29+
$this->definition = $middlewareDefinition;
30+
$this->definitionString = $this->convertDefinitionToString($middlewareDefinition);
31+
2132
$message = 'Parameter should be either PSR middleware class name or a callable.';
2233

23-
$definitionString = $this->convertDefinitionToString($middlewareDefinition);
24-
if ($definitionString !== null) {
25-
$message .= ' Got ' . $definitionString . '.';
34+
if ($this->definitionString !== null) {
35+
$message .= ' Got ' . $this->definitionString . '.';
2636
}
2737

2838
parent::__construct($message);
2939
}
3040

41+
public function getName(): string
42+
{
43+
return 'Invalid middleware definition';
44+
}
45+
46+
public function getSolution(): ?string
47+
{
48+
$solution = [];
49+
50+
if ($this->definitionString !== null) {
51+
$solution[] = <<<SOLUTION
52+
## Got definition value
53+
54+
`{$this->definitionString}`
55+
SOLUTION;
56+
}
57+
58+
$suggestion = $this->generateSuggestion();
59+
if ($suggestion !== null) {
60+
$solution[] = '## Suggestion';
61+
$solution[] = $suggestion;
62+
}
63+
64+
$solution[] = <<<SOLUTION
65+
## Middleware definition examples
66+
67+
- PSR middleware class name: `Yiisoft\Session\SessionMiddleware::class`.
68+
- Action in controller: `[App\Backend\UserController::class, 'index']`.
69+
70+
## Related links
71+
72+
- [Callable PHP documentation](https://www.php.net/manual/language.types.callable.php)
73+
SOLUTION;
74+
75+
return implode("\n\n", $solution);
76+
}
77+
78+
private function generateSuggestion(): ?string
79+
{
80+
if ($this->isControllerWithNonExistAction()) {
81+
return <<<SOLUTION
82+
Class `{$this->definition[0]}` exists, but does not contain method `{$this->definition[1]}()`.
83+
84+
Try adding `{$this->definition[1]}()` action to `{$this->definition[0]}` controller:
85+
86+
```php
87+
public function {$this->definition[1]}(): ResponseInterface
88+
{
89+
// TODO: Implement your action
90+
}
91+
```
92+
SOLUTION;
93+
}
94+
95+
if ($this->isNotMiddlewareClassName()) {
96+
return sprintf(
97+
'Class `%s` exists, but does not implement `%s`.',
98+
$this->definition,
99+
MiddlewareInterface::class
100+
);
101+
}
102+
103+
if ($this->isStringNotClassName()) {
104+
return sprintf(
105+
'Class `%s` not found. It may be needed to install a package with this middleware.',
106+
$this->definition
107+
);
108+
}
109+
110+
return null;
111+
}
112+
113+
/**
114+
* @psalm-assert-if-true string $this->definition
115+
*/
116+
private function isStringNotClassName(): bool
117+
{
118+
return is_string($this->definition)
119+
&& !class_exists($this->definition);
120+
}
121+
122+
/**
123+
* @psalm-assert-if-true class-string $this->definition
124+
*/
125+
private function isNotMiddlewareClassName(): bool
126+
{
127+
return is_string($this->definition)
128+
&& class_exists($this->definition);
129+
}
130+
131+
/**
132+
* @psalm-assert-if-true array{0:class-string,1:string} $this->definition
133+
*/
134+
private function isControllerWithNonExistAction(): bool
135+
{
136+
return is_array($this->definition)
137+
&& array_keys($this->definition) === [0, 1]
138+
&& is_string($this->definition[0])
139+
&& class_exists($this->definition[0]);
140+
}
141+
31142
/**
32143
* @param mixed $middlewareDefinition
33144
*/

tests/InvalidMiddlewareDefinitionExceptionTest.php

+24-2
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,27 @@ public function dataBase(): array
1717
[
1818
'test',
1919
'"test"',
20+
'Class `test` not found',
21+
],
22+
[
23+
TestController::class,
24+
'"Yiisoft\Middleware\Dispatcher\Tests\Support\TestController"',
25+
'Class `Yiisoft\Middleware\Dispatcher\Tests\Support\TestController` exists, but does not implement',
2026
],
2127
[
2228
new TestController(),
2329
'an instance of "Yiisoft\Middleware\Dispatcher\Tests\Support\TestController"',
30+
'Related links',
2431
],
2532
[
2633
[TestController::class, 'notExistsAction'],
2734
'["Yiisoft\Middleware\Dispatcher\Tests\Support\TestController", "notExistsAction"]',
35+
'Try adding `notExistsAction()` action to `Yiisoft\Middleware\Dispatcher\Tests\Support\TestController` controller:',
2836
],
2937
[
3038
['class' => TestController::class, 'index'],
3139
'["class" => "Yiisoft\Middleware\Dispatcher\Tests\Support\TestController", "index"]',
40+
null,
3241
],
3342
];
3443
}
@@ -38,10 +47,13 @@ public function dataBase(): array
3847
*
3948
* @param mixed $definition
4049
*/
41-
public function testBase($definition, string $expected): void
50+
public function testBase($definition, string $expectedMessage, ?string $expectedSolution): void
4251
{
4352
$exception = new InvalidMiddlewareDefinitionException($definition);
44-
self::assertStringEndsWith('. Got ' . $expected . '.', $exception->getMessage());
53+
self::assertStringEndsWith('. Got ' . $expectedMessage . '.', $exception->getMessage());
54+
if ($expectedSolution !== null) {
55+
self::assertStringContainsString($expectedSolution, $exception->getSolution());
56+
}
4557
}
4658

4759
public function dataUnknownDefinition(): array
@@ -65,4 +77,14 @@ public function testUnknownDefinition($definition): void
6577
$exception->getMessage()
6678
);
6779
}
80+
81+
public function testName(): void
82+
{
83+
$exception = new InvalidMiddlewareDefinitionException('test');
84+
85+
self::assertSame(
86+
'Invalid middleware definition',
87+
$exception->getName()
88+
);
89+
}
6890
}

0 commit comments

Comments
 (0)