Passed
Push — master ( 904fca...2ba06e )
by butschster
07:38
created

CoreHandler   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 152
Duplicated Lines 0 %

Test Coverage

Coverage 98.67%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 18
eloc 71
dl 0
loc 152
ccs 74
cts 75
cp 0.9867
rs 10
c 2
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A withVerbActions() 0 6 1
A __construct() 0 7 1
A getAction() 0 7 2
A withContext() 0 8 1
B wrapResponse() 0 24 7
A mapException() 0 8 1
A handle() 0 51 5
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
use Spiral\Telemetry\NullTracer;
23
use Spiral\Telemetry\TracerInterface;
24
25
final class CoreHandler implements RequestHandlerInterface
26
{
27
    use JsonTrait;
28
29
    private readonly TracerInterface $tracer;
30
31
    private ?string $controller = null;
32
    private string $action;
33
    private ?bool $verbActions = null;
34
    private ?array $parameters = null;
35
36 60
    public function __construct(
37
        private readonly CoreInterface $core,
38
        private readonly ScopeInterface $scope,
39
        private readonly ResponseFactoryInterface $responseFactory,
40
        ?TracerInterface $tracer = null
41
    ) {
42 60
        $this->tracer = $tracer ?? new NullTracer($scope);
0 ignored issues
show
Bug introduced by
The property tracer is declared read-only in Spiral\Router\CoreHandler.
Loading history...
43
    }
44
45 58
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
46
    {
47 58
        $handler = clone $this;
48 58
        $handler->controller = $controller;
49 58
        $handler->action = $action;
50 58
        $handler->parameters = $parameters;
51
52 58
        return $handler;
53
    }
54
55
    /**
56
     * Disable or enable HTTP prefix for actions.
57
     */
58 58
    public function withVerbActions(bool $verbActions): CoreHandler
59
    {
60 58
        $handler = clone $this;
61 58
        $handler->verbActions = $verbActions;
62
63 58
        return $handler;
64
    }
65
66
    /**
67
     * @psalm-suppress UnusedVariable
68
     * @throws \Throwable
69
     */
70 58
    public function handle(Request $request): Response
71
    {
72 58
        if ($this->controller === null) {
73 1
            throw new HandlerException('Controller and action pair is not set');
74
        }
75
76 57
        $outputLevel = \ob_get_level();
77 57
        \ob_start();
78
79 57
        $output = $result = null;
80
81 57
        $response = $this->responseFactory->createResponse(200);
82
        try {
83 57
            $action = $this->getAction($request);
84
85
            // run the core withing PSR-7 Request/Response scope
86 57
            $result = $this->scope->runScope(
87 57
                [
88 57
                    Request::class  => $request,
89 57
                    Response::class => $response,
90 57
                ],
91 57
                fn () => $this->tracer->trace(
92 57
                    name: \sprintf('Controller [%s:%s]', $this->controller, $action),
93 57
                    callback: fn () => $this->core->callAction(
94 57
                        controller: $this->controller,
95 57
                        action: $action,
96 57
                        parameters: $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

96
                        /** @scrutinizer ignore-type */ parameters: $this->parameters
Loading history...
97 57
                    ),
98 57
                    attributes: [
99 57
                        'route.controller' => $this->controller,
100 57
                        'route.action' => $action,
101 57
                        'route.parameters' => \array_keys($this->parameters),
0 ignored issues
show
Bug introduced by
It seems like $this->parameters can also be of type null; however, parameter $array of array_keys() 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

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