Completed
Push — master ( be3445...b92c0f )
by Anton
04:02
created

AbstractRoute::middleware()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 10
rs 9.4285
cc 2
eloc 6
nc 2
nop 1
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 Cocur\Slugify\Slugify;
11
use Cocur\Slugify\SlugifyInterface;
12
use Psr\Http\Message\ResponseInterface as Response;
13
use Psr\Http\Message\ServerRequestInterface as Request;
14
use Spiral\Core\ContainerInterface;
15
use Spiral\Core\Exceptions\ControllerException;
16
use Spiral\Core\HMVC\CoreInterface;
17
use Spiral\Http\Exceptions\ClientException;
18
use Spiral\Http\MiddlewareInterface;
19
use Spiral\Http\MiddlewarePipeline;
20
use Spiral\Http\Uri;
21
22
/**
23
 * Base for all spiral routes.
24
 *
25
 * Routing format (examples given in context of Core->bootstrap() method and Route):
26
 *
27
 * Static routes.
28
 * $this->http->route('profile-<id>', 'Controllers\UserController::showProfile');
29
 *
30
 * Dynamic actions:
31
 * $this->http->route('account/<action>', 'Controllers\AccountController::<action>');
32
 *
33
 * Optional segments:
34
 * $this->http->route('profile[/<id>]', 'Controllers\UserController::showProfile');
35
 *
36
 * This route will react on URL's like /profile/ and /profile/someSegment/
37
 *
38
 * To determinate your own pattern for segment use construction <segmentName:pattern>
39
 * $this->http->route('profile[/<id:\d+>]', 'Controllers\UserController::showProfile');
40
 *
41
 * Will react only on /profile/ and /profile/1384978/
42
 *
43
 * You can use custom pattern for controller and action segments.
44
 * $this->http->route('users[/<action:edit|save|open>]', 'Controllers\UserController::<action>');
45
 *
46
 * Routes can be applied to URI host.
47
 * $this->http->route(
48
 *      '<username>.domain.com[/<action>[/<id>]]',
49
 *      'Controllers\UserController::<action>'
50
 * )->useHost();
51
 *
52
 * Routes can be used non only with controllers (no idea why you may need it):
53
 * $this->http->route('users', function () {
54
 *      return "This is users route.";
55
 * });
56
 */
57
abstract class AbstractRoute implements RouteInterface
58
{
59
    /**
60
     * Default segment pattern, this patter can be applied to controller names, actions and etc.
61
     */
62
    const DEFAULT_SEGMENT = '[^\.\/]+';
63
64
    /**
65
     * To execute actions.
66
     *
67
     * @invisible
68
     * @var CoreInterface
69
     */
70
    protected $core = null;
71
72
    /**
73
     * @var string
74
     */
75
    protected $name = '';
76
77
    /**
78
     * @var array
79
     */
80
    protected $middlewares = [];
81
82
    /**
83
     * Route pattern includes simplified regular expressing later compiled to real regexp.
84
     *
85
     * @var string
86
     */
87
    protected $pattern = '';
88
89
    /**
90
     * Default set of values to fill route matches and target pattern (if specified as pattern).
91
     *
92
     * @var array
93
     */
94
    protected $defaults = [];
95
96
    /**
97
     * If true route will be matched with URI host in addition to path. BasePath will be ignored.
98
     *
99
     * @var bool
100
     */
101
    protected $withHost = false;
102
103
    /**
104
     * Compiled route options, pattern and etc. Internal data.
105
     *
106
     * @invisible
107
     * @var array
108
     */
109
    protected $compiled = [];
110
111
    /**
112
     * Route matches, populated after match() method executed. Internal.
113
     *
114
     * @todo not sure if it's good idea to store matches in route?
115
     * @var array
116
     */
117
    protected $matches = [];
118
119
    /**
120
     * @param CoreInterface $core
121
     * @return $this
122
     */
123
    public function setCore(CoreInterface $core)
124
    {
125
        $this->core = $core;
126
127
        return $this;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function getName()
134
    {
135
        return $this->name;
136
    }
137
138
    /**
139
     * Declared route pattern.
140
     *
141
     * @return string
142
     */
143
    public function getPattern()
144
    {
145
        return $this->pattern;
146
    }
147
148
    /**
149
     * If true (default) route will be matched against path + URI host.
150
     *
151
     * @param bool $withHost
152
     * @return $this
153
     */
154
    public function matchHost($withHost = true)
155
    {
156
        $this->withHost = $withHost;
157
158
        return $this;
159
    }
160
161
    /**
162
     * Update route defaults (new values will be merged with existed data).
163
     *
164
     * @deprecated User withDefault method
165
     * @param array $defaults
166
     * @return $this
167
     */
168
    public function defaults(array $defaults)
169
    {
170
        $this->defaults = $defaults + $this->defaults;
171
172
        return $this;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function withDefaults($name, array $matches)
179
    {
180
        $copy = clone $this;
181
        $copy->name = (string)$name;
182
        $copy->defaults($matches);
0 ignored issues
show
Deprecated Code introduced by
The method Spiral\Http\Routing\AbstractRoute::defaults() has been deprecated with message: User withDefault method

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
183
184
        return $copy;
185
    }
186
187
    /**
188
     * Associated middleware with route.
189
     *
190
     * Example:
191
     * $route->with(new CacheMiddleware(100));
192
     * $route->with(ProxyMiddleware::class);
193
     * $route->with([ProxyMiddleware::class, OtherMiddleware::class]);
194
     *
195
     * @param callable|MiddlewareInterface|array $middleware
196
     * @return $this
197
     */
198
    public function middleware($middleware)
199
    {
200
        if (is_array($middleware)) {
201
            $this->middlewares = array_merge($this->middlewares, $middleware);
202
        } else {
203
            $this->middlewares[] = $middleware;
204
        }
205
206
        return $this;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function match(Request $request, $basePath = '/')
213
    {
214
        if (empty($this->compiled)) {
215
            $this->compile();
216
        }
217
218
        $path = $request->getUri()->getPath();
219
        if (empty($path) || $path[0] !== '/') {
220
            $path = '/' . $path;
221
        }
222
223
        if ($this->withHost) {
224
            $uri = $request->getUri()->getHost() . $path;
225
        } else {
226
            $uri = substr($path, strlen($basePath));
227
        }
228
229
        if (preg_match($this->compiled['pattern'], rtrim($uri, '/'), $matches)) {
230
            $route = clone $this;
231
232
            $route->matches = $matches;
233
234
            //To get only named matches
235
            $route->matches = array_intersect_key($route->matches, $route->compiled['options']);
236
            $route->matches = array_merge(
237
                $route->compiled['options'],
238
                $route->defaults,
239
                $route->matches
240
            );
241
242
            return $route;
243
        }
244
245
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Spiral\Http\Routing\RouteInterface::match of type Spiral\Http\Routing\RouteInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function perform(Request $request, Response $response, ContainerInterface $container)
252
    {
253
        $pipeline = new MiddlewarePipeline($this->middlewares, $container);
254
255
        return $pipeline->target($this->createEndpoint($container))->run($request, $response);
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     *
261
     * @todo need improvement
262
     */
263
    public function uri(
264
        $parameters = [],
265
        $basePath = '/',
266
        SlugifyInterface $slugify = null
267
    ) {
268
        if (empty($this->compiled)) {
269
            $this->compile();
270
        }
271
272
        if (empty($slugify)) {
273
            $slugify = new Slugify();
274
        }
275
276
        $parameters = $this->fetchParameters($parameters, $slugify);
277
        $parameters = $parameters + $this->matches + $this->defaults + $this->compiled['options'];
278
279
        //Uri without empty blocks (pretty stupid implementation)
280
        $path = strtr(
281
            \Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'),
282
            ['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/']
283
        );
284
285
        $uri = new Uri(
286
            ($this->withHost ? '' : $basePath) . rtrim($path, '/')
287
        );
288
289
        //Getting additional query parameters
290
        if (!empty($queryParameters = array_diff_key($parameters, $this->compiled['options']))) {
291
            $uri = $uri->withQuery(http_build_query($queryParameters));
292
        }
293
294
        return $uri;
295
    }
296
297
    /**
298
     * Generate parameters list.
299
     *
300
     * @param \Traversable|array $parameters
301
     * @param SlugifyInterface   $slugify
302
     * @return array
303
     */
304
    protected function fetchParameters($parameters, SlugifyInterface $slugify)
305
    {
306
        $result = [];
307
        $allowed = array_keys($this->compiled['options']);
308
309
        foreach ($parameters as $key => $parameter) {
310
            if (!array_key_exists($key, $this->compiled['options'])) {
311
                //Numeric key?
312
                if (isset($allowed[$key])) {
313
                    $key = $allowed[$key];
314
                } else {
315
                    continue;
316
                }
317
            }
318
319
            if (is_string($parameter) && !preg_match('/^[a-z\-_0-9]+$/i', $parameter)) {
320
                //Default Slugify is pretty slow, we'd better not apply it for every value
321
                $result[$key] = $slugify->slugify($parameter);
322
                continue;
323
            }
324
325
            $result[$key] = (string)$parameter;
326
        }
327
328
        return $result;
329
    }
330
331
    /**
332
     * Create callable route endpoint.
333
     *
334
     * @param ContainerInterface $container
335
     * @return callable
336
     */
337
    abstract protected function createEndpoint(ContainerInterface $container);
338
339
    /**
340
     * Internal helper used to create execute controller action using associated core instance.
341
     *
342
     * @param ContainerInterface $container
343
     * @param string             $controller
344
     * @param string             $action
345
     * @param array              $parameters
346
     * @return mixed
347
     * @throws ClientException
348
     */
349
    protected function callAction(
350
        ContainerInterface $container,
351
        $controller,
352
        $action,
353
        array $parameters = []
354
    ) {
355
        if (empty($this->core)) {
356
            $this->core = $container->get(CoreInterface::class);
357
        }
358
359
        try {
360
            return $this->core->callAction($controller, $action, $parameters);
361
        } catch (ControllerException $e) {
362
            throw $this->convertException($e);
363
        }
364
    }
365
366
    /**
367
     * Converts controller exceptions into client exceptions.
368
     *
369
     * @param ControllerException $exception
370
     * @return ClientException
371
     */
372
    protected function convertException(ControllerException $exception)
373
    {
374
        switch ($exception->getCode()) {
375
            case ControllerException::BAD_ACTION:
376
            case ControllerException::NOT_FOUND:
377
                return new ClientException(ClientException::NOT_FOUND, $exception->getMessage());
378
            case  ControllerException::FORBIDDEN:
0 ignored issues
show
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
379
                return new ClientException(ClientException::FORBIDDEN, $exception->getMessage());
380
            default:
381
                return new ClientException(ClientException::BAD_DATA, $exception->getMessage());
382
        }
383
    }
384
385
    /**
386
     * Compile router pattern into valid regexp.
387
     */
388
    private function compile()
389
    {
390
        $replaces = ['/' => '\\/', '[' => '(?:', ']' => ')?', '.' => '\.'];
391
392
        $options = [];
393
        if (preg_match_all('/<(\w+):?(.*?)?>/', $this->pattern, $matches)) {
394
            $variables = array_combine($matches[1], $matches[2]);
395
396
            foreach ($variables as $name => $segment) {
397
                //Segment regex
398
                $segment = !empty($segment) ? $segment : self::DEFAULT_SEGMENT;
399
                $replaces["<$name>"] = "(?P<$name>$segment)";
400
                $options[] = $name;
401
            }
402
        }
403
404
        $template = preg_replace('/<(\w+):?.*?>/', '<\1>', $this->pattern);
405
406
        $this->compiled = [
407
            'pattern'  => '/^' . strtr($template, $replaces) . '$/iu',
408
            'template' => stripslashes(str_replace('?', '', $template)),
409
            'options'  => array_fill_keys($options, null)
410
        ];
411
    }
412
}
413