Completed
Push — master ( 9215e8...6b72d6 )
by Anton
03:42
created

AbstractRoute::getMatch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Http\Routing;
9
10
use Psr\Http\Message\ResponseInterface as Response;
11
use Psr\Http\Message\ServerRequestInterface as Request;
12
use Spiral\Core\ContainerInterface;
13
use Spiral\Http\Exceptions\RouteException;
14
use Spiral\Http\MiddlewareInterface;
15
use Spiral\Http\MiddlewarePipeline;
16
use Spiral\Http\Uri;
17
use Spiral\Support\Strings;
18
19
/**
20
 * Abstract route with ability to execute endpoint using middleware pipeline and context container.
21
 *
22
 * Attention, route does not extends container is it's mandatory to be set.
23
 */
24
abstract class AbstractRoute implements RouteInterface
25
{
26
    /**
27
     * Default segment pattern, this patter can be applied to controller names, actions and etc.
28
     */
29
    const DEFAULT_SEGMENT = '[^\.\/]+';
30
31
    /**
32
     * @var string
33
     */
34
    private $name = '';
35
36
    /**
37
     * Path prefix (base path).
38
     *
39
     * @var string
40
     */
41
    private $prefix = '';
42
43
    /**
44
     * Default set of values to fill route matches and target pattern (if specified as pattern).
45
     *
46
     * @var array
47
     */
48
    private $defaults = [];
49
50
    /**
51
     * If true route will be matched with URI host in addition to path. BasePath will be ignored.
52
     *
53
     * @var bool
54
     */
55
    private $withHost = false;
56
57
    /**
58
     * Route matches, populated after match() method executed. Internal.
59
     *
60
     * @var array
61
     */
62
    private $matches = [];
63
64
    /**
65
     * Compiled route options, pattern and etc. Internal data.
66
     *
67
     * @invisible
68
     * @var array
69
     */
70
    private $compiled = [];
71
72
    /**
73
     * Route pattern includes simplified regular expressing later compiled to real regexp.
74
     *
75
     * @var string
76
     */
77
    protected $pattern = '';
78
79
    /**
80
     * @var array
81
     */
82
    protected $middlewares = [];
83
84
    /**
85
     * Route endpoint container context.
86
     *
87
     * @invisible
88
     * @var ContainerInterface|null
89
     */
90
    protected $container = null;
91
92
    /**
93
     * @param string $name
94
     * @param array  $defaults
95
     */
96
    public function __construct($name, array $defaults)
97
    {
98
        $this->name = $name;
99
        $this->defaults = $defaults;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function withContainer(ContainerInterface $container)
106
    {
107
        $route = clone $this;
108
        $route->container = $container;
109
110
        return $route;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function withName($name)
117
    {
118
        $route = clone $this;
119
        $route->name = $name;
120
121
        return $route;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function getName()
128
    {
129
        return $this->name;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function withPrefix($prefix)
136
    {
137
        $route = clone $this;
138
        $route->prefix = $prefix;
139
140
        return $route;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function getPrefix()
147
    {
148
        return $this->prefix;
149
    }
150
151
    /**
152
     * If true (default) route will be matched against path + URI host. Returns new route instance.
153
     *
154
     * @param bool $withHost
155
     * @return self
156
     */
157
    public function withHost($withHost = true)
158
    {
159
        $route = clone $this;
160
        $route->withHost = $withHost;
161
162
        return $route;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function withDefaults(array $defaults)
169
    {
170
        $copy = clone $this;
171
        $copy->defaults = $defaults;
172
173
        return $copy;
174
    }
175
176
    /**
177
     * Get default route values.
178
     *
179
     * @return array
180
     */
181
    public function getDefaults()
182
    {
183
        return $this->defaults;
184
    }
185
186
    /**
187
     * Associated middleware with route. New instance of route will be returned.
188
     *
189
     * Example:
190
     * $route->withMiddleware(new CacheMiddleware(100));
191
     * $route->withMiddleware(ProxyMiddleware::class);
192
     * $route->withMiddleware([ProxyMiddleware::class, OtherMiddleware::class]);
193
     *
194
     * @param callable|MiddlewareInterface|array $middleware
195
     * @return self
196
     */
197
    public function withMiddleware($middleware)
198
    {
199
        $route = clone $this;
200
        if (is_array($middleware)) {
201
            $route->middlewares = array_merge($route->middlewares, $middleware);
202
        } else {
203
            $route->middlewares[] = $middleware;
204
        }
205
206
        return $route;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function match(Request $request)
213
    {
214
        if (empty($this->compiled)) {
215
            $this->compile();
216
        }
217
218
        if (preg_match($this->compiled['pattern'], $this->getSubject($request), $matches)) {
219
220
            //To get only named matches
221
            $matches = array_intersect_key($matches, $this->compiled['options']);
222
            $matches = array_merge($this->compiled['options'], $this->defaults, $matches);
223
224
            $route = clone $this;
225
            $route->matches = $matches;
226
227
            return $route;
228
        }
229
230
        return null;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function perform(Request $request, Response $response)
237
    {
238
        if (empty($this->container)) {
239
            throw new RouteException("Unable to perform route endpoint without given container");
240
        }
241
242
        $pipeline = new MiddlewarePipeline($this->middlewares, $this->container);
243
244
        return $pipeline->target($this->createEndpoint())->run($request, $response);
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function uri($parameters = [])
251
    {
252
        if (empty($this->compiled)) {
253
            $this->compile();
254
        }
255
256
        $parameters = array_merge(
257
            $this->compiled['options'],
258
            $this->defaults,
259
            $this->matches,
260
            $this->fetchSegments($parameters, $query)
261
        );
262
263
        //Uri without empty blocks (pretty stupid implementation)
264
        $path = strtr(
265
            \Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'),
266
            ['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/']
267
        );
268
269
        $uri = new Uri(($this->withHost ? '' : $this->prefix) . rtrim($path, '/'));
270
271
        return empty($query) ? $uri : $uri->withQuery(http_build_query($query));
272
    }
273
274
    /**
275
     * Fetch uri segments and query parameters.
276
     *
277
     * @param \Traversable|array $parameters
278
     * @param array              $query Query parameters.
279
     * @return array
280
     */
281
    protected function fetchSegments($parameters, &$query)
282
    {
283
        $allowed = array_keys($this->compiled['options']);
284
285
        $result = [];
286
        foreach ($parameters as $key => $parameter) {
287
            if (is_numeric($key) && isset($allowed[$key])) {
288
                $key = $allowed[$key];
289
            } elseif (
290
                !array_key_exists($key, $this->compiled['options'])
291
                && is_array($parameters)
292
            ) {
293
                $query[$key] = $parameters;
294
                continue;
295
            }
296
297
            if (is_string($parameter) && !preg_match('/^[a-z\-_0-9]+$/i', $parameter)) {
298
                $result[$key] = Strings::slug($parameter);
299
                continue;
300
            }
301
302
            $result[$key] = (string)$parameter;
303
        }
304
305
        return $result;
306
    }
307
308
    /**
309
     * Route matches.
310
     *
311
     * @return array
312
     */
313
    protected function getMatches()
314
    {
315
        return $this->matches;
316
    }
317
318
    /**
319
     * @param string $name
320
     * @param mixed  $default
321
     * @return mixed
322
     */
323
    public function getMatch($name, $default = null)
324
    {
325
        if (array_key_exists($name, $this->matches)) {
326
            return $this->matches[$name];
327
        }
328
329
        return $default;
330
    }
331
332
    /**
333
     * Create callable route endpoint.
334
     *
335
     * @return callable
336
     */
337
    abstract protected function createEndpoint();
338
339
    /**
340
     * {@inheritdoc}
341
     */
342
    protected function container()
343
    {
344
        if (empty($this->container)) {
345
            throw new RouteException("Route context container has not been set");
346
        }
347
348
        return $this->container;
349
    }
350
351
    /**
352
     * Compile router pattern into valid regexp.
353
     */
354
    private function compile()
355
    {
356
        $replaces = ['/' => '\\/', '[' => '(?:', ']' => ')?', '.' => '\.'];
357
358
        $options = [];
359
        if (preg_match_all('/<(\w+):?(.*?)?>/', $this->pattern, $matches)) {
360
            $variables = array_combine($matches[1], $matches[2]);
361
362
            foreach ($variables as $name => $segment) {
363
                //Segment regex
364
                $segment = !empty($segment) ? $segment : self::DEFAULT_SEGMENT;
365
                $replaces["<$name>"] = "(?P<$name>$segment)";
366
                $options[] = $name;
367
            }
368
        }
369
370
        $template = preg_replace('/<(\w+):?.*?>/', '<\1>', $this->pattern);
371
372
        $this->compiled = [
373
            'pattern'  => '/^' . strtr($template, $replaces) . '$/iu',
374
            'template' => stripslashes(str_replace('?', '', $template)),
375
            'options'  => array_fill_keys($options, null)
376
        ];
377
    }
378
379
    /**
380
     * @param Request $request
381
     * @return string
382
     */
383
    private function getSubject(Request $request)
384
    {
385
        $path = $request->getUri()->getPath();
386
387
        if (empty($path) || $path[0] !== '/') {
388
            $path = '/' . $path;
389
        }
390
391
        if ($this->withHost) {
392
            $uri = $request->getUri()->getHost() . $path;
393
        } else {
394
            $uri = substr($path, strlen($this->prefix));
395
        }
396
397
        return rtrim($uri, '/');
398
    }
399
}
400