Router::bind()   F
last analyzed

Complexity

Conditions 14
Paths 524

Size

Total Lines 61
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 14.1132

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 35
c 8
b 0
f 0
dl 0
loc 61
ccs 22
cts 24
cp 0.9167
rs 2.7611
cc 14
nc 524
nop 3
crap 14.1132

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * Defaults parameters to use when generating URLs in a dispatching context.
81
     *
82
     * @var array
83
     */
84
    protected $_defaults = [];
85
86
    /**
87
     * Default handler
88
     *
89
     * @var callable|null
90
     */
91
    protected $defaultHandler = null;
92
93
    /**
94
     * Constructor
95
     *
96
     * @param array $config
97
     */
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
                'route'      => 'Lead\Router\Route',
109
                'scope'      => 'Lead\Router\Scope'
110
            ]
111 55
        ];
112
113 55
        $config += $defaults;
114 55
        $this->_classes = $config['classes'];
115 55
        $this->_strategies = $config['strategies'];
116 55
        $this->setDefaultHandler($config['defaultHandler']);
117 55
        $this->setBasePath($config['basePath']);
118
119 55
        $scope = $this->_classes['scope'];
120 55
        $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 55
        $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 55
        return $this;
134
    }
135
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 16
        return end($this->_scopes);
144
    }
145
146
    /**
147
     * Pushes a new router scope context.
148
     *
149
     * @param  object $scope A scope instance.
150
     * @return self
151
     */
152
    public function pushScope($scope): RouterInterface
153
    {
154 16
        $this->_scopes[] = $scope;
155
156 16
        return $this;
157
    }
158
159
    /**
160
     * Pops the current router scope context.
161
     *
162
     * @return \Lead\Router\ScopeInterface The popped scope instance.
163
     */
164
    public function popScope(): ScopeInterface
165
    {
166 16
        return array_pop($this->_scopes);
167
    }
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
     */
175
    public function getBasePath(): string
176
    {
177 8
        return $this->_basePath;
178
    }
179
180
    /**
181
     * Sets the base path
182
     *
183
     * @param  string $basePath Base Path
184
     * @return $this
185
     */
186
    public function setBasePath(string $basePath): self
187
    {
188 55
        $basePath = trim($basePath, '/');
189 55
        $this->_basePath = $basePath ? '/' . $basePath : '';
190
191 55
        return $this;
192
    }
193
194
    /**
195
     * Gets/sets the base path of the router.
196
     *
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 1
        return $this->setBasePath($basePath);
208
    }
209
210
    /**
211
     * Adds a route to the router
212
     *
213
     * @param \Lead\Router\RouteInterface $route Route object
214
     * @return \Lead\Router\RouterInterface
215
     */
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...
Unused Code introduced by
The assignment to $pattern is dead and can be removed.
Loading history...
218
         $options['handler'] = $route->getHandler();
219
         $options['scope'] = $route->getScope();
220
221
         $scheme = $options['scheme'];
222
         $host = $options['host'];
223
224
         if (isset($this->_hosts[$scheme][$host])) {
225
             $options['host'] = $this->_hosts[$scheme][$host];
226
         }
227
228
         $patternKey = md5($options['pattern'] . '-' . $options['name']);
229
230
         if (isset($this->_pattern[$scheme][$host][$patternKey])) {
231
             $route = $this->_pattern[$scheme][$host][$patternKey];
232
         } else {
233
             $this->_hosts[$scheme][$host] = $route->getHost();
234
         }
235
236
         if (!isset($this->_pattern[$scheme][$host][$patternKey])) {
237
             $this->_pattern[$scheme][$host][$patternKey] = $route;
238
         }
239
240
         $methods = $route->getMethods();
241
         foreach ($methods as $method) {
242
             $this->_routes[$scheme][$host][strtoupper($method)][] = $route;
243
         }
244
245
         $this->_data[$route->getName()] = $route;
246
247
         return $this;
248
    }
249
250
    /**
251
     * Adds a route.
252
     *
253
     * @param  string|array  $pattern The route's pattern.
254
     * @param  Closure|array $options An array of options or the callback handler.
255
     * @param  Closure|null  $handler The callback handler.
256
     * @return self
257
     */
258
    public function bind($pattern, $options = [], $handler = null): RouteInterface
259
    {
260
        if (!is_array($options)) {
261 18
            $handler = $options;
262 18
            $options = [];
263
        }
264
265
        if (empty($handler) && !empty($this->_defaultHandler)) {
266
            $handler = $this->_defaultHandler;
267
        }
268
269
        if ($handler !== null) {
270
            if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
271 1
                throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
272
            }
273
        }
274
275
        if (isset($options['method'])) {
276 1
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
277
        }
278
279 45
        $scope = end($this->_scopes);
280 45
        $options = $scope->scopify($options);
281 45
        $options['pattern'] = $pattern;
282 45
        $options['handler'] = $handler;
283 45
        $options['scope'] = $scope;
284
285 45
        $scheme = $options['scheme'];
286 45
        $host = $options['host'];
287
288
        if (isset($this->_hosts[$scheme][$host])) {
289
            $options['host'] = $this->_hosts[$scheme][$host];
290
        }
291
292 45
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
293
294
        if (isset($this->_pattern[$scheme][$host][$patternKey])) {
295 2
            $instance = $this->_pattern[$scheme][$host][$patternKey];
296
        } else {
297 45
            $route = $this->_classes['route'];
298 45
            $instance = new $route($options);
299 45
            $this->_hosts[$scheme][$host] = $instance->getHost();
300
        }
301
302
        if (!isset($this->_pattern[$scheme][$host][$patternKey])) {
303 45
            $this->_pattern[$scheme][$host][$patternKey] = $instance;
304
        }
305
306 45
        $methods = $options['methods'] ? (array)$options['methods'] : [];
307
308 45
        $instance->allow($methods);
309
310
        foreach ($methods as $method) {
311 45
            $this->_routes[$scheme][$host][strtoupper($method)][] = $instance;
312
        }
313
314
        if (isset($options['name'])) {
315 45
            $this->_data[$options['name']] = $instance;
316
        }
317
318 45
        return $instance;
319
    }
320
321
    /**
322
     * Groups some routes inside a new scope.
323
     *
324
     * @param  string|array $prefix  The group's prefix pattern or the options array.
325
     * @param  Closure|array $options An array of options or the callback handler.
326
     * @param  Closure|null  $handler The callback handler.
327
     * @return \Lead\Router\ScopeInterface The newly created scope instance.
328
     */
329
    public function group($prefix, $options, $handler = null)
330
    {
331
        if (!is_array($options)) {
332 11
            $handler = $options;
333
            if (is_string($prefix)) {
334 10
                $options = [];
335
            } else {
336 1
                $options = $prefix;
337 1
                $prefix = '';
338
            }
339
        }
340
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
341 1
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
342
        }
343
344 16
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
345
346 16
        $scope = $this->scope();
347
348 16
        $this->pushScope($scope->seed($options));
349
350 16
        $handler($this);
351
352 16
        return $this->popScope();
353
    }
354
355
    /**
356
     * Gets information required for routing from a server request
357
     *
358
     * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
359
     * @return array
360
     */
361
    protected function _getRequestInformation(ServerRequestInterface $request): array
362
    {
363 1
        $uri = $request->getUri();
364
365
        if (method_exists($request, 'basePath')) {
366
            $this->setBasePath($request->basePath());
367
        }
368
369
        return [
370
            'scheme' => $uri->getScheme(),
371
            'host' => $uri->getHost(),
372
            'method' => $request->getMethod(),
373
            'path' => $uri->getPath()
374 1
        ];
375
    }
376
377
    /**
378
     * Routes a Request.
379
     *
380
     * @param mixed $request The request to route.
381
     * @return \Lead\Router\RouteInterface A route matching the request or a "route not found" route.
382
     */
383
    public function route($request): RouteInterface
384
    {
385
        $defaults = [
386
            'path' => '',
387
            'method' => 'GET',
388
            'host' => '*',
389
            'scheme' => '*'
390 40
        ];
391
392 40
        $this->_defaults = [];
393
394
        if ($request instanceof ServerRequestInterface) {
395 1
            $r = $this->_getRequestInformation($request);
396
        } elseif (!is_array($request)) {
397 38
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
398
        } else {
399 1
            $r = $request + $defaults;
400
        }
401
402 40
        $r = $this->_normalizeRequest($r);
403
404 40
        $route = $this->_route($r);
405
        if ($route instanceof RouteInterface) {
406 35
            $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...
407
            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

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

589
        return $this->setStrategy($name, /** @scrutinizer ignore-type */ $handler);
Loading history...
590
    }
591
592
    /**
593
     * Adds a route based on a custom HTTP verb.
594
     *
595
     * @param string $name   The HTTP verb to define a route on.
596
     * @param array  $params The route's parameters.
597
     * @return mixed
598
     */
599
    public function __call($name, $params)
600
    {
601
        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

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