Completed
Push — master ( 511324...c26f0f )
by Konstantinos
08:12
created

Controller   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 82.11%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 51
lcom 1
cbo 10
dl 0
loc 411
ccs 101
cts 123
cp 0.8211
rs 8.3206
c 1
b 1
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A getController() 0 7 1
A callAction() 0 8 1
A getDefaultActionName() 0 4 2
A getAction() 0 8 2
A forward() 0 20 3
A setup() 0 3 1
A cleanup() 0 3 1
B callMethod() 0 21 5
C getObjectFromParameters() 0 40 11
B findModelInParameters() 0 20 5
A renderDefault() 0 6 1
A handleReturnValue() 0 17 4
A getName() 0 4 1
A getQueryBuilder() 0 7 2
A generate() 0 4 1
A getRequest() 0 4 1
A getMe() 0 4 1
A isDebug() 0 4 1
A getLogger() 0 8 2
A getLogChannel() 0 4 1
A dispatch() 0 4 1
A render() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file contains the skeleton for all of the controllers
5
 *
6
 * @package    BZiON\Controllers
7
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
8
 */
9
10
use BZIon\Debug\Debug;
11
use Symfony\Component\DependencyInjection\ContainerAware;
12
use Symfony\Component\EventDispatcher\Event;
13
use Symfony\Component\HttpFoundation\ParameterBag;
14
use Symfony\Component\HttpFoundation\Response;
15
16
/**
17
 * The Controller class represents a bunch of pages relating to the same
18
 * subject (Model in most cases) - for example, there is a PlayerController,
19
 * a TeamController and a HomeController.
20
 *
21
 * Controllers contain special methods called 'actions', which are essentially
22
 * different pages performing different actions - for example, the
23
 * TeamController might contain a 'show' action, which renders the team's page,
24
 * and a 'new' action, which renders the page that is shown to the user when
25
 * they want to create a new team.
26
 *
27
 * Actions have some unique characteristics. Take a look at this sample action:
28
 *
29
 * <pre><code>public function showAction(Request $request, Team $team) {
30
 *   return array('team' => $team);
31
 * }
32
 * </code></pre>
33
 *
34
 * The following route will make sure that `showAction()` handles the request:
35
 *
36
 * <pre><code>team_show:
37
 *   pattern:    /teams/{team}
38
 *   defaults: { _controller: 'Team', _action: 'show' }
39
 * </code></pre>
40
 *
41
 * First of all, the method's name should end with `Action`. The parameters
42
 * are passed dynamically, and the order is insignificant.
43
 *
44
 * You can request Symfony's Request or Session class, or even a model, which
45
 * will be generated based on the route parameters. For example, the route
46
 * pattern `/posts/{post}/comments/{commentId}` (note how you can use both
47
 * `comment` and `commentId` as parameters - just make sure to use the correct
48
 * variable name on the method later) and can be used with actions like these:
49
 *
50
 * <code>
51
 * public function sampleAction
52
 *   (Request $request, NewsArticle $post, Comment $comment)
53
 * </code>
54
 *
55
 * <code>
56
 * public function sampleAction
57
 *   (NewsArticle $post, Session $session, Request $request, Comment $comment)
58
 * </code>
59
 *
60
 * A method's return value can be:
61
 * - Symfony's Response Class
62
 * - A string representing the text you want the user to see
63
 * - An array representing the variables you want to pass to the controller's
64
 *   view, so that it can be rendered
65
 *
66
 * @package BZiON\Controllers
67
 */
68
abstract class Controller extends ContainerAware
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Depend...njection\ContainerAware has been deprecated with message: since version 2.8, to be removed in 3.0. Use the ContainerAwareTrait instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
69
{
70
    /**
71
     * Parameters specified by the route
72
     * @var ParameterBag
73
     */
74
    protected $parameters;
75
76
    /**
77
     * The first controller that was invoked
78
     * @var Controller
79
     */
80
    protected $parent;
81
82
    /*
83
     * An array of data to pass between different parts of the application
84
     *
85
     * @var ParameterBag
86
     */
87
    public $data;
88
89
    /**
90
     * @param ParameterBag    $parameters The array returned by $request->attributes
91
     * @param Controller|null $parent     The controller who invoked this controller
92
     */
93 1
    public function __construct($parameters, Controller $parent = null)
94
    {
95 1
        $this->parameters = $parameters;
96 1
        $this->parent = $parent ?: $this;
97 1
        $this->data = new ParameterBag();
98
99 1
        $this->setContainer(Service::getContainer());
100 1
    }
101
102
    /**
103
     * Returns the controller that is assigned to a route
104
     *
105
     * @param  ParameterBag $parameters The array returned by $request->attributes
106
     * @return Controller   The controller
107
     */
108 1
    public static function getController($parameters)
109
    {
110 1
        $ref = new ReflectionClass($parameters->get('_controller') . 'Controller');
111 1
        $controller = $ref->newInstance($parameters);
112
113 1
        return $controller;
114
    }
115
116
    /**
117
     * Call the controller's action specified by the $parameters array
118
     *
119
     * @param  string|null $action The action name to call (e.g. `show`), null to invoke the default one
120
     * @return Response    The action's response
121
     */
122 1
    public function callAction($action = null)
123
    {
124 1
        $this->setup();
125 1
        $response = $this->forward($action);
126 1
        $this->cleanup();
127
128 1
        return $response->prepare($this->getRequest());
129
    }
130
131
    /**
132
     * Get the controller's default action name
133
     *
134
     * @return string The action's name without the `Action` suffix
135
     */
136 1
    protected function getDefaultActionName()
137
    {
138 1
        return $this->parameters->get('_action') ?: 'default';
139
    }
140
141
    /**
142
     * Get a controller's action
143
     *
144
     * @param  string|null       $action The action name to call (e.g. `show`), null to invoke the default one
145
     * @return \ReflectionMethod The action method
146
     */
147 1
    public function getAction($action = null)
148
    {
149 1
        if (!$action) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
150
            $action = $this->getDefaultActionName();
151
        }
152
153 1
        return new ReflectionMethod($this, $action . 'Action');
154
    }
155
156
    /**
157
     * Forward the request to another action
158
     *
159
     * Please note that this doesn't generate an HTTP redirect, but an
160
     * internal one - the user sees the original URL, but a different page
161
     *
162
     * @param  string $action The action to forward the request to
163
     * @param  array  $params An additional associative array of parameters to
164
     *                        provide to the action
165
     * @param  string|null $controllerName The name of the controller of the
166
     *                                     action, without the 'Controller'
167
     *                                     suffix (defaults to the current
168
     *                                     controller)
169
     * @return Response
170
     */
171 1
    protected function forward($action, $params = array(), $controllerName = null)
172
    {
173 1
        if (!$action) {
174 1
            $action = $this->getDefaultActionName();
175
        }
176
177 1
        $args = clone $this->parameters;
178 1
        $args->add($params);
179
180 1
        if ($controllerName === null) {
181 1
            $controller = $this;
182
        } else {
183
            $ref = new ReflectionClass($controllerName . 'Controller');
184
            $controller = $ref->newInstance($args, $this->parent);
185
        }
186
187 1
        $ret = $controller->callMethod($controller->getAction($action), $args);
188
189 1
        return $controller->handleReturnValue($ret, $action);
190
    }
191
192
    /**
193
     * Method that will be called before any action
194
     *
195
     * @return void
196
     */
197 1
    public function setup()
198
    {
199 1
    }
200
201
    /**
202
     * Method that will be called after all actions
203
     *
204
     * @return void
205
     */
206 1
    public function cleanup()
207
    {
208 1
    }
209
210
    /**
211
     * Call one of the controller's methods
212
     *
213
     * The arguments are passed dynamically to the method, based on its
214
     * definition - check the description of the Controller class for more
215
     * information
216
     *
217
     * @param  \ReflectionMethod $method     The method
218
     * @param  ParameterBag      $parameters The parameter bag representing the route's parameters
219
     * @return mixed             The return value of the called method
220
     */
221 1
    protected function callMethod($method, $parameters)
222
    {
223 1
        $params = array();
224
225 1
        foreach ($method->getParameters() as $p) {
226 1
            if ($model = $this->getObjectFromParameters($p, $parameters)) {
227 1
                $params[] = $model;
228 1
            } elseif ($parameters->has($p->name)) {
229
                $params[] = $parameters->get($p->name);
230 1
            } elseif ($p->isOptional()) {
231 1
                $params[] = $p->getDefaultValue();
232
            } else {
233
                throw new MissingArgumentException(
234
                    "Missing parameter '$p->name' for " . $this->getName() . "::"
235 1
                    . $method->getName() . "()"
236
                );
237
            }
238
        }
239
240 1
        return $method->invokeArgs($this, $params);
241
    }
242
243
    /**
244
     * Find what to pass as an argument on an action
245
     *
246
     * @param ReflectionParameter $modelParameter  The model's parameter we want to investigate
247
     * @param ParameterBag        $routeParameters The route's parameters
248
     */
249 1
    protected function getObjectFromParameters($modelParameter, $routeParameters)
250
    {
251 1
        $refClass = $modelParameter->getClass();
252 1
        $paramName  = $modelParameter->getName();
253
254 1
        if ($refClass !== null && $refClass->isSubclassOf("Model")) {
255
            // Look for the object's ID/slugs in the routeParameters array
256 1
            $model = $this->findModelInParameters($modelParameter, $routeParameters);
257
258 1
            if ($model !== null) {
259 1
                return $model;
260
            }
261
        }
262
263
        // $me -> currently logged in user
264 1
        if ($paramName == "me") {
265 1
            return self::getMe();
266
        }
267
268 1
        if ($refClass === null) {
269
            // No class provived by the method's definition, we don't know
270
            // what we should pass
271
            return null;
272
        }
273
274 1
        switch ($refClass->getName()) {
275 1
            case 'Symfony\Component\HttpFoundation\Request':
276 1
                return $this->getRequest();
277 1
            case 'Symfony\Component\HttpFoundation\Session\Session':
278 1
                return $this->getRequest()->getSession();
279 1
            case 'Symfony\Component\HttpFoundation\Session\Flash\FlashBag':
280
                return $this->getRequest()->getSession()->getFlashBag();
281 1
            case 'Monolog\Logger':
282
                return $this->getLogger();
283 1
            case 'Symfony\Component\Form\FormFactory':
284
                return Service::getFormFactory();
285
        }
286
287 1
        return null;
288
    }
289
290
    /**
291
     * Try locating a method's parameter in an array
292
     *
293
     * @param  ReflectionParameter $modelParameter  The model's parameter we want to investigate
294
     * @param  ParameterBag        $routeParameters The route's parameters
295
     * @return Model|null          A Model or null if it couldn't be found
296
     */
297 1
    protected function findModelInParameters($modelParameter, $routeParameters)
298
    {
299 1
        $refClass = $modelParameter->getClass();
300 1
        $paramName  = $modelParameter->getName();
301
302 1
        if ($routeParameters->has($paramName)) {
303 1
            $parameter = $routeParameters->get($paramName);
304 1
            if (is_object($parameter) && $refClass->getName() === get_class($parameter)) {
305
                // The model has already been instantiated - we don't need to do anything
306
                return $parameter;
307
            }
308
309 1
            return $refClass->getMethod("fetchFromSlug")->invoke(null, $parameter);
310
        }
311
312 1
        if ($routeParameters->has($paramName . 'Id')) {
313
            return $refClass->getMethod('get')
314
                            ->invoke(null, $routeParameters->get($paramName . 'Id'));
315
        }
316 1
    }
317
318
    /**
319
     * Render the action's template
320
     * @param  array  $params The variables to pass to the template
321
     * @param  string $action The controller's action
322
     * @return string The content
323
     */
324 1
    protected function renderDefault($params, $action)
325
    {
326 1
        $templatePath = $this->getName() . "/$action.html.twig";
327
328 1
        return $this->render($templatePath, $params);
329
    }
330
331
    /**
332
     * Get a Response from the return value of an action
333
     * @param  mixed    $return Whatever the method returned
334
     * @param  string   $action The name of the action
335
     * @return Response The response that the controller wants us to send to the client
336
     */
337 1
    protected function handleReturnValue($return, $action)
338
    {
339 1
        if ($return instanceof Response) {
340 1
            return $return;
341
        }
342
343 1
        $content = null;
344 1
        if (is_array($return)) {
345
            // The controller is probably expecting us to show a view to the
346
            // user, using the array provided to set variables for the template
347 1
            $content = $this->renderDefault($return, $action);
348
        } elseif (is_string($return)) {
349 1
            $content = $return;
350
        }
351
352 1
        return new Response($content);
353
    }
354
355
    /**
356
     * Returns the name of the controller without the "Controller" part
357
     * @return string
358
     */
359 1
    public static function getName()
360
    {
361 1
        return preg_replace('/Controller$/', '', get_called_class());
362
    }
363
364
    /**
365
     * Returns a configured QueryBuilder for the corresponding model
366
     *
367
     * The returned QueryBuilder will only show models visible to the currently
368
     * logged in user
369
     *
370
     * @param  string|null The model whose query builder we should get (null
371
     *                     to get the builder of the controller's model)
372
     * @param string $type
373
     * @return QueryBuilder
374
     */
375 1
    public static function getQueryBuilder($type = null)
376
    {
377 1
        $type = ($type) ?: static::getName();
378
379 1
        return $type::getQueryBuilder()
380 1
            ->visibleTo(static::getMe(), static::getRequest()->get('showDeleted'));
381
    }
382
383
     /**
384
      * Generates a URL from the given parameters.
385
      * @param  string  $name       The name of the route
386
      * @param  mixed   $parameters An array of parameters
387
      * @param  bool $absolute   Whether to generate an absolute URL
388
      * @return string  The generated URL
389
      */
390
     public static function generate($name, $parameters = array(), $absolute = false)
391
     {
392
         return Service::getGenerator()->generate($name, $parameters, $absolute);
0 ignored issues
show
Documentation introduced by
$absolute is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
393
     }
394
395
    /**
396
     * Gets the browser's request
397
     * @return Symfony\Component\HttpFoundation\Request
398
     */
399 1
    public static function getRequest()
400
    {
401 1
        return Service::getRequest();
402
    }
403
404
    /**
405
     * Gets the currently logged in player
406
     *
407
     * If the user is not logged in, a Player object that is invalid will be
408
     * returned
409
     *
410
     * @return Player
411
     */
412 1
    public static function getMe()
413
    {
414 1
        return Player::get(self::getRequest()->getSession()->get('playerId'));
415
    }
416
417
    /**
418
     * Find out whether debugging is enabled
419
     *
420
     * @return bool
421
     */
422 1
    public function isDebug()
423
    {
424 1
        return $this->container->getParameter('kernel.debug');
425
    }
426
427
    /**
428
     * Gets the monolog logger
429
     *
430
     * @param  string         $channel The log channel, defaults to the Controller's default
431
     * @return Monolog\Logger
432
     */
433
    protected static function getLogger($channel = null)
434
    {
435
        if (!$channel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $channel of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
436
            $channel = static::getLogChannel();
437
        }
438
439
        return Service::getContainer()->get("monolog.logger.$channel");
440
    }
441
442
    /**
443
     * Gets the logging channel for monolog
444
     * @return string
445
     */
446
    protected static function getLogChannel()
447
    {
448
        return 'app';
449
    }
450
451
    /**
452
     * Uses symfony's dispatcher to announce an event
453
     * @param  string $eventName The name of the event to dispatch.
454
     * @param  Event  $event     The event to pass to the event handlers/listeners.
455
     * @return Event
456
     */
457 1
    protected function dispatch($eventName, Event $event = null)
458
    {
459 1
        return $this->container->get('event_dispatcher')->dispatch($eventName, $event);
460
    }
461
462
    /**
463
     * Renders a view
464
     * @param  string $view       The view name
465
     * @param  array  $parameters An array of parameters to pass to the view
466
     * @return string The rendered view
467
     */
468 1
    protected function render($view, $parameters = array())
469
    {
470 1
        Debug::startStopwatch('view.render');
471
472 1
        $ret = $this->container->get('twig')->render($view, $parameters);
473
474 1
        Debug::finishStopwatch('view.render');
475
476 1
        return $ret;
477
    }
478
}
479
480
class MissingArgumentException extends Exception
481
{
482
}
483