DNode::getPath()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
nc 2
nop 2
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 callable (string packet)
21
     */
22
    protected $emitCallback;
23
24
    /**
25
     * @var array Associative array of persistent callback functions registered by callRemote()
26
     */
27
    protected $persistentCallbacks = [];
28
29
    /**
30
     * @var boolean If true, callRemote() will register callbacks as persistent ones
31
     */
32
    protected $persistentMode = false;
33
34
    /**
35
     * @var integer Incremental counter of callback functions registered by callRemote()
36
     */
37
    protected $counter = 0;
38
39
    /**
40
     * @var array Associative array of registered remote methods (received in 'methods' call)
41
     */
42
    protected $remoteMethods = [];
43
44
    /**
45
     * @var array Associative array of local methods, set by defineLocalMethods()
46
     */
47
    protected $localMethods = [];
48
49
    /**
50
     * @var boolean Was this object cleaned up?
51
     */
52
    protected $cleaned = false;
53
54
    /**
55
     * @var boolean Should __call method call parent::__call()?
56
     */
57
    protected $magicCallParent = false;
58
59
    /**
60
     * Ensures that the variable passed by reference holds a valid callback-function
61
     * If it doesn't, its value will be reset to null
62
     * @param  mixed &$arg Argument
63
     * @return boolean
64
     */
65
    public static function ensureCallback(&$arg)
66
    {
67
        if ($arg instanceof \Closure) {
68
            return true;
69
        }
70
        if (is_array($arg) && sizeof($arg) === 2) {
71
            if (isset($arg[0]) && is_object($arg[0]) && !$arg[0] instanceof \stdClass) {
72 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...
73
                    return true;
74
                }
75
            }
76
        }
77
        $arg = null;
78
        return false;
79
    }
80
81
    /**
82
     * Encodes value into JSON for debugging purposes
83
     * @param mixed $m Data
84
     * @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...
85
     */
86
    public static function toJsonDebug($m)
87
    {
88
        static::toJsonDebugResursive($m);
89
        return static::toJson($m);
90
    }
91
92
    /**
93
     * Recursion handler for toJsonDebug()
94
     * @param  array &$a Data
95
     * @return void
96
     */
97
    public static function toJsonDebugResursive(&$m)
98
    {
99
        if ($m instanceof \Closure) {
100
            $m = '__CALLBACK__';
101
        } elseif (is_array($m)) {
102
            if (sizeof($m) === 2 && isset($m[0]) && $m[0] instanceof \PHPDaemon\WebSocket\Route) {
103 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...
104
                    $m = '__CALLBACK__';
105
                }
106
            } else {
107
                foreach ($m as &$v) {
108
                    static::toJsonDebugResursive($v);
109
                }
110
            }
111
        } elseif (is_object($m)) {
112
            foreach ($m as &$v) {
113
                static::toJsonDebugResursive($v);
114
            }
115
        }
116
    }
117
118
    /**
119
     * Default onHandshake() method
120
     * @return void
121
     */
122
    public function onHandshake()
123
    {
124
        $this->defineLocalMethods();
125
    }
126
127
    /**
128
     * Defines local methods
129
     * @param  array $arr Associative array of callbacks (methodName => callback)
130
     * @return void
131
     */
132
    protected function defineLocalMethods($arr = [])
133
    {
134
        foreach (get_class_methods($this) as $m) {
135
            if (substr($m, -6) === 'Method') {
136
                $k = substr($m, 0, -6);
137
                if ($k === 'methods') {
138
                    continue;
139
                }
140
                $arr[$k] = [$this, $m];
141
            }
142
        }
143
        foreach ($arr as $k => $v) {
144
            $this->localMethods[$k] = $v;
145
        }
146
        $this->persistentMode = true;
147
        $this->callRemote('methods', $arr);
148
        $this->persistentMode = false;
149
    }
150
151
    /**
152
     * Calls a remote method
153
     * @param  string $method Method name
154
     * @param  mixed ...$args Arguments
155
     * @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...
156
     */
157
    public function callRemote($method, ...$args)
158
    {
159
        $this->callRemoteArray($method, $args);
160
        return $this;
161
    }
162
163
164
    /**
165
     * Export object methods
166
     * @param $object
167
     * @return array
168
     */
169
    public static function exportObjectMethods($object) {
170
        $methods = [];
171
        foreach (get_class_methods($object) as $method) {
172
            if ($method[0] === '_') {
173
                continue;
174
            }
175
            $methods[$method] = [$object, $method];
176
        }
177
        return $methods;
178
    }
179
180
    /**
181
     * Calls a remote method with array of arguments
182
     * @param  string $method Method name
183
     * @param  array $args Arguments
184
     * @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...
185
     */
186
    public function callRemoteArray($method, $args)
187
    {
188
        if (isset($this->remoteMethods[$method])) {
189
            $this->remoteMethods[$method](...$args);
190
            return $this;
191
        }
192
        $pct = [
193
            'method' => $method,
194
        ];
195
        if (sizeof($args)) {
196
            $callbacks = [];
197
            $path = [];
198
            $this->extractCallbacks($args, $callbacks, $path);
199
            $pct['arguments'] = $args;
200
            if (sizeof($callbacks)) {
201
                $pct['callbacks'] = $callbacks;
202
            }
203
        }
204
        $this->sendPacket($pct);
205
        return $this;
206
    }
207
208
    /**
209
     * Extracts callback functions from array of arguments
210
     * @param  array &$args Arguments
211
     * @param  array &$list Output array for 'callbacks' property
212
     * @param  array &$path Recursion path holder
213
     * @return void
214
     */
215
    protected function extractCallbacks(&$args, &$list, &$path)
216
    {
217
        foreach ($args as $k => &$v) {
218
            if (is_array($v)) {
219
                if (sizeof($v) === 2) {
220
                    if (isset($v[0]) && is_object($v[0])) {
221
                        if (isset($v[1]) && is_string($v[1])) {
222
                            $id = ++$this->counter;
223 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...
224
                                $this->persistentCallbacks[$id] = $v;
225
                            } else {
226
                                $this->callbacks[$id] = $v;
227
                            }
228
                            $v = '';
229
                            $list[$id] = $path;
230
                            $list[$id][] = $k;
231
                            continue;
232
                        }
233
                    }
234
                }
235
                $path[] = $k;
236
                $this->extractCallbacks($v, $list, $path);
237
                array_pop($path);
238
            } elseif ($v instanceof \Closure) {
239
                $id = ++$this->counter;
240 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...
241
                    $this->persistentCallbacks[$id] = $v;
242
                } else {
243
                    $this->callbacks[$id] = $v;
244
                }
245
                $v = '';
246
                $list[$id] = $path;
247
                $list[$id][] = $k;
248
            }
249
        }
250
    }
251
252
    /**
253
     * Sends a packet
254
     * @param  array $pct Data
255
     * @return void
256
     */
257
    protected function sendPacket($pct)
258
    {
259
        if (is_string($pct['method']) && ctype_digit($pct['method'])) {
260
            $pct['method'] = (int)$pct['method'];
261
        }
262
263
        if ($this->emitCallback !== null) {
264
            ($this->emitCallback)(static::toJson($pct) . "\n");
265
        } elseif ($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...
266
            $this->client->sendFrame(static::toJson($pct) . "\n");
267
        }
268
    }
269
270
    /**
271
     * Encodes value into JSON
272
     * @param  mixed $m Value
273
     * @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...
274
     */
275
    public static function toJson($m)
276
    {
277
        return json_encode($m, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
278
    }
279
280
    /**
281
     * Calls a local method
282
     * @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...
283
     * @param  mixed ...$args Arguments
284
     * @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...
285
     */
286
    public function callLocal(...$args)
287
    {
288
        if (!sizeof($args)) {
289
            return $this;
290
        }
291
        $method = array_shift($args);
292
        $p = [
293
            'method' => $method,
294
            'arguments' => $args,
295
        ];
296
        $this->onPacket($p);
297
        return $this;
298
    }
299
300
    /**
301
     * Called when new packet is received
302
     * @param  array $pct Packet
303
     * @return void
304
     */
305
    public function onPacket($pct)
306
    {
307
        if ($this->cleaned) {
308
            return;
309
        }
310
        $m = isset($pct['method']) ? $pct['method'] : null;
311
        $args = isset($pct['arguments']) ? $pct['arguments'] : [];
312
        if (isset($pct['callbacks']) && is_array($pct['callbacks'])) {
313
            foreach ($pct['callbacks'] as $id => $path) {
314
                static::setPath($args, $path, [$this, 'remote_' . $id]);
315
            }
316
        }
317
        if (isset($pct['links']) && is_array($pct['links'])) {
318
            foreach ($pct['links'] as $link) {
319
                static::setPath($args, $link['to'], static::getPath($args, $link['from']));
320
            }
321
        }
322
        try {
323
            if (is_string($m)) {
324
                if (isset($this->localMethods[$m])) {
325
                    $this->localMethods[$m](...$args);
326
                } elseif (method_exists($this, $m . 'Method')) {
327
                    $func = [$this, $m . 'Method'];
328
                    $func(...$args);
329
                } else {
330
                    $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...
331
                        'DNode: local method ' . json_encode($m) . ' does not exist. Packet: ' . json_encode($pct)
332
                    ));
333
                }
334
            } elseif (is_int($m)) {
335
                if (isset($this->callbacks[$m])) {
336
                    if (!$this->callbacks[$m](...$args)) {
337
                        unset($this->callbacks[$m]);
338
                    }
339
                } elseif (isset($this->persistentCallbacks[$m])) {
340
                    $this->persistentCallbacks[$m](...$args);
341
                } else {
342
                    $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...
343
                        'DNode: local callback # ' . $m . ' is not registered. Packet: ' . json_encode($pct)
344
                    ));
345
                }
346
            } else {
347
                $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...
348
                    'DNode: \'method\' must be string or integer. Packet: ' . json_encode($pct)
349
                ));
350
            }
351
        } catch (\Throwable $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
352
        }
353
    }
354
355
    /**
356
     * Sets value by materialized path
357
     * @param  array &$m
358
     * @param  array $path
359
     * @param  mixed $val
360
     * @return void
361
     */
362
    protected static function setPath(&$m, $path, $val)
363
    {
364
        foreach ($path as $p) {
365
            $m =& $m[$p];
366
        }
367
        $m = $val;
368
    }
369
370
    /**
371
     * Finds value by materialized path
372
     * @param  array &$m
373
     * @param  array $path
374
     * @return mixed Value
375
     */
376
    protected static function &getPath(&$m, $path)
377
    {
378
        foreach ($path as $p) {
379
            $m =& $m[$p];
380
        }
381
        return $m;
382
    }
383
384
    /**
385
     * Called when session is finished
386
     * @return void
387
     */
388
    public function onFinish()
389
    {
390
        $this->cleanup();
391
        parent::onFinish();
392
    }
393
394
    /**
395
     * Swipes internal structures
396
     * @return void
397
     */
398
    public function cleanup()
399
    {
400
        $this->cleaned = true;
401
        $this->remoteMethods = [];
402
        $this->localMethods = [];
403
        $this->persistentCallbacks = [];
404
        $this->callbacks = [];
405
    }
406
407
    /**
408
     * Magic __call method
409
     * @param  string $method Method name
410
     * @param  array $args Arguments
411
     * @throws UndefinedMethodCalled if method name not start from 'remote_'
412
     * @return mixed
413
     */
414
    public function __call($method, $args)
415
    {
416
        if (strncmp($method, 'remote_', 7) === 0) {
417
            $this->callRemoteArray(substr($method, 7), $args);
418
        } elseif ($this->magicCallParent) {
419
            return parent::__call($method, $args);
420
        } else {
421
            throw new UndefinedMethodCalled('Call to undefined method ' . get_class($this) . '->' . $method);
422
        }
423
    }
424
425
    /**
426
     * Called when new frame is received
427
     * @param string $data Frame's contents
428
     * @param integer $type Frame's type
429
     * @return void
430
     */
431
    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...
432
    {
433
        foreach (explode("\n", $data) as $pct) {
434
            if ($pct === '') {
435
                continue;
436
            }
437
            $this->onPacket(json_decode($pct, true));
438
        }
439
    }
440
441
    /**
442
     * Handler of the 'methods' method
443
     * @param  array $methods Associative array of methods
444
     * @return void
445
     */
446
    protected function methodsMethod($methods)
447
    {
448
        $this->remoteMethods = $methods;
449
    }
450
}
451