Route::__set()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
namespace mindplay\walkway;
4
5
use Closure;
6
use ArrayAccess;
7
8
/**
9
 * This class represents an individual Route: a part of a path (referred to as a token)
10
 * and a set of patterns to be matched and mapped against nested Route definition-functions.
11
 *
12
 * It implements ArrayAccess as the method for defining patterns/functions.
13
 *
14
 * It also implements a collection of HTTP method-handlers (e.g. GET, PUT, POST, DELETE)
15
 * which can be defined and accessed via e.g. <code>$get</code> and other properties.
16
 *
17
 * @property Closure|null $get
18
 * @property Closure|null $head
19
 * @property Closure|null $post
20
 * @property Closure|null $put
21
 * @property Closure|null $delete
22
 */
23
class Route implements ArrayAccess
24
{
25
    /**
26
     * @var mixed[] map of parameter-names to values collected during traversal
27
     */
28
    public $vars = array();
29
30
    /**
31
     * @var Module the Module to which this Route belongs
32
     */
33
    public $module;
34
35
    /**
36
     * @var Route|null parent Route; or null if this is the root-Route.
37
     */
38
    public $parent;
39
40
    /**
41
     * @var string the token that was matched when this Route was constructed.
42
     */
43
    public $token;
44
45
    /**
46
     * @var string the (partial) path associated with this Route.
47
     */
48
    public $path;
49
50
    /**
51
     * @var bool true, if routing has been explicitly aborted
52
     */
53
    public $aborted;
54
55
    /**
56
     * @var Closure[] map of patterns to Route definition-functions
57
     *
58
     * @see resolve()
59
     */
60
    protected $patterns = array();
61
62
    /**
63
     * @var Closure[] map of method-names to functions
64
     *
65
     * @see execute()
66
     */
67
    protected $methods = array();
68
69
    /**
70
     * @var Module|null a Module instance being delegated to
71
     *
72
     * @see delegate()
73
     */
74
    private $_delegate;
75
76
    /**
77
     * @param Route  $parent parent Route
78
     * @param string $token  the token (partial path) that was matched when this Route was constructed.
79
     *
80
     * @return void
81
     */
82 1
    protected function setParent(Route $parent, $token)
83
    {
84 1
        $this->parent = $parent;
85 1
        $this->token = $token;
86
87 1
        $this->path = ($parent === null || $parent->path === '')
88 1
            ? $token
89 1
            : "{$parent->path}/{$token}";
90 1
    }
91
92
    /**
93
     * Sends a log entry to the parent Module for diagnostic purposes.
94
     *
95
     * Note that this has no effect unless the parent Module has a defined {@link Module::$onLog} callback.
96
     *
97
     * @param string $message
98
     *
99
     * @see Module::$onLog
100
     */
101 1
    public function log($message)
102
    {
103 1
        if ($log = $this->module->onLog) {
104 1
            $log($message);
105 1
        }
106 1
    }
107
108
    /**
109
     * @param string  $pattern
110
     * @param Closure $init
111
     *
112
     * @return void
113
     *
114
     * @see ArrayAccess::offsetSet()
115
     */
116 1
    public function offsetSet($pattern, $init)
117
    {
118 1
        $this->log("define pattern: {$pattern}");
119 1
        $this->patterns[$pattern] = $init;
120 1
    }
121
122
    /**
123
     * @param string $pattern
124
     *
125
     * @return bool
126
     *
127
     * @see ArrayAccess::offsetExists()
128
     */
129 1
    public function offsetExists($pattern)
130
    {
131 1
        return isset($this->patterns[$pattern]);
132
    }
133
134
    /**
135
     * @param string $pattern
136
     *
137
     * @return void
138
     *
139
     * @see ArrayAccess::offsetUnset()
140
     */
141 1
    public function offsetUnset($pattern)
142
    {
143 1
        unset($this->patterns[$pattern]);
144 1
    }
145
146
    /**
147
     * @param string $pattern
148
     *
149
     * @return string
150
     *
151
     * @see ArrayAccess::offsetGet()
152
     */
153 1
    public function offsetGet($pattern)
154
    {
155 1
        return $this->patterns[$pattern];
156
    }
157
158
    /**
159
     * @param string $name HTTP method-name ("get", "head", "post", "put", "delete", etc.)
160
     *
161
     * @return Closure
162
     */
163 1
    public function __get($name)
164
    {
165 1
        $name = strtolower($name);
166
167 1
        return isset($this->methods[$name])
168 1
            ? $this->methods[$name]
169 1
            : null;
170
    }
171
172
    /**
173
     * @param string  $name  HTTP method-name ("get", "head", "post", "put", "delete", etc.)
174
     * @param Closure $value HTTP method callback
175
     *
176
     * @return void
177
     */
178 1
    public function __set($name, $value)
179
    {
180 1
        $this->log("define method: {$name}");
181
182 1
        $name = strtolower($name);
183
184 1
        $this->methods[$name] = $value;
185 1
    }
186
187
    /**
188
     * Follow a (relative) path, walking from this Route to a destination Route.
189
     *
190
     * @param string $path relative path
191
     *
192
     * @return Route|null returns the resolved Route, or null if no Route was matched
193
     *
194
     * @throws RoutingException if a bad Route is encountered
195
     */
196 1
    public function resolve($path)
197
    {
198
        /**@var string $part partial path being resolved in the current iteration */
199 1
        $part = trim($path, '/'); // trim leading/trailing slashes
200
201
        /** @var bool $matched indicates whether the last partial path matched a pattern */
202 1
        $matched = true; // assume success (empty path will successfully resolve as root)
203
204
        /** @var Route $route the current route (switches as we walk through each token in the path) */
205 1
        $route = $this; // track the current Route, starting from $this
206
207 1
        $iteration = 0;
208
209 1
        while ($part) {
210 1
            $iteration += 1;
211
212 1
            $this->log("* resolving partial path '{$part}' (iteration {$iteration} of path '{$path}')");
213
214 1
            if (count($route->patterns) === 0) {
215 1
                $this->log("end of routes - no match found");
216
217 1
                return null;
218
            }
219
220 1
            $matched = false; // assume failure
221
222 1
            foreach ($route->patterns as $pattern => $init) {
223
                /**
224
                 * @var string  $pattern the pattern, with substitutions applied
225
                 * @var Closure $init    route initialization function
226
                 */
227
228
                // apply pattern-substitutions:
229
230 1
                $pattern = $this->module->preparePattern($pattern);
231
232 1
                $this->log("testing pattern '{$pattern}'");
233
234
                /** @var int|bool $match result of preg_match() against $pattern */
235 1
                $match = @preg_match('#^' . $pattern . '(?=$|/)#i', $part, $matches);
236
237 1
                if ($match === false) {
238 1
                    throw new RoutingException("invalid pattern '{$pattern}' (preg_match returned false)", $init);
239
                }
240
241 1
                if ($match !== 1) {
242 1
                    continue; // this pattern was not a match - continue with the next pattern
243
                }
244
245 1
                $matched = true;
246
247
                /** @var string $token the matched token */
248 1
                $token = array_shift($matches);
249
250 1
                $this->log("token '{$token}' matched by pattern '{$pattern}'");
251
252 1
                $route = $this->createRoute($route, $token);
253
254
                // identify named variables:
255
256 1
                if (count($matches)) {
257 1
                    $total = 0;
258 1
                    $last = 0;
259
260 1
                    foreach ($matches as $key => $value) {
261 1
                        if (is_int($key)) {
262 1
                            $last = $key;
263 1
                            continue;
264
                        }
265
266 1
                        $this->log("captured named variable '{$key}' as '{$value}'");
267
268 1
                        $route->vars[$key] = $value;
269
270 1
                        $total += 1;
271 1
                    }
272
273 1
                    if ($total - 1 !== $last) {
274 1
                        throw new RoutingException('pattern defines an unnamed substring capture: ' . $pattern);
275
                    }
276 1
                }
277
278
                // initialize the nested Route:
279
280 1
                $route->invoke($init);
281
282 1
                if ($route->aborted) {
283
                    // the function explicitly aborted the route
284 1
                    $this->log("aborted");
285
286 1
                    return null;
287
                }
288
289 1
                if ($route->_delegate) {
290
                    // delegate() was called - delegate routing to the specified module:
291
292 1
                    $this->log("delegating routing to " . get_class($this->_delegate));
293
294 1
                    $route->_delegate->setParent($route, $token);
295
296 1
                    $route = $route->_delegate;
297 1
                }
298
299 1
                break; // skip any remaining patterns
300 1
            }
301
302 1
            if (isset($token)) {
303
                // remove previous token from remaining part of path:
304 1
                $part = substr($part, strlen($token) + 1);
305 1
            } else {
306 1
                break;
307
            }
308 1
        }
309
310 1
        return $matched ? $route : null;
311
    }
312
313
    /**
314
     * Execute an HTTP method callback with a given name, and return the result.
315
     *
316
     * @param $method string name of HTTP method-handler to execute (e.g. 'get', 'put', 'post', 'delete', etc.)
317
     *
318
     * @return mixed|bool the value returned by the HTTP method-handler; true if the method-handler returned
319
     *                    no value - or false if the method-handler was not found (or returned false)
320
     */
321 1
    public function execute($method = 'get')
322
    {
323 1
        $func = $this->__get($method);
324
325 1
        if ($func === null) {
326 1
            return false; // method-handler not found
327
        }
328
329 1
        $result = $this->invoke($func);
330
331
        return $result === null
332 1
            ? true // method-handler executed but returned no value
333 1
            : $result; // method-handler returned a result
334
    }
335
336
    /**
337
     * Delegate control to a different Module.
338
     *
339
     * When called from a route definition function, while resolving a route, control
340
     * will be delegated to the given Module, meaning routing will continue for the
341
     * remainder of the unresolve URL tokens within a given Module.
342
     *
343
     * This provides a means of creating modular routers, in which a subset of routes
344
     * is packaged into a class derived from Module. (This approach also provides
345
     * convenient reuse.)
346
     *
347
     * @param Module $module a Module to which to delegate the routing during resolve()
348
     */
349 1
    public function delegate(Module $module)
350
    {
351 1
        $this->_delegate = $module;
352 1
    }
353
354
    /**
355
     * Call this method to manually abort any further routing and abort from the
356
     * current URL being resolved.
357
     */
358 1
    public function abort()
359
    {
360 1
        $this->aborted = true;
361 1
    }
362
363
    /**
364
     * When a URL token has been resolved, this function is called to generate the
365
     * next Route instance to be configured by the route callback function.
366
     *
367
     * @param Route   $parent
368
     * @param string  $token
369
     *
370
     * @return Route
371
     */
372 1
    protected function createRoute(Route $parent, $token)
373
    {
374 1
        $route = new Route();
375
376 1
        $route->module = $parent->module;
377 1
        $route->vars = $parent->vars;
378 1
        $route->vars['route'] = $route;
379 1
        $route->vars['module'] = $route->module;
380
381 1
        $route->setParent($parent, $token);
382
383 1
        return $route;
384
    }
385
386
    /**
387
     * Invoke a function using variables collected during traversal.
388
     *
389
     * @param Closure $func the function to be invoked.
390
     *
391
     * @return mixed the value returned by the invoked function
392
     *
393
     * @throws InvocationException
394
     *
395
     * @see $vars
396
     */
397 1
    protected function invoke($func)
398
    {
399 1
        return $this->module->invoker->invoke($func, $this->vars);
400
    }
401
}
402