Passed
Push — refactor ( 6a891c...eadb4f )
by Florian
01:44
created

Router::key()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 1
c 1
b 1
f 0
dl 0
loc 3
ccs 0
cts 1
cp 0
rs 10
nc 1
nop 0
cc 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Lead\Router;
6
7
use ArrayAccess;
8
use Closure;
9
use Countable;
10
use Iterator;
11
use Lead\Router\Exception\ParserException;
12
use Lead\Router\Exception\RouteNotFoundException;
13
use Lead\Router\Exception\RouterException;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
use RuntimeException;
17
18
/**
19
 * The Router class.
20
 */
21
class Router implements ArrayAccess, Iterator, Countable, RouterInterface
22
{
23
    /**
24
     * @var bool
25
     */
26
    protected $skipNext;
27
28
    /**
29
     * @var array
30
     */
31
    protected $data = [];
32
33
    /**
34
     * @var array
35
     */
36
    protected $pattern = [];
37
38
    /**
39
     * Class dependencies.
40
     *
41
     * @var array
42
     */
43
    protected $classes = [];
44
45
    /**
46
     * Hosts.
47
     *
48
     * @var array
49
     */
50
    protected $hosts = [];
51
52
    /**
53
     * Routes.
54
     *
55
     * @var array
56
     */
57
    protected $routes = [];
58
59
    /**
60
     * Scopes stack.
61
     *
62
     * @var array
63
     */
64
    protected $scopes = [];
65
66
    /**
67
     * Base path.
68
     *
69
     * @var string
70
     */
71
    protected $basePath = '';
72
73
    /**
74
     * Dispatching strategies.
75
     *
76
     * @var array
77
     */
78
    protected $strategies = [];
79
80
    /**
81
     * Defaults parameters to use when generating URLs in a dispatching context.
82
     *
83
     * @var array
84
     */
85
    protected $defaults = [];
86
87
    /**
88
     * Default handler
89
     *
90
     * @var callable|null
91
     */
92
    protected $defaultHandler = null;
93
94
    /**
95
     * Constructor
96
     *
97
     * @param array $config
98
     */
99
    public function __construct($config = [])
100
    {
101
        $defaults = [
102
            'basePath'       => '',
103
            'scope'          => [],
104
            'strategies'     => [],
105
            'defaultHandler' => null,
106
            'classes'        => [
107
                'parser'     => 'Lead\Router\Parser',
108
                'host'       => 'Lead\Router\Host',
109
                'route'      => 'Lead\Router\Route',
110
                'scope'      => 'Lead\Router\Scope'
111
            ]
112 54
        ];
113 54
        $config += $defaults;
114 54
        $this->classes = $config['classes'];
115 54
        $this->strategies = $config['strategies'];
116 54
        $this->setDefaultHandler($config['defaultHandler']);
117 54
        $this->setBasePath($config['basePath']);
118 54
        $scope = $this->classes['scope'];
119 54
        $this->scopes[] = new $scope(['router' => $this]);
120
    }
121
122
    /**
123
     * Sets the default handler for routes
124
     *
125
     * @param mixed $handler
126
     * @return $this
127
     */
128
    public function setDefaultHandler($handler): RouterInterface
129
    {
130 54
        $this->defaultHandler = $handler;
131
132 54
        return $this;
133
    }
134
135
    /**
136
     * Returns the current router scope.
137
     *
138
     * @return \Lead\Router\ScopeInterface The current scope instance.
139
     */
140
    public function scope(): ScopeInterface
141
    {
142 16
        return end($this->scopes);
143
    }
144
145
    /**
146
     * Pushes a new router scope context.
147
     *
148
     * @param \Lead\Router\ScopeInterface $scope A scope instance.
149
     * @return self
150
     */
151
    public function pushScope(ScopeInterface $scope): RouterInterface
152
    {
153 16
        $this->scopes[] = $scope;
154 16
        return $this;
155
    }
156
157
    /**
158
     * Pops the current router scope context.
159
     *
160
     * @return \Lead\Router\ScopeInterface The popped scope instance.
161
     */
162
    public function popScope(): ScopeInterface
163
    {
164 16
        return array_pop($this->scopes);
165
    }
166
167
    /**
168
     * Gets the base path
169
     *
170
     * @return string
171
     */
172
    public function getBasePath(): string
173
    {
174 8
        return $this->basePath;
175
    }
176
177
    /**
178
     * Sets the base path
179
     *
180
     * @param  string $basePath Base Path
181
     * @return $this
182
     */
183
    public function setBasePath(string $basePath): self
184
    {
185 54
        $basePath = trim($basePath, '/');
186 54
        $this->basePath = $basePath ? '/' . $basePath : '';
187 54
        return $this;
188
    }
189
190
    /**
191
     * Gets/sets the base path of the router.
192
     *
193
     * @deprecated Use setBasePath() and getBasePath() instead
194
     * @param      string|null $basePath The base path to set or none to get the setted one.
195
     * @return     string|self
196
     */
197
    public function basePath(?string $basePath = null)
198
    {
199
        if ($basePath === null) {
200
            return $this->basePath;
201
        }
202
203 1
        return $this->setBasePath($basePath);
204
    }
205
206
    /**
207
     * Adds a route to the router
208
     *
209
     * @param \Lead\Router\RouteInterface $route Route object
210
     * @return \Lead\Router\RouterInterface
211
     */
212
    public function addRoute(RouteInterface $route): RouterInterface
213
    {
214
        $options['pattern'] = $pattern = $route->pattern();
0 ignored issues
show
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...
215
        $options['handler'] = $route->handler();
216
        $options['scope'] = $route->scope();
217
        $scheme = $options['scheme'];
218
        $host = $options['host'];
219
        if (isset($this->hosts[$scheme][$host])) {
220
            $options['host'] = $this->hosts[$scheme][$host];
221
        }
222
223
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
224
        if (isset($this->pattern[$scheme][$host][$patternKey])) {
225
            $route = $this->pattern[$scheme][$host][$patternKey];
226
        } else {
227
            $this->hosts[$scheme][$host] = $route->host();
228
        }
229
230
        if (!isset($this->pattern[$scheme][$host][$patternKey])) {
231
            $this->pattern[$scheme][$host][$patternKey] = $route;
232
        }
233
234
        $methods = $route->methods();
235
        foreach ($methods as $method) {
236
            $this->routes[$scheme][$host][strtoupper($method)][] = $route;
237
        }
238
239
        $this->data[$route->name()] = $route;
240
        return $this;
241
    }
242
243
    /**
244
     * Adds a route.
245
     *
246
     * @param  string|array  $pattern The route's pattern.
247
     * @param  Closure|array $options An array of options or the callback handler.
248
     * @param  Closure|null  $handler The callback handler.
249
     * @return \Lead\Router\RouteInterface
250
     */
251
    public function bind($pattern, $options = [], $handler = null): RouteInterface
252
    {
253
        if (!is_array($options)) {
254 18
            $handler = $options;
255 18
            $options = [];
256
        }
257
258
        if (empty($handler) && !empty($this->_defaultHandler)) {
0 ignored issues
show
Bug introduced by
The property _defaultHandler does not exist on Lead\Router\Router. Did you mean defaultHandler?
Loading history...
259
            $handler = $this->_defaultHandler;
260
        }
261
262
        if ($handler !== null) {
263
            if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
264 1
                throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
265
            }
266
        }
267
268
        if (isset($options['method'])) {
269 1
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
270
        }
271
272 45
        $scope = end($this->scopes);
273 45
        $options = $scope->scopify($options);
274 45
        $options['pattern'] = $pattern;
275 45
        $options['handler'] = $handler;
276 45
        $options['scope'] = $scope;
277 45
        $scheme = $options['scheme'];
278 45
        $host = $options['host'];
279
        if (isset($this->hosts[$scheme][$host])) {
280
            $options['host'] = $this->hosts[$scheme][$host];
281
        }
282
283 45
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
284
        if (isset($this->pattern[$scheme][$host][$patternKey])) {
285 2
            $instance = $this->pattern[$scheme][$host][$patternKey];
286
        } else {
287 45
            $route = $this->classes['route'];
288 45
            $instance = new $route($options);
289 45
            $this->hosts[$scheme][$host] = $instance->host();
290
        }
291
292
        if (!isset($this->pattern[$scheme][$host][$patternKey])) {
293 45
            $this->pattern[$scheme][$host][$patternKey] = $instance;
294
        }
295
296 45
        $methods = $options['methods'] ? (array)$options['methods'] : [];
297 45
        $instance->allow($methods);
298
        foreach ($methods as $method) {
299 45
            $this->routes[$scheme][$host][strtoupper($method)][] = $instance;
300
        }
301
302
        if (isset($options['name'])) {
303 45
            $this->data[$options['name']] = $instance;
304
        }
305
306 45
        return $instance;
307
    }
308
309
    /**
310
     * Groups some routes inside a new scope.
311
     *
312
     * @param  string|array $prefix  The group's prefix pattern or the options array.
313
     * @param  Closure|array $options An array of options or the callback handler.
314
     * @param  Closure|null  $handler The callback handler.
315
     * @return \Lead\Router\ScopeInterface The newly created scope instance.
316
     */
317
    public function group($prefix, $options, $handler = null)
318
    {
319
        if (!is_array($options)) {
320 11
            $handler = $options;
321
            if (is_string($prefix)) {
322 10
                $options = [];
323
            } else {
324 1
                $options = $prefix;
325 1
                $prefix = '';
326
            }
327
        }
328
329
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
330 1
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
331
        }
332
333 16
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
334 16
        $scope = $this->scope();
335 16
        $this->pushScope($scope->seed($options));
336 16
        $handler($this);
337
338 16
        return $this->popScope();
339
    }
340
341
    /**
342
     * Gets information required for routing from a server request
343
     *
344
     * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
345
     * @return array
346
     */
347
    protected function getRequestInformation(ServerRequestInterface $request): array
348
    {
349 1
        $uri = $request->getUri();
350
        if (method_exists($request, 'basePath')) {
351
            $this->setBasePath($request->basePath());
352
        }
353
354
        return [
355
            'scheme' => $uri->getScheme(),
356
            'host' => $uri->getHost(),
357
            'method' => $request->getMethod(),
358
            'path' => $uri->getPath()
359 1
        ];
360
    }
361
362
    /**
363
     * Routes a Request.
364
     *
365
     * @param mixed $request The request to route.
366
     * @return \Lead\Router\RouteInterface A route matching the request or a "route not found" route.
367
     */
368
    public function route($request): RouteInterface
369
    {
370
        $defaults = [
371
            'path' => '',
372
            'method' => 'GET',
373
            'host' => '*',
374
            'scheme' => '*'
375 40
        ];
376 40
        $this->defaults = [];
377
        if ($request instanceof ServerRequestInterface) {
378 1
            $r = $this->getRequestInformation($request);
379
        } elseif (!is_array($request)) {
380 38
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
381
        } else {
382 1
            $r = $request + $defaults;
383
        }
384
385 40
        $r = $this->normalizeRequest($r);
386 40
        $route = $this->_route($r);
387
        if ($route instanceof RouteInterface) {
388 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...
389
            foreach ($route->persistentParams() as $key) {
390 2
                $routeParams = $route->params();
391
                if (isset($routeParams[$key])) {
392 2
                    $this->defaults[$key] = $routeParams[$key];
393
                }
394
            }
395
396 35
            return $route;
397
        }
398
399 13
        $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
400 13
        throw new RouteNotFoundException($message);
401
    }
402
403
    /**
404
     * Normalizes a request.
405
     *
406
     * @param  array $request The request to normalize.
407
     * @return array          The normalized request.
408
     */
409
    protected function normalizeRequest(array $request): array
410
    {
411
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
412 5
            $parsed = array_intersect_key(parse_url($request['path']), $request);
413 5
            $request = $parsed + $request;
414
        }
415
416 40
        $request['path'] = (ltrim((string)strtok($request['path'], '?'), '/'));
417 40
        $request['method'] = strtoupper($request['method']);
418
419 40
        return $request;
420
    }
421
422
    /**
423
     * Routes a request.
424
     *
425
     * @param array $request The request to route.
426
     * @return null|\Lead\Router\RouteInterface
427
     */
428
    protected function _route(array $request): ?RouteInterface
429
    {
430 40
        $path = $request['path'];
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
431 40
        $httpMethod = $request['method'];
432 40
        $host = $request['host'];
0 ignored issues
show
Unused Code introduced by
The assignment to $host is dead and can be removed.
Loading history...
433 40
        $scheme = $request['scheme'];
434 40
        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
435 40
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);
436
        if ($httpMethod === 'HEAD') {
437 3
            $allowedMethods += ['GET' => 'GET'];
438
        }
439
440
        foreach ($this->routes as $scheme => $hostBasedRoutes) {
441
            if (!isset($allowedSchemes[$scheme])) {
442 1
                continue;
443
            }
444
445
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
446
                foreach ($methodBasedRoutes as $method => $routes) {
447
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
448 4
                        continue;
449
                    }
450
451
                    foreach ($routes as $route) {
452
                        /* @var $route \Lead\Router\RouteInterface */
453
                        if (!$route->match($request, $variables, $hostVariables)) {
454
                            if ($hostVariables === null) {
455 3
                                continue 3;
456
                            }
457 10
                            continue;
458
                        }
459
460 39
                        return $route;
461
                    }
462
                }
463
            }
464
        }
465
466 13
        return null;
467
    }
468
469
    /**
470
     * Middleware generator.
471
     *
472
     * @return callable
473
     */
474
    public function middleware()
475
    {
476
        foreach ($this->scopes[0]->middleware() as $middleware) {
477 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...
478
        }
479
    }
480
481
    /**
482
     * Adds a middleware to the list of middleware.
483
     *
484
     * @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...
485
     * @return $this
486
     */
487
    public function apply($middleware)
488
    {
489
        foreach (func_get_args() as $mw) {
490 3
            $this->scopes[0]->apply($mw);
491
        }
492
493 3
        return $this;
494
    }
495
496
    /**
497
     * Sets a dispatcher strategy
498
     *
499
     * @param  string   $name    Name
500
     * @param  callable $handler Handler
501
     * @return $this
502
     */
503
    public function setStrategy(string $name, callable $handler)
504
    {
505 2
        $this->strategies[$name] = $handler;
506
507 2
        return $this;
508
    }
509
510
    /**
511
     * @param string $name
512
     * @return bool
513
     */
514
    public function hasStrategy(string $name): bool
515
    {
516
        return isset($this->strategies[$name]);
517
    }
518
519
    /**
520
     * Get a strategy
521
     *
522
     * @return callable
523
     */
524
    public function getStrategy(string $name): callable
525
    {
526
        if (isset($this->strategies[$name])) {
527 2
            return $this->strategies[$name];
528
        }
529
530 1
        throw new RuntimeException(sprintf('Strategy `%s` not found.', $name));
531
    }
532
533
    /**
534
     * Unsets a strategy
535
     *
536
     * @param  string $name
537
     * @return $this
538
     */
539
    public function unsetStrategy(string $name)
540
    {
541
        if (isset($this->strategies[$name])) {
542 1
            unset($this->strategies[$name]);
543
        }
544
545 1
        return $this;
546
    }
547
548
    /**
549
     * Gets/sets router's strategies.
550
     *
551
     * @deprecated Use setStrategy(), unsetStrategy() and getStrategy()
552
     * @param      string $name    A routing strategy name.
553
     * @param      mixed  $handler The strategy handler or none to get the setted one.
554
     * @return     mixed           The strategy handler (or `null` if not found) on get or `$this` on set.
555
     */
556
    public function strategy($name, $handler = null)
557
    {
558
        if (func_num_args() === 1) {
559
            try {
560 2
                return $this->getStrategy($name);
561
            } catch (RuntimeException $e) {
562 1
                return null;
563
            }
564
        }
565
566
        if ($handler === false) {
567
            try {
568
                return $this->unsetStrategy($name);
569
            } catch (RuntimeException $e) {
570
                return null;
571
            }
572
        }
573
574
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
575
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
576
        }
577
578
        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

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