Passed
Pull Request — master (#813)
by Alexander
06:06
created

CoreHandler   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 138
Duplicated Lines 0 %

Test Coverage

Coverage 98.25%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 18
eloc 62
dl 0
loc 138
ccs 56
cts 57
cp 0.9825
rs 10
c 1
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A handle() 0 41 5
A withVerbActions() 0 6 1
A __construct() 0 5 1
A getAction() 0 7 2
A withContext() 0 8 1
B wrapResponse() 0 24 7
A mapException() 0 8 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Router;
6
7
use Psr\Http\Message\ResponseFactoryInterface;
8
use Psr\Http\Message\ResponseInterface as Response;
9
use Psr\Http\Message\ServerRequestInterface as Request;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use Spiral\Core\CoreInterface;
12
use Spiral\Core\Exception\ControllerException;
13
use Spiral\Core\ScopeInterface;
14
use Spiral\Http\Exception\ClientException;
15
use Spiral\Http\Exception\ClientException\BadRequestException;
16
use Spiral\Http\Exception\ClientException\ForbiddenException;
17
use Spiral\Http\Exception\ClientException\NotFoundException;
18
use Spiral\Http\Exception\ClientException\UnauthorizedException;
19
use Spiral\Http\Stream\GeneratorStream;
20
use Spiral\Http\Traits\JsonTrait;
21
use Spiral\Router\Exception\HandlerException;
22
23
final class CoreHandler implements RequestHandlerInterface
24
{
25
    use JsonTrait;
26
27
    private ?string $controller = null;
28
    private string $action;
29
    private ?bool $verbActions = null;
30
    private ?array $parameters = null;
31
32 60
    public function __construct(
33
        private readonly CoreInterface $core,
34
        private readonly ScopeInterface $scope,
35
        private readonly ResponseFactoryInterface $responseFactory
36
    ) {
37
    }
38
39 58
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
40
    {
41 58
        $handler = clone $this;
42 58
        $handler->controller = $controller;
43 58
        $handler->action = $action;
44 58
        $handler->parameters = $parameters;
45
46 58
        return $handler;
47
    }
48
49
    /**
50
     * Disable or enable HTTP prefix for actions.
51
     */
52 58
    public function withVerbActions(bool $verbActions): CoreHandler
53
    {
54 58
        $handler = clone $this;
55 58
        $handler->verbActions = $verbActions;
56
57 58
        return $handler;
58
    }
59
60
    /**
61
     * @psalm-suppress UnusedVariable
62
     * @throws \Throwable
63
     */
64 58
    public function handle(Request $request): Response
65
    {
66 58
        if ($this->controller === null) {
67 1
            throw new HandlerException('Controller and action pair is not set');
68
        }
69
70 57
        $outputLevel = \ob_get_level();
71 57
        \ob_start();
72
73 57
        $output = $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
74
75 57
        $response = $this->responseFactory->createResponse(200);
76
        try {
77
            // run the core withing PSR-7 Request/Response scope
78 57
            $result = $this->scope->runScope(
79
                [
80 57
                    Request::class  => $request,
81
                    Response::class => $response,
82
                ],
83 57
                fn () => $this->core->callAction(
84 57
                    $this->controller,
85 57
                    $this->getAction($request),
86 57
                    $this->parameters
0 ignored issues
show
Bug introduced by
It seems like $this->parameters can also be of type null; however, parameter $parameters of Spiral\Core\CoreInterface::callAction() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
                    /** @scrutinizer ignore-type */ $this->parameters
Loading history...
87
                )
88
            );
89 9
        } catch (ControllerException $e) {
90 6
            \ob_get_clean();
91 6
            throw $this->mapException($e);
92 3
        } catch (\Throwable $e) {
93 3
            \ob_get_clean();
94 3
            throw $e;
95 48
        } finally {
96 57
            while (\ob_get_level() > $outputLevel + 1) {
97 3
                $output = \ob_get_clean() . $output;
98
            }
99
        }
100
101 48
        return $this->wrapResponse(
102
            $response,
103
            $result,
104 48
            \ob_get_clean() . $output
105
        );
106
    }
107
108 57
    private function getAction(Request $request): string
109
    {
110 57
        if ($this->verbActions) {
111 1
            return \strtolower($request->getMethod()) . \ucfirst($this->action);
112
        }
113
114 56
        return $this->action;
115
    }
116
117
    /**
118
     * Convert endpoint result into valid response.
119
     *
120
     * @param Response $response Initial pipeline response.
121
     * @param mixed    $result   Generated endpoint output.
122
     * @param string   $output   Buffer output.
123
     */
124 48
    private function wrapResponse(Response $response, mixed $result = null, string $output = ''): Response
125
    {
126 48
        if ($result instanceof Response) {
127 1
            if ($output !== '' && $result->getBody()->isWritable()) {
128 1
                $result->getBody()->write($output);
129
            }
130
131 1
            return $result;
132
        }
133
134 47
        if ($result instanceof \Generator) {
135
            return $response->withBody(new GeneratorStream($result));
136
        }
137
138 47
        if (\is_array($result) || $result instanceof \JsonSerializable) {
139 16
            $response = $this->writeJson($response, $result);
140
        } else {
141 32
            $response->getBody()->write((string)$result);
142
        }
143
144
        //Always glue buffered output
145 47
        $response->getBody()->write($output);
146
147 47
        return $response;
148
    }
149
150
    /**
151
     * Converts core specific ControllerException into HTTP ClientException.
152
     */
153 6
    private function mapException(ControllerException $exception): ClientException
154
    {
155 6
        return match ($exception->getCode()) {
156
            ControllerException::BAD_ACTION,
157 6
            ControllerException::NOT_FOUND => new NotFoundException('Not found', $exception),
158 2
            ControllerException::FORBIDDEN => new ForbiddenException('Forbidden', $exception),
159 1
            ControllerException::UNAUTHORIZED => new UnauthorizedException('Unauthorized', $exception),
160 6
            default => new BadRequestException('Bad request', $exception),
161
        };
162
    }
163
}
164