Completed
Push — master ( f451ed...7bd00e )
by Marco
02:07
created

RoutingServiceProvider::addRoute()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
cc 6
eloc 16
nc 18
nop 3
1
<?php
2
namespace MJanssen\Provider;
3
4
use InvalidArgumentException;
5
use MJanssen\Route\Name;
6
use MJanssen\Route\Route;
7
use RuntimeException;
8
use Silex\Application;
9
use Silex\Controller;
10
use Pimple\ServiceProviderInterface;
11
use Pimple\Container;
12
use Silex\Api\BootableProviderInterface;
13
use Silex\Api\EventListenerProviderInterface;
14
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15
16
/**
17
 * @package MJanssen\Provider
18
 */
19
class RoutingServiceProvider implements
20
    ServiceProviderInterface,
21
    BootableProviderInterface,
22
    EventListenerProviderInterface
23
{
24
    /**
25
     * @var string
26
     */
27
    protected $routingContainerId;
28
29
    /**
30
     * @param string $routingContainerId
31
     */
32
    public function __construct($routingContainerId = 'config.routes')
33
    {
34
        $this->routingContainerId = $routingContainerId;
35
    }
36
37
    /**
38
     * @param Container $container
39
     */
40
    public function register(Container $container)
41
    {
42
        if (!$container->offsetExists($this->routingContainerId)) {
43
            throw new RuntimeException('Routing container id not set');
44
        }
45
46
        $routes = $container->offsetGet($this->routingContainerId);
47
48
        if (!is_array($routes)) {
49
            throw new InvalidArgumentException(
50
                sprintf('Supplied routes in container with id %s must be of type Array', $this->routingContainerId)
51
            );
52
        }
53
54
        $this->addRoutes($container, $routes);
55
    }
56
57
    /**
58
     * @param Application $app
59
     * @codeCoverageIgnore
60
     */
61
    public function boot(Application $app)
62
    {
63
    }
64
65
    /**
66
     * @param Container $container
67
     * @param EventDispatcherInterface $dispatcher
68
     * @codeCoverageIgnore
69
     */
70
    public function subscribe(Container $container, EventDispatcherInterface $dispatcher)
71
    {
72
    }
73
74
    /**
75
     * Add routes
76
     *
77
     * @param Container $container
78
     * @param array $routes
79
     */
80
    public function addRoutes(Container $container, array $routes)
81
    {
82
        foreach ($routes as $name => $route) {
83
            $this->addRoute($container, $route, $name);
84
        }
85
    }
86
87
    /**
88
     * Adds a route, a given route-name (for named routes) and all of its methods
89
     *
90
     * @param Container $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
91
     * @param array $route
92
     * @throws InvalidArgumentException
93
     */
94
    public function addRoute(Container $container, array $route, $name = '')
95
    {
96
        $route2 = Route::fromArray($route);
97
98
        if ($route2->getName()) {
99
            $name = $route2->getName();
100
        }
101
102
        $name = new Name($name);
103
104
        $controller = $container->match($route2->getPattern(), $route2->getController())
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...
105
            ->bind((string) $name)
106
            ->method(
107
                join('|', array_map('strtoupper', $route2->getMethods()))
108
            );
109
110
        $supportedProperties = array('value', 'assert', 'convert', 'before', 'after');
111
        foreach ($supportedProperties AS $property) {
112
            if (isset($route[$property])) {
113
                $this->addActions($controller, $route[$property], $property);
114
            }
115
        }
116
117
        if (isset($route['scheme'])) {
118
            if ('https' === $route['scheme']) {
119
                $controller->requireHttps();
120
            }
121
        }
122
    }
123
124
    /**
125
     * Sanitizes the routeName for named route:
126
     *
127
     * - replaces '/', ':', '|', '-' with '_'
128
     * - removes special characters
129
     *
130
     *  Algorithm copied from \Silex\Controller->generateRouteName
131
     *  see: https://github.com/silexphp/Silex/blob/1.2/src/Silex/Controller.php
132
     *
133
     * @param string $routeName
134
     * @return string
135
     */
136
    protected function sanitizeRouteName($routeName)
137
    {
138
        if (empty($routeName)) {
139
            //If no routeName is specified,
140
            //we set an empty route name to force the default route name e.g. "GET_myRouteName"
141
            return '';
142
        }
143
144
        $routeName = str_replace(array('/', ':', '|', '-'), '_', $routeName);
145
        $routeName = preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
146
147
        return $routeName;
148
    }
149
150
    /**
151
     * @param Controller $controller
152
     * @param $actions
153
     * @param $type
154
     * @throws \InvalidArgumentException
155
     */
156
    protected function addActions(Controller $controller, $actions, $type)
157
    {
158
        if (!is_array($actions)){
159
            if ($type === 'before' || $type === 'after') {
160
                $actions = array($actions);
161
            } else {
162
                throw new InvalidArgumentException(
163
                    sprintf(
164
                        'Action %s is not of type Array (%s)',
165
                        $type, gettype($actions)
166
                    )
167
                );
168
            }
169
        }
170
171
        foreach ($actions as $name => $value) {
172
            switch ($type) {
173
                case 'after':
174
                    $this->addBeforeAfterMiddleware($controller, $type, $value);
175
                    break;
176
                case 'before':
177
                    $this->addBeforeAfterMiddleware($controller, $type, $value);
178
                    break;
179
                default:
180
                    $this->addAction($controller, $name, $value, $type);
181
                    break;
182
            }
183
        }
184
    }
185
186
    /**
187
     * @param Controller $controller
188
     * @param $name
189
     * @param $value
190
     * @param $type
191
     */
192
    protected function addAction(Controller $controller, $name, $value, $type)
193
    {
194
        call_user_func_array(array($controller, $type), array($name, $value));
195
    }
196
197
    protected function isClosure($param)
198
    {
199
        return is_object($param) && ($param instanceof \Closure);
200
    }
201
202
    /**
203
     * Adds a middleware (before/after)
204
     *
205
     * @param Controller $controller
206
     * @param string $type | 'before' or 'after'
207
     * @param $value
208
     */
209
    protected function addBeforeAfterMiddleware(Controller $controller, $type, $value)
210
    {
211
        $supportedMWTypes = ['before', 'after'];
212
213
        if (!in_array($type, $supportedMWTypes)) {
214
            throw new \UnexpectedValueException(
215
                sprintf(
216
                    'type %s not supported',
217
                    $type
218
                )
219
            );
220
        }
221
222
        if ($this->isClosure($value)) {
223
            //When a closure is provided, we will just load it as a middleware type
224
            $controller->$type($value);
225
        } else {
226
            //In this case a yaml/xml configuration was used
227
            $this->addMiddlewareFromConfig($controller, $type, $value);
228
        }
229
    }
230
231
    /**
232
     * Adds a before/after middleware by its configuration
233
     *
234
     * @param Controller $controller
235
     * @param $type
236
     * @param $value
237
     */
238
    protected function addMiddlewareFromConfig(Controller $controller, $type, $value)
239
    {
240
        if (!is_string($value) || strpos($value, '::') === FALSE) {
241
            throw new InvalidArgumentException(
242
                sprintf(
243
                    '%s is no valid Middleware callback. Please provide the following syntax: NamespaceName\SubNamespaceName\ClassName::methodName',
244
                    $value
245
                )
246
            );
247
        }
248
249
        list($class, $method) = explode('::', $value, 2);
250
251
        if ($class && $method) {
252
253
            if (!method_exists($class, $method)) {
254
                throw new \BadMethodCallException(sprintf('Method "%s::%s" does not exist.', $class, $method));
255
            }
256
257
            $controller->$type([new $class, $method]);
258
        }
259
    }
260
}
261