Completed
Pull Request — master (#6)
by Florian
04:02 queued 12s
created

Router   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 797
Duplicated Lines 0 %

Test Coverage

Coverage 99.07%

Importance

Changes 0
Metric Value
wmc 92
eloc 232
dl 0
loc 797
c 0
b 0
f 0
ccs 106
cts 107
cp 0.9907
rs 2

34 Methods

Rating   Name   Duplication   Size   Complexity  
A middleware() 0 4 2
A count() 0 3 1
A offsetExists() 0 3 1
A __call() 0 16 3
A apply() 0 7 2
B route() 0 35 7
A next() 0 6 3
A current() 0 3 1
A _normalizeRequest() 0 11 2
A offsetGet() 0 3 1
A setStrategy() 0 5 1
A valid() 0 3 1
A __construct() 0 23 1
A _getRequestInformation() 0 13 2
A getStrategy() 0 7 2
A key() 0 3 1
A clear() 0 8 1
A setDefaultHandler() 0 5 1
A link() 0 15 2
A offsetUnset() 0 4 1
A addRoute() 0 30 5
B _route() 0 40 11
A rewind() 0 5 1
A group() 0 24 6
A scope() 0 3 1
A pushScope() 0 5 1
B strategy() 0 23 7
A popScope() 0 3 1
A getBasePath() 0 3 1
A offsetSet() 0 8 2
A basePath() 0 7 2
F bind() 0 59 14
A unsetStrategy() 0 9 2
A setBasePath() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Router 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 Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Lead\Router;
5
6
use ArrayAccess;
7
use Closure;
8
use Countable;
9
use Iterator;
10
use Lead\Router\Exception\ParserException;
11
use Lead\Router\Exception\RouteNotFoundException;
12
use Lead\Router\Exception\RouterException;
13
use Psr\Http\Message\RequestInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use RuntimeException;
16
17
/**
18
 * The Router class.
19
 */
20
class Router implements ArrayAccess, Iterator, Countable, RouterInterface
21
{
22
    /**
23
     * @var bool
24
     */
25
    protected $_skipNext;
26
27
    /**
28
     * @var array
29
     */
30
    protected $_data = [];
31
32
    /**
33
     * @var array
34
     */
35
    protected $_pattern = [];
36
37
    /**
38
     * Class dependencies.
39
     *
40
     * @var array
41
     */
42
    protected $_classes = [];
43
44
    /**
45
     * Hosts.
46
     *
47
     * @var array
48
     */
49
    protected $_hosts = [];
50
51
    /**
52
     * Routes.
53
     *
54
     * @var array
55
     */
56
    protected $_routes = [];
57
58
    /**
59
     * Scopes stack.
60
     *
61
     * @var array
62
     */
63
    protected $_scopes = [];
64
65
    /**
66
     * Base path.
67
     *
68
     * @param string
69
     */
70
    protected $_basePath = '';
71
72
    /**
73
     * Dispatching strategies.
74
     *
75
     * @param array
76
     */
77
    protected $_strategies = [];
78
79
    /**
80 50
     * Defaults parameters to use when generating URLs in a dispatching context.
81 50
     *
82 50
     * @var array
83 50
     */
84 50
    protected $_defaults = [];
85
86 50
    /**
87 50
     * Default handler
88
     *
89
     * @var callable|null
90
     */
91
    protected $defaultHandler = null;
92
93
    /**
94
     * Constructor
95
     *
96
     * @param array $config
97 14
     */
98
    public function __construct($config = [])
99
    {
100
        $defaults = [
101
            'basePath'       => '',
102
            'scope'          => [],
103
            'strategies'     => [],
104
            'defaultHandler' => null,
105
            'classes'        => [
106
                'parser'     => 'Lead\Router\Parser',
107
                'host'       => 'Lead\Router\Host',
108 14
                'route'      => 'Lead\Router\Route',
109 14
                'scope'      => 'Lead\Router\Scope'
110
            ]
111
        ];
112
113
        $config += $defaults;
114
        $this->_classes = $config['classes'];
115
        $this->_strategies = $config['strategies'];
116
        $this->setDefaultHandler($config['defaultHandler']);
117
        $this->setBasePath($config['basePath']);
118
119 14
        $scope = $this->_classes['scope'];
120
        $this->_scopes[] = new $scope(['router' => $this]);
121
    }
122
123
    /**
124
     * Sets the default handler for routes
125
     *
126
     * @param mixed $handler
127
     * @return $this
128
     */
129
    public function setDefaultHandler($handler): RouterInterface
130
    {
131 8
        $this->_defaultHandler = $handler;
0 ignored issues
show
Bug Best Practice introduced by
The property _defaultHandler does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
132
133 50
        return $this;
134 50
    }
135 50
136
    /**
137
     * Returns the current router scope.
138
     *
139
     * @return \Lead\Router\ScopeInterface The current scope instance.
140
     */
141
    public function scope(): ScopeInterface
142
    {
143
        return end($this->_scopes);
144
    }
145
146
    /**
147
     * Pushes a new router scope context.
148
     *
149 14
     * @param  object $scope A scope instance.
150 14
     * @return self
151
     */
152
    public function pushScope($scope): RouterInterface
153 1
    {
154
        $this->_scopes[] = $scope;
155
156
        return $this;
157 1
    }
158
159
    /**
160 40
     * Pops the current router scope context.
161 40
     *
162 40
     * @return \Lead\Router\ScopeInterface The popped scope instance.
163 40
     */
164 40
    public function popScope(): ScopeInterface
165
    {
166 40
        return array_pop($this->_scopes);
167 40
    }
168
169
    /**
170
     * Gets the base path
171
     *
172
     * @param  string $basePath The base path to set or none to get the setted one.
173
     * @return string
174 2
     */
175
    public function getBasePath(): string
176 40
    {
177 40
        return $this->_basePath;
178 40
    }
179
180
    /**
181
     * Sets the base path
182 40
     *
183
     * @param  string $basePath Base Path
184
     * @return $this
185 40
     */
186
    public function setBasePath(string $basePath): self
187 40
    {
188
        $basePath = trim($basePath, '/');
189
        $this->_basePath = $basePath ? '/' . $basePath : '';
190 40
191
        return $this;
192
    }
193
194 40
    /**
195
     * Gets/sets the base path of the router.
196 40
     *
197
     * @deprecated Use setBasePath() and getBasePath() instead
198
     * @param      string|null $basePath The base path to set or none to get the setted one.
199
     * @return     string|self
200
     */
201
    public function basePath(?string $basePath = null)
202
    {
203
        if ($basePath === null) {
204
            return $this->_basePath;
205
        }
206
207
        return $this->setBasePath($basePath);
208
    }
209
210 9
    /**
211
     * Adds a route to the router
212 8
     *
213
     * @param \Lead\Router\RouteInterface $route Route object
214 1
     * @return \Lead\Router\RouterInterface
215 1
     */
216
    public function addRoute(RouteInterface $route): RouterInterface {
217
         $options['pattern'] = $pattern = $route->getPattern();
0 ignored issues
show
Bug introduced by
The method getPattern() does not exist on Lead\Router\RouteInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Lead\Router\RouteInterface. ( Ignorable by Annotation )

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

217
         $options['pattern'] = $pattern = $route->/** @scrutinizer ignore-call */ getPattern();
Loading history...
Comprehensibility Best Practice introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.
Loading history...
218
         $options['handler'] = $route->getHandler();
219 1
         $options['scope'] = $route->getScope();
220
221
         $scheme = $options['scheme'];
222 14
         $host = $options['host'];
223
224 14
         if (isset($this->_hosts[$scheme][$host])) {
225
             $options['host'] = $this->_hosts[$scheme][$host];
226 14
         }
227
228 14
         if (isset($this->_pattern[$scheme][$host][$pattern])) {
229
             $route = $this->_pattern[$scheme][$host][$pattern];
230 14
         } else {
231
             $this->_hosts[$scheme][$host] = $route->getHost();
232
         }
233
234
         if (!isset($this->_pattern[$scheme][$host][$pattern])) {
235
             $this->_pattern[$scheme][$host][$pattern] = $route;
236
         }
237
238
         $methods = $route->getMethods();
239
         foreach ($methods as $method) {
240
             $this->_routes[$scheme][$host][strtoupper($method)][] = $route;
241
         }
242
243
         $this->_data[$route->getName()] = $route;
244
245
         return $this;
246 35
    }
247
248 35
    /**
249
     * Adds a route.
250
     *
251 1
     * @param  string|array  $pattern The route's pattern.
252
     * @param  Closure|array $options An array of options or the callback handler.
253
     * @param  Closure|null  $handler The callback handler.
254
     * @return self
255
     */
256
    public function bind($pattern, $options = [], $handler = null): RouteInterface
257 1
    {
258
        if (!is_array($options)) {
259 1
            $handler = $options;
260
            $options = [];
261
        }
262 33
263
        if (empty($handler) && !empty($this->_defaultHandler)) {
264 1
            $handler = $this->_defaultHandler;
265
        }
266 35
267
        if ($handler !== null) {
268
            if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
269 31
                throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
270
            }
271
        }
272 2
273
        if (isset($options['method'])) {
274
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
275
        }
276 12
277 12
        $scope = end($this->_scopes);
278 12
        $options = $scope->scopify($options);
279 12
        $options['pattern'] = $pattern;
280
        $options['handler'] = $handler;
281
        $options['scope'] = $scope;
282 35
283
        $scheme = $options['scheme'];
284
        $host = $options['host'];
285
286
        if (isset($this->_hosts[$scheme][$host])) {
287
            $options['host'] = $this->_hosts[$scheme][$host];
288
        }
289
290
        if (isset($this->_pattern[$scheme][$host][$pattern])) {
291
            $instance = $this->_pattern[$scheme][$host][$pattern];
292
        } else {
293
            $route = $this->_classes['route'];
294 5
            $instance = new $route($options);
295 5
            $this->_hosts[$scheme][$host] = $instance->getHost();
296
        }
297 35
298 35
        if (!isset($this->_pattern[$scheme][$host][$pattern])) {
299 35
            $this->_pattern[$scheme][$host][$pattern] = $instance;
300
        }
301
302
        $methods = $options['methods'] ? (array)$options['methods'] : [];
303
304
        $instance->allow($methods);
305
306
        foreach ($methods as $method) {
307
            $this->_routes[$scheme][$host][strtoupper($method)][] = $instance;
308
        }
309 35
310 35
        if (isset($options['name'])) {
311 35
            $this->_data[$options['name']] = $instance;
312 35
        }
313
314 35
        return $instance;
315 35
    }
316
317
    /**
318 3
     * Groups some routes inside a new scope.
319
     *
320
     * @param  string|array $prefix  The group's prefix pattern or the options array.
321
     * @param  Closure|array $options An array of options or the callback handler.
322
     * @param  Closure|null  $handler The callback handler.
323 1
     * @return \Lead\Router\ScopeInterface The newly created scope instance.
324
     */
325
    public function group($prefix, $options, $handler = null)
326
    {
327
        if (!is_array($options)) {
328 3
            $handler = $options;
329
            if (is_string($prefix)) {
330
                $options = [];
331
            } else {
332
                $options = $prefix;
333 3
                $prefix = '';
334
            }
335 9
        }
336
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
337 31
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
338
        }
339
340
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
341
342
        $scope = $this->scope();
343
344
        $this->pushScope($scope->seed($options));
345
346
        $handler($this);
347
348
        return $this->popScope();
349
    }
350
351
    /**
352 1
     * Gets information required for routing from a server request
353
     *
354
     * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
355
     * @return array
356
     */
357
    protected function _getRequestInformation(ServerRequestInterface $request): array
358
    {
359
        $uri = $request->getUri();
360
361
        if (method_exists($request, 'basePath')) {
362
            $this->setBasePath($request->basePath());
363
        }
364 3
365
        return [
366 3
            'scheme' => $uri->getScheme(),
367
            'host' => $uri->getHost(),
368
            'method' => $request->getMethod(),
369
            'path' => $uri->getPath()
370
        ];
371
    }
372
373
    /**
374
     * Routes a Request.
375
     *
376
     * @param mixed $request The request to route.
377
     * @return \Lead\Router\RouteInterface A route matching the request or a "route not found" route.
378
     */
379
    public function route($request): RouteInterface
380 16
    {
381
        $defaults = [
382 2
            'path' => '',
383
            'method' => 'GET',
384
            'host' => '*',
385 1
            'scheme' => '*'
386 1
        ];
387
388
        $this->_defaults = [];
389 1
390
        if ($request instanceof ServerRequestInterface) {
391 2
            $r = $this->_getRequestInformation($request);
392 2
        } elseif (!is_array($request)) {
393
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
394
        } else {
395
            $r = $request + $defaults;
396
        }
397
398
        $r = $this->_normalizeRequest($r);
399
400
        $route = $this->_route($r);
401
        if ($route instanceof RouteInterface) {
402
            $route->request = is_object($request) ? $request : $r;
0 ignored issues
show
Bug introduced by
Accessing request on the interface Lead\Router\RouteInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
403
            foreach ($route->getPersistentParams() as $key) {
0 ignored issues
show
Bug introduced by
The method getPersistentParams() does not exist on Lead\Router\RouteInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Lead\Router\RouteInterface. ( Ignorable by Annotation )

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

403
            foreach ($route->/** @scrutinizer ignore-call */ getPersistentParams() as $key) {
Loading history...
404 1
                if (isset($route->params[$key])) {
0 ignored issues
show
Bug introduced by
Accessing params on the interface Lead\Router\RouteInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
405 1
                    $this->_defaults[$key] = $route->params[$key];
406
                }
407
            }
408 11
409 11
            return $route;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $route could return the type null which is incompatible with the type-hinted return Lead\Router\RouteInterface. Consider adding an additional type-check to rule them out.
Loading history...
410
        }
411 15
412 15
        $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
413
        throw new RouteNotFoundException($message);
414
    }
415
416
    /**
417
     * Normalizes a request.
418
     *
419
     * @param  array $request The request to normalize.
420
     * @return array          The normalized request.
421
     */
422
    protected function _normalizeRequest(array $request): array
423
    {
424
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
425
            $parsed = array_intersect_key(parse_url($request['path']), $request);
426
            $request = $parsed + $request;
427
        }
428
429
        $request['path'] = (ltrim((string)strtok($request['path'], '?'), '/'));
430
        $request['method'] = strtoupper($request['method']);
431
432
        return $request;
433 5
    }
434 5
435
    /**
436 5
     * Routes a request.
437
     *
438
     * @param array $request The request to route.
439 1
     * @return null|\Lead\Router\RouteInterface
440
     */
441 4
    protected function _route($request): ?RouteInterface
442 4
    {
443
        $path = $request['path'];
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
444
        $httpMethod = $request['method'];
445
        $host = $request['host'];
0 ignored issues
show
Unused Code introduced by
The assignment to $host is dead and can be removed.
Loading history...
446
        $scheme = $request['scheme'];
447
448
        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
449
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);
450 1
451 1
        if ($httpMethod === 'HEAD') {
452 1
            $allowedMethods += ['GET' => 'GET'];
453 1
        }
454 1
455 1
        foreach ($this->_routes as $scheme => $hostBasedRoutes) {
456
            if (!isset($allowedSchemes[$scheme])) {
457
                continue;
458
            }
459
460
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
461
                foreach ($methodBasedRoutes as $method => $routes) {
462
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
463
                        continue;
464
                    }
465
                    foreach ($routes as $route) {
466
                        /* @var $route \Lead\Router\RouteInterface */
467
                        if (!$route->match($request, $variables, $hostVariables)) {
468
                            if ($hostVariables === null) {
469
                                continue 3;
470
                            }
471
                            continue;
472
                        }
473
474
                        return $route;
475
                    }
476
                }
477
            }
478
        }
479
480
        return null;
481
    }
482
483
    /**
484
     * Middleware generator.
485
     *
486
     * @return callable
487
     */
488
    public function middleware()
489
    {
490
        foreach ($this->_scopes[0]->middleware() as $middleware) {
491
            yield $middleware;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $middleware returns the type Generator which is incompatible with the documented return type callable.
Loading history...
492
        }
493
    }
494
495
    /**
496
     * Adds a middleware to the list of middleware.
497
     *
498
     * @param  object|Closure A callable middleware.
0 ignored issues
show
Bug introduced by
The type Lead\Router\A was not found. Did you mean A? If so, make sure to prefix the type with \.
Loading history...
499
     * @return $this
500
     */
501
    public function apply($middleware)
502
    {
503
        foreach (func_get_args() as $mw) {
504
            $this->_scopes[0]->apply($mw);
505
        }
506
507
        return $this;
508
    }
509
510
    /**
511
     * Sets a dispatcher strategy
512
     *
513
     * @param  string   $name    Name
514
     * @param  callable $handler Handler
515
     * @return $this
516
     */
517
    public function setStrategy(string $name, callable $handler)
518
    {
519
        $this->_strategies[$name] = $handler;
520
521
        return $this;
522
    }
523
524
    /**
525
     * Get a strategy
526
     *
527
     * @return callable
528
     */
529
    public function getStrategy(string $name): callable
530
    {
531
        if (isset($this->_strategies[$name])) {
532
            return $this->_strategies[$name];
533
        }
534
535
        throw new RuntimeException(sprintf('Strategy `%s` not found.', $name));
536
    }
537
538
    /**
539
     * Unsets a strategy
540
     *
541
     * @param  string $name
542
     * @return $this
543
     */
544
    public function unsetStrategy(string $name)
545
    {
546
        if (isset($this->_strategies[$name])) {
547
            unset($this->_strategies[$name]);
548
549
            return $this;
550
        }
551
552
        throw new RuntimeException(sprintf('Strategy `%s` not found.', $name));
553
    }
554
555
    /**
556
     * Gets/sets router's strategies.
557
     *
558
     * @deprecated Use setStrategy(), unsetStrategy() and getStrategy()
559
     * @param      string $name    A routing strategy name.
560
     * @param      mixed  $handler The strategy handler or none to get the setted one.
561
     * @return     mixed           The strategy handler (or `null` if not found) on get or `$this` on set.
562
     */
563
    public function strategy($name, $handler = null)
564
    {
565
        if (func_num_args() === 1) {
566
            try {
567
                return $this->getStrategy($name);
568
            } catch (RuntimeException $e) {
569
                return null;
570
            }
571
        }
572
573
        if ($handler === false) {
574
            try {
575
                return $this->unsetStrategy($name);
576
            } catch (RuntimeException $e) {
577
                return null;
578
            }
579
        }
580
581
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
582
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
583
        }
584
585
        return $this->setStrategy($name, $handler);
0 ignored issues
show
Bug introduced by
It seems like $handler can also be of type object; however, parameter $handler of Lead\Router\Router::setStrategy() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

585
        return $this->setStrategy($name, /** @scrutinizer ignore-type */ $handler);
Loading history...
586
    }
587
588
    /**
589
     * Adds a route based on a custom HTTP verb.
590
     *
591
     * @param string $name   The HTTP verb to define a route on.
592
     * @param array  $params The route's parameters.
593
     * @return mixed
594
     */
595
    public function __call($name, $params)
596
    {
597
        if ($strategy = $this->strategy($name)) {
0 ignored issues
show
Deprecated Code introduced by
The function Lead\Router\Router::strategy() has been deprecated: Use setStrategy(), unsetStrategy() and getStrategy() ( Ignorable by Annotation )

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

597
        if ($strategy = /** @scrutinizer ignore-deprecated */ $this->strategy($name)) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
598
            array_unshift($params, $this);
599
600
            return call_user_func_array($strategy, $params);
601
        }
602
603
        if (is_callable($params[1])) {
604
            $params[2] = $params[1];
605
            $params[1] = [];
606
        }
607
608
        $params[1]['methods'] = [$name];
609
610
        return call_user_func_array([$this, 'bind'], $params);
611
    }
612
613
    /**
614
     * Returns a route's link.
615
     *
616
     * @param string $name    A route name.
617
     * @param array  $params  The route parameters.
618
     * @param array  $options Options for generating the proper prefix. Accepted values are:
619
     *                        - `'absolute'` _boolean_: `true` or `false`. - `'scheme'`  
620
     *                        _string_ : The scheme. - `'host'`     _string_ : The host
621
     *                        name. - `'basePath'` _string_ : The base path. - `'query'`   
622
     *                        _string_ : The query string. - `'fragment'` _string_ : The
623
     *                        fragment string.
624
     *
625
     * @return string          The link.
626
     */
627
    public function link(string $name, array $params = [], array $options = []): string
628
    {
629
        $defaults = [
630
            'basePath' => $this->getBasePath()
631
        ];
632
        $options += $defaults;
633
634
        $params += $this->_defaults;
635
636
        if (!isset($this[$name])) {
637
            throw new RouterException("No binded route defined for `'{$name}'`, bind it first with `bind()`.");
638
        }
639
        $route = $this[$name];
640
641
        return $route->link($params, $options);
642
    }
643
644
    /**
645
     * Clears the router.
646
     */
647
    public function clear()
648
    {
649
        $this->_basePath = '';
650
        $this->_strategies = [];
651
        $this->_defaults = [];
652
        $this->_routes = [];
653
        $scope = $this->_classes['scope'];
654
        $this->_scopes = [new $scope(['router' => $this])];
655
    }
656
657
    /**
658
     * Return the current element
659
     *
660
     * @link   https://php.net/manual/en/iterator.current.php
661
     * @return mixed Can return any type.
662
     * @since  5.0.0
663
     */
664
    public function current()
665
    {
666
        return current($this->_data);
667
    }
668
669
    /**
670
     * Move forward to next element
671
     *
672
     * @link   https://php.net/manual/en/iterator.next.php
673
     * @return void Any returned value is ignored.
674
     * @since  5.0.0
675
     */
676
    public function next()
677
    {
678
        $value = $this->_skipNext ? current($this->_data) : next($this->_data);
679
        $this->_skipNext = false;
680
681
        key($this->_data) !== null ? $value : null;
682
    }
683
684
    /**
685
     * Return the key of the current element
686
     *
687
     * @link   https://php.net/manual/en/iterator.key.php
688
     * @return mixed scalar on success, or null on failure.
689
     * @since  5.0.0
690
     */
691
    public function key()
692
    {
693
        return array_keys($this->_data);
694
    }
695
696
    /**
697
     * Checks if current position is valid
698
     *
699
     * @link   https://php.net/manual/en/iterator.valid.php
700
     * @return bool The return value will be casted to boolean and then evaluated.
701
     * Returns true on success or false on failure.
702
     * @since  5.0.0
703
     */
704
    public function valid()
705
    {
706
        return key($this->_data) !== null;
707
    }
708
709
    /**
710
     * Rewind the Iterator to the first element
711
     *
712
     * @link   https://php.net/manual/en/iterator.rewind.php
713
     * @return void Any returned value is ignored.
714
     * @since  5.0.0
715
     */
716
    public function rewind()
717
    {
718
        $this->_skipNext = false;
719
720
        reset($this->_data);
721
    }
722
723
    /**
724
     * Whether a offset exists
725
     *
726
     * @link   https://php.net/manual/en/arrayaccess.offsetexists.php
727
     * @param  mixed $offset <p>
728
     *                       An offset to check for.
729
     *                       </p>
730
     * @return bool true on success or false on failure.
731
     * </p>
732
     * <p>
733
     * The return value will be casted to boolean if non-boolean was returned.
734
     * @since  5.0.0
735
     */
736
    public function offsetExists($offset)
737
    {
738
        return array_key_exists($offset, $this->_data);
739
    }
740
741
    /**
742
     * Offset to retrieve
743
     *
744
     * @link   https://php.net/manual/en/arrayaccess.offsetget.php
745
     * @param  mixed $offset <p>
746
     *                       The offset to retrieve.
747
     *                       </p>
748
     * @return mixed Can return all value types.
749
     * @since  5.0.0
750
     */
751
    public function offsetGet($offset)
752
    {
753
        return $this->_data[$offset];
754
    }
755
756
    /**
757
     * Offset to set
758
     *
759
     * @link  https://php.net/manual/en/arrayaccess.offsetset.php
760
     * @param mixed $offset <p>
761
     *                      The offset to assign the value to.
762
     *                      </p>
763
     * @param mixed $value  <p>
764
     *                      The
765
     *                      value
766
     *                      to
767
     *                      set.
768
     *                      </p>
769
     *
770
     * @return void
771
     * @since  5.0.0
772
     */
773
    public function offsetSet($offset, $value)
774
    {
775
        if (is_null($offset)) {
776
            $this->_data[] = $value;
777
            return;
778
        }
779
780
        $this->_data[$offset] = $value;
781
    }
782
783
    /**
784
     * Offset to unset
785
     *
786
     * @link   https://php.net/manual/en/arrayaccess.offsetunset.php
787
     * @param  mixed $offset <p>
788
     *                       The offset to unset.
789
     *                       </p>
790
     * @return void
791
     * @since  5.0.0
792
     */
793
    public function offsetUnset($offset)
794
    {
795
        $this->_skipNext = $offset === key($this->_data);
796
        unset($this->_data[$offset]);
797
    }
798
799
    /**
800
     * Count elements of an object
801
     *
802
     * @link   https://php.net/manual/en/countable.count.php
803
     * @return int The custom count as an integer.
804
     * </p>
805
     * <p>
806
     * The return value is cast to an integer.
807
     * @since  5.1.0
808
     */
809
    /**
810
     * Counts the items of the object.
811
     *
812
     * @return integer Returns the number of items in the collection.
813
     */
814
    public function count()
815
    {
816
        return count($this->_data);
817
    }
818
}
819