Issues (101)

src/Router.php (9 issues)

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
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...
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
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
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
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'];
448 40
        $httpMethod = $request['method'];
449 40
        $host = $request['host'];
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
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
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)) {
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