Passed
Pull Request — master (#1095)
by Aleksei
11:10
created

CoreHandler   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Test Coverage

Coverage 98.86%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 82
dl 0
loc 179
ccs 87
cts 88
cp 0.9886
rs 10
c 1
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A withVerbActions() 0 6 1
B handle() 0 61 6
A mapException() 0 9 1
A __construct() 0 8 1
A withContext() 0 8 1
A checkValues() 0 4 2
B wrapResponse() 0 24 7
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\ScopeInterface;
13
use Spiral\Http\Exception\ClientException;
14
use Spiral\Http\Exception\ClientException\BadRequestException;
15
use Spiral\Http\Exception\ClientException\ForbiddenException;
16
use Spiral\Http\Exception\ClientException\NotFoundException;
17
use Spiral\Http\Exception\ClientException\ServerErrorException;
18
use Spiral\Http\Exception\ClientException\UnauthorizedException;
19
use Spiral\Http\Stream\GeneratorStream;
20
use Spiral\Http\Traits\JsonTrait;
21
use Spiral\Interceptors\Context\CallContext;
22
use Spiral\Interceptors\Context\Target;
23
use Spiral\Interceptors\Exception\TargetCallException;
24
use Spiral\Interceptors\HandlerInterface;
25
use Spiral\Router\Exception\HandlerException;
26
use Spiral\Telemetry\NullTracer;
27
use Spiral\Telemetry\TracerInterface;
28
29
final class CoreHandler implements RequestHandlerInterface
30
{
31
    use JsonTrait;
32
33
    private readonly TracerInterface $tracer;
34
35
    /** @readonly */
36
    private ?string $controller = null;
37
    /** @readonly */
38
    private ?string $action = null;
39
    /** @readonly */
40
    private ?bool $verbActions = null;
41
    /** @readonly */
42
    private ?array $parameters = null;
43
44
    private bool $isLegacyPipeline;
45
46 56
    public function __construct(
47
        private readonly HandlerInterface|CoreInterface $core,
48
        private readonly ScopeInterface $scope,
49
        private readonly ResponseFactoryInterface $responseFactory,
50
        ?TracerInterface $tracer = null
51
    ) {
52 56
        $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...
53 56
        $this->isLegacyPipeline = !$core instanceof HandlerInterface;
54
    }
55
56
    /**
57
     * @mutation-free
58
     */
59 54
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
60
    {
61 54
        $handler = clone $this;
62 54
        $handler->controller = $controller;
63 54
        $handler->action = $action;
64 54
        $handler->parameters = $parameters;
65
66 54
        return $handler;
67
    }
68
69
    /**
70
     * Disable or enable HTTP prefix for actions.
71
     *
72
     * @mutation-free
73
     */
74 54
    public function withVerbActions(bool $verbActions): CoreHandler
75
    {
76 54
        $handler = clone $this;
77 54
        $handler->verbActions = $verbActions;
78
79 54
        return $handler;
80
    }
81
82
    /**
83
     * @psalm-suppress UnusedVariable
84
     * @throws \Throwable
85
     */
86 54
    public function handle(Request $request): Response
87
    {
88 54
        $this->checkValues();
89 53
        $controller = $this->controller;
90 53
        $parameters = $this->parameters;
91
92 53
        $outputLevel = \ob_get_level();
93 53
        \ob_start();
94
95 53
        $result = null;
96 53
        $output = '';
97
98 53
        $response = $this->responseFactory->createResponse(200);
99
        try {
100 53
            $action = $this->verbActions
101 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

101
                ? \strtolower($request->getMethod()) . \ucfirst(/** @scrutinizer ignore-type */ $this->action)
Loading history...
102 52
                : $this->action;
103
104
            // run the core withing PSR-7 Request/Response scope
105 53
            $result = $this->scope->runScope(
106 53
                [
107 53
                    Request::class  => $request,
108 53
                    Response::class => $response,
109 53
                ],
110 53
                fn (): mixed => $this->tracer->trace(
111 53
                    name: 'Controller [' . $controller . ':' . $action . ']',
112 53
                    callback: $this->isLegacyPipeline
113 2
                        ? fn (): mixed => $this->core->callAction(
0 ignored issues
show
Bug introduced by
The method callAction() does not exist on Spiral\Interceptors\HandlerInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Spiral\Interceptors\Handler\ReflectionHandler or Spiral\Interceptors\Handler\InterceptorPipeline. Are you sure you never get one of those? ( Ignorable by Annotation )

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

113
                        ? fn (): mixed => $this->core->/** @scrutinizer ignore-call */ callAction(
Loading history...
114 2
                            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

114
                            /** @scrutinizer ignore-type */ controller: $controller,
Loading history...
115 2
                            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

115
                            /** @scrutinizer ignore-type */ action: $action,
Loading history...
116 2
                            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

116
                            /** @scrutinizer ignore-type */ parameters: $parameters,
Loading history...
117 2
                        )
118 53
                        : fn (): mixed => $this->core->handle(
0 ignored issues
show
Bug introduced by
The method handle() does not exist on Spiral\Core\CoreInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Spiral\Console\CommandCore or Spiral\Events\Interceptor\Core. Are you sure you never get one of those? ( Ignorable by Annotation )

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

118
                        : fn (): mixed => $this->core->/** @scrutinizer ignore-call */ handle(
Loading history...
119 53
                            new CallContext(
120 53
                                Target::fromPair($controller, $action),
0 ignored issues
show
Bug introduced by
It seems like $action can also be of type null; however, parameter $action of Spiral\Interceptors\Context\Target::fromPair() 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

120
                                Target::fromPair($controller, /** @scrutinizer ignore-type */ $action),
Loading history...
Bug introduced by
It seems like $controller can also be of type null; however, parameter $controller of Spiral\Interceptors\Context\Target::fromPair() 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

120
                                Target::fromPair(/** @scrutinizer ignore-type */ $controller, $action),
Loading history...
121 53
                                $parameters,
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type null; however, parameter $arguments of Spiral\Interceptors\Cont...lContext::__construct() 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

121
                                /** @scrutinizer ignore-type */ $parameters,
Loading history...
122 53
                            ),
123 53
                        ),
124 53
                    attributes: [
125 53
                        'route.controller' => $this->controller,
126 53
                        'route.action' => $action,
127 53
                        '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

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