Passed
Push — master ( f5a1ef...a14569 )
by butschster
29:16 queued 19:38
created

CoreHandler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 4
crap 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
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
    /** @readonly */
32
    private ?string $controller = null;
33
    /** @readonly */
34
    private ?string $action = null;
35
    /** @readonly */
36
    private ?bool $verbActions = null;
37
    /** @readonly */
38
    private ?array $parameters = null;
39
40 60
    public function __construct(
41
        private readonly CoreInterface $core,
42
        private readonly ScopeInterface $scope,
43
        private readonly ResponseFactoryInterface $responseFactory,
44
        ?TracerInterface $tracer = null
45
    ) {
46 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...
47
    }
48
49
    /**
50
     * @mutation-free
51
     */
52 58
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
53
    {
54 58
        $handler = clone $this;
55 58
        $handler->controller = $controller;
56 58
        $handler->action = $action;
57 58
        $handler->parameters = $parameters;
58
59 58
        return $handler;
60
    }
61
62
    /**
63
     * Disable or enable HTTP prefix for actions.
64
     *
65
     * @mutation-free
66
     */
67 58
    public function withVerbActions(bool $verbActions): CoreHandler
68
    {
69 58
        $handler = clone $this;
70 58
        $handler->verbActions = $verbActions;
71
72 58
        return $handler;
73
    }
74
75
    /**
76
     * @psalm-suppress UnusedVariable
77
     * @throws \Throwable
78
     */
79 58
    public function handle(Request $request): Response
80
    {
81 58
        $this->checkValues();
82 57
        $controller = $this->controller;
83 57
        $parameters = $this->parameters;
84
85 57
        $outputLevel = \ob_get_level();
86 57
        \ob_start();
87
88 57
        $result = null;
89 57
        $output = '';
90
91 57
        $response = $this->responseFactory->createResponse(200);
92
        try {
93 57
            $action = $this->verbActions
94 1
                ? \strtolower($request->getMethod()) . \ucfirst($this->action)
0 ignored issues
show
Bug introduced by
It seems like $this->action can also be of type null; however, parameter $string of ucfirst() does only seem to accept string, 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

94
                ? \strtolower($request->getMethod()) . \ucfirst(/** @scrutinizer ignore-type */ $this->action)
Loading history...
95 56
                : $this->action;
96
97
            // run the core withing PSR-7 Request/Response scope
98 57
            $result = $this->scope->runScope(
99 57
                [
100 57
                    Request::class  => $request,
101 57
                    Response::class => $response,
102 57
                ],
103 57
                fn (): mixed => $this->tracer->trace(
104 57
                    name: 'Controller [' . $controller . ':' . $action . ']',
105 57
                    callback: fn (): mixed => $this->core->callAction(
106 57
                        controller: $controller,
0 ignored issues
show
Bug introduced by
It seems like $controller can also be of type null; however, parameter $controller of Spiral\Core\CoreInterface::callAction() does only seem to accept string, 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

106
                        /** @scrutinizer ignore-type */ controller: $controller,
Loading history...
107 57
                        action: $action,
0 ignored issues
show
Bug introduced by
It seems like $action can also be of type null; however, parameter $action of Spiral\Core\CoreInterface::callAction() does only seem to accept string, 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

107
                        /** @scrutinizer ignore-type */ action: $action,
Loading history...
108 57
                        parameters: $parameters,
0 ignored issues
show
Bug introduced by
It seems like $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

108
                        /** @scrutinizer ignore-type */ parameters: $parameters,
Loading history...
109 57
                    ),
110 57
                    attributes: [
111 57
                        'route.controller' => $this->controller,
112 57
                        'route.action' => $action,
113 57
                        'route.parameters' => \array_keys($parameters),
0 ignored issues
show
Bug introduced by
It seems like $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

113
                        'route.parameters' => \array_keys(/** @scrutinizer ignore-type */ $parameters),
Loading history...
114 57
                    ]
115 57
                )
116 57
            );
117 9
        } catch (ControllerException $e) {
118 6
            \ob_get_clean();
119 6
            throw $this->mapException($e);
120 3
        } catch (\Throwable $e) {
121 3
            \ob_get_clean();
122 3
            throw $e;
123
        } finally {
124 57
            while (\ob_get_level() > $outputLevel + 1) {
125 3
                $output = \ob_get_clean() . $output;
126
            }
127
        }
128
129 48
        return $this->wrapResponse(
130 48
            $response,
131 48
            $result,
132 48
            \ob_get_clean() . $output,
133 48
        );
134
    }
135
136
    /**
137
     * Convert endpoint result into valid response.
138
     *
139
     * @param Response $response Initial pipeline response.
140
     * @param mixed    $result   Generated endpoint output.
141
     * @param string   $output   Buffer output.
142
     */
143 48
    private function wrapResponse(Response $response, mixed $result = null, string $output = ''): Response
144
    {
145 48
        if ($result instanceof Response) {
146 1
            if ($output !== '' && $result->getBody()->isWritable()) {
147 1
                $result->getBody()->write($output);
148
            }
149
150 1
            return $result;
151
        }
152
153 47
        if ($result instanceof \Generator) {
154
            return $response->withBody(new GeneratorStream($result));
155
        }
156
157 47
        if (\is_array($result) || $result instanceof \JsonSerializable) {
158 16
            $response = $this->writeJson($response, $result);
159
        } else {
160 32
            $response->getBody()->write((string)$result);
161
        }
162
163
        //Always glue buffered output
164 47
        $response->getBody()->write($output);
165
166 47
        return $response;
167
    }
168
169
    /**
170
     * Converts core specific ControllerException into HTTP ClientException.
171
     */
172 6
    private function mapException(ControllerException $exception): ClientException
173
    {
174 6
        return match ($exception->getCode()) {
175 6
            ControllerException::BAD_ACTION,
176 6
            ControllerException::NOT_FOUND => new NotFoundException('Not found', $exception),
177 6
            ControllerException::FORBIDDEN => new ForbiddenException('Forbidden', $exception),
178 6
            ControllerException::UNAUTHORIZED => new UnauthorizedException('Unauthorized', $exception),
179 6
            default => new BadRequestException('Bad request', $exception),
180 6
        };
181
    }
182
183
    /**
184
     * @psalm-assert !null $this->controller
185
     * @psalm-assert !null $this->action
186
     * @psalm-assert !null $this->parameters
187
     * @mutation-free
188
     */
189 58
    private function checkValues(): void
190
    {
191 58
        if ($this->controller === null) {
192 1
            throw new HandlerException('Controller and action pair are not set.');
193
        }
194
    }
195
}
196