Completed
Push — master ( 867724...e578f3 )
by Vasily
06:19
created

DNode::callRemoteArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 20
rs 9.2
cc 4
eloc 15
nc 4
nop 2
1
<?php
2
namespace PHPDaemon\WebSocket\Traits;
3
use PHPDaemon\Core\Daemon;
4
use PHPDaemon\Core\Debug;
5
use PHPDaemon\Core\CallbackWrapper;
6
use PHPDaemon\Exceptions\UndefinedMethodCalled;
7
use PHPDaemon\Exceptions\ProtocolError;
8
9
/**
10
 * DNode
11
 * @package PHPDaemon\WebSocket\Traits
12
 * @author  Vasily Zorin <[email protected]>
13
 */
14
trait DNode {
15
	/**
16
	 * @var array Associative array of callback functions registered by callRemote()
17
	 */
18
	protected $callbacks = [];
19
20
	/**
21
	 * @var array Associative array of persistent callback functions registered by callRemote()
22
	 */
23
	protected $persistentCallbacks = [];
24
25
	/**
26
	 * @var boolean If true, callRemote() will register callbacks as persistent ones
27
	 */
28
	protected $persistentMode = false;
29
30
	/**
31
	 * @var integer Incremental counter of callback functions registered by callRemote() 
32
	 */
33
	protected $counter = 0;
34
35
	/**
36
	 * @var array Associative array of registered remote methods (received in 'methods' call)
37
	 */
38
	protected $remoteMethods = [];
39
40
	/**
41
	 * @var array Associative array of local methods, set by defineLocalMethods()
42
	 */
43
	protected $localMethods = [];
44
45
	/**
46
	 * @var boolean Was this object cleaned up?
47
	 */
48
	protected $cleaned = false;
49
	
50
	/**
51
	 * @var boolean Should __call method call parent::__call()? 
52
	 */
53
	protected $magicCallParent = false;
54
55
	/**
56
	 * Default onHandshake() method
57
	 * @return void
58
	 */
59
	public function onHandshake() {
60
		$this->defineLocalMethods();
61
	}
62
63
	/**
64
	 * Defines local methods
65
	 * @param  array $arr Associative array of callbacks (methodName => callback)
66
	 * @return void
67
	 */
68
	protected function defineLocalMethods($arr = []) {
69
		foreach (get_class_methods($this) as $m) {
70
			if (substr($m, -6) === 'Method') {
71
				$k = substr($m, 0, -6);
72
				if ($k === 'methods') {
73
					continue;
74
				}
75
				$arr[$k] = [$this, $m];
76
			}
77
		}
78
		foreach ($arr as $k => $v) {
79
			$this->localMethods[$k] = $v;
80
		}
81
		$this->persistentMode = true;
82
		$this->callRemote('methods', $arr);
83
		$this->persistentMode = false;
84
	}
85
86
	/**
87
	 * Calls a local method
88
	 * @param  string $method  Method name
0 ignored issues
show
Bug introduced by
There is no parameter named $method. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
89
	 * @param  mixed  ...$args Arguments
90
	 * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be DNode?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
91
	 */
92
	public function callLocal() {
93
		$args = func_get_args();
94
		if (!sizeof($args)) {
95
			return $this;
96
		}
97
		$method = array_shift($args);
98
		$p = [
99
			'method' => $method,
100
			'arguments' => $args,
101
		];
102
		$this->onPacket($p);
103
		return $this;
104
	}
105
106
107
	/**
108
	 * Ensures that the variable passed by reference holds a valid callback-function
109
	 * If it doesn't, its value will be reset to null
110
	 * @param  mixed   &$arg Argument
111
	 * @return boolean
112
	 */
113
	protected static function ensureCallback(&$arg) {
114
		if ($arg instanceof \Closure) {
115
			return true;
116
		}
117
		if (is_array($arg) && sizeof($arg) === 2) {
118
			if (isset($arg[0]) && $arg[0] instanceof \PHPDaemon\WebSocket\Route) {
119 View Code Duplication
				if (isset($arg[1]) && is_string($arg[1]) && strncmp($arg[1], 'remote_', 7) === 0) {
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...
120
					return true;
121
				}
122
			}
123
		}
124
		$arg = null;
125
		return false;
126
	}
127
128
	/**
129
	 * Extracts callback functions from array of arguments
130
	 * @param  array &$args Arguments
131
	 * @param  array &$list Output array for 'callbacks' property
132
	 * @param  array &$path Recursion path holder
133
	 * @return void
134
	 */
135
	protected function extractCallbacks(&$args, &$list, &$path) {
136
		foreach ($args as $k => &$v) {
137
			if (is_array($v)) {
138
				if (sizeof($v) === 2) {
139
					if (isset($v[0]) && is_object($v[0])) {
140
						if (isset($v[1]) && is_string($v[1])) {
141
							$id = ++$this->counter;
142 View Code Duplication
							if ($this->persistentMode) {
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...
143
								$this->persistentCallbacks[$id] = $v;
144
							} else {
145
								$this->callbacks[$id] = $v;
146
							}
147
							$v = '';
148
							$list[$id] = $path;
149
							$list[$id][] = $k;
150
							continue;
151
						}
152
					}
153
				}
154
				$path[] = $k;
155
				$this->extractCallbacks($v, $list, $path);
156
				array_pop($path);
157
			} elseif ($v instanceof \Closure) {
158
				$id = ++$this->counter;
159 View Code Duplication
				if ($this->persistentMode) {
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...
160
					$this->persistentCallbacks[$id] = $v;
161
				} else {
162
					$this->callbacks[$id] = $v;
163
				}
164
				$v = '';
165
				$list[$id] = $path;
166
				$list[$id][] = $k;
167
			}
168
		}
169
	}
170
171
	/**
172
	 * Calls a remote method
173
	 * @param  string $method  Method name
0 ignored issues
show
Bug introduced by
There is no parameter named $method. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
174
	 * @param  mixed  ...$args Arguments
175
	 * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be DNode?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
176
	 */
177
	public function callRemote() {
178
		$args = func_get_args();
179
		if (!sizeof($args)) {
180
			return $this;
181
		}
182
		$method = array_shift($args);
183
		$this->callRemoteArray($method, $args);
184
		return $this;
185
	}
186
187
	/**
188
	 * Calls a remote method with array of arguments
189
	 * @param  string $method Method name
190
	 * @param  array  $args   Arguments
191
	 * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be DNode?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
192
	 */
193
	public function callRemoteArray($method, $args) {
194
		if (isset($this->remoteMethods[$method])) {
195
			call_user_func_array($this->remoteMethods[$method], $args);
196
			return $this;
197
		}
198
		$pct = [
199
			'method' => $method,
200
		];
201
		if (sizeof($args)) {
202
			$callbacks = [];
203
			$path = [];
204
			$this->extractCallbacks($args, $callbacks, $path);
205
			$pct['arguments'] = $args;
206
			if (sizeof($callbacks)) {
207
				$pct['callbacks'] = $callbacks;
208
			}
209
		}
210
		$this->sendPacket($pct);
211
		return $this;
212
	}
213
214
	/**
215
	 * Handler of the 'methods' method
216
	 * @param  array $methods Associative array of methods
217
	 * @return void
218
	 */
219
	protected function methodsMethod($methods) {
220
		$this->remoteMethods = $methods;
221
	}
222
223
	/**
224
	 * Encodes value into JSON
225
	 * @param  mixed $m Value
226
	 * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
227
	 */
228
	public static function toJson($m) {
229
		return json_encode($m, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
230
	}
231
232
	/**
233
	 * Recursion handler for toJsonDebug()
234
	 * @param  array &$a Data
235
	 * @return void
236
	 */
237
	public static function toJsonDebugResursive(&$m) {
238
		if ($m instanceof \Closure) {
239
			$m = '__CALLBACK__';
240
		}
241
		elseif (is_array($m)) {
242
			if (sizeof($m) === 2 && isset($m[0]) && $m[0] instanceof \PHPDaemon\WebSocket\Route) {
243 View Code Duplication
				if (isset($m[1]) && is_string($m[1]) && strncmp($m[1], 'remote_', 7) === 0) {
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...
244
					$m = '__CALLBACK__';
245
				}
246
			} else {
247
				foreach ($m as &$v) {
248
					static::toJsonDebugResursive($v);
249
				}
250
			}
251
		} elseif (is_object($m)) {
252
			foreach ($m as &$v) {
253
				static::toJsonDebugResursive($v);
254
			}
255
		}
256
	}
257
258
	/**
259
	 * Encodes value into JSON for debugging purposes
260
	 * @param mixed $m Data
261
	 * @return void
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
262
	 */
263
	public static function toJsonDebug($m) {
264
		static::toJsonDebugResursive($m);
265
		return static::toJson($m);
266
	}
267
268
	/**
269
	 * Sends a packet
270
	 * @param  array $pct Data
271
	 * @return void
272
	 */
273
	protected function sendPacket($pct) {
274
		if (!$this->client) {
0 ignored issues
show
Bug introduced by
The property client does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
275
			return;
276
		}
277
		if (is_string($pct['method']) && ctype_digit($pct['method'])) {
278
			$pct['method'] = (int) $pct['method'];
279
		}
280
		$this->client->sendFrame(static::toJson($pct) . "\n");
281
	}
282
283
	/**
284
	 * Called when session is finished
285
	 * @return void
286
	 */
287
	public function onFinish() {
288
		$this->cleanup();
289
		parent::onFinish();
290
	}
291
292
	/**
293
	 * Swipes internal structures
294
	 * @return void
295
	 */
296
	public function cleanup() {
297
		$this->cleaned = true;
298
		$this->remoteMethods = [];
299
		$this->localMethods = [];
300
		$this->persistentCallbacks = [];
301
		$this->callbacks = [];
302
	}
303
304
305
	/**
306
	 * Sets value by materialized path
307
	 * @param  array &$m
308
	 * @param  array $path
309
	 * @param  mixed $val
310
	 * @return void
311
	 */
312
	protected static function setPath(&$m, $path, $val) {
313
		foreach ($path as $p) {
314
			$m =& $m[$p];
315
		}
316
		$m = $val;
317
	}
318
319
	/**
320
	 * Finds value by materialized path
321
	 * @param  array &$m
322
	 * @param  array $path
323
	 * @return mixed Value
324
	 */
325
	protected static function &getPath(&$m, $path) {
326
		foreach ($path as $p) {
327
			$m =& $m[$p];
328
		}
329
		return $m;
330
	}
331
332
	/**
333
	 * Magic __call method
334
	 * @param  string $method Method name
335
	 * @param  array  $args   Arguments
336
	 * @throws UndefinedMethodCalled if method name not start from 'remote_'
337
	 * @return mixed
338
	 */
339
	public function __call($method, $args) {
340
		if (strncmp($method, 'remote_', 7) === 0) {
341
			$this->callRemoteArray(substr($method, 7), $args);
342
		}
343
		elseif ($this->magicCallParent) {
344
			return parent::__call($method, $args);
345
		}
346
		else {
347
			throw new UndefinedMethodCalled('Call to undefined method ' . get_class($this) . '->' . $method);
348
		}
349
	}
350
351
	/**
352
	 * Called when new packet is received
353
	 * @param  array $pct Packet
354
	 * @return void
355
	 */
356
	public function onPacket($pct) {
357
		if ($this->cleaned) {
358
			return;
359
		}
360
		$m = isset($pct['method']) ? $pct['method'] : null;
361
		$args = isset($pct['arguments']) ? $pct['arguments'] : [];
362
		if (isset($pct['callbacks']) && is_array($pct['callbacks'])) {
363
			foreach ($pct['callbacks'] as $id => $path) {
364
				static::setPath($args, $path, [$this, 'remote_' . $id]);
365
			}
366
		}
367
		if (isset($pct['links']) && is_array($pct['links'])) {
368
			foreach ($pct['links'] as $link) {
369
				static::setPath($args, $link['to'], static::getPath($args, $link['from']));
370
			}
371
		}
372
373
		if (is_string($m)) {
374
			if (isset($this->localMethods[$m])) {
375
				call_user_func_array($this->localMethods[$m], $args);
376
			}
377
			elseif (method_exists($this, $m . 'Method')) {
378
				call_user_func_array([$this, $m . 'Method'], $args);
379
			} else {
380
				$this->handleException(new UndefinedMethodCalled);
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\WebSocket\Traits\DNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
381
			}
382
		}
383
		elseif (is_int($m)) {
384
			if (isset($this->callbacks[$m])) {
385
				if (!call_user_func_array($this->callbacks[$m], $args)) {
386
					unset($this->callbacks[$m]);
387
				}
388
			}
389
			elseif (isset($this->persistentCallbacks[$m])) {
390
				call_user_func_array($this->persistentCallbacks[$m], $args);
391
			}
392
			else {
393
				$this->handleException(new UndefinedMethodCalled);
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\WebSocket\Traits\DNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
394
			}
395
		} else {
396
			$this->handleException(new ProtocolError);
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\WebSocket\Traits\DNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
397
		}
398
	}
399
400
	/**
401
	 * Called when new frame is received
402
	 * @param string $data Frame's contents
403
	 * @param integer $type Frame's type
404
	 * @return void
405
	 */
406
	public function onFrame($data, $type) {
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
407
		foreach (explode("\n", $data) as $pct) {
408
			if ($pct === '') {
409
				continue;
410
			}
411
			$this->onPacket(json_decode($pct, true));
412
		}
413
	}
414
}
415