Completed
Branch 09branch (946dde)
by Anton
05:16
created

MiddlewarePipeline   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 208
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 208
rs 10
c 0
b 0
f 0
wmc 19
lcom 1
cbo 6

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A target() 0 6 1
A __invoke() 0 4 1
A run() 0 8 2
A next() 0 20 3
B mountResponse() 0 31 4
B wrapResponse() 0 21 6
A getNext() 0 20 1
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Http;
10
11
use Psr\Http\Message\ResponseInterface as Response;
12
use Psr\Http\Message\ServerRequestInterface as Request;
13
use Spiral\Core\ContainerInterface;
14
use Spiral\Core\Exceptions\ScopeException;
15
use Spiral\Http\Exceptions\MiddlewareException;
16
use Spiral\Http\Traits\JsonTrait;
17
use Spiral\Http\Traits\MiddlewaresTrait;
18
19
/**
20
 * Pipeline used to pass request and response thought the chain of middlewares.
21
 *
22
 * Spiral middlewares are similar to Laravel's one. However router and http itself
23
 * can be in used in zend expressive.
24
 */
25
class MiddlewarePipeline
26
{
27
    use JsonTrait, MiddlewaresTrait;
28
29
    /**
30
     * @invisible
31
     * @var ContainerInterface
32
     */
33
    private $container;
34
35
    /**
36
     * Endpoint should be called at the deepest level of pipeline.
37
     *
38
     * @var callable
39
     */
40
    private $target = null;
41
42
    /**
43
     * @param callable[]|MiddlewareInterface[] $middlewares
44
     * @param ContainerInterface               $container Spiral container is needed, due scoping.
45
     *
46
     * @throws ScopeException
47
     */
48
    public function __construct(array $middlewares = [], ContainerInterface $container)
49
    {
50
        $this->middlewares = $middlewares;
51
        $this->container = $container;
52
    }
53
54
    /**
55
     * Set pipeline target.
56
     *
57
     * @param callable $target
58
     *
59
     * @return $this|self
60
     */
61
    public function target(callable $target): MiddlewarePipeline
62
    {
63
        $this->target = $target;
64
65
        return $this;
66
    }
67
68
    /**
69
     * @param Request  $request
70
     * @param Response $response
71
     *
72
     * @return Response
73
     */
74
    public function __invoke(Request $request, Response $response): Response
75
    {
76
        return $this->run($request, $response);
77
    }
78
79
    /**
80
     * Pass request and response though every middleware to target and return generated and wrapped
81
     * response.
82
     *
83
     * @param Request  $request
84
     * @param Response $response
85
     *
86
     * @return Response
87
     *
88
     * @throws MiddlewareException
89
     */
90
    public function run(Request $request, Response $response): Response
91
    {
92
        if (empty($this->target)) {
93
            throw new MiddlewareException("Unable to run pipeline without specified target");
94
        }
95
96
        return $this->next(0, $request, $response);
97
    }
98
99
    /**
100
     * Get next chain to be called. Exceptions will be converted to responses.
101
     *
102
     * @param int      $position
103
     * @param Request  $request
104
     * @param Response $response
105
     *
106
     * @return null|Response
107
     * @throws \Exception
108
     */
109
    protected function next(int $position, Request $request, Response $response)
110
    {
111
        if (!isset($this->middlewares[$position])) {
112
            //Middleware target endpoint to be called and converted into response
113
            return $this->mountResponse($request, $response);
114
        }
115
116
        /**
117
         * @var callable $next
118
         */
119
        $next = $this->middlewares[$position];
120
121
        if (is_string($next)) {
122
            //Resolve using container
123
            $next = $this->container->get($next);
124
        }
125
126
        //Executing next middleware
127
        return $next($request, $response, $this->getNext($position, $request, $response));
128
    }
129
130
    /**
131
     * Run pipeline target and return generated response.
132
     *
133
     * @param Request  $request
134
     * @param Response $response
135
     *
136
     * @return Response
137
     *
138
     * @throws \Throwable
139
     */
140
    protected function mountResponse(Request $request, Response $response): Response
141
    {
142
        $outputLevel = ob_get_level();
143
        ob_start();
144
145
        $output = '';
146
        $result = null;
147
148
        $scope = [
149
            $this->container->replace(Request::class, $request),
0 ignored issues
show
Documentation introduced by
$request is of type object<Psr\Http\Message\ServerRequestInterface>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
150
            $this->container->replace(Response::class, $response)
0 ignored issues
show
Documentation introduced by
$response is of type object<Psr\Http\Message\ResponseInterface>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
151
        ];
152
153
        try {
154
            $result = call_user_func($this->target, $request, $response);
155
        } catch (\Throwable $e) {
156
            //Close buffer due error
157
            ob_get_clean();
158
            throw  $e;
159
        } finally {
160
            foreach (array_reverse($scope) as $payload) {
161
                $this->container->restore($payload);
162
            }
163
164
            while (ob_get_level() > $outputLevel + 1) {
165
                $output = ob_get_clean() . $output;
166
            }
167
        }
168
169
        return $this->wrapResponse($response, $result, ob_get_clean() . $output);
170
    }
171
172
    /**
173
     * Convert endpoint result into valid response.
174
     *
175
     * @param Response $response Initial pipeline response.
176
     * @param mixed    $result   Generated endpoint output.
177
     * @param string   $output   Buffer output.
178
     *
179
     * @return Response
180
     */
181
    private function wrapResponse(Response $response, $result = null, string $output = ''): Response
182
    {
183
        if ($result instanceof Response) {
184
            if (!empty($output) && $result->getBody()->isWritable()) {
185
                $result->getBody()->write($output);
186
            }
187
188
            return $result;
189
        }
190
191
        if (is_array($result) || $result instanceof \JsonSerializable) {
0 ignored issues
show
Bug introduced by
The class JsonSerializable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
192
            $response = $this->writeJson($response, $result);
193
        } else {
194
            $response->getBody()->write($result);
195
        }
196
197
        //Always glue buffered output
198
        $response->getBody()->write($output);
199
200
        return $response;
201
    }
202
203
    /**
204
     * Get next callable element.
205
     *
206
     * @param int      $position
207
     * @param Request  $outerRequest
208
     * @param Response $outerResponse
209
     *
210
     * @return \Closure
211
     */
212
    private function getNext(
213
        int $position,
214
        Request $outerRequest,
215
        Response $outerResponse
216
    ): \Closure {
217
        $next = function ($request = null, $response = null) use (
218
            $position,
219
            $outerRequest,
220
            $outerResponse
221
        ) {
222
            //This function will be provided to next (deeper) middleware
223
            return $this->next(
224
                ++$position,
225
                $request ?? $outerRequest,
226
                $response ?? $outerResponse
227
            );
228
        };
229
230
        return $next;
231
    }
232
}
233