Completed
Push — master ( e5db64...945d9a )
by Schlaefer
05:09 queued 28s
created

Embedly::apicall()   F

Complexity

Conditions 15
Paths 592

Size

Total Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 592
nop 3
dl 0
loc 76
rs 2.3769
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Embedly;
4
5
/**
6
 *
7
 * @author Embed.ly, Inc.
8
 * @author Sven Eisenschmidt <[email protected]>
9
 */
10
class Embedly {
11
12
    /**
13
     *
14
     * @const
15
     */
16
    const VERSION = '0.1.0';
17
18
    /**
19
     *
20
     * @var string
21
     */
22
    protected $hostname = 'api.embed.ly';
23
24
    /**
25
     *
26
     * @var string
27
     */
28
    protected $key = null;
29
30
    /**
31
     *
32
     * @var array
33
     */
34
    protected $api_version = array(
35
        'oembed' => 1,
36
        'objectify' => 2,
37
        'preview' => 1
38
    );
39
40
    /**
41
     *
42
     * @var string
43
     */
44
    protected $user_agent = "";
45
46
    /**
47
     *
48
     * @var array|object
49
     */
50
    protected $services = null;
51
52
    /**
53
     *
54
     * @param array $args
55
     */
56
    public function __construct(array $args = array())
57
    {
58
        $args = array_merge(array(
59
            'user_agent' => sprintf("Mozilla/5.0 (compatible; embedly-php/%s)", self::VERSION),
60
            'key' => null,
61
            'hostname' => null,
62
            'api_version' => null
63
        ), $args);
64
65
        if ($args['user_agent']) {
66
            $this->user_agent = $args['user_agent'];
67
        }
68
        if ($args['key']) {
69
            $this->key = $args['key'];
70
        }
71
        if ($args['hostname']) {
72
            $this->hostname = $args['hostname'];
73
        }
74
        if ($args['api_version']) {
75
            $this->api_version = array_merge($this->api_version, $args['api_version']);
76
        }
77
    }
78
79
    /**
80
     *
81
     * Flexibly parse host strings.
82
     *
83
     * Returns an array of
84
     * { protocol:
85
     * , host:
86
     * , port:
87
     * , url:
88
     * }
89
     *
90
     * @param string $host
91
     * @return array
92
     */
93
    protected function parse_host($host)
94
    {
95
        $port = 80;
96
        $protocol = 'http';
97
98
        preg_match('/^(https?:\/\/)?([^\/]+)(:\d+)?\/?$/', $host, $matches);
99
100
        if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
101
            throw new \Exception(sprintf('invalid host %s', host));
102
        }
103
104
        $hostname = $matches[2];
105
106
        if ($matches[1] == 'https://') {
107
            $protocol = 'https';
108
        }
109
110
        if (array_key_exists(3, $matches) && $matches[3]) {
111
            $port = intval($matches[3]);
112
        } else if ($matches[1] == 'https://') {
113
            $port = 443;
114
        }
115
116
        $portpart = "";
117
        if (array_key_exists(3, $matches) && $matches[3]) {
118
            $portpart = sprintf(":%s", $matches[3]);
119
        }
120
121
        $url = sprintf("%s://%s%s/", $protocol, $hostname, $portpart);
122
123
        return array(
124
            'url' => $url,
125
            'scheme' => $protocol,
126
            'hostname' => $hostname,
127
            'port' => $port
128
        );
129
    }
130
131
    /**
132
     *
133
     * @return string|array
134
     */
135
    public function oembed($params)
136
    {
137
        return $this->apicall($this->api_version['oembed'], 'oembed', $params);
0 ignored issues
show
Documentation introduced by
'oembed' is of type string, but the function expects a array.

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...
138
    }
139
140
    /**
141
     *
142
     * @param string|array $params
143
     * @return object
144
     */
145
    public function preview($params)
146
    {
147
        return $this->apicall($this->api_version['preview'], 'preview', $params);
0 ignored issues
show
Documentation introduced by
'preview' is of type string, but the function expects a array.

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...
Bug introduced by
It seems like $params defined by parameter $params on line 145 can also be of type string; however, Embedly\Embedly::apicall() 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...
148
    }
149
150
    /**
151
     *
152
     * @param array $params
153
     * @return object
154
     */
155
    public function objectify($params)
156
    {
157
        return $this->apicall($this->api_version['objectify'], 'objectify', $params);
0 ignored issues
show
Documentation introduced by
'objectify' is of type string, but the function expects a array.

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...
158
    }
159
160
    /**
161
     *
162
     * @return string
163
     */
164
    public function api_version()
165
    {
166
        return $this->api_version;
167
    }
168
169
    /**
170
     *
171
     * @param string $version
172
     * @param array $action
173
     * @param array $params
174
     * @return object
175
     */
176
    public function apicall($version, $action, $params)
177
    {
178
        $justone = is_string($params);
179
        $params  = self::paramify($params);
0 ignored issues
show
Documentation introduced by
$params is of type array, but the function expects a string.

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...
180
181
        if (!array_key_exists('urls', $params)) {
182
            $params['urls'] = array();
183
        }
184
185
        if (!is_array($params['urls'])) {
186
            $urls = array($params['urls']);
187
            $params['urls'] = $urls;
188
        }
189
190
        if (array_key_exists('url', $params) && $params['url']) {
191
            array_push($params['urls'], $params['url']);
192
            unset($params['url']);
193
        }
194
195
        $rejects = array();
196
        if ($this->key) {
197
            $params['key'] = $this->key;
198
        } else {
199
            $regex = $this->services_regex();
200
            foreach ($params['urls'] as $i => $url) {
201
                $match = preg_match($regex, $url);
202
                if (!$match) {
203
                    //print("rejecting $url");
204
                    unset($params['urls'][$i]);
205
                    $rejects[$i] = (object)array(
206
                        'error_code' => '401',
207
                        'error_message' => 'This service requires an Embedly key',
208
                        'type' => 'error'
209
                    );
210
                }
211
            };
212
        }
213
214
        $result = array();
215
216
        if (sizeof($rejects) < sizeof($params['urls'])) {
217
            if (count($params['urls']) > 20) {
218
                throw new \Exception(
219
                    sprintf("Max of 20 urls can be queried at once, %s passed",
220
                    count($params['urls'])));
221
            }
222
            $path = sprintf("%s/%s", $version, $action);
223
            $url_parts = $this->parse_host($this->hostname);
224
            $apiUrl = sprintf("%s%s?%s", $url_parts['url'], $path, $this->q($params));
225
226
            $ch = curl_init($apiUrl);
227
            $this->setCurlOptions($ch, array(
228
                sprintf('Host: %s', $url_parts['hostname']),
229
                sprintf('User-Agent: %s', $this->user_agent)
230
            ));
231
            $res = $this->curlExec($ch);
232
            $result = json_decode($res) ?: array();
233
        }
234
        $merged_result = array();
235
        foreach ($result as $i => $v) {
236
            if (array_key_exists($i, $rejects)) {
237
                array_push($merged_result, array_shift($rejects));
238
            }
239
            array_push($merged_result, $v);
240
        };
241
        // grab any leftovers
242
        foreach ($rejects as $obj) {
243
            array_push($merged_result, $obj);
244
        }
245
246
        if($justone) {
247
            return array_shift($merged_result);
248
        }
249
250
        return $merged_result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $merged_result; (array) is incompatible with the return type documented by Embedly\Embedly::apicall of type object.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
251
    }
252
253
    /**
254
     *
255
     * @return array
256
     */
257
    public function services() {
258
        if (!$this->services) {
259
            $url = $this->parse_host($this->hostname);
260
            $apiUrl = sprintf("%s1/services/php", $url['url']);
261
            $ch = curl_init($apiUrl);
262
            $this->setCurlOptions($ch, array(
263
                sprintf('Host: %s', $url['hostname']),
264
                sprintf('User-Agent: %s', $this->user_agent)
265
            ));
266
            $res = $this->curlExec($ch);
267
            $this->services = json_decode($res);
268
        }
269
        return $this->services;
270
    }
271
272
    /**
273
     *
274
     * @return string
275
     */
276
    public function services_regex() {
277
    	$services = $this->services();
278
    	$regexes = array_map(array(__CLASS__, 'reg_imploder'), $services);
279
    	return '#'.implode('|', $regexes).'#i';
280
	}
281
282
    /**
283
     *
284
     * @return string
285
     */
286
    protected function q($params) {
287
        $pairs = array_map(array(__CLASS__, 'url_encode'), array_keys($params), array_values($params));
288
        return implode('&', $pairs);
289
    }
290
291
    /**
292
     *
293
     * @param resource $ch
294
     * @param array $headers
295
     * @return void
296
     */
297
    protected function setCurlOptions(&$ch, $headers = array())
298
    {
299
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
300
        curl_setopt($ch, CURLOPT_HEADER, false);
301
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
302
        curl_setopt($ch, CURLOPT_BUFFERSIZE, 4096);
303
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 25);
304
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
305
    }
306
307
    /**
308
     *
309
     * @param resource $ch
310
     * @return string
311
     */
312
    protected function curlExec(&$ch)
313
    {
314
        $res = curl_exec($ch);
315
        if (false === $res) {
316
            throw new \Exception(curl_error($ch), curl_errno($ch));
317
        }
318
        return $res;
319
    }
320
321
322
    /**
323
     *
324
     * @param string $r
325
     * @return string
326
     */
327
    public static function reg_delim_stripper($r)
328
    {
329
        # we need to strip off regex delimeters and options to make
330
        # one giant regex
331
        return substr($r, 1, -2);
332
    }
333
334
    /**
335
     *
336
     * @param stdClass $o
337
     * @return string
338
     */
339
    public static function reg_imploder(\stdClass $o)
340
    {
341
        return implode('|', array_map(array(__CLASS__, 'reg_delim_stripper'), $o->regex));
342
    }
343
344
    /**
345
     *
346
     * @param string $key
347
     * @param string|array $value
348
     * @return string
349
     */
350
    public static function url_encode($key, $value)
351
    {
352
        $key = urlencode($key);
353
        if (is_array($value)) {
354
            $value = implode(',', array_map('urlencode', $value));
355
        } else {
356
            $value = urlencode($value);
357
        }
358
        return sprintf("%s=%s", $key, $value);
359
    }
360
361
    /**
362
     *
363
     * @param string $input
364
     * @return array
365
     */
366
    public static function paramify($input)
367
    {
368
        if(is_string($input)) {
369
            return array('urls' => $input);
370
        }
371
372
        return $input;
373
    }
374
}
375