Passed
Pull Request — 3.x (#211)
by
unknown
05:40 queued 04:07
created

Map::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 *
4
 * This file is part of Aura for PHP.
5
 *
6
 * @license http://opensource.org/licenses/bsd-license.php BSD
7
 *
8
 */
9
namespace Aura\Router;
10
11
use ArrayIterator;
12
use IteratorAggregate;
13
14
/**
15
 *
16
 * A collection of route objects.
17
 *
18
 * @package Aura.Router
19
 *
20
 * @method Route accepts(string|array $accepts)
21
 *
22
 * @method Route allows(string|array $allows)
23
 *
24
 * @method Route attributes(array $attributes)
25
 *
26
 * @method Route auth(mixed $auth)
27
 *
28
 * @method Route defaults(array $defaults)
29
 *
30
 * @method Route extras(array $extras)
31
 *
32
 * @method Route failedRule(mixed $failedRule)
33
 *
34
 * @method Route handler(mixed $handler)
35
 *
36
 * @method Route host(mixed $host)
37
 *
38
 * @method Route isRoutable(bool $isRoutable = true)
39
 *
40
 * @method Route namePrefix(string $namePrefix)
41
 *
42
 * @method Route path(string $path)
43
 *
44
 * @method Route pathPrefix(string $pathPrefix)
45
 *
46
 * @method Route secure(bool|null $secure = true)
47
 *
48
 * @method Route special(callable|null $host)
49
 *
50
 * @method Route tokens(array $tokens)
51
 *
52
 * @method Route wildcard(string $wildcard)
53
 *
54
 */
55
class Map implements IteratorAggregate
56
{
57
    /**
58
     *
59
     * An array of route objects.
60
     *
61
     * @var Route[]
62
     *
63
     */
64
    protected $routes = [];
65
66
    /**
67
     *
68
     * A prototype Route.
69
     *
70
     * @var Route
71
     *
72
     */
73
    protected $protoRoute;
74
75
    /**
76
     *
77
     * Constructor.
78
     *
79
     * @param Route $protoRoute A prototype Route.
80
     *
81
     */
82 24
    public function __construct(Route $protoRoute)
83
    {
84 24
        $this->protoRoute = $protoRoute;
85 24
    }
86
87
    /**
88
     *
89
     * Proxy unknown method calls to the proto-route.
90
     *
91
     * @param string $method The method name.
92
     *
93
     * @param array $params The method params.
94
     *
95
     * @return $this
96
     *
97
     */
98 3
    public function __call($method, $params)
99
    {
100 3
        call_user_func_array([$this->protoRoute, $method], $params);
101 3
        return $this;
102
    }
103
104
    /**
105
     *
106
     * IteratorAggregate: returns the iterator object.
107
     *
108
     * @return ArrayIterator
109
     *
110
     */
111 5
    #[\ReturnTypeWillChange]
112
    public function getIterator()
113 5
    {
114
        return new ArrayIterator($this->routes);
115
    }
116
117
    /**
118
     *
119
     * Sets the array of route objects to use.
120
     *
121
     * @param Route[] $routes Use this array of routes.
122
     *
123
     * @return void
124
     *
125
     * @see getRoutes()
126
     *
127 1
     */
128
    public function setRoutes(array $routes)
129 1
    {
130 1
        $this->routes = $routes;
131
    }
132
133
    /**
134
     *
135
     * Gets the route collection.
136
     *
137
     * @return Route[]
138
     *
139
     * @see setRoutes()
140
     *
141 3
     */
142
    public function getRoutes()
143 3
    {
144
        return $this->routes;
145
    }
146
147
    /**
148
     *
149
     * Adds a pre-built route to the collection.
150
     *
151
     * @param Route $route The pre-built route.
152
     *
153
     * @return void
154
     *
155
     * @throws Exception\RouteAlreadyExists when the route name is already
156
     * mapped.
157
     *
158 23
     */
159
    public function addRoute(Route $route)
160 23
    {
161
        $name = $route->name;
162 23
163 1
        if (! $name) {
164 1
            $this->routes[] = $route;
165
            return;
166
        }
167 22
168 1
        if (isset($this->routes[$name])) {
169
            throw new Exception\RouteAlreadyExists($name);
170
        }
171 22
172 22
        $this->routes[$name] = $route;
173
    }
174
175
    /**
176
     *
177
     * Gets a route by name.
178
     *
179
     * @param string $name The route name.
180
     *
181
     * @throws Exception\RouteNotFound
182
     *
183
     * @return Route
184
     *
185 14
     */
186
    public function getRoute($name)
187 14
    {
188 1
        if (! isset($this->routes[$name])) {
189
            throw new Exception\RouteNotFound($name);
190
        }
191 13
192
        return $this->routes[$name];
193
    }
194
195
    /**
196
     *
197
     * Adds a generic route.
198
     *
199
     * @param string $name The route name.
200
     *
201
     * @param string $path The route path.
202
     *
203
     * @param mixed $handler The route leads to this handler.
204
     *
205
     * @throws Exception\ImmutableProperty
206
     *
207 23
     * @throws Exception\RouteAlreadyExists
208
     *
209 23
     * @return Route The newly-added route object.
210 23
     *
211 23
     */
212 23
    public function route($name, $path, $handler = null)
213 23
    {
214 23
        $route = clone $this->protoRoute;
215
        $route->name($name);
216
        $route->path($path);
217
        $route->handler($handler);
218
        $this->addRoute($route);
219
        return $route;
220
    }
221
222
    /**
223
     *
224
     * Adds a GET route.
225
     *
226
     * @param string $name The route name.
227
     *
228
     * @param string $path The route path.
229
     *
230 1
     * @param mixed $handler The route leads to this handler.
231
     *
232 1
     * @throws Exception\ImmutableProperty
233 1
     *
234 1
     * @throws Exception\RouteAlreadyExists
235
     *
236
     * @return Route The newly-added route object.
237
     *
238
     */
239
    public function get($name, $path, $handler = null)
240
    {
241
        $route = $this->route($name, $path, $handler);
242
        $route->allows('GET');
243
        return $route;
244
    }
245
246
    /**
247
     *
248
     * Adds a DELETE route.
249
     *
250 2
     * @param string $name The route name.
251
     *
252 2
     * @param string $path The route path.
253 2
     *
254 2
     * @param mixed $handler The route leads to this handler.
255
     *
256
     * @throws Exception\ImmutableProperty
257
     *
258
     * @throws Exception\RouteAlreadyExists
259
     *
260
     * @return Route The newly-added route object.
261
     *
262
     */
263
    public function delete($name, $path, $handler = null)
264
    {
265
        $route = $this->route($name, $path, $handler);
266
        $route->allows('DELETE');
267
        return $route;
268
    }
269
270 1
    /**
271
     *
272 1
     * Adds a HEAD route.
273 1
     *
274 1
     * @param string $name The route name.
275
     *
276
     * @param string $path The route path.
277
     *
278
     * @param mixed $handler The route leads to this handler.
279
     *
280
     * @throws Exception\ImmutableProperty
281
     *
282
     * @throws Exception\RouteAlreadyExists
283
     *
284
     * @return Route The newly-added route object.
285
     *
286
     */
287
    public function head($name, $path, $handler = null)
288
    {
289
        $route = $this->route($name, $path, $handler);
290 1
        $route->allows('HEAD');
291
        return $route;
292 1
    }
293 1
294 1
    /**
295
     *
296
     * Adds an OPTIONS route.
297
     *
298
     * @param string $name The route name.
299
     *
300
     * @param string $path The route path.
301
     *
302
     * @param mixed $handler The route leads to this handler.
303
     *
304
     * @throws Exception\ImmutableProperty
305
     *
306
     * @throws Exception\RouteAlreadyExists
307
     *
308
     * @return Route The newly-added route object.
309
     *
310 1
     */
311
    public function options($name, $path, $handler = null)
312 1
    {
313 1
        $route = $this->route($name, $path, $handler);
314 1
        $route->allows('OPTIONS');
315
        return $route;
316
    }
317
318
    /**
319
     *
320
     * Adds a PATCH route.
321
     *
322
     * @param string $name The route name.
323
     *
324
     * @param string $path The route path.
325
     *
326
     * @param mixed $handler The route leads to this handler.
327
     *
328
     * @throws Exception\ImmutableProperty
329
     *
330 3
     * @throws Exception\RouteAlreadyExists
331
     *
332 3
     * @return Route The newly-added route object.
333 3
     *
334 3
     */
335
    public function patch($name, $path, $handler = null)
336
    {
337
        $route = $this->route($name, $path, $handler);
338
        $route->allows('PATCH');
339
        return $route;
340
    }
341
342
    /**
343
     *
344
     * Adds a POST route.
345
     *
346
     * @param string $name The route name.
347
     *
348
     * @param string $path The route path.
349
     *
350 1
     * @param mixed $handler The route leads to this handler.
351
     *
352 1
     * @throws Exception\ImmutableProperty
353 1
     *
354 1
     * @throws Exception\RouteAlreadyExists
355
     *
356
     * @return Route The newly-added route object.
357
     *
358
     */
359
    public function post($name, $path, $handler = null)
360
    {
361
        $route = $this->route($name, $path, $handler);
362
        $route->allows('POST');
363
        return $route;
364
    }
365
366
    /**
367
     *
368
     * Adds a PUT route.
369
     *
370
     * @param string $name The route name.
371
     *
372
     * @param string $path The route path.
373 4
     *
374
     * @param mixed $handler The route leads to this handler.
375
     *
376 4
     * @throws Exception\ImmutableProperty
377
     *
378
     * @throws Exception\RouteAlreadyExists
379 4
     *
380 4
     * @return Route The newly-added route object.
381 4
     *
382 4
     */
383
    public function put($name, $path, $handler = null)
384
    {
385 4
        $route = $this->route($name, $path, $handler);
386 4
        $route->allows('PUT');
387 4
        return $route;
388
    }
389
390
    /****
391
     * Groups routes under a common name and path prefix, applying the prefixes to all routes added within the provided callable.
392
     *
393
     * Temporarily updates the prototype route with the given name and path prefixes, invokes the callable to add routes using the updated prototype, and then restores the original prototype. All routes added within the callable will have the specified prefixes applied.
394
     *
395
     * @param string $namePrefix Prefix to prepend to the names of attached routes.
396
     * @param string $pathPrefix Prefix to prepend to the paths of attached routes.
397
     * @param callable $callable Function that receives this map instance and adds routes.
398
     * @throws Exception\ImmutableProperty If the prototype route's properties are immutable.
399
     */
400
    public function attach($namePrefix, $pathPrefix, callable $callable)
401
    {
402
        // retain current prototype
403
        $old = $this->protoRoute;
404
405
        // clone a new prototype, update prefixes, and retain it
406
        $new = clone $old;
407
        $new->namePrefix($old->namePrefix . $namePrefix);
408
        $new->pathPrefix($old->pathPrefix . $pathPrefix);
409
        $this->protoRoute = $new;
410
411
        // run the callable and restore the old prototype
412
        $callable($this);
413
        $this->protoRoute = $old;
414
    }
415
416
    /****
417
     * Converts all routable routes with defined paths into a hierarchical tree structure keyed by path segments.
418
     *
419
     * Each route path is normalized by replacing grouped optional parameters and individual parameters with generic placeholders (`{}`), then split into segments to build a nested associative array. Routes are stored at leaf nodes keyed by their object hash, and routes with grouped optional parameters are also stored at parent nodes. This structure optimizes route matching by reducing the number of routes to check per segment.
420
     *
421
     * @return array<string, Route|array<string, mixed>> Nested array representing the route tree, where each segment is a key and parameter segments use the key '{}'.
422
     */
423
    public function getAsTreeRouteNode()
424
    {
425
        $treeRoutes = [];
426
        foreach ($this->routes as $route) {
427
            if (! $route->isRoutable || $route->path === null) {
428
                continue;
429
            }
430
431
            // replace "{/year,month,day}" parameters with /{}/{}/{}
432
            $routePath = preg_replace_callback(
433
                '~{/((?:\w+,?)+)}~',
434
                static function (array $matches) {
435
                    $variables = explode(',', $matches[1]);
436
437
                    return '/' . implode('/', array_fill(0, count($variables), '{}'));
438
                },
439
                $route->path
440
            ) ?: $route->path;
441
            $paramsAreOptional = $routePath !== $route->path;
442
443
            // This regexp will also work with "{controller:[a-zA-Z][a-zA-Z0-9_-]{1,}}"
444
            $routePath = preg_replace('~{(?:[^{}]*|(?R))*}~', '{}', $routePath) ?: $routePath;
445
            $node = &$treeRoutes;
446
            foreach (explode('/', trim($routePath, '/')) as $segment) {
447
                if (strpos($segment, '{') === 0) {
448
                    if ($paramsAreOptional) {
449
                        $node[spl_object_hash($route)] = $route;
450
                    }
451
                    $node = &$node['{}'];
452
                    $node[spl_object_hash($route)] = $route;
453
                    continue;
454
                }
455
                $node = &$node[$segment];
456
            }
457
458
            $node[spl_object_hash($route)] = $route;
459
            unset($node);
460
        }
461
462
        return $treeRoutes;
463
    }
464
}
465