Completed
Push — master ( d8465d...85c396 )
by Stefano
03:17
created

Deserializer::visitOuterObject()   C

Complexity

Conditions 8
Paths 42

Size

Total Lines 38
Code Lines 20

Duplication

Lines 3
Ratio 7.89 %

Code Coverage

Tests 21
CRAP Score 8

Importance

Changes 0
Metric Value
dl 3
loc 38
ccs 21
cts 21
cp 1
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 42
nop 4
crap 8
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 13
    public function __construct(
47
        DescriptionInterface $description,
48
        $process,
49
        array $responseLocations = []
50
    ) {
51 13
        static $defaultResponseLocations;
52 13
        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 13
        $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 13
        $this->description = $description;
65 13
        $this->process = $process;
66 13
    }
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 12
    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 12
        if ($this->process === false) {
80
            return $response;
81
        }
82
83 12
        $name = $command->getName();
84 12
        $operation = $this->description->getOperation($name);
85
86 12
        $this->handleErrorResponses($response, $request, $command, $operation);
87
88
        // Add a default Model as the result if no matching schema was found
89 9
        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 9
        $model = $operation->getServiceDescription()->getModel($modelName);
96 9
        if (!$model) {
97
            throw new \RuntimeException("Unknown model: {$modelName}");
98
        }
99
100 9
        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 9
    protected function visit(Parameter $model, ResponseInterface $response)
111
    {
112 9
        $result = new Result();
113 9
        $context = ['visitors' => []];
114
115 9
        if ($model->getType() === 'object') {
116 7
            $result = $this->visitOuterObject($model, $result, $response, $context);
117 9
        } 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 9
        foreach ($context['visitors'] as $visitor) {
126 8
            $result = $visitor->after($result, $response, $model);
127 9
        }
128
129 9
        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 8
    private function triggerBeforeVisitor(
143
        $location,
144
        Parameter $model,
145
        ResultInterface $result,
146
        ResponseInterface $response,
147
        array &$context
148
    ) {
149 8
        if (!isset($this->responseLocations[$location])) {
150
            throw new \RuntimeException("Unknown location: $location");
151
        }
152
153 8
        $context['visitors'][$location] = $this->responseLocations[$location];
154
155 8
        $result = $this->responseLocations[$location]->before(
156 8
            $result,
157 8
            $response,
158
            $model
159 8
        );
160
161 8
        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 7
    private function visitOuterObject(
174
        Parameter $model,
175
        ResultInterface $result,
176
        ResponseInterface $response,
177
        array &$context
178
    ) {
179 7
        $parentLocation = $model->getLocation();
180
181
        // If top-level additionalProperties is a schema, then visit it
182 7
        $additional = $model->getAdditionalProperties();
183 7
        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 7
        $visitProperties = [];
193 7
        foreach ($model->getProperties() as $schema) {
194 4
            $location = $schema->getLocation() ?: $parentLocation;
195 4
            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 4
                $visitProperties[] = [$location, $schema];
197
                // Trigger the before method on each unique visitor location
198 4 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 2
                    $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
200 2
                }
201 4
            }
202 7
        }
203
204
        // Actually visit each response element
205 7
        foreach ($visitProperties as $property) {
206 4
            $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]);
207 7
        }
208
209 7
        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 12
    protected function handleErrorResponses(
256
        ResponseInterface $response,
257
        RequestInterface $request,
258
        CommandInterface $command,
259
        Operation $operation
260
    ) {
261 12
        $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 12
        $bestException = null;
266
267 12
        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 12
        }
286
287 12
        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 9
    }
294
}
295