Passed
Pull Request — master (#1168)
by Aleksei
12:55
created

CoreHandler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 8
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
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;
10
use Psr\Http\Message\ServerRequestInterface as Request;
11
use Psr\Http\Server\RequestHandlerInterface;
12
use Spiral\Core\CoreInterface;
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 60
    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 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...
55 60
        $this->isLegacyPipeline = !$core instanceof HandlerInterface;
56
    }
57
58
    /**
59
     * @mutation-free
60
     */
61 58
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
62
    {
63 58
        $handler = clone $this;
64 58
        $handler->controller = $controller;
65 58
        $handler->action = $action;
66 58
        $handler->parameters = $parameters;
67
68 58
        return $handler;
69
    }
70
71
    /**
72
     * Disable or enable HTTP prefix for actions.
73
     *
74
     * @mutation-free
75
     */
76 58
    public function withVerbActions(bool $verbActions): CoreHandler
77
    {
78 58
        $handler = clone $this;
79 58
        $handler->verbActions = $verbActions;
80
81 58
        return $handler;
82
    }
83
84
    /**
85
     * @psalm-suppress UnusedVariable
86
     * @throws \Throwable
87
     */
88 56
    public function handle(Request $request): Response
89
    {
90 56
        $this->checkValues();
91 55
        $controller = $this->controller;
92 55
        $parameters = $this->parameters;
93
94 55
        $outputLevel = \ob_get_level();
95 55
        \ob_start();
96
97 55
        $result = null;
98 55
        $output = '';
99
100 55
        $response = $this->responseFactory->createResponse(200);
101
        try {
102 55
            $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 54
                : $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 55
            $result = $this->scope->runScope(
112 55
                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 55
                    name: 'http-request',
114 55
                    bindings: [Request::class => $request, Response::class => $response, $controller => $controller],
115 55
                ),
116 55
                fn (): mixed => $this->tracer->trace(
117 55
                    name: 'Controller [' . $controller . ':' . $action . ']',
118 55
                    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\AutowireHandler or Spiral\Interceptors\Handler\CallableHandler 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 55
                        : fn (): mixed => $this->core->handle(
0 ignored issues
show
Bug introduced by
The method handle() does not exist on Spiral\Core\CoreInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Spiral\Core\CoreInterface. ( 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 55
                            new CallContext(
126 55
                                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 55
                                $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 55
                                [
129 55
                                    ServerRequestInterface::class => $request,
130 55
                                ],
131 55
                            ),
132 55
                        ),
133 55
                    attributes: [
134 55
                        'route.controller' => $this->controller,
135 55
                        'route.action' => $action,
136 55
                        '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

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