Passed
Push — coderabbitai/docstrings/e4Aw ( f1c010...f38670 )
by
unknown
04:08 queued 02:24
created

Map::head()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 5
rs 10
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
    public function __construct(Route $protoRoute)
83
    {
84
        $this->protoRoute = $protoRoute;
85
    }
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
    public function __call($method, $params)
99
    {
100
        call_user_func_array([$this->protoRoute, $method], $params);
101
        return $this;
102
    }
103
104
    /**
105
     *
106
     * IteratorAggregate: returns the iterator object.
107
     *
108
     * @return ArrayIterator
109
     *
110
     */
111
    #[\ReturnTypeWillChange]
112
    public function getIterator()
113
    {
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
     */
128
    public function setRoutes(array $routes)
129
    {
130
        $this->routes = $routes;
131
    }
132
133
    /**
134
     *
135
     * Gets the route collection.
136
     *
137
     * @return Route[]
138
     *
139
     * @see setRoutes()
140
     *
141
     */
142
    public function getRoutes()
143
    {
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
     */
159
    public function addRoute(Route $route)
160
    {
161
        $name = $route->name;
162
163
        if (! $name) {
164
            $this->routes[] = $route;
165
            return;
166
        }
167
168
        if (isset($this->routes[$name])) {
169
            throw new Exception\RouteAlreadyExists($name);
170
        }
171
172
        $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
     */
186
    public function getRoute($name)
187
    {
188
        if (! isset($this->routes[$name])) {
189
            throw new Exception\RouteNotFound($name);
190
        }
191
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
     * @throws Exception\RouteAlreadyExists
208
     *
209
     * @return Route The newly-added route object.
210
     *
211
     */
212
    public function route($name, $path, $handler = null)
213
    {
214
        $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
     * @param mixed $handler The route leads to this handler.
231
     *
232
     * @throws Exception\ImmutableProperty
233
     *
234
     * @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
     * @param string $name The route name.
251
     *
252
     * @param string $path The route path.
253
     *
254
     * @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
    /**
271
     *
272
     * Adds a HEAD route.
273
     *
274
     * @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
        $route->allows('HEAD');
291
        return $route;
292
    }
293
294
    /**
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
     */
311
    public function options($name, $path, $handler = null)
312
    {
313
        $route = $this->route($name, $path, $handler);
314
        $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
     * @throws Exception\RouteAlreadyExists
331
     *
332
     * @return Route The newly-added route object.
333
     *
334
     */
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
     * @param mixed $handler The route leads to this handler.
351
     *
352
     * @throws Exception\ImmutableProperty
353
     *
354
     * @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
     *
374
     * @param mixed $handler The route leads to this handler.
375
     *
376
     * @throws Exception\ImmutableProperty
377
     *
378
     * @throws Exception\RouteAlreadyExists
379
     *
380
     * @return Route The newly-added route object.
381
     *
382
     */
383
    public function put($name, $path, $handler = null)
384
    {
385
        $route = $this->route($name, $path, $handler);
386
        $route->allows('PUT');
387
        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