Passed
Push — refactor ( eadb4f...8f74b7 )
by Florian
02:10 queued 11s
created

Router::setDefaultHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
nc 1
nop 1
cc 1
crap 1
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 53
        ];
113 53
        $config += $defaults;
114 53
        $this->classes = $config['classes'];
115 53
        $this->strategies = $config['strategies'];
116 53
        $this->setDefaultHandler($config['defaultHandler']);
117 53
        $this->setBasePath($config['basePath']);
118 53
        $scope = $this->classes['scope'];
119 53
        $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 53
        $this->defaultHandler = $handler;
131
132 53
        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 53
        $basePath = trim($basePath, '/');
186 53
        $this->basePath = $basePath ? '/' . $basePath : '';
187 53
        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 17
            $handler = $options;
255 17
            $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 (isset($options['method'])) {
263 1
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
264
        }
265
266 45
        $scope = end($this->scopes);
267 45
        $options = $scope->scopify($options);
268 45
        $options['pattern'] = $pattern;
269 45
        $options['handler'] = $handler;
270 45
        $options['scope'] = $scope;
271 45
        $scheme = $options['scheme'];
272 45
        $host = $options['host'];
273
        if (isset($this->hosts[$scheme][$host])) {
274
            $options['host'] = $this->hosts[$scheme][$host];
275
        }
276
277 45
        $patternKey = md5($options['pattern'] . '-' . $options['name']);
278
        if (isset($this->pattern[$scheme][$host][$patternKey])) {
279 2
            $instance = $this->pattern[$scheme][$host][$patternKey];
280
        } else {
281 45
            $route = $this->classes['route'];
282 45
            $instance = new $route($options);
283 45
            $this->hosts[$scheme][$host] = $instance->host();
284
        }
285
286
        if (!isset($this->pattern[$scheme][$host][$patternKey])) {
287 45
            $this->pattern[$scheme][$host][$patternKey] = $instance;
288
        }
289
290 45
        $methods = $options['methods'] ? (array)$options['methods'] : [];
291 45
        $instance->allow($methods);
292
        foreach ($methods as $method) {
293 45
            $this->routes[$scheme][$host][strtoupper($method)][] = $instance;
294
        }
295
296
        if (isset($options['name'])) {
297 45
            $this->data[$options['name']] = $instance;
298
        }
299
300 45
        return $instance;
301
    }
302
303
    /**
304
     * Groups some routes inside a new scope.
305
     *
306
     * @param  string|array $prefix  The group's prefix pattern or the options array.
307
     * @param  Closure|array $options An array of options or the callback handler.
308
     * @param  Closure|null  $handler The callback handler.
309
     * @return \Lead\Router\ScopeInterface The newly created scope instance.
310
     */
311
    public function group($prefix, $options, $handler = null)
312
    {
313
        if (!is_array($options)) {
314 11
            $handler = $options;
315
            if (is_string($prefix)) {
316 10
                $options = [];
317
            } else {
318 1
                $options = $prefix;
319 1
                $prefix = '';
320
            }
321
        }
322
323
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
324 1
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
325
        }
326
327 16
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
328 16
        $scope = $this->scope();
329 16
        $this->pushScope($scope->seed($options));
330 16
        $handler($this);
331
332 16
        return $this->popScope();
333
    }
334
335
    /**
336
     * Gets information required for routing from a server request
337
     *
338
     * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
339
     * @return array
340
     */
341
    protected function getRequestInformation(ServerRequestInterface $request): array
342
    {
343 1
        $uri = $request->getUri();
344
        if (method_exists($request, 'basePath')) {
345
            $this->setBasePath($request->basePath());
346
        }
347
348
        return [
349
            'scheme' => $uri->getScheme(),
350
            'host' => $uri->getHost(),
351
            'method' => $request->getMethod(),
352
            'path' => $uri->getPath()
353 1
        ];
354
    }
355
356
    /**
357
     * Routes a Request.
358
     *
359
     * @param mixed $request The request to route.
360
     * @return \Lead\Router\RouteInterface A route matching the request or a "route not found" route.
361
     */
362
    public function route($request): RouteInterface
363
    {
364
        $defaults = [
365
            'path' => '',
366
            'method' => 'GET',
367
            'host' => '*',
368
            'scheme' => '*'
369 40
        ];
370 40
        $this->defaults = [];
371
        if ($request instanceof ServerRequestInterface) {
372 1
            $r = $this->getRequestInformation($request);
373
        } elseif (!is_array($request)) {
374 38
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
375
        } else {
376 1
            $r = $request + $defaults;
377
        }
378
379 40
        $r = $this->normalizeRequest($r);
380 40
        $route = $this->_route($r);
381
        if ($route instanceof RouteInterface) {
382 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...
383
            foreach ($route->persistentParams() as $key) {
384 2
                $routeParams = $route->params();
385
                if (isset($routeParams[$key])) {
386 2
                    $this->defaults[$key] = $routeParams[$key];
387
                }
388
            }
389
390 35
            return $route;
391
        }
392
393 13
        $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
394
395 13
        throw new RouteNotFoundException($message);
396
    }
397
398
    /**
399
     * Normalizes a request.
400
     *
401
     * @param  array $request The request to normalize.
402
     * @return array          The normalized request.
403
     */
404
    protected function normalizeRequest(array $request): array
405
    {
406
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
407 5
            $parsed = array_intersect_key(parse_url($request['path']), $request);
408 5
            $request = $parsed + $request;
409
        }
410
411 40
        $request['path'] = (ltrim((string)strtok($request['path'], '?'), '/'));
412 40
        $request['method'] = strtoupper($request['method']);
413
414 40
        return $request;
415
    }
416
417
    /**
418
     * Routes a request.
419
     *
420
     * @param array $request The request to route.
421
     * @return null|\Lead\Router\RouteInterface
422
     */
423
    protected function _route(array $request): ?RouteInterface
424
    {
425 40
        $path = $request['path'];
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
426 40
        $httpMethod = $request['method'];
427 40
        $host = $request['host'];
0 ignored issues
show
Unused Code introduced by
The assignment to $host is dead and can be removed.
Loading history...
428 40
        $scheme = $request['scheme'];
429 40
        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
430 40
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);
431
        if ($httpMethod === 'HEAD') {
432 3
            $allowedMethods += ['GET' => 'GET'];
433
        }
434
435
        foreach ($this->routes as $scheme => $hostBasedRoutes) {
436
            if (!isset($allowedSchemes[$scheme])) {
437 1
                continue;
438
            }
439
440
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
441
                foreach ($methodBasedRoutes as $method => $routes) {
442
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
443 4
                        continue;
444
                    }
445
446
                    foreach ($routes as $route) {
447
                        /* @var $route \Lead\Router\RouteInterface */
448
                        if (!$route->match($request, $variables, $hostVariables)) {
449
                            if ($hostVariables === null) {
450 3
                                continue 3;
451
                            }
452 10
                            continue;
453
                        }
454
455 39
                        return $route;
456
                    }
457
                }
458
            }
459
        }
460
461 13
        return null;
462
    }
463
464
    /**
465
     * Middleware generator.
466
     *
467
     * @return callable
468
     */
469
    public function middleware()
470
    {
471
        foreach ($this->scopes[0]->middleware() as $middleware) {
472 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...
473
        }
474
    }
475
476
    /**
477
     * Adds a middleware to the list of middleware.
478
     *
479
     * @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...
480
     * @return $this
481
     */
482
    public function apply($middleware)
483
    {
484
        foreach (func_get_args() as $mw) {
485 3
            $this->scopes[0]->apply($mw);
486
        }
487
488 3
        return $this;
489
    }
490
491
    /**
492
     * Sets a dispatcher strategy
493
     *
494
     * @param  string   $name    Name
495
     * @param  callable $handler Handler
496
     * @return $this
497
     */
498
    public function setStrategy(string $name, callable $handler)
499
    {
500 2
        $this->strategies[$name] = $handler;
501
502 2
        return $this;
503
    }
504
505
    /**
506
     * @param string $name
507
     * @return bool
508
     */
509
    public function hasStrategy(string $name): bool
510
    {
511
        return isset($this->strategies[$name]);
512
    }
513
514
    /**
515
     * Get a strategy
516
     *
517
     * @return callable
518
     */
519
    public function getStrategy(string $name): callable
520
    {
521
        if (isset($this->strategies[$name])) {
522 2
            return $this->strategies[$name];
523
        }
524
525 1
        throw new RuntimeException(sprintf('Strategy `%s` not found.', $name));
526
    }
527
528
    /**
529
     * Unsets a strategy
530
     *
531
     * @param  string $name
532
     * @return $this
533
     */
534
    public function unsetStrategy(string $name)
535
    {
536
        if (isset($this->strategies[$name])) {
537 1
            unset($this->strategies[$name]);
538
        }
539
540 1
        return $this;
541
    }
542
543
    /**
544
     * Gets/sets router's strategies.
545
     *
546
     * @deprecated Use setStrategy(), unsetStrategy() and getStrategy()
547
     * @param      string $name    A routing strategy name.
548
     * @param      mixed  $handler The strategy handler or none to get the setted one.
549
     * @return     mixed           The strategy handler (or `null` if not found) on get or `$this` on set.
550
     */
551
    public function strategy($name, $handler = null)
552
    {
553
        if (func_num_args() === 1) {
554
            try {
555 2
                return $this->getStrategy($name);
556
            } catch (RuntimeException $e) {
557 1
                return null;
558
            }
559
        }
560
561
        if ($handler === false) {
562
            try {
563
                return $this->unsetStrategy($name);
564
            } catch (RuntimeException $e) {
565
                return null;
566
            }
567
        }
568
569
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
570
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
571
        }
572
573
        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

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