ResponseCollection   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 581
Duplicated Lines 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
eloc 129
c 6
b 2
f 0
dl 0
loc 581
rs 4.5599
wmc 58

25 Methods

Rating   Name   Duplication   Size   Complexity  
A setIndex() 0 16 5
A getIndex() 0 3 1
A __invoke() 0 5 2
A __construct() 0 8 3
A __debugInfo() 0 3 1
A toArray() 0 20 5
A getAllTagged() 0 7 2
A offsetSet() 0 2 1
A prev() 0 5 1
A orderBy() 0 6 1
A rewind() 0 3 1
A count() 0 4 1
A getAllOfType() 0 7 2
A end() 0 5 1
A valid() 0 4 1
A __call() 0 5 1
A next() 0 5 1
A key() 0 4 2
A offsetExists() 0 6 2
A seek() 0 10 4
A offsetUnset() 0 2 1
A current() 0 4 2
A offsetGet() 0 7 3
A getPropertyMap() 0 16 5
B compare() 0 36 9

How to fix   Complexity   

Complex Class

Complex classes like ResponseCollection 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.

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

1
<?php
2
3
/**
4
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Id$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
/**
19
 * The namespace declaration.
20
 */
21
namespace PEAR2\Net\RouterOS;
22
23
/**
24
 * Implemented by this class.
25
 */
26
use ArrayAccess;
27
28
/**
29
 * Implemented by this class.
30
 */
31
use Countable;
32
33
/**
34
 * Implemented by this class.
35
 */
36
use SeekableIterator;
37
38
/**
39
 * Represents a collection of RouterOS responses.
40
 *
41
 * @category Net
42
 * @package  PEAR2_Net_RouterOS
43
 * @author   Vasil Rangelov <[email protected]>
44
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
45
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
46
 *
47
 * @method string getType()
48
 *     Calls {@link Response::getType()}
49
 *     on the response pointed by the pointer.
50
 * @method string[] getUnrecognizedWords()
51
 *     Calls {@link Response::getUnrecognizedWords()}
52
 *     on the response pointed by the pointer.
53
 * @method string|resource|null getProperty(string $name)
54
 *     Calls {@link Response::getProperty()}
55
 *     on the response pointed by the pointer.
56
 * @method string getTag()
57
 *     Calls {@link Response::getTag()}
58
 *     on the response pointed by the pointer.
59
 */
60
class ResponseCollection implements ArrayAccess, SeekableIterator, Countable
61
{
62
63
    /**
64
     * Used in {@link static::toArray()} as part of a bit mask.
65
     *
66
     * If set, it is effectively ignored. It is used merely as a placeholder.
67
     */
68
    const ARRAY_DEFAULT = 0;
69
70
    /**
71
     * Used in {@link static::toArray()} as part of a bit mask.
72
     *
73
     * If set, uses an index when available.
74
     * if not set, ignores it even if one was set with
75
     * {@link static::setIndex()}.
76
     */
77
    const ARRAY_INDEXED = 1;
78
79
    /**
80
     * Used in {@link static::toArray()} as part of a bit mask.
81
     *
82
     * If set, also serializes all {@link Response} objects to a plain array
83
     * using the __debugInfo() method.
84
     * If not set, the result will be an array of {@link Response} objects.
85
     */
86
    const ARRAY_RECURSIVE = 2;
87
88
    /**
89
     * An array with all {@link Response} objects.
90
     *
91
     * An array with all Response objects.
92
     *
93
     * @var Response[]
94
     */
95
    protected $responses = array();
96
97
    /**
98
     * An array with each Response object's type.
99
     *
100
     * An array with each {@link Response} object's type.
101
     *
102
     * @var string[]
103
     */
104
    protected $responseTypes = array();
105
106
    /**
107
     * An array with each Response object's tag.
108
     *
109
     * An array with each {@link Response} object's tag.
110
     *
111
     * @var string[]
112
     */
113
    protected $responseTags = array();
114
115
    /**
116
     * An array with positions of responses, based on an property name.
117
     *
118
     * The name of each property is the array key, and the array value
119
     * is another array where the key is the value for that property, and
120
     * the value is the position of the response. For performance reasons,
121
     * each key is built only when {@link static::setIndex()} is called with
122
     * that property, and remains available for the lifetime of this collection.
123
     *
124
     * @var array<string,array<string,int>>
125
     */
126
    protected $responsesIndex = array();
127
128
    /**
129
     * An array with all distinct properties.
130
     *
131
     * An array with all distinct properties across all {@link Response}
132
     * objects. Created at the first call of {@link static::getPropertyMap()}.
133
     *
134
     * @var array<string,int[]>
135
     */
136
    protected $propertyMap = null;
137
138
    /**
139
     * A pointer, as required by SeekableIterator.
140
     *
141
     * @var int
142
     */
143
    protected $position = 0;
144
145
    /**
146
     * Name of property to use as index
147
     *
148
     * NULL when disabled.
149
     *
150
     * @var string|null
151
     */
152
    protected $index = null;
153
154
    /**
155
     * Compare criteria.
156
     *
157
     * Used by {@link static::compare()} to determine the order between
158
     * two responses. See {@link static::orderBy()} for a detailed description
159
     * of this array's format.
160
     *
161
     * @var string[]|array<string,null|int|array<int|callable>>
162
     */
163
    protected $compareBy = array();
164
165
    /**
166
     * Creates a new collection.
167
     *
168
     * @param Response[] $responses An array of responses, in network order.
169
     */
170
    public function __construct(array $responses)
171
    {
172
        $pos = 0;
173
        foreach ($responses as $response) {
174
            if ($response instanceof Response) {
175
                $this->responseTypes[$pos] = $response->getType();
176
                $this->responseTags[$pos] = $response->getTag();
177
                $this->responses[$pos++] = $response;
178
            }
179
        }
180
    }
181
182
    /**
183
     * A shorthand gateway.
184
     *
185
     * This is a magic PHP method that allows you to call the object as a
186
     * function. Depending on the argument given, one of the other functions in
187
     * the class is invoked and its returned value is returned by this function.
188
     *
189
     * @param int|string|null $offset The offset of the response to seek to.
190
     *     If the offset is negative, seek to that relative to the end.
191
     *     If the collection is indexed, you can also supply a value to seek to.
192
     *     Setting NULL will get the current response's iterator.
193
     *
194
     * @return Response|\ArrayObject The {@link Response} at the specified
195
     *     offset, the current response's iterator (which is an ArrayObject)
196
     *     when NULL is given, or FALSE if the offset is invalid
197
     *     or the collection is empty.
198
     */
199
    public function __invoke($offset = null)
200
    {
201
        return null === $offset
202
            ? $this->current()->getIterator()
203
            : $this->seek($offset);
204
    }
205
206
    /**
207
     * Sets a property to be usable as a key in the collection.
208
     *
209
     * @param string|null $name The name of the property to use. Future calls
210
     *     that accept a position will then also be able to search values of
211
     *     that property for a matching value.
212
     *     Specifying NULL will disable such lookups (as is by default).
213
     *     Note that in case this value occurs multiple times within the
214
     *     collection, only the last matching response will be accessible by
215
     *     that value.
216
     *
217
     * @return $this The object itself.
218
     */
219
    public function setIndex($name)
220
    {
221
        if (null !== $name) {
222
            $name = (string)$name;
223
            if (!isset($this->responsesIndex[$name])) {
224
                $this->responsesIndex[$name] = array();
225
                foreach ($this->responses as $pos => $response) {
226
                    $val = $response->getProperty($name);
227
                    if (null !== $val) {
228
                        $this->responsesIndex[$name][$val] = $pos;
229
                    }
230
                }
231
            }
232
        }
233
        $this->index = $name;
234
        return $this;
235
    }
236
237
    /**
238
     * Gets the name of the property used as an index.
239
     *
240
     * @return string|null Name of property used as index. NULL when disabled.
241
     */
242
    public function getIndex()
243
    {
244
        return $this->index;
245
    }
246
247
    /**
248
     * Gets the whole collection as an array.
249
     *
250
     * @param int $flags A bit mask of this class' ARRAY_* constants.
251
     *
252
     * @return Response[] An array with all responses, in network order.
253
     */
254
    public function toArray($flags = self::ARRAY_DEFAULT)
255
    {
256
        $result = $this->responses;
257
        if (($flags & self::ARRAY_INDEXED) === self::ARRAY_INDEXED
258
            && null !== $this->index
259
        ) {
260
            $positions = $this->responsesIndex[$this->index];
261
            asort($positions, SORT_NUMERIC);
262
            $positions = array_flip($positions);
263
            $result = array_combine(
264
                $positions,
265
                array_intersect_key($this->responses, $positions)
266
            );
267
        }
268
        if (($flags & self::ARRAY_RECURSIVE) === self::ARRAY_RECURSIVE) {
269
            foreach ($result as $key => $value) {
270
                $result[$key] = $value->__debugInfo();
271
            }
272
        }
273
        return $result;
274
    }
275
276
    /**
277
     * Get actionable debug info.
278
     *
279
     * This is a magic method available to PHP 5.6 and above, due to which
280
     * output of var_dump() will be more actionable.
281
     *
282
     * You can still call it in earlier versions to get the object as a
283
     * plain array.
284
     *
285
     * @return array The info, as an associative array.
286
     */
287
    public function __debugInfo()
288
    {
289
        return $this->toArray(self::ARRAY_INDEXED | self::ARRAY_RECURSIVE);
290
    }
291
292
    /**
293
     * Counts the responses in the collection.
294
     *
295
     * @return int The number of responses in the collection.
296
     */
297
    #[\ReturnTypeWillChange]
298
    public function count()
299
    {
300
        return count($this->responses);
301
    }
302
303
    /**
304
     * Checks if an offset exists.
305
     *
306
     * @param int|string $offset The offset to check. If the
307
     *     collection is indexed, you can also supply a value to check.
308
     *     Note that negative numeric offsets are NOT accepted.
309
     *
310
     * @return bool TRUE if the offset exists, FALSE otherwise.
311
     */
312
    #[\ReturnTypeWillChange]
313
    public function offsetExists($offset)
314
    {
315
        return is_int($offset)
316
            ? array_key_exists($offset, $this->responses)
317
            : array_key_exists($offset, $this->responsesIndex[$this->index]);
318
    }
319
320
    /**
321
     * Gets a {@link Response} from a specified offset.
322
     *
323
     * @param int|string $offset The offset of the desired response. If the
324
     *     collection is indexed, you can also supply the value to search for.
325
     *
326
     * @return Response The response at the specified offset.
327
     */
328
    public function offsetGet($offset)
329
    {
330
        return is_int($offset)
331
            ? $this->responses[$offset >= 0
332
            ? $offset
333
            : count($this->responses) + $offset]
334
            : $this->responses[$this->responsesIndex[$this->index][$offset]];
335
    }
336
337
    /**
338
     * N/A
339
     *
340
     * This method exists only because it is required for ArrayAccess. The
341
     * collection is read only.
342
     *
343
     * @param int|string $offset N/A
344
     * @param Response   $value  N/A
345
     *
346
     * @return void
347
     *
348
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
349
     */
350
    public function offsetSet($offset, $value)
351
    {
352
    }
353
354
    /**
355
     * N/A
356
     *
357
     * This method exists only because it is required for ArrayAccess. The
358
     * collection is read only.
359
     *
360
     * @param int|string $offset N/A
361
     *
362
     * @return void
363
     *
364
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
365
     */
366
    public function offsetUnset($offset)
367
    {
368
    }
369
370
    /**
371
     * Resets the pointer to 0, and returns the first response.
372
     *
373
     * @return Response|false The first response in the collection,
374
     *     or FALSE if the collection is empty.
375
     */
376
    public function rewind()
377
    {
378
        return $this->seek(0);
379
    }
380
381
    /**
382
     * Moves the position pointer to a specified position.
383
     *
384
     * @param int|string $offset The position to move to. If the collection is
385
     *     indexed, you can also supply a value to move the pointer to.
386
     *     A non-existent index will move the pointer to "-1".
387
     *
388
     * @return Response|false The {@link Response} at the specified position,
389
     *     or FALSE if the specified position is not valid.
390
     */
391
    public function seek($offset)
392
    {
393
        $this->position = is_int($offset)
394
            ? ($offset >= 0
395
            ? $offset
396
            : count($this->responses) + $offset)
397
            : ($this->offsetExists($offset)
398
            ? $this->responsesIndex[$this->index][$offset]
399
            : -1);
400
        return $this->current();
401
    }
402
403
    /**
404
     * Moves the pointer forward by 1, and gets the next response.
405
     *
406
     * @return Response|false The next {@link Response} object,
407
     *     or FALSE if the position is not valid.
408
     */
409
    #[\ReturnTypeWillChange]
410
    public function next()
411
    {
412
        ++$this->position;
413
        return $this->current();
414
    }
415
416
    /**
417
     * Gets the response at the current pointer position.
418
     *
419
     * @return Response|false The response at the current pointer position,
420
     *     or FALSE if the position is not valid.
421
     */
422
    #[\ReturnTypeWillChange]
423
    public function current()
424
    {
425
        return $this->valid() ? $this->responses[$this->position] : false;
426
    }
427
428
    /**
429
     * Moves the pointer backwards by 1, and gets the previous response.
430
     *
431
     * @return Response|false The next {@link Response} object,
432
     *     or FALSE if the position is not valid.
433
     */
434
    #[\ReturnTypeWillChange]
435
    public function prev()
436
    {
437
        --$this->position;
438
        return $this->current();
439
    }
440
441
    /**
442
     * Moves the pointer to the last valid position, and returns the last
443
     * response.
444
     *
445
     * @return Response|false The last response in the collection,
446
     *     or FALSE if the collection is empty.
447
     */
448
    #[\ReturnTypeWillChange]
449
    public function end()
450
    {
451
        $this->position = count($this->responses) - 1;
452
        return $this->current();
453
    }
454
455
    /**
456
     * Gets the key at the current pointer position.
457
     *
458
     * @return int|false The key at the current pointer position,
459
     *     i.e. the pointer position itself, or FALSE if the position
460
     *     is not valid.
461
     */
462
    #[\ReturnTypeWillChange]
463
    public function key()
464
    {
465
        return $this->valid() ? $this->position : false;
466
    }
467
468
    /**
469
     * Checks if the pointer is still pointing to an existing offset.
470
     *
471
     * @return bool TRUE if the pointer is valid, FALSE otherwise.
472
     */
473
    #[\ReturnTypeWillChange]
474
    public function valid()
475
    {
476
        return $this->offsetExists($this->position);
477
    }
478
479
    /**
480
     * Gets all distinct property names.
481
     *
482
     * Gets all distinct property names across all responses.
483
     *
484
     * @return array<string,int[]> An array with
485
     *     all distinct property names as keys, and
486
     *     the indexes at which they occur as values.
487
     */
488
    public function getPropertyMap()
489
    {
490
        if (null === $this->propertyMap) {
491
            $properties = array();
492
            foreach ($this->responses as $index => $response) {
493
                $names = array_keys($response->getIterator()->getArrayCopy());
494
                foreach ($names as $name) {
495
                    if (!isset($properties[$name])) {
496
                        $properties[$name] = array();
497
                    }
498
                    $properties[$name][] = $index;
499
                }
500
            }
501
            $this->propertyMap = $properties;
502
        }
503
        return $this->propertyMap;
504
    }
505
506
    /**
507
     * Gets all responses of a specified type.
508
     *
509
     * @param string $type The response type to filter by. Valid values are the
510
     *     Response::TYPE_* constants.
511
     *
512
     * @return static A new collection with responses of the
513
     *     specified type.
514
     */
515
    public function getAllOfType($type)
516
    {
517
        $result = array();
518
        foreach (array_keys($this->responseTypes, $type, true) as $index) {
519
            $result[] = $this->responses[$index];
520
        }
521
        return new static($result);
522
    }
523
524
    /**
525
     * Gets all responses with a specified tag.
526
     *
527
     * @param string $tag The tag to filter by.
528
     *
529
     * @return static A new collection with responses having the
530
     *     specified tag.
531
     */
532
    public function getAllTagged($tag)
533
    {
534
        $result = array();
535
        foreach (array_keys($this->responseTags, $tag, true) as $index) {
536
            $result[] = $this->responses[$index];
537
        }
538
        return new static($result);
539
    }
540
541
    /**
542
     * Order resones by criteria.
543
     *
544
     * @param string[]|array<string,null|int|array<int|callable>> $criteria The
545
     *     criteria to order responses by. It takes the
546
     *     form of an array where each key is the name of the property to use
547
     *     as (N+1)th sorting key. The value of each member can be either NULL
548
     *     (for that property, sort normally in ascending order), a single sort
549
     *     order constant (SORT_ASC or SORT_DESC) to sort normally in the
550
     *     specified order, an array where the first member is an order
551
     *     constant, and the second one is sorting flags (same as built in PHP
552
     *     array functions) or a callback.
553
     *     If a callback is provided, it must accept two arguments
554
     *     (the two values to be compared), and return -1, 0 or 1 if the first
555
     *     value is respectively less than, equal to or greater than the second
556
     *     one.
557
     *     Each key of $criteria can also be numeric, in which case the
558
     *     value is the name of the property, and sorting is done normally in
559
     *     ascending order.
560
     *
561
     * @return static A new collection with the responses sorted in the
562
     *     specified order.
563
     */
564
    public function orderBy(array $criteria)
565
    {
566
        $this->compareBy = $criteria;
567
        $sortedResponses = $this->responses;
568
        usort($sortedResponses, array($this, 'compare'));
569
        return new static($sortedResponses);
570
    }
571
572
    /**
573
     * Calls a method of the response pointed by the pointer.
574
     *
575
     * Calls a method of the response pointed by the pointer. This is a magic
576
     * PHP method, thanks to which any function you call on the collection that
577
     * is not defined will be redirected to the response.
578
     *
579
     * @param string $method The name of the method to call.
580
     * @param array  $args   The arguments to pass to the method.
581
     *
582
     * @return mixed Whatever the called function returns.
583
     */
584
    public function __call($method, array $args)
585
    {
586
        return call_user_func_array(
587
            array($this->current(), $method),
588
            $args
589
        );
590
    }
591
592
    /**
593
     * Compares two responses.
594
     *
595
     * Compares two responses, based on criteria defined in
596
     * {@link static::$compareBy}.
597
     *
598
     * @param Response $itemA The response to compare.
599
     * @param Response $itemB The response to compare $a against.
600
     *
601
     * @return int Returns 0 if the two responses are equal according to every
602
     *     criteria specified, -1 if $a should be placed before $b, and 1 if $b
603
     *     should be placed before $a.
604
     */
605
    protected function compare(Response $itemA, Response $itemB)
606
    {
607
        foreach ($this->compareBy as $name => $spec) {
608
            if (!is_string($name)) {
609
                $name = $spec;
610
                $spec = null;
611
            }
612
613
            $members = array(
614
                0 => $itemA->getProperty($name),
615
                1 => $itemB->getProperty($name)
616
            );
617
618
            if (is_callable($spec)) {
619
                uasort($members, $spec);
0 ignored issues
show
Bug introduced by
$spec of type null is incompatible with the type callable expected by parameter $callback of uasort(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

619
                uasort($members, /** @scrutinizer ignore-type */ $spec);
Loading history...
620
            } elseif ($members[0] === $members[1]) {
621
                continue;
622
            } else {
623
                $flags = SORT_REGULAR;
624
                $order = SORT_ASC;
625
                if (is_array($spec)) {
626
                    list($order, $flags) = $spec;
627
                } elseif (null !== $spec) {
628
                    $order = $spec;
629
                }
630
631
                if (SORT_ASC === $order) {
632
                    asort($members, $flags);
633
                } else {
634
                    arsort($members, $flags);
635
                }
636
            }
637
            return (key($members) === 0) ? -1 : 1;
638
        }
639
640
        return 0;
641
    }
642
}
643