Passed
Pull Request — master (#656)
by Abdul Malik
09:06 queued 02:28
created

CoreHandler::wrapResponse()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.0283

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 12
nc 5
nop 3
dl 0
loc 24
ccs 11
cts 12
cp 0.9167
crap 7.0283
rs 8.8333
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Router;
13
14
use Throwable;
15
use Generator;
16
use JsonSerializable;
17
use Psr\Http\Message\ResponseFactoryInterface;
18
use Psr\Http\Message\ResponseInterface as Response;
19
use Psr\Http\Message\ServerRequestInterface as Request;
20
use Psr\Http\Server\RequestHandlerInterface;
21
use Spiral\Core\CoreInterface;
22
use Spiral\Core\Exception\ControllerException;
23
use Spiral\Core\ScopeInterface;
24
use Spiral\Http\Exception\ClientException;
25
use Spiral\Http\Exception\ClientException\BadRequestException;
26
use Spiral\Http\Exception\ClientException\ForbiddenException;
27
use Spiral\Http\Exception\ClientException\NotFoundException;
28
use Spiral\Http\Exception\ClientException\UnauthorizedException;
29
use Spiral\Http\Stream\GeneratorStream;
30
use Spiral\Http\Traits\JsonTrait;
31
use Spiral\Router\Exception\HandlerException;
32
33
final class CoreHandler implements RequestHandlerInterface
34
{
35
    use JsonTrait;
36
37
    private CoreInterface $core;
38
39
    private ScopeInterface $scope;
40
41
    private ?string $controller = null;
42
43
    private ?string $action = null;
44
45
    private ?bool $verbActions = null;
46
47
    private ?array $parameters = null;
48
49
    private ResponseFactoryInterface $responseFactory;
50
51 73
    public function __construct(
52
        CoreInterface $core,
53
        ScopeInterface $scope,
54
        ResponseFactoryInterface $responseFactory
55
    ) {
56 73
        $this->core = $core;
57 73
        $this->scope = $scope;
58 73
        $this->responseFactory = $responseFactory;
59
    }
60
61
    /**
62
     * @param string|null $action
63
     */
64 71
    public function withContext(string $controller, string $action, array $parameters): CoreHandler
65
    {
66 71
        $handler = clone $this;
67 71
        $handler->controller = $controller;
68 71
        $handler->action = $action;
69 71
        $handler->parameters = $parameters;
70
71 71
        return $handler;
72
    }
73
74
    /**
75
     * Disable or enable HTTP prefix for actions.
76
     */
77 71
    public function withVerbActions(bool $verbActions): CoreHandler
78
    {
79 71
        $handler = clone $this;
80 71
        $handler->verbActions = $verbActions;
81
82 71
        return $handler;
83
    }
84
85
    /**
86
     * @inheritdoc
87
     *
88
     * @psalm-suppress UnusedVariable
89
     * @throws Throwable
90
     */
91 71
    public function handle(Request $request): Response
92
    {
93 71
        if ($this->controller === null) {
94 1
            throw new HandlerException('Controller and action pair is not set');
95
        }
96
97 70
        $outputLevel = ob_get_level();
98 70
        ob_start();
99
100 70
        $output = $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
101
102 70
        $response = $this->responseFactory->createResponse(200);
103
        try {
104
            // run the core withing PSR-7 Request/Response scope
105 70
            $result = $this->scope->runScope(
106
                [
107 70
                    Request::class  => $request,
108
                    Response::class => $response,
109
                ],
110 70
                fn() => $this->core->callAction(
111 70
                    $this->controller,
112 70
                    $this->getAction($request),
113 70
                    $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

113
                    /** @scrutinizer ignore-type */ $this->parameters
Loading history...
114
                )
115
            );
116 11
        } catch (ControllerException $e) {
117 6
            ob_get_clean();
118 6
            throw $this->mapException($e);
119 5
        } catch (Throwable $e) {
120 5
            ob_get_clean();
121 5
            throw $e;
122 59
        } finally {
123 70
            while (ob_get_level() > $outputLevel + 1) {
124 3
                $output = ob_get_clean() . $output;
125
            }
126
        }
127
128 59
        return $this->wrapResponse(
129
            $response,
130
            $result,
131 59
            ob_get_clean() . $output
132
        );
133
    }
134
135 70
    private function getAction(Request $request): string
136
    {
137 70
        if ($this->verbActions) {
138 1
            return 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

138
            return strtolower($request->getMethod()) . ucfirst(/** @scrutinizer ignore-type */ $this->action);
Loading history...
139
        }
140
141 69
        return $this->action;
142
    }
143
144
    /**
145
     * Convert endpoint result into valid response.
146
     *
147
     * @param Response $response Initial pipeline response.
148
     * @param mixed    $result   Generated endpoint output.
149
     * @param string   $output   Buffer output.
150
     */
151 59
    private function wrapResponse(Response $response, $result = null, string $output = ''): Response
152
    {
153 59
        if ($result instanceof Response) {
154 1
            if ($output !== '' && $result->getBody()->isWritable()) {
155 1
                $result->getBody()->write($output);
156
            }
157
158 1
            return $result;
159
        }
160
161 58
        if ($result instanceof Generator) {
162
            return $response->withBody(new GeneratorStream($result));
163
        }
164
165 58
        if (\is_array($result) || $result instanceof JsonSerializable) {
166 18
            $response = $this->writeJson($response, $result);
167
        } else {
168 42
            $response->getBody()->write((string)$result);
169
        }
170
171
        //Always glue buffered output
172 58
        $response->getBody()->write($output);
173
174 58
        return $response;
175
    }
176
177
    /**
178
     * Converts core specific ControllerException into HTTP ClientException.
179
     */
180 6
    private function mapException(ControllerException $exception): ClientException
181
    {
182 6
        switch ($exception->getCode()) {
183 6
            case ControllerException::BAD_ACTION:
184 2
            case ControllerException::NOT_FOUND:
185 4
                return new NotFoundException($exception->getMessage());
186 2
            case ControllerException::FORBIDDEN:
187 1
                return new ForbiddenException($exception->getMessage());
188 1
            case ControllerException::UNAUTHORIZED:
189
                return new UnauthorizedException($exception->getMessage());
190
            default:
191 1
                return new BadRequestException($exception->getMessage());
192
        }
193
    }
194
}
195