Skip to content
153 changes: 153 additions & 0 deletions src/Exception/AbstractInvalidMiddlewareException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Middleware\Dispatcher\Exception;

use InvalidArgumentException;
use Psr\Http\Server\MiddlewareInterface;
use Throwable;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Helpers\DefinitionValidator;
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
use Yiisoft\Middleware\Dispatcher\Helper\DefinitionHelper;

use function is_array;

abstract class AbstractInvalidMiddlewareException extends InvalidArgumentException implements FriendlyExceptionInterface
{
protected string $definitionString;

public function __construct(
protected mixed $definition,
string $message,
?Throwable $previous = null,
) {
$this->definitionString = DefinitionHelper::convertDefinitionToString($definition);

parent::__construct($message, 0, $previous);
}

public function getSolution(): ?string
{
$solution = [
<<<SOLUTION
## Got definition value

`{$this->definitionString}`
SOLUTION
];

$suggestion = $this->generateSuggestion();
if ($suggestion !== null) {
$solution[] = '## Suggestion';
$solution[] = $suggestion;
}

$solution[] = <<<SOLUTION
## Middleware definition examples

PSR middleware class name:

```php
Yiisoft\Session\SessionMiddleware::class
```

PSR middleware array definition:

```php
[
'class' => MyMiddleware::class,
'__construct()' => [
'someVar' => 42,
],
]
```

Closure that returns `ResponseInterface`:

```php
static function (): ResponseInterface {
return new Response(418);
},
```

Closure that returns `MiddlewareInterface`:

```php
static function (): MiddlewareInterface {
return new TestMiddleware();
}
```

Action in controller:

```php
[App\Backend\UserController::class, 'index']
```

## Related links

- [Array definition syntax](https://github.com/yiisoft/definitions#arraydefinition)
- [Callable PHP documentation](https://www.php.net/manual/language.types.callable.php)
SOLUTION;

return implode("\n\n", $solution);
}

private function generateSuggestion(): ?string
{
if (DefinitionHelper::isControllerWithNonExistAction($this->definition)) {
return <<<SOLUTION
Class `{$this->definition[0]}` exists, but does not contain method `{$this->definition[1]}()`.

Try adding `{$this->definition[1]}()` action to `{$this->definition[0]}` controller:

```php
public function {$this->definition[1]}(): ResponseInterface
{
// TODO: Implement your action
}
```
SOLUTION;
}

if (DefinitionHelper::isNotMiddlewareClassName($this->definition)) {
return sprintf(
'Class `%s` exists, but does not implement `%s`.',
$this->definition,
MiddlewareInterface::class
);
}

if (DefinitionHelper::isStringNotClassName($this->definition)) {
return sprintf(
'Class `%s` not found. It may be needed to install a package with this middleware.',
$this->definition
);
}

if (is_array($this->definition)) {
try {
DefinitionValidator::validateArrayDefinition($this->definition);
} catch (InvalidConfigException $e) {
return <<<SOLUTION
You may have an error in array definition. Array definition validation result:

```
{$e->getMessage()}
```
SOLUTION;
}

/** @psalm-suppress MixedArgument In valid array definition element "class" always is string */
return sprintf(
'Array definition is valid, class `%s` exists, but does not implement `%s`.',
$this->definition['class'],
MiddlewareInterface::class
);
}

return null;
}
}
39 changes: 39 additions & 0 deletions src/Exception/InvalidMiddlewareReturnTypeException.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need add solution for friendly exception also. Solution from AbstractInvalidMiddlewareException does not fit here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Middleware\Dispatcher\Exception;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Throwable;
use Yiisoft\Middleware\Dispatcher\Helper\DefinitionHelper;
use Yiisoft\Middleware\Dispatcher\Helper\ResponseHelper;

final class InvalidMiddlewareReturnTypeException extends AbstractInvalidMiddlewareException
{
public function __construct(
mixed $definition,
private readonly mixed $result,
?Throwable $previous = null,
) {
$this->definitionString = DefinitionHelper::convertDefinitionToString($definition);

parent::__construct(
$definition,
sprintf(
'Middleware %s must return an instance of `%s` or `%s`, %s returned.',
$this->definitionString,
MiddlewareInterface::class,
ResponseInterface::class,
ResponseHelper::convertToString($this->result),
),
$previous,
);
}

public function getName(): string
{
return sprintf('Invalid middleware result type %s', get_debug_type($this->result));
}
}
94 changes: 94 additions & 0 deletions src/Helper/DefinitionHelper.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This methods need for excpetion only. May be keep thier in AbstractInvalidMiddlewareException but change visibility from private to protected?

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Middleware\Dispatcher\Helper;

use function array_slice;
use function count;
use function gettype;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_object;
use function is_string;

final class DefinitionHelper
{
/**
* @psalm-assert-if-true string $definition
*/
public static function isStringNotClassName(mixed $definition): bool
{
return is_string($definition)
&& !class_exists($definition);
}

/**
* @psalm-assert-if-true class-string $definition
*/
public static function isNotMiddlewareClassName(mixed $definition): bool
{
return is_string($definition)
&& class_exists($definition);
}

/**
* @psalm-assert-if-true array{0:class-string,1:string} $definition
*/
public static function isControllerWithNonExistAction(mixed $definition): bool
{
return is_array($definition)
&& array_keys($definition) === [0, 1]
&& is_string($definition[0])
&& class_exists($definition[0]);
}

public static function convertDefinitionToString(mixed $middlewareDefinition): string
{
if (is_object($middlewareDefinition)) {
return 'an instance of `' . $middlewareDefinition::class . '`';
}

if (is_string($middlewareDefinition)) {
return '"' . $middlewareDefinition . '"';
}

if (is_array($middlewareDefinition)) {
$items = [];
/** @var mixed $value */
foreach (array_slice($middlewareDefinition, 0, 2) as $key => $value) {
$items[] = (is_string($key) ? '"' . $key . '" => ' : '') . self::convertToString($value);
}
return '[' . implode(', ', $items) . (count($middlewareDefinition) > 2 ? ', ...' : '') . ']';
}

return self::convertToString($middlewareDefinition);
}

private static function convertToString(mixed $value): string
{
if (is_string($value)) {
return '"' . $value . '"';
}

if (is_int($value) || is_float($value)) {
return (string) $value;
}

if (is_bool($value)) {
return $value ? 'true' : 'false';
}

if ($value === null) {
return '"null"';
}

if (is_object($value)) {
return sprintf('"%s"', $value::class);
}

return sprintf('"%s"', gettype($value));
}
}
57 changes: 57 additions & 0 deletions src/Helper/ResponseHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Middleware\Dispatcher\Helper;

use function gettype;
use function is_array;
use function is_bool;
use function is_object;

final class ResponseHelper
{
public static function convertToString(mixed $response): string
{
if (is_object($response)) {
return sprintf('"%s" (object)', $response::class);
}

if (is_array($response)) {
$items = [];

if (!array_is_list($response)) {
/** @var mixed $value */
foreach (array_keys($response) as $key) {
$items[] = sprintf('"%s" => ...', $key);
}
} else {
$items[] = '...';
}
return sprintf(
'"[%s]" (array of %d%s)',
implode(', ', $items),
$count = count($response),
$count === 1 ? ' element' : ' elements'
);
}

if (is_bool($response)) {
return sprintf('"%s" (bool)', $response ? 'true' : 'false');
}

if (is_scalar($response)) {
return sprintf(
'"%s" (%s)',
(string)$response,
match (true) {
is_int($response) => 'int',
is_float($response) => 'float',
default => gettype($response)
}
);
}

return sprintf('"%s" (%s)', gettype($response), get_debug_type($response));
}
}
Loading