Completed
Push — master ( 83f9c7...11aefc )
by Anton
03:38
created

AbstractRoute::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
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
            return $this->withMatches($matches);
225
        }
226
227
        return null;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function perform(Request $request, Response $response)
234
    {
235
        if (empty($this->container)) {
236
            throw new RouteException("Unable to perform route endpoint without given container");
237
        }
238
239
        $pipeline = new MiddlewarePipeline($this->middlewares, $this->container);
240
241
        return $pipeline->target($this->createEndpoint())->run($request, $response);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    public function uri($parameters = [])
248
    {
249
        if (empty($this->compiled)) {
250
            $this->compile();
251
        }
252
253
        $parameters = array_merge(
254
            $this->compiled['options'],
255
            $this->defaults,
256
            $this->matches,
257
            $this->fetchSegments($parameters, $query)
258
        );
259
260
        //Uri without empty blocks (pretty stupid implementation)
261
        $path = strtr(
262
            \Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'),
263
            ['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/']
264
        );
265
266
        $uri = new Uri(($this->withHost ? '' : $this->prefix) . rtrim($path, '/'));
267
268
        return empty($query) ? $uri : $uri->withQuery(http_build_query($query));
269
    }
270
271
    /**
272
     * Route matches.
273
     *
274
     * @return array
275
     */
276
    public function getMatches()
277
    {
278
        return $this->matches;
279
    }
280
281
    /**
282
     * @param string $name
283
     * @param mixed  $default
284
     * @return mixed
285
     */
286
    public function getMatch($name, $default = null)
287
    {
288
        if (array_key_exists($name, $this->matches)) {
289
            return $this->matches[$name];
290
        }
291
292
        return $default;
293
    }
294
295
    /**
296
     * @param array $matches
297
     * @return AbstractRoute
298
     */
299
    protected function withMatches(array $matches)
300
    {
301
        $route = clone $this;
302
        $route->matches = $matches;
303
304
        return $route;
305
    }
306
307
    /**
308
     * Fetch uri segments and query parameters.
309
     *
310
     * @param \Traversable|array $parameters
311
     * @param array              $query Query parameters.
312
     * @return array
313
     */
314
    protected function fetchSegments($parameters, &$query)
315
    {
316
        $allowed = array_keys($this->compiled['options']);
317
318
        $result = [];
319
        foreach ($parameters as $key => $parameter) {
320
            if (is_numeric($key) && isset($allowed[$key])) {
321
                $key = $allowed[$key];
322
            } elseif (
323
                !array_key_exists($key, $this->compiled['options'])
324
                && is_array($parameters)
325
            ) {
326
                $query[$key] = $parameters;
327
                continue;
328
            }
329
330
            if (is_string($parameter) && !preg_match('/^[a-z\-_0-9]+$/i', $parameter)) {
331
                $result[$key] = Strings::slug($parameter);
332
                continue;
333
            }
334
335
            $result[$key] = (string)$parameter;
336
        }
337
338
        return $result;
339
    }
340
341
342
    /**
343
     * Create callable route endpoint.
344
     *
345
     * @return callable
346
     */
347
    abstract protected function createEndpoint();
348
349
    /**
350
     * {@inheritdoc}
351
     */
352
    protected function container()
353
    {
354
        if (empty($this->container)) {
355
            throw new RouteException("Route context container has not been set");
356
        }
357
358
        return $this->container;
359
    }
360
361
    /**
362
     * Compile router pattern into valid regexp.
363
     */
364
    private function compile()
365
    {
366
        $replaces = ['/' => '\\/', '[' => '(?:', ']' => ')?', '.' => '\.'];
367
368
        $options = [];
369
        if (preg_match_all('/<(\w+):?(.*?)?>/', $this->pattern, $matches)) {
370
            $variables = array_combine($matches[1], $matches[2]);
371
372
            foreach ($variables as $name => $segment) {
373
                //Segment regex
374
                $segment = !empty($segment) ? $segment : self::DEFAULT_SEGMENT;
375
                $replaces["<$name>"] = "(?P<$name>$segment)";
376
                $options[] = $name;
377
            }
378
        }
379
380
        $template = preg_replace('/<(\w+):?.*?>/', '<\1>', $this->pattern);
381
382
        $this->compiled = [
383
            'pattern'  => '/^' . strtr($template, $replaces) . '$/iu',
384
            'template' => stripslashes(str_replace('?', '', $template)),
385
            'options'  => array_fill_keys($options, null)
386
        ];
387
    }
388
389
    /**
390
     * @param Request $request
391
     * @return string
392
     */
393
    private function getSubject(Request $request)
394
    {
395
        $path = $request->getUri()->getPath();
396
397
        if (empty($path) || $path[0] !== '/') {
398
            $path = '/' . $path;
399
        }
400
401
        if ($this->withHost) {
402
            $uri = $request->getUri()->getHost() . $path;
403
        } else {
404
            $uri = substr($path, strlen($this->prefix));
405
        }
406
407
        return rtrim($uri, '/');
408
    }
409
}
410