Driver_Simple   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 99.2%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 9
dl 0
loc 468
ccs 124
cts 125
cp 0.992
rs 8.48
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A environment() 0 4 1
A content() 0 11 2
A initialize() 0 7 1
A get() 0 4 1
A request() 0 26 4
A current_path() 0 4 1
A current_url() 0 4 1
A user_agent() 0 4 1
A clear() 0 6 1
A forms() 0 4 1
A xpath() 0 4 1
A dom() 0 4 2
A post() 0 4 1
A tag_name() 0 4 1
A attribute() 0 6 2
A text() 0 7 1
A value() 0 4 1
A is_visible() 0 7 1
A is_selected() 0 4 1
A is_checked() 0 4 1
A set() 0 4 1
A select_option() 0 4 1
A serialize_form() 0 4 1
A visit() 0 4 1
A is_page_active() 0 4 1
A cookie() 0 4 1
A request_factory() 0 10 2
A html() 0 9 2
B click() 0 41 10
A all() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like Driver_Simple 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Driver_Simple, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Openbuildings\Spiderling;
4
5
use Openbuildings\EnvironmentBackup\Environment;
6
use Openbuildings\EnvironmentBackup\Environment_Group_Globals;
7
use Openbuildings\EnvironmentBackup\Environment_Group_Server;
8
use Openbuildings\EnvironmentBackup\Environment_Group_Static;
9
10
/**
11
 * Use Curl to load urls.
12
 * In memory.
13
 * No Javascript
14
 *
15
 * @package    Openbuildings\Spiderling
16
 * @author     Ivan Kerin
17
 * @copyright  (c) 2013 OpenBuildings Ltd.
18
 * @license    http://spdx.org/licenses/BSD-3-Clause
19
 */
20
class Driver_Simple extends Driver {
21
22
	/**
23
	 * The name of the driver
24
	 * @var string
25
	 */
26
	public $name = 'simple';
27
28
	/**
29
	 * Wait time is meaningless in simple driver as it does not support javascript
30
	 * @var integer
31
	 */
32
	public $default_wait_time = 10;
33
34
	/**
35
	 * The raw html content of the page
36
	 * @var string
37
	 */
38
	protected $_content;
39
40
	/**
41
	 * The DOMDocument of the current page
42
	 * @var DOMDocument
43
	 */
44
	protected $_dom;
45
46
	/**
47
	 * The DOMXpath object for finding elements on the page
48
	 * @var DOMXpath
49
	 */
50
	protected $_xpath;
51
52
	/**
53
	 * Object for handling forms on the page
54
	 * @var Driver_Simple_Forms
55
	 */
56
	protected $_forms;
57
58
	/**
59
	 * Environment object for handling backups of environment state
60
	 * @var Environment
61
	 */
62
	protected $_environment;
63
64
	/**
65
	 * Driver_Simple_RequestFactory object for opening new pages
66
	 * @var Driver_Simple_RequestFactory
67
	 */
68
	protected $_request_factory;
69
70 57
	function __construct()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
71
	{
72 57
		$this->_dom = new \DOMDocument('1.0', 'utf-8');
0 ignored issues
show
Documentation Bug introduced by
It seems like new \DOMDocument('1.0', 'utf-8') of type object<DOMDocument> is incompatible with the declared type object<Openbuildings\Spiderling\DOMDocument> of property $_dom.

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...
73 57
		$this->_environment = new Environment(array(
74 57
			'globals' => new Environment_Group_Globals(),
75 57
			'server' => new Environment_Group_Server(),
76 57
			'static' => new Environment_Group_Static(),
77
		));
78
79 57
		$this->_request_factory = new Driver_Simple_RequestFactory_HTTP();
80 57
	}
81
82
	/**
83
	 * Getter / Setter for the request factory object for the driver
84
	 * @param  Driver_Simple_RequestFactory $request_factory
85
	 * @return Driver_Simple_RequestFactory|Driver_Simple
86
	 */
87 18
	public function request_factory(Driver_Simple_RequestFactory $request_factory = NULL)
88
	{
89 18
		if ($request_factory !== NULL)
90
		{
91 14
			$this->_request_factory = $request_factory;
92 14
			return $this;
93
		}
94
95 9
		return $this->_request_factory;
96
	}
97
98
	/**
99
	 * Getter for the current environment
100
	 * @return Environment
101
	 */
102 7
	public function environment()
103
	{
104 7
		return $this->_environment;
105
	}
106
107
	/**
108
	 * Restore the environment
109
	 * @return Driver_Simple $this
110
	 */
111 1
	public function clear()
112
	{
113 1
		$this->environment()->restore();
114
115 1
		return $this;
116
	}
117
118
	/**
119
	 * Getter / Setter of the raw content html
120
	 * @param  string $content
121
	 * @return string|Driver_Simple
122
	 */
123 48
	public function content($content = NULL)
124
	{
125 48
		if ($content !== NULL)
126
		{
127 48
			$this->_content = (string) $content;
128 48
			$this->initialize();
129
130 48
			return $this;
131
		}
132 48
		return $this->_content;
133
	}
134
135
	/**
136
	 * Initialze the dom, xpath and forms objects, based on the content string
137
	 */
138 48
	public function initialize()
139
	{
140 48
		@ $this->_dom->loadHTML($this->content());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
141 48
		$this->_dom->encoding = 'utf-8';
142 48
		$this->_xpath = new Driver_Simple_Xpath($this->_dom);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Openbuildings\Spide...mple_Xpath($this->_dom) of type object<Openbuildings\Spi...ng\Driver_Simple_Xpath> is incompatible with the declared type object<Openbuildings\Spiderling\DOMXpath> of property $_xpath.

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...
143 48
		$this->_forms = new Driver_Simple_Forms($this->_xpath);
144 48
	}
145
146
	/**
147
	 * Getter the current forms object
148
	 * @return Driver_Simple_Forms
149
	 */
150 7
	public function forms()
151
	{
152 7
		return $this->_forms;
153
	}
154
155
	/**
156
	 * Getter for the current xpath object for the page
157
	 * @return DOMXpath
158
	 */
159 20
	public function xpath()
160
	{
161 20
		return $this->_xpath;
162
	}
163
164
	/**
165
	 * Get the DOMElement for the current id, or root if no id is given
166
	 * @param  string $id
167
	 * @return DOMElement
168
	 */
169 15
	public function dom($id = NULL)
170
	{
171 15
		return $id ? $this->xpath()->find($id) : $this->_dom;
172
	}
173
174
	/**
175
	 * Initiate a get request to a given uri
176
	 * @param  string $uri
177
	 * @param  array  $query an array for the http query
178
	 * @return Driver_Simple        $this
179
	 */
180 5
	public function get($uri, array $query = array())
181
	{
182 5
		return $this->request('GET', $uri, $query);
183
	}
184
185
	/**
186
	 * Initiate a post request to a given uri
187
	 * @param  string $uri
188
	 * @param  array  $query
189
	 * @param  array  $post
190
	 * @param  array  $files set the $_FILES array appropriately
191
	 * @return Driver_Simple        $this
192
	 */
193 1
	public function post($uri, array $query = array(), array $post = array(), array $files = array())
194
	{
195 1
		return $this->request('POST', $uri, $query, $post, $files);
196
	}
197
198
	/**
199
	 * Initiate a request with a custom method
200
	 * @param  string $method
201
	 * @param  string $uri
202
	 * @param  array  $query
203
	 * @param  array  $post
204
	 * @param  array  $files  set the $_FILES array
205
	 * @return Driver_Simple         $this
206
	 */
207 5
	public function request($method, $uri, array $query = array(), array $post = array(), array $files = array())
208
	{
209 5
		$uri_params = parse_url($uri, PHP_URL_QUERY);
210
211 5
		if ($uri_params)
0 ignored issues
show
Bug Best Practice introduced by
The expression $uri_params of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
212
		{
213 1
			parse_str($uri_params, $params);
214 1
			$query = array_merge($params, $query);
215 1
			$uri = str_replace('?'.$uri_params, '', $uri);
216
		}
217
218 5
		$url = $uri.($query ? '?'.http_build_query($query) : '');
219
220 5
		$this->environment()->backup_and_set(array(
221 5
			'_GET' => $query,
222 5
			'_POST' => $post,
223 5
			'_FILES' => $files,
224 4
			'_SESSION' => isset($_SESSION) ? $_SESSION : array(),
225
		));
226
227 5
		$response = $this->request_factory()->execute($method, $url, $post);
0 ignored issues
show
Unused Code introduced by
The call to Driver_Simple::execute() has too many arguments starting with $post.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228
229 4
		$this->content($response);
230
231 4
		return $this;
232
	}
233
234
	/**
235
	 * NODE GETTERS
236
	 * =====================================
237
	 */
238
239
	/**
240
	 * Get the tag name of a Node with id. e.g. DIV, SPAN ...
241
	 * @param  string $id
242
	 * @return string
243
	 */
244 3
	public function tag_name($id)
245
	{
246 3
		return $this->dom($id)->tagName;
247
	}
248
249
	/**
250
	 * Get the attribute of a Node with id. If the attribute does not exist, returns NULL
251
	 * @param  string $id
252
	 * @param  string $name
253
	 * @return string
254
	 */
255 4
	public function attribute($id, $name)
256
	{
257 4
		$node = $this->dom($id);
258
259 4
		return $node->hasAttribute($name) ? $node->getAttribute($name) : NULL;
260
	}
261
262
	/**
263
	 * Return the raw html of a Node with id, along with all of its children.
264
	 * @param  string $id
265
	 * @return string
266
	 */
267 3
	public function html($id)
268
	{
269 3
		if ($id === NULL)
270
			return $this->dom()->saveHTML();
271
272 3
		$node = $this->dom($id);
273
274 3
		return $node->ownerDocument->saveXml($node);
275
	}
276
277
	/**
278
	 * Return the text of a Node with id, with all the spaces collapsed, similar to browser rendering.
279
	 * @param  string $id
280
	 * @return string
281
	 */
282 3
	public function text($id)
283
	{
284 3
		$text = $this->dom($id)->textContent;
285 3
		$text = preg_replace('/[ \s\f\n\r\t\v ]+/u', ' ', $text);
286
287 3
		return trim($text);
288
	}
289
290
	/**
291
	 * Return the value of a Node of a form element, e.g. INPUT, TEXTAREA or SELECT
292
	 * @param  string $id
293
	 * @return string
294
	 */
295 5
	public function value($id)
296
	{
297 5
		return $this->forms()->get_value($id);
298
	}
299
300
	/**
301
	 * Check if a Node with id is visible.
302
	 * A Node is considered hidden if it has a "display:none" inline style or its a script or head tag.
303
	 * @param  string  $id
304
	 * @return boolean
305
	 */
306 2
	public function is_visible($id)
307
	{
308 2
		$node = $this->dom($id);
309
310 2
		$hidden_nodes = $this->xpath()->query("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or name()='script' or name()='head']", $node);
311 2
		return $hidden_nodes->length == 0;
312
	}
313
314
	/**
315
	 * Check if a Node with id of an option element is selected
316
	 * @param  string  $id
317
	 * @return boolean
318
	 */
319 2
	public function is_selected($id)
320
	{
321 2
		return (bool) $this->dom($id)->getAttribute('selected');
322
	}
323
324
	/**
325
	 * Check if a Node with id of an input element (radio or checkbox) is checked
326
	 * @param  string  $id
327
	 * @return boolean
328
	 */
329 2
	public function is_checked($id)
330
	{
331 2
		return (bool) $this->dom($id)->getAttribute('checked');
332
	}
333
334
	/**
335
	 * Set the value of a Node with id of a form element
336
	 * @param string $id
337
	 * @param string $value
338
	 */
339 4
	public function set($id, $value)
340
	{
341 4
		$this->forms()->set_value($id, $value);
342 4
	}
343
344
	/**
345
	 * Set the option value that is selected of a Node of a select element
346
	 * @param  string $id
347
	 * @param  string $value
348
	 */
349 4
	public function select_option($id, $value)
350
	{
351 4
		$this->forms()->set_value($id, $value);
352 4
	}
353
354
	/**
355
	 * Return the serialized representation of the input values of a form.
356
	 * Do not include files or disabled inputs, as well as submit inputs and buttons
357
	 * @param  string $id
358
	 * @return string
359
	 */
360 1
	public function serialize_form($id)
361
	{
362 1
		return $this->forms()->serialize_form($id);
363
	}
364
365
	/**
366
	 * Click on a Node with id, triggering a link or form submit
367
	 * @param  string $id
368
	 * @throws Exception_Driver If not a clickable element
369
	 */
370 3
	public function click($id)
371
	{
372 3
		$node = $this->dom($id);
373
374 3
		if ($node->hasAttribute('href'))
375
		{
376 3
			$this->get($node->getAttribute('href'));
377
		}
378 2
		elseif (($node->tagName == 'input' AND $node->getAttribute('type') == 'submit') OR $node->tagName == 'button')
379
		{
380 2
			$form = $this->xpath()->find('./ancestor::form', $node);
381
382 2
			$action = $form->hasAttribute('action') ? $form->getAttribute('action') : $this->request->uri();
0 ignored issues
show
Bug introduced by
The property request does not seem to exist. Did you mean _request_factory?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
383
384 2
			$method = $form->hasAttribute('method') ? strtoupper($form->getAttribute('method')) : 'GET';
385
386 2
			$post = $this->forms()->serialize_form($form);
387
388 2
			$files = $this->forms()->serialize_files($form);
389
390 2
			if (in_array($node->tagName, array('button', 'input')) AND $node->hasAttribute('name'))
391
			{
392 2
				$post = $post.'&'.$node->getAttribute('name').'='.$node->getAttribute('value');
393
			}
394 2
			parse_str($post, $post);
395 2
			parse_str($files, $files);
396
397 2
			if ($method == 'GET')
398
			{
399 1
				$this->get($action, $post);
0 ignored issues
show
Bug introduced by
It seems like $post can also be of type null; however, Openbuildings\Spiderling\Driver_Simple::get() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
400
			}
401
			else
402
			{
403 2
				$this->post($action, array(), $post, $files);
0 ignored issues
show
Bug introduced by
It seems like $post can also be of type null; however, Openbuildings\Spiderling\Driver_Simple::post() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $files defined by $this->forms()->serialize_files($form) on line 388 can also be of type null; however, Openbuildings\Spiderling\Driver_Simple::post() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
404
			}
405
		}
406
		else
407
		{
408 1
			throw new Exception_Driver('The html tag :tag cannot be clicked', array(':tag' => $node->tagName));
409
		}
410 3
	}
411
412
	/**
413
	 * Go to a given url address
414
	 * @param  string $uri
415
	 * @param  array  $query
416
	 * @return Driver_Simple        $this
417
	 */
418 1
	public function visit($uri, array $query = array())
419
	{
420 1
		return $this->get($uri, $query);
421
	}
422
423
	/**
424
	 * Get the current path (without host and protocol)
425
	 * @return string
426
	 */
427 2
	public function current_path()
428
	{
429 2
		return $this->request_factory()->current_path();
430
	}
431
432
	/**
433
	 * Get the current url
434
	 * @return string
435
	 */
436 2
	public function current_url()
437
	{
438 2
		return $this->request_factory()->current_url();
439
	}
440
441
	/**
442
	 * Find all ids of a given XPath
443
	 * @param  string $xpath
444
	 * @param  string $parent id of the parent node
445
	 * @return array
446
	 */
447 15
	public function all($xpath, $parent = NULL)
448
	{
449 15
		$xpath = $parent.$xpath;
450 15
		$ids = array();
451 15
		foreach ($this->xpath()->query($xpath) as $index => $elmenets)
452
		{
453 15
			$ids[] = "($xpath)[".($index+1)."]";
454
		}
455 15
		return $ids;
456
	}
457
458
	/**
459
	 * Check if anything has been loaded
460
	 * @return boolean
461
	 */
462 1
	public function is_page_active()
463
	{
464 1
		return (bool) $this->content();
465
	}
466
467
	/**
468
	 * Return the current user agent
469
	 * @return string
470
	 */
471 2
	public function user_agent()
472
	{
473 2
		return $this->request_factory()->user_agent();
474
	}
475
476
	/**
477
	 * Set the cookie (by setting the $_COOKIE array directly)
478
	 * @param  string $name
479
	 * @param  mixed $value
480
	 * @param  array  $parameters - not utalized by this driver
481
	 */
482 1
	public function cookie($name, $value, array $parameters = array())
483
	{
484 1
		$_COOKIE[$name] = $value;
485 1
	}
486
487
}
488