Passed
Push — refactor ( 885968...d87829 )
by Florian
02:43
created

Router::basePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 3
c 3
b 1
f 0
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 10
cc 2
nc 2
nop 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
     * @var string
96
     */
97
    protected $parserClass = Parser::class;
98
99
    /**
100
     * @var string
101
     */
102
    protected $hostClass = Host::class;
103
104
    /**
105
     * @var string
106
     */
107
    protected $routeClass = Route::class;
108
109
    /**
110
     * @var string
111
     */
112
    protected $scopeClass = Scope::class;
113
114
    /**
115
     * Constructor
116
     *
117
     * @param array $config
118
     */
119
    public function __construct($config = [])
120
    {
121
        $defaults = [
122
            'basePath'       => '',
123
            'scope'          => [],
124
            'strategies'     => [],
125
            'defaultHandler' => null,
126
            'classes'        => [
127
                'parser'     => $this->parserClass,
128
                'host'       => $this->hostClass,
129
                'route'      => $this->routeClass,
130
                'scope'      => $this->scopeClass
131
            ]
132 53
        ];
133 53
        $config += $defaults;
134
135 53
        $this->parserClass = $config['classes']['parser'];
136 53
        $this->hostClass = $config['classes']['host'];
137 53
        $this->routeClass = $config['classes']['route'];
138 53
        $this->scopeClass = $config['classes']['scope'];
139
140 53
        $this->strategies = $config['strategies'];
141 53
        $this->setDefaultHandler($config['defaultHandler']);
142 53
        $this->setBasePath($config['basePath']);
143
144 53
        $scope = $this->scopeClass;
145 53
        $this->scopes[] = new $scope(['router' => $this]);
146
    }
147
148
    /**
149
     * Sets the default handler for routes
150
     *
151
     * @param mixed $handler
152
     * @return $this
153
     */
154
    public function setDefaultHandler($handler): RouterInterface
155
    {
156 53
        $this->defaultHandler = $handler;
157
158 53
        return $this;
159
    }
160
161
    /**
162
     * Returns the current router scope.
163
     *
164
     * @return \Lead\Router\ScopeInterface The current scope instance.
165
     */
166
    public function scope(): ScopeInterface
167
    {
168 16
        return end($this->scopes);
169
    }
170
171
    /**
172
     * Pushes a new router scope context.
173
     *
174
     * @param \Lead\Router\ScopeInterface $scope A scope instance.
175
     * @return self
176
     */
177
    public function pushScope(ScopeInterface $scope): RouterInterface
178
    {
179 16
        $this->scopes[] = $scope;
180 16
        return $this;
181
    }
182
183
    /**
184
     * Pops the current router scope context.
185
     *
186
     * @return \Lead\Router\ScopeInterface The popped scope instance.
187
     */
188
    public function popScope(): ScopeInterface
189
    {
190 16
        return array_pop($this->scopes);
191
    }
192
193
    /**
194
     * Gets the base path
195
     *
196
     * @return string
197
     */
198
    public function getBasePath(): string
199
    {
200 5
        return $this->basePath;
201
    }
202
203
    /**
204
     * Sets the base path
205
     *
206
     * @param  string $basePath Base Path
207
     * @return $this
208
     */
209
    public function setBasePath(string $basePath): self
210
    {
211 53
        $basePath = trim($basePath, '/');
212 53
        $this->basePath = $basePath ? '/' . $basePath : '';
213
214 53
        return $this;
215
    }
216
217
    /**
218
     * Gets/sets the base path of the router.
219
     *
220
     * @deprecated Use setBasePath() and getBasePath() instead
221
     * @param      string|null $basePath The base path to set or none to get the setted one.
222
     * @return     string|self
223
     */
224
    public function basePath(?string $basePath = null)
225
    {
226
        if ($basePath === null) {
227 3
            return $this->basePath;
228
        }
229
230 1
        return $this->setBasePath($basePath);
231
    }
232
233
    /**
234
     * Adds a route to the router
235
     *
236
     * @param \Lead\Router\RouteInterface $route Route object
237
     * @return \Lead\Router\RouterInterface
238
     */
239
    public function addRoute(RouteInterface $route): RouterInterface
240
    {
241
        $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...
242
        $options['handler'] = $route->handler();
243
        $options['scope'] = $route->scope();
244
        $scheme = $options['scheme'];
245
        $host = $options['host'];
246
        if (isset($this->hosts[$scheme][$host])) {
247
            $options['host'] = $this->hosts[$scheme][$host];
248
        }
249
250
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
251
        if (isset($this->pattern[$scheme][$host][$patternKey])) {
252
            $route = $this->pattern[$scheme][$host][$patternKey];
253
        } else {
254
            $this->hosts[$scheme][$host] = $route->host();
255
        }
256
257
        if (!isset($this->pattern[$scheme][$host][$patternKey])) {
258
            $this->pattern[$scheme][$host][$patternKey] = $route;
259
        }
260
261
        $methods = $route->methods();
262
        foreach ($methods as $method) {
263
            $this->routes[$scheme][$host][strtoupper($method)][] = $route;
264
        }
265
266
        $this->data[$route->name()] = $route;
267
        return $this;
268
    }
269
270
    /**
271
     * Adds a route.
272
     *
273
     * @param  string|array  $pattern The route's pattern.
274
     * @param  Closure|array $options An array of options or the callback handler.
275
     * @param  Closure|null  $handler The callback handler.
276
     * @return \Lead\Router\RouteInterface
277
     */
278
    public function bind($pattern, $options = [], $handler = null): RouteInterface
279
    {
280
        if (!is_array($options)) {
281 17
            $handler = $options;
282 17
            $options = [];
283
        }
284
285
        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...
286
            $handler = $this->_defaultHandler;
287
        }
288
289
        if (isset($options['method'])) {
290 1
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
291
        }
292
293 45
        $scope = end($this->scopes);
294 45
        $options = $scope->scopify($options);
295 45
        $options['pattern'] = $pattern;
296 45
        $options['handler'] = $handler;
297 45
        $options['scope'] = $scope;
298 45
        $scheme = $options['scheme'];
299 45
        $host = $options['host'];
300
        if (isset($this->hosts[$scheme][$host])) {
301
            $options['host'] = $this->hosts[$scheme][$host];
302
        }
303
304 45
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
305
        if (isset($this->pattern[$scheme][$host][$patternKey])) {
306 2
            $instance = $this->pattern[$scheme][$host][$patternKey];
307
        } else {
308 45
            $route = $this->routeClass;
309 45
            $instance = new $route($options);
310 45
            $this->hosts[$scheme][$host] = $instance->host();
311
        }
312
313
        if (!isset($this->pattern[$scheme][$host][$patternKey])) {
314 45
            $this->pattern[$scheme][$host][$patternKey] = $instance;
315
        }
316
317 45
        $methods = $options['methods'] ? (array)$options['methods'] : [];
318 45
        $instance->allow($methods);
319
        foreach ($methods as $method) {
320 45
            $this->routes[$scheme][$host][strtoupper($method)][] = $instance;
321
        }
322
323
        if (isset($options['name'])) {
324 45
            $this->data[$options['name']] = $instance;
325
        }
326
327 45
        return $instance;
328
    }
329
330
    /**
331
     * Groups some routes inside a new scope.
332
     *
333
     * @param  string|array $prefix  The group's prefix pattern or the options array.
334
     * @param  Closure|array $options An array of options or the callback handler.
335
     * @param  Closure|null  $handler The callback handler.
336
     * @return \Lead\Router\ScopeInterface The newly created scope instance.
337
     */
338
    public function group($prefix, $options, $handler = null)
339
    {
340
        if (!is_array($options)) {
341 11
            $handler = $options;
342
            if (is_string($prefix)) {
343 10
                $options = [];
344
            } else {
345 1
                $options = $prefix;
346 1
                $prefix = '';
347
            }
348
        }
349
350
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
351 1
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
352
        }
353
354 16
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
355 16
        $scope = $this->scope();
356 16
        $this->pushScope($scope->seed($options));
357 16
        $handler($this);
358
359 16
        return $this->popScope();
360
    }
361
362
    /**
363
     * Gets information required for routing from a server request
364
     *
365
     * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
366
     * @return array
367
     */
368
    protected function getRequestInformation(ServerRequestInterface $request): array
369
    {
370 1
        $uri = $request->getUri();
371
        if (method_exists($request, 'basePath')) {
372
            $this->setBasePath($request->basePath());
373
        }
374
375
        return [
376
            'scheme' => $uri->scheme(),
0 ignored issues
show
Bug introduced by
The method scheme() does not exist on Psr\Http\Message\UriInterface. ( Ignorable by Annotation )

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

376
            'scheme' => $uri->/** @scrutinizer ignore-call */ scheme(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

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