Passed
Pull Request — master (#1104)
by Aleksei
26:20
created

CoreHandler::handle()   B

Complexity

Conditions 6
Paths 44

Size

Total Lines 65
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 44
dl 0
loc 65
ccs 50
cts 50
cp 1
rs 8.5937
c 1
b 0
f 0
cc 6
nc 44
nop 1
crap 6

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

103
                ? \strtolower($request->getMethod()) . \ucfirst(/** @scrutinizer ignore-type */ $this->action)
Loading history...
104 53
                : $this->action;
105
106
            // run the core withing PSR-7 Request/Response scope
107
            /**
108
             * @psalm-suppress InvalidArgument
109
             * TODO: Can we bind all controller classes at the bootstrap stage?
110
             */
111 54
            $result = $this->scope->runScope(
112 54
                new Scope(
0 ignored issues
show
Bug introduced by
new Spiral\Core\Scope('h...roller => $controller)) of type Spiral\Core\Scope is incompatible with the type array expected by parameter $bindings of Spiral\Core\ScopeInterface::runScope(). ( Ignorable by Annotation )

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

112
                /** @scrutinizer ignore-type */ new Scope(
Loading history...
113 54
                    name: 'http.request',
114 54
                    bindings: [Request::class => $request, Response::class => $response, $controller => $controller],
115 54
                ),
116 54
                fn (): mixed => $this->tracer->trace(
117 54
                    name: 'Controller [' . $controller . ':' . $action . ']',
118 54
                    callback: $this->isLegacyPipeline
119 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

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

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

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

122
                            /** @scrutinizer ignore-type */ parameters: $parameters,
Loading history...
123 2
                        )
124 54
                        : 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

124
                        : fn (): mixed => $this->core->/** @scrutinizer ignore-call */ handle(
Loading history...
125 54
                            new CallContext(
126 54
                                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

126
                                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 object|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

126
                                Target::fromPair(/** @scrutinizer ignore-type */ $controller, $action),
Loading history...
127 54
                                $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

127
                                /** @scrutinizer ignore-type */ $parameters,
Loading history...
128 54
                            ),
129 54
                        ),
130 54
                    attributes: [
131 54
                        'route.controller' => $this->controller,
132 54
                        'route.action' => $action,
133 54
                        '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

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