Test Failed
Push — master ( 357bf5...71cdad )
by Russell
03:53
created

WebApiClient   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
wmc 63
eloc 129
c 2
b 1
f 1
dl 0
loc 372
rs 3.36

How to fix   Complexity   

Complex Class

Complex classes like WebApiClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WebApiClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
4
Copyright (c) 2009, SilverStripe Australia Limited - www.silverstripe.com.au
5
All rights reserved.
6
7
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
8
9
    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
10
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
11
      documentation and/or other materials provided with the distribution.
12
    * Neither the name of SilverStripe nor the names of its contributors may be used to endorse or promote products derived from this software
13
      without specific prior written permission.
14
15
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
17
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
18
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
19
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
20
OF SUCH DAMAGE.
21
22
*/
23
24
/**
25
 * An object that acts as a client to a web API of some sort.
26
 *
27
 * The WebApiClient takes in a mapping of method calls of the form
28
 *
29
 *
30
 * auth => Whether the user needs to be authenticted
31
 * url => The URL to call. This may have placeholders in it (marked as {enclosed} items) which will be replaced
32
 * 			by any arguments in the passed in args array that match these keys.
33
 * enctype => What encoding to use when calling this method (defaults to Zend_Http_Client::ENC_URLENCODED)
34
 * contentType => Whether to use a specific content type for the request (alfresco sometimes needs
35
 * 			a specific content type eg application/cmisquery+xml)
36
 * return => The expected response; this could be a single object (eg cmisobject) or a list of objects (cmislist)
37
 * 			alternatively, it could be raw XML. The returned item will have an implementation object looked
38
 * 			up the "returnHandlers" map, to see if it needs to be handled in a particular way, in which
39
 * 			case handling the returned value will be passed off to it.
40
 * params => The names of parameters that could be passed through in the 'args' map.
41
 * cache => Whether this method should be cached, and how long for. Only GET requests can be cached
42
 *
43
 * Note that the "callMethod" method is separated out into several submethods to provide for override
44
 * points for implementing a specific API
45
 *
46
 * @author Marcus Nyeholt <[email protected]>
47
 *
48
 */
49
class WebApiClient
50
{
51
    public static $cache_length = 1200;
52
53
    /**
54
     * The base URL to use for all the calls
55
     * @var String
56
     */
57
    protected $baseUrl;
58
59
    public function setBaseUrl($u)
60
    {
61
        $this->baseUrl = $u;
62
    }
63
64
    public function getBaseUrl()
65
    {
66
        return $this->baseUrl;
67
    }
68
69
    protected $methods;
70
71
    /**
72
     * The methods to call
73
     *
74
     * @param array $v
75
     */
76
    public function setMethods($v)
77
    {
78
        $this->methods = $v;
79
    }
80
81
    /**
82
     * An array of parameters that should ALWAYS be
83
     * passed through on each request.
84
     *
85
     * @var array
86
     */
87
    protected $globalParams = null;
88
89
    /**
90
     * Sets the global parameters
91
     */
92
    public function setGlobalParams($v)
93
    {
94
        $this->globalParams = $v;
95
    }
96
97
    /**
98
     * Set a single param
99
     *
100
     * @param string $key
101
     * @param mixed $value
102
     */
103
    public function setGlobalParam($key, $value)
104
    {
105
        $this->globalParams[$key] = $value;
106
    }
107
108
    protected $returnHandlers = array();
109
110
    /**
111
     * Adds a new return handler to the list of handlers.
112
     *
113
     * Handlers must implement the 'handleReturn' method,
114
     * the result of which is returned to the caller
115
     *
116
     * @param $name
117
     * 				The name of the handler, which should match the
118
     * 				'return' param of the method definition
119
     * @param $object
120
     * 				The object which will handle the return
121
     * @return array
122
     */
123
    public function addReturnHandler($name, ReturnHandler $object)
124
    {
125
        $this->returnHandlers[$name] = $object;
126
    }
127
128
    /**
129
     * Whether or not to persist cookies (eg in a login situation
130
     *
131
     * @var boolean
132
     */
133
    protected $useCookies = false;
134
135
    public function setUseCookies($b)
136
    {
137
        $this->useCookies = $b;
138
    }
139
140
    protected $maintainSession = false;
141
142
    public function setMaintainSession($b)
143
    {
144
        $this->maintainSession = true;
145
    }
146
147
    /**
148
     * Basic HTTP Auth details
149
     *
150
     * @var array
151
     */
152
    protected $authInfo;
153
154
    public function setAuthInfo($username, $password)
155
    {
156
        $this->authInfo = array(
157
            'user' => $username,
158
            'pass' => $password,
159
        );
160
    }
161
162
    /**
163
     * Create a new webapi client
164
     *
165
     */
166
    public function __construct($url, $methods=null, $globalParams=null)
167
    {
168
        $this->baseUrl = $url;
169
        $this->methods = $methods;
170
        $this->globalParams = $globalParams;
171
172
        $this->addReturnHandler('xml', new XmlReturnHandler());
173
        $this->addReturnHandler('dom', new DomReturnHandler());
174
        $this->addReturnHandler('json', new JsonReturnHandler());
175
    }
176
177
    public function __call($method, $args)
178
    {
179
        $arg = is_array($args) && count($args) ? $args[0] : null;
180
        return $this->callMethod($method, $arg);
181
    }
182
183
    /**
184
     * Call a method with the passed in arguments
185
     *
186
     * @param String $method
187
     * @param array $args - a mapping of argumentName => argumentValue
188
     * @param array $getParams
189
     *				Specific get params to add in
190
     * @param array $postParams
191
     *				Specific post params to append
192
     * @return mixed
193
     */
194
    public function callMethod($method, $args)
195
    {
196
        $methodDetails = isset($this->methods[$method]) ? $this->methods[$method] : null;
197
        if (!$methodDetails) {
198
            throw new Exception("$method does not have an appropriate mapping");
199
        }
200
201
        $body = null;
202
203
        // use the method params to try caching the results
204
        // need to add in the baseUrl we're connecting to, and any global params
205
        // because the cache might be connecting via different users, and the
206
        // different users will have different sessions. This might need to be
207
        // tweaked to handle separate user logins at a later point in time
208
        $uri = $this->baseUrl . (isset($methodDetails['url']) ? $methodDetails['url'] : '');
209
        // 	check for any replacements that are required
210
        if (preg_match_all('/{(\w+)}/', $uri, $matches)) {
211
            foreach ($matches[1] as $match) {
212
                if (isset($args[$match])) {
213
                    $uri = str_replace('{'.$match.'}', $args[$match], $uri);
214
                }
215
            }
216
        }
217
218
        $cacheKey = md5($uri . $method . var_export($args, true) . var_export($this->globalParams, true));
219
220
        $requestType = isset($methodDetails['method']) ? $methodDetails['method'] : 'GET';
221
        $cache = isset($methodDetails['cache']) ? $methodDetails['cache'] : self::$cache_length;
222
        if (mb_strtolower($requestType) == 'get' && $cache) {
223
            $body = CacheService::inst()->get($cacheKey);
224
        }
225
226
        if (!$body) {
227
228
229
            // Note that case is important! Some servers won't respond correctly
230
            // to get or Get requests
231
            $requestType = isset($methodDetails['method']) ? $methodDetails['method'] : 'GET';
232
233
            $client = $this->getClient($uri);
234
            $client->setMethod($requestType);
235
236
            // set the encoding type
237
            $client->setEncType(isset($methodDetails['enctype']) ? $methodDetails['enctype'] : Zend_Http_Client::ENC_URLENCODED);
238
239
            $paramMethod = $requestType == 'GET' ? 'setParameterGet' : 'setParameterPost';
240
            if ($this->globalParams) {
241
                foreach ($this->globalParams as $key => $value) {
242
                    $client->$paramMethod($key, $value);
243
                }
244
            }
245
246
            if (isset($methodDetails['params'])) {
247
                $paramNames = $methodDetails['params'];
248
                foreach ($paramNames as $index => $pname) {
249
                    if (isset($args[$pname])) {
250
                        $client->$paramMethod($pname, $args[$pname]);
251
                    } elseif (isset($args[$index])) {
252
                        $client->$paramMethod($pname, $args[$index]);
253
                    }
254
                }
255
            }
256
257
            if (isset($methodDetails['get'])) {
258
                foreach ($methodDetails['get'] as $k => $v) {
259
                    $client->setParameterGet($k, $v);
260
                }
261
            }
262
263
            if (isset($methodDetails['post'])) {
264
                foreach ($methodDetails['post'] as $k => $v) {
265
                    $client->setParameterPost($k, $v);
266
                }
267
            }
268
269
            if (isset($methodDetails['raw']) && $methodDetails['raw']) {
270
                $client->setRawData($args['raw_body']);
271
            }
272
273
            // request away
274
            $response = $client->request();
275
276
            if ($response->isSuccessful()) {
277
                $body = $response->getBody();
278
                if ($cache) {
279
                    CacheService::inst()->store($cacheKey, $body, $cache);
280
                }
281
            } else {
282
                if ($response->getStatus() == 500) {
283
                    error_log("Failure: ".$response->getBody());
284
                    error_log(var_export($client, true));
285
                }
286
                throw new FailedRequestException("Failed executing $method: ".$response->getMessage()." for request to $uri (".$client->getUri(true).')', $response->getBody());
287
            }
288
        }
289
290
        $returnType = isset($methodDetails['return']) ? $methodDetails['return'] : 'raw';
291
292
        // see what we need to do with it
293
        if (isset($this->returnHandlers[$returnType])) {
294
            $handler = $this->returnHandlers[$returnType];
295
            return $handler->handleReturn($body);
296
        } else {
297
            return $body;
298
        }
299
    }
300
301
    /**
302
     * Call a URL directly, without it being mapped to a configured web method.
303
     *
304
     * This differs from the above in that the caller already knows what
305
     * URL is trying to be called, so we can bypass the business of mapping
306
     * arguments all over the place.
307
     *
308
     * We still maintain the globalParams for this client though
309
     *
310
     * @param $url
311
     * 			The URL to call
312
     * @param $args
313
     * 			Parameters to be passed on the call
314
     * @return mixed
315
     */
316
    public function callUrl($url, $args = array(), $returnType = 'raw', $requestType = 'GET', $cache = 300, $enctype = Zend_Http_Client::ENC_URLENCODED)
317
    {
318
        $body = null;
319
        // use the method params to try caching the results
320
        // need to add in the baseUrl we're connecting to, and any global params
321
        // because the cache might be connecting via different users, and the
322
        // different users will have different sessions. This might need to be
323
        // tweaked to handle separate user logins at a later point in time
324
        $cacheKey = md5($url . $requestType . var_export($args, true) . var_export($this->globalParams, true));
325
326
        $requestType = isset($methodDetails['method']) ? $methodDetails['method'] : 'GET';
327
328
        if (mb_strtolower($requestType) == 'get' && $cache) {
329
            $body = CacheService::inst()->get($cacheKey);
330
        }
331
332
        if (!$body) {
333
            $uri = $url;
334
            $client = $this->getClient($uri);
335
            $client->setMethod($requestType);
336
            // set the encoding type
337
            $client->setEncType($enctype);
338
            $paramMethod = 'setParameter'.$requestType;
339
            // make sure to add the alfTicket parameter
340
            if ($this->globalParams) {
341
                foreach ($this->globalParams as $key => $value) {
342
                    $client->$paramMethod($key, $value);
343
                }
344
            }
345
346
            foreach ($args as $index => $pname) {
347
                $client->$paramMethod($index, $pname);
348
            }
349
350
            // request away
351
            $response = $client->request();
352
353
            if ($response->isSuccessful()) {
354
                $body = $response->getBody();
355
                if ($cache) {
356
                    CacheService::inst()->store($cacheKey, $body, $cache);
357
                }
358
            } else {
359
                if ($response->getStatus() == 500) {
360
                    error_log("Failure: ".$response->getBody());
361
                    error_log(var_export($client, true));
362
                }
363
                throw new FailedRequestException("Failed executing $url: ".$response->getMessage()." for request to $uri (".$client->getUri(true).')', $response->getBody());
364
            }
365
        }
366
367
        // see what we need to do with it
368
        if (isset($this->returnHandlers[$returnType])) {
369
            $handler = $this->returnHandlers[$returnType];
370
            return $handler->handleReturn($body);
371
        } else {
372
            return $body;
373
        }
374
    }
375
376
    /**
377
     * The HTTP Client being used during the life of this request
378
     *
379
     * @var Zend_Http_Client
380
     */
381
    protected $httpClient = null;
382
383
    /**
384
     * Create and return the http client, defined in a separate method
385
     * for testing purposes
386
     *
387
     * @return Zend_Http_Client
388
     */
389
    protected function getClient($uri)
390
    {
391
        // TODO For some reason the Alfresco client goes into an infinite loop when returning
392
        // the children of an item (when you call getChildren on the company home)
393
        // it returns itself as its own child, unless you recreate the client. It seems
394
        // to maintain all the request body... or something weird.
395
        if (!$this->httpClient || !$this->maintainSession) {
396
            $this->httpClient = new Zend_Http_Client(
397
                $uri,
398
                array(
399
                    'maxredirects' => 0,
400
                    'timeout'      => 10
401
                )
402
            );
403
404
            if ($this->useCookies) {
405
                $this->httpClient->setCookieJar();
406
            }
407
        } else {
408
            $this->httpClient->setUri($uri);
409
        }
410
411
        // clear it out
412
        if ($this->maintainSession) {
413
            $this->httpClient->resetParameters();
414
        }
415
416
        if ($this->authInfo) {
417
            $this->httpClient->setAuth($this->authInfo['user'], $this->authInfo['pass']);
418
        }
419
420
        return $this->httpClient;
421
    }
422
}
423
424
class FailedRequestException extends Exception
425
{
426
    private $response;
427
    public function getResponse()
428
    {
429
        return $this->response;
430
    }
431
432
    public function __construct($message, $response)
433
    {
434
        $this->response = $response;
435
436
        parent::__construct($message);
437
    }
438
}
439
440
interface ReturnHandler
441
{
442
    /**
443
     * Handle the processing of a return response in a particular manner
444
     *
445
     * @param $rawResponse
446
     * @return mixed
447
     */
448
    public function handleReturn($rawResponse);
449
}
450
451
/**
452
 * Return a SimpleXML object for an xmlobject response type
453
 *
454
 * @author Marcus Nyeholt <[email protected]>
455
 *
456
 */
457
class XmlReturnHandler implements ReturnHandler
458
{
459
    public function handleReturn($rawResponse)
460
    {
461
        $rawResponse = trim($rawResponse);
462
        if (strpos($rawResponse, '<?xml') === 0) {
463
            // get rid of the xml prolog
464
            $rawResponse = str_replace('<?xml version="1.0" encoding="UTF-8"?>', '', $rawResponse);
465
        }
466
467
        return new SimpleXMLElement($rawResponse);
468
    }
469
}
470
471
class DomReturnHandler implements ReturnHandler
472
{
473
    public function handleReturn($rawResponse)
474
    {
475
        $rawResponse = trim($rawResponse);
476
477
478
        return new DOMDocument($rawResponse);
479
    }
480
}
481
482
/**
483
 * Return a stdClass by json_decoding raw data
484
 *
485
 * @author Marcus Nyeholt <[email protected]>
486
 */
487
class JsonReturnHandler implements ReturnHandler
488
{
489
    public function handleReturn($rawResponse)
490
    {
491
        return json_decode($rawResponse);
492
    }
493
}
494