Deserializer::visitOuterArray()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21

Duplication

Lines 3
Ratio 14.29 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 0
Metric Value
dl 3
loc 21
ccs 7
cts 8
cp 0.875
rs 9.584
c 0
b 0
f 0
cc 3
nc 3
nop 4
crap 3.0175
1
<?php
2
namespace GuzzleHttp\Command\Guzzle;
3
4
use GuzzleHttp\Command\CommandInterface;
5
use GuzzleHttp\Command\Guzzle\ResponseLocation\BodyLocation;
6
use GuzzleHttp\Command\Guzzle\ResponseLocation\HeaderLocation;
7
use GuzzleHttp\Command\Guzzle\ResponseLocation\JsonLocation;
8
use GuzzleHttp\Command\Guzzle\ResponseLocation\ReasonPhraseLocation;
9
use GuzzleHttp\Command\Guzzle\ResponseLocation\ResponseLocationInterface;
10
use GuzzleHttp\Command\Guzzle\ResponseLocation\StatusCodeLocation;
11
use GuzzleHttp\Command\Guzzle\ResponseLocation\XmlLocation;
12
use GuzzleHttp\Command\Result;
13
use GuzzleHttp\Command\ResultInterface;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
17
/**
18
 * Handler used to create response models based on an HTTP response and
19
 * a service description.
20
 *
21
 * Response location visitors are registered with this Handler to handle
22
 * locations (e.g., 'xml', 'json', 'header'). All of the locations of a response
23
 * model that will be visited first have their ``before`` method triggered.
24
 * After the before method is called on every visitor that will be walked, each
25
 * visitor is triggered using the ``visit()`` method. After all of the visitors
26
 * are visited, the ``after()`` method is called on each visitor. This is the
27
 * place in which you should handle things like additionalProperties with
28
 * custom locations (i.e., this is how it is handled in the JSON visitor).
29
 */
30
class Deserializer
31
{
32
    /** @var ResponseLocationInterface[] $responseLocations */
33
    private $responseLocations;
34
35
    /** @var DescriptionInterface $description */
36
    private $description;
37
38
    /** @var boolean $process */
39
    private $process;
40
41
    /**
42
     * @param DescriptionInterface $description
43
     * @param bool $process
44
     * @param ResponseLocationInterface[] $responseLocations Extra response locations
45
     */
46 16
    public function __construct(
47
        DescriptionInterface $description,
48
        $process,
49
        array $responseLocations = []
50
    ) {
51 16
        static $defaultResponseLocations;
52 16
        if (!$defaultResponseLocations) {
53
            $defaultResponseLocations = [
54 1
                'body'         => new BodyLocation(),
55 1
                'header'       => new HeaderLocation(),
56 1
                'reasonPhrase' => new ReasonPhraseLocation(),
57 1
                'statusCode'   => new StatusCodeLocation(),
58 1
                'xml'          => new XmlLocation(),
59 1
                'json'         => new JsonLocation(),
60 1
            ];
61 1
        }
62
63 16
        $this->responseLocations = $responseLocations + $defaultResponseLocations;
0 ignored issues
show
Documentation Bug introduced by
It seems like $responseLocations + $defaultResponseLocations of type array<integer|string,obj...onseLocationInterface>> is incompatible with the declared type array<integer,object<Guz...onseLocationInterface>> of property $responseLocations.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
64 16
        $this->description = $description;
65 16
        $this->process = $process;
66 16
    }
67
68
    /**
69
     * Deserialize the response into the specified result representation
70
     *
71
     * @param ResponseInterface     $response
72
     * @param RequestInterface|null $request
73
     * @param CommandInterface      $command
74
     * @return Result|ResultInterface|void|ResponseInterface
75
     */
76 15
    public function __invoke(ResponseInterface $response, RequestInterface $request, CommandInterface $command)
77
    {
78
        // If the user don't want to process the result, just return the plain response here
79 15
        if ($this->process === false) {
80
            return $response;
81
        }
82
83 15
        $name = $command->getName();
84 15
        $operation = $this->description->getOperation($name);
85
86 15
        $this->handleErrorResponses($response, $request, $command, $operation);
87
88
        // Add a default Model as the result if no matching schema was found
89 12
        if (!($modelName = $operation->getResponseModel())) {
90
            // Not sure if this should be empty or contains the response.
91
            // Decided to do it how it was in the old version for now.
92
            return new Result();
93
        }
94
95 12
        $model = $operation->getServiceDescription()->getModel($modelName);
96 12
        if (!$model) {
97
            throw new \RuntimeException("Unknown model: {$modelName}");
98
        }
99
100 12
        return $this->visit($model, $response);
101
    }
102
103
    /**
104
     * Handles visit() and after() methods of the Response locations
105
     *
106
     * @param Parameter         $model
107
     * @param ResponseInterface $response
108
     * @return Result|ResultInterface|void
109
     */
110 12
    protected function visit(Parameter $model, ResponseInterface $response)
111
    {
112 12
        $result = new Result();
113 12
        $context = ['visitors' => []];
114
115 12
        if ($model->getType() === 'object') {
116 10
            $result = $this->visitOuterObject($model, $result, $response, $context);
117 12
        } elseif ($model->getType() === 'array') {
118 2
            $result = $this->visitOuterArray($model, $result, $response, $context);
119 2
        } else {
120
            throw new \InvalidArgumentException('Invalid response model: ' . $model->getType());
121
        }
122
123
        // Call the after() method of each found visitor
124
        /** @var ResponseLocationInterface $visitor */
125 12
        foreach ($context['visitors'] as $visitor) {
126 11
            $result = $visitor->after($result, $response, $model);
127 12
        }
128
129 12
        return $result;
130
    }
131
132
    /**
133
     * Handles the before() method of Response locations
134
     *
135
     * @param string            $location
136
     * @param Parameter         $model
137
     * @param ResultInterface   $result
138
     * @param ResponseInterface $response
139
     * @param array             $context
140
     * @return ResultInterface
141
     */
142 11
    private function triggerBeforeVisitor(
143
        $location,
144
        Parameter $model,
145
        ResultInterface $result,
146
        ResponseInterface $response,
147
        array &$context
148
    ) {
149 11
        if (!isset($this->responseLocations[$location])) {
150
            throw new \RuntimeException("Unknown location: $location");
151
        }
152
153 11
        $context['visitors'][$location] = $this->responseLocations[$location];
154
155 11
        $result = $this->responseLocations[$location]->before(
156 11
            $result,
157 11
            $response,
158
            $model
159 11
        );
160
161 11
        return $result;
162
    }
163
164
    /**
165
     * Visits the outer object
166
     *
167
     * @param Parameter         $model
168
     * @param ResultInterface   $result
169
     * @param ResponseInterface $response
170
     * @param array             $context
171
     * @return ResultInterface
172
     */
173 10
    private function visitOuterObject(
174
        Parameter $model,
175
        ResultInterface $result,
176
        ResponseInterface $response,
177
        array &$context
178
    ) {
179 10
        $parentLocation = $model->getLocation();
180
181
        // If top-level additionalProperties is a schema, then visit it
182 10
        $additional = $model->getAdditionalProperties();
183 10
        if ($additional instanceof Parameter) {
184
            // Use the model location if none set on additionalProperties.
185 4
            $location = $additional->getLocation() ?: $parentLocation;
186 4
            $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
187 4
        }
188
189
        // Use 'location' from all individual defined properties, but fall back
190
        // to the model location if no per-property location is set. Collect
191
        // the properties that need to be visited into an array.
192 10
        $visitProperties = [];
193 10
        foreach ($model->getProperties() as $schema) {
194 7
            $location = $schema->getLocation() ?: $parentLocation;
195 7
            if ($location) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $location of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
196 7
                $visitProperties[] = [$location, $schema];
197
                // Trigger the before method on each unique visitor location
198 7 View Code Duplication
                if (!isset($context['visitors'][$location])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
199 5
                    $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
200 5
                }
201 7
            }
202 10
        }
203
204
        // Actually visit each response element
205 10
        foreach ($visitProperties as $property) {
206 7
            $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]);
207 10
        }
208
209 10
        return $result;
210
    }
211
212
    /**
213
     * Visits the outer array
214
     *
215
     * @param Parameter         $model
216
     * @param ResultInterface   $result
217
     * @param ResponseInterface $response
218
     * @param array             $context
219
     * @return ResultInterface|void
220
     */
221 2
    private function visitOuterArray(
222
        Parameter $model,
223
        ResultInterface $result,
224
        ResponseInterface $response,
225
        array &$context
226
    ) {
227
        // Use 'location' defined on the top of the model
228 2
        if (!($location = $model->getLocation())) {
229
            return;
230
        }
231
232
        // Trigger the before method on each unique visitor location
233 2 View Code Duplication
        if (!isset($context['visitors'][$location])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234 2
            $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
235 2
        }
236
237
        // Visit each item in the response
238 2
        $result = $this->responseLocations[$location]->visit($result, $response, $model);
239
240 2
        return $result;
241
    }
242
243
    /**
244
     * Reads the "errorResponses" from commands, and trigger appropriate exceptions
245
     *
246
     * In order for the exception to be properly triggered, all your exceptions must be instance
247
     * of "GuzzleHttp\Command\Exception\CommandException". If that's not the case, your exceptions will be wrapped
248
     * around a CommandException
249
     *
250
     * @param ResponseInterface $response
251
     * @param RequestInterface  $request
252
     * @param CommandInterface  $command
253
     * @param Operation         $operation
254
     */
255 15
    protected function handleErrorResponses(
256
        ResponseInterface $response,
257
        RequestInterface $request,
258
        CommandInterface $command,
259
        Operation $operation
260
    ) {
261 15
        $errors = $operation->getErrorResponses();
262
263
        // We iterate through each errors in service description. If the descriptor contains both a phrase and
264
        // status code, there must be an exact match of both. Otherwise, a match of status code is enough
265 15
        $bestException = null;
266
267 15
        foreach ($errors as $error) {
268 4
            $code = (int) $error['code'];
269
270 4
            if ($response->getStatusCode() !== $code) {
271 1
                continue;
272
            }
273
274 3
            if (isset($error['phrase']) && ! ($error['phrase'] === $response->getReasonPhrase())) {
275
                continue;
276
            }
277
278 3
            $bestException = $error['class'];
279
280
            // If there is an exact match of phrase + code, then we cannot find a more specialized exception in
281
            // the array, so we can break early instead of iterating the remaining ones
282 3
            if (isset($error['phrase'])) {
283 2
                break;
284
            }
285 15
        }
286
287 15
        if (null !== $bestException) {
288 3
            throw new $bestException($response->getReasonPhrase(), $command, null, $request, $response);
289
        }
290
291
        // If we reach here, no exception could be match from descriptor, and Guzzle exception will propagate if
292
        // option "http_errors" is set to true, which is the default setting.
293 12
    }
294
}
295