RoutingServiceProvider::addRoute()   C
last analyzed

Complexity

Conditions 8
Paths 36

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 36
nop 3
1
<?php
2
namespace MJanssen\Provider;
3
4
use InvalidArgumentException;
5
use Silex\Application;
6
use Silex\Controller;
7
use Silex\Route;
8
use Pimple\ServiceProviderInterface;
9
use Pimple\Container;
10
use Silex\Api\BootableProviderInterface;
11
use Silex\Api\EventListenerProviderInterface;
12
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
13
14
/**
15
 * Class RoutingServiceProvider
16
 * @package MJanssen\Provider
17
 */
18
class RoutingServiceProvider implements
19
    ServiceProviderInterface,
20
    BootableProviderInterface,
21
    EventListenerProviderInterface
22
{
23
    /**
24
     * @var
25
     */
26
    protected $appRoutingKey;
27
28
    /**
29
     * @param string $appRoutingKey
30
     */
31
    public function __construct($appRoutingKey = 'config.routes')
32
    {
33
        $this->appRoutingKey = $appRoutingKey;
34
    }
35
36
    /**
37
     * @param Container $app
38
     * @throws \InvalidArgumentException
39
     */
40
    public function register(Container $app)
41
    {
42
        if (isset($app[$this->appRoutingKey])) {
43
            if (is_array($app[$this->appRoutingKey])) {
44
                $this->addRoutes($app, $app[$this->appRoutingKey]);
45
            } else {
46
                throw new InvalidArgumentException('config.routes must be of type Array');
47
            }
48
        }
49
    }
50
51
    /**
52
     * @param Application $app
53
     * @codeCoverageIgnore
54
     */
55
    public function boot(Application $app)
56
    {
57
    }
58
59
    /**
60
     * @param Container $app
61
     * @param EventDispatcherInterface $dispatcher
62
     * @codeCoverageIgnore
63
     */
64
    public function subscribe(Container $app, EventDispatcherInterface $dispatcher)
65
    {
66
    }
67
68
    /**
69
     * Adds all routes
70
     *
71
     * @param Container $app
72
     * @param $routes
73
     */
74
    public function addRoutes(Container $app, $routes)
75
    {
76
        foreach ($routes as $name => $route) {
77
78
            if (is_numeric($name)) {
79
                $name = '';
80
            }
81
82
            $this->addRoute($app, $route, $name);
83
        }
84
    }
85
86
    /**
87
     * Adds a route, a given route-name (for named routes) and all of its methods
88
     *
89
     * @param Container $app
90
     * @param array $route
91
     * @throws InvalidArgumentException
92
     */
93
    public function addRoute(Container $app, array $route, $name = '')
94
    {
95
        if (isset($route['method']) && is_string($route['method'])) {
96
            $route['method'] = array($route['method']);
97
        }
98
99
        $this->validateRoute($route);
100
101
        if (array_key_exists('name', $route)) {
102
            $name = $route['name'];
103
        }
104
105
        $controller = $app->match(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Pimple\Container as the method match() does only exist in the following sub-classes of Pimple\Container: Silex\Application, Silex\Tests\Application\FormApplication, Silex\Tests\Application\MonologApplication, Silex\Tests\Application\SecurityApplication, Silex\Tests\Application\SwiftmailerApplication, Silex\Tests\Application\TranslationApplication, Silex\Tests\Application\TwigApplication, Silex\Tests\Application\UrlGeneratorApplication, Silex\Tests\SpecialApplication. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
106
            $route['pattern'],
107
            $route['controller'])
108
            ->bind(
109
                $this->sanitizeRouteName($name)
110
            )->method(
111
                join('|', array_map('strtoupper', $route['method']))
112
            );
113
114
        $supportedProperties = array('value', 'assert', 'convert', 'before', 'after', 'secure');
115
        foreach ($supportedProperties AS $property) {
116
            if (isset($route[$property])) {
117
                $this->addActions($controller, $route[$property], $property);
118
            }
119
        }
120
121
        if (isset($route['scheme'])) {
122
            if ('https' === $route['scheme']) {
123
                $controller->requireHttps();
124
            }
125
        }
126
    }
127
128
    /**
129
     * Validates the given methods. Only get, put, post, delete, options, head
130
     * are allowed
131
     *
132
     * @param array $methods
133
     */
134
    protected function validateMethods(Array $methods)
135
    {
136
        $availableMethods = array('get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'purge', 'options', 'trace', 'connect');
137
        foreach (array_map('strtolower', $methods) as $method) {
138
            if (!in_array($method, $availableMethods)) {
139
                throw new InvalidArgumentException('Method "' . $method . '" is not valid, only the following methods are allowed: ' . join(', ', $availableMethods));
140
            }
141
        }
142
    }
143
144
    /**
145
     * Validates the given $route Array
146
     *
147
     * @param $route
148
     * @throws \InvalidArgumentException
149
     */
150
    protected function validateRoute($route)
151
    {
152
        if (!isset($route['pattern']) || !isset($route['method']) || !isset($route['controller'])) {
153
            throw new InvalidArgumentException('Required parameter (pattern/method/controller) is not set.');
154
        }
155
156
        $arrayParameters = array('method', 'assert', 'value');
157
158
        foreach ($arrayParameters as $parameter) {
159
            if (isset($route[$parameter]) && !is_array($route[$parameter])) {
160
                throw new InvalidArgumentException(sprintf(
161
                    '%s is not of type Array (%s)',
162
                    $parameter, gettype($route[$parameter])
163
                ));
164
            }
165
        }
166
167
        $this->validateMethods($route['method']);
168
    }
169
170
171
    /**
172
     * Sanitizes the routeName for named route:
173
     *
174
     * - replaces '/', ':', '|', '-' with '_'
175
     * - removes special characters
176
     *
177
     *  Algorithm copied from \Silex\Controller->generateRouteName
178
     *  see: https://github.com/silexphp/Silex/blob/1.2/src/Silex/Controller.php
179
     *
180
     * @param string $routeName
181
     * @return string
182
     */
183
    protected function sanitizeRouteName($routeName)
184
    {
185
        if (empty($routeName)) {
186
            //If no routeName is specified,
187
            //we set an empty route name to force the default route name e.g. "GET_myRouteName"
188
            return '';
189
        }
190
191
        $routeName = str_replace(array('/', ':', '|', '-'), '_', $routeName);
192
        $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
193
194
        return $routeName;
195
    }
196
197
    /**
198
     * @param Controller $controller
199
     * @param $actions
200
     * @param $type
201
     * @throws \InvalidArgumentException
202
     */
203
    protected function addActions(Controller $controller, $actions, $type)
204
    {
205
        if (!is_array($actions)){
206
            if ($type === 'before' || $type === 'after') {
207
                $actions = array($actions);
208
            } else {
209
                throw new InvalidArgumentException(
210
                    sprintf(
211
                        'Action %s is not of type Array (%s)',
212
                        $type, gettype($actions)
213
                    )
214
                );
215
            }
216
        }
217
218
        foreach ($actions as $name => $value) {
219
            switch ($type) {
220
                case 'after':
221
                    $this->addBeforeAfterMiddleware($controller, $type, $value);
222
                    break;
223
                case 'before':
224
                    $this->addBeforeAfterMiddleware($controller, $type, $value);
225
                    break;
226
                case 'secure':
227
                    $this->addSecure($controller, $type, $actions);
228
                    break;
229
                default:
230
                    $this->addAction($controller, $name, $value, $type);
231
                    break;
232
            }
233
        }
234
    }
235
236
    /**
237
     * @param Controller $controller
238
     * @param $name
239
     * @param $value
240
     * @param $type
241
     */
242
    protected function addAction(Controller $controller, $name, $value, $type)
243
    {
244
        call_user_func_array(array($controller, $type), array($name, $value));
245
    }
246
247
    /**
248
     * @param Controller $controller
249
     * @param $type
250
     * @param array $values
251
     */
252
    protected function addSecure(Controller $controller, $type, Array $values)
253
    {
254
        call_user_func_array(array($controller, $type), $values);
255
    }
256
257
    protected function isClosure($param)
258
    {
259
        return is_object($param) && is_callable($param);
260
    }
261
262
    /**
263
     * Adds a middleware (before/after)
264
     *
265
     * @param Controller $controller
266
     * @param string $type | 'before' or 'after'
267
     * @param $value
268
     */
269
    protected function addBeforeAfterMiddleware(Controller $controller, $type, $value)
270
    {
271
        $supportedMWTypes = ['before', 'after'];
272
273
        if (!in_array($type, $supportedMWTypes)) {
274
            throw new \UnexpectedValueException(
275
                sprintf(
276
                    'type %s not supported',
277
                    $type
278
                )
279
            );
280
        }
281
282
        if ($this->isClosure($value)) {
283
            //When a closure is provided, we will just load it as a middleware type
284
            $controller->$type($value);
285
        } else {
286
            //In this case a yaml/xml configuration was used
287
            $this->addMiddlewareFromConfig($controller, $type, $value);
288
        }
289
    }
290
291
    /**
292
     * Adds a before/after middleware by its configuration
293
     *
294
     * @param Controller $controller
295
     * @param $type
296
     * @param $value
297
     */
298
    protected function addMiddlewareFromConfig(Controller $controller, $type, $value)
299
    {
300
        if (!is_string($value) || strpos($value, '::') === FALSE) {
301
            throw new InvalidArgumentException(
302
                sprintf(
303
                    '%s is no valid Middleware callback. Please provide the following syntax: NamespaceName\SubNamespaceName\ClassName::methodName',
304
                    $value
305
                )
306
            );
307
        }
308
309
        list($class, $method) = explode('::', $value, 2);
310
311
        if ($class && $method) {
312
313
            if (!method_exists($class, $method)) {
314
                throw new \BadMethodCallException(sprintf('Method "%s::%s" does not exist.', $class, $method));
315
            }
316
317
            $controller->$type([new $class, $method]);
318
        }
319
    }
320
}