Completed
Push — master ( 65974f...854b36 )
by Vasily
08:49 queued 04:15
created

DNode::toJsonDebug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
eloc 3
nc 1
nop 1
1
<?php
2
namespace PHPDaemon\DNode;
3
4
use PHPDaemon\Exceptions\ProtocolError;
5
use PHPDaemon\Exceptions\UndefinedMethodCalled;
6
7
/**
8
 * DNode
9
 * @package PHPDaemon\WebSocket\Traits
10
 * @author  Vasily Zorin <[email protected]>
11
 */
12
trait DNode
13
{
14
    /**
15
     * @var array Associative array of callback functions registered by callRemote()
16
     */
17
    protected $callbacks = [];
18
19
    /**
20
     * @var array Associative array of persistent callback functions registered by callRemote()
21
     */
22
    protected $persistentCallbacks = [];
23
24
    /**
25
     * @var boolean If true, callRemote() will register callbacks as persistent ones
26
     */
27
    protected $persistentMode = false;
28
29
    /**
30
     * @var integer Incremental counter of callback functions registered by callRemote()
31
     */
32
    protected $counter = 0;
33
34
    /**
35
     * @var array Associative array of registered remote methods (received in 'methods' call)
36
     */
37
    protected $remoteMethods = [];
38
39
    /**
40
     * @var array Associative array of local methods, set by defineLocalMethods()
41
     */
42
    protected $localMethods = [];
43
44
    /**
45
     * @var boolean Was this object cleaned up?
46
     */
47
    protected $cleaned = false;
48
49
    /**
50
     * @var boolean Should __call method call parent::__call()?
51
     */
52
    protected $magicCallParent = false;
53
54
    /**
55
     * Ensures that the variable passed by reference holds a valid callback-function
56
     * If it doesn't, its value will be reset to null
57
     * @param  mixed &$arg Argument
58
     * @return boolean
59
     */
60
    public static function ensureCallback(&$arg)
61
    {
62
        if ($arg instanceof \Closure) {
63
            return true;
64
        }
65
        if (is_array($arg) && sizeof($arg) === 2) {
66
            if (isset($arg[0]) && $arg[0] instanceof \PHPDaemon\WebSocket\Route) {
67 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...
68
                    return true;
69
                }
70
            }
71
        }
72
        $arg = null;
73
        return false;
74
    }
75
76
    /**
77
     * Encodes value into JSON for debugging purposes
78
     * @param mixed $m Data
79
     * @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...
80
     */
81
    public static function toJsonDebug($m)
82
    {
83
        static::toJsonDebugResursive($m);
84
        return static::toJson($m);
85
    }
86
87
    /**
88
     * Recursion handler for toJsonDebug()
89
     * @param  array &$a Data
90
     * @return void
91
     */
92
    public static function toJsonDebugResursive(&$m)
93
    {
94
        if ($m instanceof \Closure) {
95
            $m = '__CALLBACK__';
96
        } elseif (is_array($m)) {
97
            if (sizeof($m) === 2 && isset($m[0]) && $m[0] instanceof \PHPDaemon\WebSocket\Route) {
98 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...
99
                    $m = '__CALLBACK__';
100
                }
101
            } else {
102
                foreach ($m as &$v) {
103
                    static::toJsonDebugResursive($v);
104
                }
105
            }
106
        } elseif (is_object($m)) {
107
            foreach ($m as &$v) {
108
                static::toJsonDebugResursive($v);
109
            }
110
        }
111
    }
112
113
    /**
114
     * Default onHandshake() method
115
     * @return void
116
     */
117
    public function onHandshake()
118
    {
119
        $this->defineLocalMethods();
120
    }
121
122
    /**
123
     * Defines local methods
124
     * @param  array $arr Associative array of callbacks (methodName => callback)
125
     * @return void
126
     */
127
    protected function defineLocalMethods($arr = [])
128
    {
129
        foreach (get_class_methods($this) as $m) {
130
            if (substr($m, -6) === 'Method') {
131
                $k = substr($m, 0, -6);
132
                if ($k === 'methods') {
133
                    continue;
134
                }
135
                $arr[$k] = [$this, $m];
136
            }
137
        }
138
        foreach ($arr as $k => $v) {
139
            $this->localMethods[$k] = $v;
140
        }
141
        $this->persistentMode = true;
142
        $this->callRemote('methods', $arr);
143
        $this->persistentMode = false;
144
    }
145
146
    /**
147
     * Calls a remote method
148
     * @param  string $method Method name
149
     * @param  mixed ...$args Arguments
150
     * @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...
151
     */
152
    public function callRemote($method, ...$args)
153
    {
154
        $this->callRemoteArray($method, $args);
155
        return $this;
156
    }
157
158
159
    /**
160
     * Export object methods
161
     * @param $object
162
     * @return array
163
     */
164
    public static function exportObjectMethods($object) {
165
        $methods = [];
166
        foreach (get_class_methods($object) as $method) {
167
            if ($method[0] === '_') {
168
                continue;
169
            }
170
            $methods[$method] = [$object, $method];
171
        }
172
        return $methods;
173
    }
174
175
    /**
176
     * Calls a remote method with array of arguments
177
     * @param  string $method Method name
178
     * @param  array $args Arguments
179
     * @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...
180
     */
181
    public function callRemoteArray($method, $args)
182
    {
183
        if (isset($this->remoteMethods[$method])) {
184
            $this->remoteMethods[$method](...$args);
185
            return $this;
186
        }
187
        $pct = [
188
            'method' => $method,
189
        ];
190
        if (sizeof($args)) {
191
            $callbacks = [];
192
            $path = [];
193
            $this->extractCallbacks($args, $callbacks, $path);
194
            $pct['arguments'] = $args;
195
            if (sizeof($callbacks)) {
196
                $pct['callbacks'] = $callbacks;
197
            }
198
        }
199
        $this->sendPacket($pct);
200
        return $this;
201
    }
202
203
    /**
204
     * Extracts callback functions from array of arguments
205
     * @param  array &$args Arguments
206
     * @param  array &$list Output array for 'callbacks' property
207
     * @param  array &$path Recursion path holder
208
     * @return void
209
     */
210
    protected function extractCallbacks(&$args, &$list, &$path)
211
    {
212
        foreach ($args as $k => &$v) {
213
            if (is_array($v)) {
214
                if (sizeof($v) === 2) {
215
                    if (isset($v[0]) && is_object($v[0])) {
216
                        if (isset($v[1]) && is_string($v[1])) {
217
                            $id = ++$this->counter;
218 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...
219
                                $this->persistentCallbacks[$id] = $v;
220
                            } else {
221
                                $this->callbacks[$id] = $v;
222
                            }
223
                            $v = '';
224
                            $list[$id] = $path;
225
                            $list[$id][] = $k;
226
                            continue;
227
                        }
228
                    }
229
                }
230
                $path[] = $k;
231
                $this->extractCallbacks($v, $list, $path);
232
                array_pop($path);
233
            } elseif ($v instanceof \Closure) {
234
                $id = ++$this->counter;
235 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...
236
                    $this->persistentCallbacks[$id] = $v;
237
                } else {
238
                    $this->callbacks[$id] = $v;
239
                }
240
                $v = '';
241
                $list[$id] = $path;
242
                $list[$id][] = $k;
243
            }
244
        }
245
    }
246
247
    /**
248
     * Sends a packet
249
     * @param  array $pct Data
250
     * @return void
251
     */
252
    protected function sendPacket($pct)
253
    {
254
        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...
255
            return;
256
        }
257
        if (is_string($pct['method']) && ctype_digit($pct['method'])) {
258
            $pct['method'] = (int)$pct['method'];
259
        }
260
        $this->client->sendFrame(static::toJson($pct) . "\n");
261
    }
262
263
    /**
264
     * Encodes value into JSON
265
     * @param  mixed $m Value
266
     * @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...
267
     */
268
    public static function toJson($m)
269
    {
270
        return json_encode($m, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
271
    }
272
273
    /**
274
     * Calls a local method
275
     * @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...
276
     * @param  mixed ...$args Arguments
277
     * @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...
278
     */
279
    public function callLocal(...$args)
280
    {
281
        if (!sizeof($args)) {
282
            return $this;
283
        }
284
        $method = array_shift($args);
285
        $p = [
286
            'method' => $method,
287
            'arguments' => $args,
288
        ];
289
        $this->onPacket($p);
290
        return $this;
291
    }
292
293
    /**
294
     * Called when new packet is received
295
     * @param  array $pct Packet
296
     * @return void
297
     */
298
    public function onPacket($pct)
299
    {
300
        if ($this->cleaned) {
301
            return;
302
        }
303
        $m = isset($pct['method']) ? $pct['method'] : null;
304
        $args = isset($pct['arguments']) ? $pct['arguments'] : [];
305
        if (isset($pct['callbacks']) && is_array($pct['callbacks'])) {
306
            foreach ($pct['callbacks'] as $id => $path) {
307
                static::setPath($args, $path, [$this, 'remote_' . $id]);
308
            }
309
        }
310
        if (isset($pct['links']) && is_array($pct['links'])) {
311
            foreach ($pct['links'] as $link) {
312
                static::setPath($args, $link['to'], static::getPath($args, $link['from']));
313
            }
314
        }
315
316
        if (is_string($m)) {
317
            if (isset($this->localMethods[$m])) {
318
                $this->localMethods[$m](...$args);
319
            } elseif (method_exists($this, $m . 'Method')) {
320
                $func = [$this, $m . 'Method'];
321
                $func(...$args);
322
            } else {
323
                $this->handleException(new UndefinedMethodCalled(
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\DNode\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...
324
                    'DNode: local method ' . json_encode($m) . ' does not exist. Packet: ' . json_encode($pct)
325
                ));
326
            }
327
        } elseif (is_int($m)) {
328
            if (isset($this->callbacks[$m])) {
329
                if (!$this->callbacks[$m](...$args)) {
330
                    unset($this->callbacks[$m]);
331
                }
332
            } elseif (isset($this->persistentCallbacks[$m])) {
333
                $this->persistentCallbacks[$m](...$args);
334
            } else {
335
                $this->handleException(new UndefinedMethodCalled(
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\DNode\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...
336
                    'DNode: local callback # ' . $m . ' is not registered. Packet: ' . json_encode($pct)
337
                ));
338
            }
339
        } else {
340
            $this->handleException(new ProtocolError(
0 ignored issues
show
Documentation Bug introduced by
The method handleException does not exist on object<PHPDaemon\DNode\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...
341
                'DNode: \'method\' must be string or integer. Packet: ' . json_encode($pct)
342
            ));
343
        }
344
    }
345
346
    /**
347
     * Sets value by materialized path
348
     * @param  array &$m
349
     * @param  array $path
350
     * @param  mixed $val
351
     * @return void
352
     */
353
    protected static function setPath(&$m, $path, $val)
354
    {
355
        foreach ($path as $p) {
356
            $m =& $m[$p];
357
        }
358
        $m = $val;
359
    }
360
361
    /**
362
     * Finds value by materialized path
363
     * @param  array &$m
364
     * @param  array $path
365
     * @return mixed Value
366
     */
367
    protected static function &getPath(&$m, $path)
368
    {
369
        foreach ($path as $p) {
370
            $m =& $m[$p];
371
        }
372
        return $m;
373
    }
374
375
    /**
376
     * Called when session is finished
377
     * @return void
378
     */
379
    public function onFinish()
380
    {
381
        $this->cleanup();
382
        parent::onFinish();
383
    }
384
385
    /**
386
     * Swipes internal structures
387
     * @return void
388
     */
389
    public function cleanup()
390
    {
391
        $this->cleaned = true;
392
        $this->remoteMethods = [];
393
        $this->localMethods = [];
394
        $this->persistentCallbacks = [];
395
        $this->callbacks = [];
396
    }
397
398
    /**
399
     * Magic __call method
400
     * @param  string $method Method name
401
     * @param  array $args Arguments
402
     * @throws UndefinedMethodCalled if method name not start from 'remote_'
403
     * @return mixed
404
     */
405
    public function __call($method, $args)
406
    {
407
        if (strncmp($method, 'remote_', 7) === 0) {
408
            $this->callRemoteArray(substr($method, 7), $args);
409
        } elseif ($this->magicCallParent) {
410
            return parent::__call($method, $args);
411
        } else {
412
            throw new UndefinedMethodCalled('Call to undefined method ' . get_class($this) . '->' . $method);
413
        }
414
    }
415
416
    /**
417
     * Called when new frame is received
418
     * @param string $data Frame's contents
419
     * @param integer $type Frame's type
420
     * @return void
421
     */
422
    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...
423
    {
424
        foreach (explode("\n", $data) as $pct) {
425
            if ($pct === '') {
426
                continue;
427
            }
428
            $this->onPacket(json_decode($pct, true));
429
        }
430
    }
431
432
    /**
433
     * Handler of the 'methods' method
434
     * @param  array $methods Associative array of methods
435
     * @return void
436
     */
437
    protected function methodsMethod($methods)
438
    {
439
        $this->remoteMethods = $methods;
440
    }
441
}
442