AbstractCommand::setClient()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Guzzle\Service\Command;
4
5
use Guzzle\Common\Collection;
6
use Guzzle\Common\Exception\InvalidArgumentException;
7
use Guzzle\Http\Message\RequestInterface;
8
use Guzzle\Http\Curl\CurlHandle;
9
use Guzzle\Service\Client;
10
use Guzzle\Service\ClientInterface;
11
use Guzzle\Service\Description\Operation;
12
use Guzzle\Service\Description\OperationInterface;
13
use Guzzle\Service\Description\ValidatorInterface;
14
use Guzzle\Service\Description\SchemaValidator;
15
use Guzzle\Service\Exception\CommandException;
16
use Guzzle\Service\Exception\ValidationException;
17
18
/**
19
 * Command object to handle preparing and processing client requests and responses of the requests
20
 */
21
abstract class AbstractCommand extends Collection implements CommandInterface
22
{
23
    // @deprecated: Option used to specify custom headers to add to the generated request
24
    const HEADERS_OPTION = 'command.headers';
25
    // @deprecated: Option used to add an onComplete method to a command
26
    const ON_COMPLETE = 'command.on_complete';
27
    // @deprecated: Option used to change the entity body used to store a response
28
    const RESPONSE_BODY = 'command.response_body';
29
30
    // Option used to add request options to the request created by a command
31
    const REQUEST_OPTIONS = 'command.request_options';
32
    // command values to not count as additionalParameters
33
    const HIDDEN_PARAMS = 'command.hidden_params';
34
    // Option used to disable any pre-sending command validation
35
    const DISABLE_VALIDATION = 'command.disable_validation';
36
    // Option used to override how a command result will be formatted
37
    const RESPONSE_PROCESSING = 'command.response_processing';
38
    // Different response types that commands can use
39
    const TYPE_RAW = 'raw';
40
    const TYPE_MODEL = 'model';
41
    const TYPE_NO_TRANSLATION = 'no_translation';
42
43
    /** @var ClientInterface Client object used to execute the command */
44
    protected $client;
45
46
    /** @var RequestInterface The request object associated with the command */
47
    protected $request;
48
49
    /** @var mixed The result of the command */
50
    protected $result;
51
52
    /** @var OperationInterface API information about the command */
53
    protected $operation;
54
55
    /** @var mixed callable */
56
    protected $onComplete;
57
58
    /** @var ValidatorInterface Validator used to prepare and validate properties against a JSON schema */
59
    protected $validator;
60
61
    /**
62
     * @param array|Collection   $parameters Collection of parameters to set on the command
63
     * @param OperationInterface $operation Command definition from description
64
     */
65
    public function __construct($parameters = array(), OperationInterface $operation = null)
66
    {
67
        parent::__construct($parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters defined by parameter $parameters on line 65 can also be of type object<Guzzle\Common\Collection>; however, Guzzle\Common\Collection::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
68
        $this->operation = $operation ?: $this->createOperation();
69
        foreach ($this->operation->getParams() as $name => $arg) {
70
            $currentValue = $this[$name];
71
            $configValue = $arg->getValue($currentValue);
72
            // If default or static values are set, then this should always be updated on the config object
73
            if ($currentValue !== $configValue) {
74
                $this[$name] = $configValue;
75
            }
76
        }
77
78
        $headers = $this[self::HEADERS_OPTION];
79
        if (!$headers instanceof Collection) {
80
            $this[self::HEADERS_OPTION] = new Collection((array) $headers);
81
        }
82
83
        // You can set a command.on_complete option in your parameters to set an onComplete callback
84
        if ($onComplete = $this['command.on_complete']) {
85
            unset($this['command.on_complete']);
86
            $this->setOnComplete($onComplete);
87
        }
88
89
        // Set the hidden additional parameters
90
        if (!$this[self::HIDDEN_PARAMS]) {
91
            $this[self::HIDDEN_PARAMS] = array(
92
                self::HEADERS_OPTION,
93
                self::RESPONSE_PROCESSING,
94
                self::HIDDEN_PARAMS,
95
                self::REQUEST_OPTIONS
96
            );
97
        }
98
99
        $this->init();
100
    }
101
102
    /**
103
     * Custom clone behavior
104
     */
105
    public function __clone()
106
    {
107
        $this->request = null;
108
        $this->result = null;
109
    }
110
111
    /**
112
     * Execute the command in the same manner as calling a function
113
     *
114
     * @return mixed Returns the result of {@see AbstractCommand::execute}
115
     */
116
    public function __invoke()
117
    {
118
        return $this->execute();
119
    }
120
121
    public function getName()
122
    {
123
        return $this->operation->getName();
124
    }
125
126
    /**
127
     * Get the API command information about the command
128
     *
129
     * @return OperationInterface
130
     */
131
    public function getOperation()
132
    {
133
        return $this->operation;
134
    }
135
136
    public function setOnComplete($callable)
137
    {
138
        if (!is_callable($callable)) {
139
            throw new InvalidArgumentException('The onComplete function must be callable');
140
        }
141
142
        $this->onComplete = $callable;
143
144
        return $this;
145
    }
146
147
    public function execute()
148
    {
149
        if (!$this->client) {
150
            throw new CommandException('A client must be associated with the command before it can be executed.');
151
        }
152
153
        return $this->client->execute($this);
154
    }
155
156
    public function getClient()
157
    {
158
        return $this->client;
159
    }
160
161
    public function setClient(ClientInterface $client)
162
    {
163
        $this->client = $client;
164
165
        return $this;
166
    }
167
168
    public function getRequest()
169
    {
170
        if (!$this->request) {
171
            throw new CommandException('The command must be prepared before retrieving the request');
172
        }
173
174
        return $this->request;
175
    }
176
177
    public function getResponse()
178
    {
179
        if (!$this->isExecuted()) {
180
            $this->execute();
181
        }
182
183
        return $this->request->getResponse();
184
    }
185
186
    public function getResult()
187
    {
188
        if (!$this->isExecuted()) {
189
            $this->execute();
190
        }
191
192
        if (null === $this->result) {
193
            $this->process();
194
            // Call the onComplete method if one is set
195
            if ($this->onComplete) {
196
                call_user_func($this->onComplete, $this);
197
            }
198
        }
199
200
        return $this->result;
201
    }
202
203
    public function setResult($result)
204
    {
205
        $this->result = $result;
206
207
        return $this;
208
    }
209
210
    public function isPrepared()
211
    {
212
        return $this->request !== null;
213
    }
214
215
    public function isExecuted()
216
    {
217
        return $this->request !== null && $this->request->getState() == 'complete';
218
    }
219
220
    public function prepare()
221
    {
222
        if (!$this->isPrepared()) {
223
            if (!$this->client) {
224
                throw new CommandException('A client must be associated with the command before it can be prepared.');
225
            }
226
227
            // If no response processing value was specified, then attempt to use the highest level of processing
228
            if (!isset($this[self::RESPONSE_PROCESSING])) {
229
                $this[self::RESPONSE_PROCESSING] = self::TYPE_MODEL;
230
            }
231
232
            // Notify subscribers of the client that the command is being prepared
233
            $this->client->dispatch('command.before_prepare', array('command' => $this));
234
235
            // Fail on missing required arguments, and change parameters via filters
236
            $this->validate();
237
            // Delegate to the subclass that implements the build method
238
            $this->build();
239
240
            // Add custom request headers set on the command
241
            if ($headers = $this[self::HEADERS_OPTION]) {
242
                foreach ($headers as $key => $value) {
243
                    $this->request->setHeader($key, $value);
244
                }
245
            }
246
247
            // Add any curl options to the request
248
            if ($options = $this[Client::CURL_OPTIONS]) {
249
                $this->request->getCurlOptions()->overwriteWith(CurlHandle::parseCurlConfig($options));
250
            }
251
252
            // Set a custom response body
253
            if ($responseBody = $this[self::RESPONSE_BODY]) {
254
                $this->request->setResponseBody($responseBody);
255
            }
256
257
            $this->client->dispatch('command.after_prepare', array('command' => $this));
258
        }
259
260
        return $this->request;
261
    }
262
263
    /**
264
     * Set the validator used to validate and prepare command parameters and nested JSON schemas. If no validator is
265
     * set, then the command will validate using the default {@see SchemaValidator}.
266
     *
267
     * @param ValidatorInterface $validator Validator used to prepare and validate properties against a JSON schema
268
     *
269
     * @return self
270
     */
271
    public function setValidator(ValidatorInterface $validator)
272
    {
273
        $this->validator = $validator;
274
275
        return $this;
276
    }
277
278
    public function getRequestHeaders()
279
    {
280
        return $this[self::HEADERS_OPTION];
281
    }
282
283
    /**
284
     * Initialize the command (hook that can be implemented in subclasses)
285
     */
286
    protected function init() {}
287
288
    /**
289
     * Create the request object that will carry out the command
290
     */
291
    abstract protected function build();
292
293
    /**
294
     * Hook used to create an operation for concrete commands that are not associated with a service description
295
     *
296
     * @return OperationInterface
297
     */
298
    protected function createOperation()
299
    {
300
        return new Operation(array('name' => get_class($this)));
301
    }
302
303
    /**
304
     * Create the result of the command after the request has been completed.
305
     * Override this method in subclasses to customize this behavior
306
     */
307
    protected function process()
308
    {
309
        $this->result = $this[self::RESPONSE_PROCESSING] != self::TYPE_RAW
310
            ? DefaultResponseParser::getInstance()->parse($this)
311
            : $this->request->getResponse();
312
    }
313
314
    /**
315
     * Validate and prepare the command based on the schema and rules defined by the command's Operation object
316
     *
317
     * @throws ValidationException when validation errors occur
318
     */
319
    protected function validate()
320
    {
321
        // Do not perform request validation/transformation if it is disable
322
        if ($this[self::DISABLE_VALIDATION]) {
323
            return;
324
        }
325
326
        $errors = array();
327
        $validator = $this->getValidator();
328
        foreach ($this->operation->getParams() as $name => $schema) {
329
            $value = $this[$name];
330 View Code Duplication
            if (!$validator->validate($schema, $value)) {
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...
331
                $errors = array_merge($errors, $validator->getErrors());
332
            } elseif ($value !== $this[$name]) {
333
                // Update the config value if it changed and no validation errors were encountered
334
                $this->data[$name] = $value;
335
            }
336
        }
337
338
        // Validate additional parameters
339
        $hidden = $this[self::HIDDEN_PARAMS];
340
341
        if ($properties = $this->operation->getAdditionalParameters()) {
342
            foreach ($this->toArray() as $name => $value) {
343
                // It's only additional if it isn't defined in the schema
344
                if (!$this->operation->hasParam($name) && !in_array($name, $hidden)) {
345
                    // Always set the name so that error messages are useful
346
                    $properties->setName($name);
347 View Code Duplication
                    if (!$validator->validate($properties, $value)) {
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...
348
                        $errors = array_merge($errors, $validator->getErrors());
349
                    } elseif ($value !== $this[$name]) {
350
                        $this->data[$name] = $value;
351
                    }
352
                }
353
            }
354
        }
355
356
        if (!empty($errors)) {
357
            $e = new ValidationException('Validation errors: ' . implode("\n", $errors));
358
            $e->setErrors($errors);
359
            throw $e;
360
        }
361
    }
362
363
    /**
364
     * Get the validator used to prepare and validate properties. If no validator has been set on the command, then
365
     * the default {@see SchemaValidator} will be used.
366
     *
367
     * @return ValidatorInterface
368
     */
369
    protected function getValidator()
370
    {
371
        if (!$this->validator) {
372
            $this->validator = SchemaValidator::getInstance();
373
        }
374
375
        return $this->validator;
376
    }
377
378
    /**
379
     * Get array of any validation errors
380
     * If no validator has been set then return false
381
     */
382
    public function getValidationErrors()
383
    {
384
        if (!$this->validator) {
385
            return false;
386
        }
387
388
        return $this->validator->getErrors();
389
    }
390
}
391