ControllerHandler::performRoute()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 9
ccs 7
cts 7
cp 1
crap 1
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
namespace Vectorface\SnappyRouter\Handler;
4
5
use \Exception;
6
use FastRoute\Dispatcher;
7
use Vectorface\SnappyRouter\Controller\AbstractController;
8
use Vectorface\SnappyRouter\Encoder\EncoderInterface;
9
use Vectorface\SnappyRouter\Encoder\NullEncoder;
10
use Vectorface\SnappyRouter\Encoder\TwigViewEncoder;
11
use Vectorface\SnappyRouter\Exception\ResourceNotFoundException;
12
use Vectorface\SnappyRouter\Request\HttpRequest;
13
use Vectorface\SnappyRouter\Response\AbstractResponse;
14
use Vectorface\SnappyRouter\Response\Response;
15
16
/**
17
 * Handles MVC requests mapping URIs like /controller/action/param1/param2/...
18
 * to its corresponding controller action.
19
 * @copyright Copyright (c) 2014, VectorFace, Inc.
20
 * @author Dan Bruce <[email protected]>
21
 */
22
class ControllerHandler extends PatternMatchHandler
23
{
24
    /** Options key for the base path */
25
    const KEY_BASE_PATH = 'basePath';
26
    /** Options key for the view config */
27
    const KEY_VIEWS = 'views';
28
    /** Options key for the view path config */
29
    const KEY_VIEWS_PATH = 'path';
30
31
    /** The current web request */
32
    protected $request;
33
    /** The current encoder */
34
    protected $encoder;
35
    /** The current route parameters */
36
    protected $routeParams;
37
38
    /** Constants indicating the type of route */
39
    const MATCHES_NOTHING = 0;
40
    const MATCHES_CONTROLLER = 1;
41
    const MATCHES_ACTION = 2;
42
    const MATCHES_CONTROLLER_AND_ACTION = 3;
43
    const MATCHES_PARAMS = 4;
44
    const MATCHES_CONTROLLER_ACTION_AND_PARAMS = 7;
45
46
    /** controller route pattern */
47
    const ROUTE_PATTERN_CONTROLLER = '{controller:[a-zA-Z]\w*}';
48
49
    /** action route pattern */
50
    const ROUTE_PATTERN_ACTION = '{action:[a-zA-Z]\w*}';
51
52
    /** URL parameters route pattern */
53
    const ROUTE_PATTERN_PARAMS = '{params:.+}';
54
55
    /**
56
     * Returns true if the handler determines it should handle this request and false otherwise.
57
     * @param string $path The URL path for the request.
58
     * @param array $query The query parameters.
59
     * @param array $post The post data.
60
     * @param string $verb The HTTP verb used in the request.
61
     * @return boolean Returns true if this handler will handle the request and false otherwise.
62
     */
63 29
    public function isAppropriate($path, $query, $post, $verb)
64
    {
65
        // remove the leading base path option if present
66 29
        $options = $this->getOptions();
67 29
        $path = $this->extractPathFromBasePath($path, $options);
68
69
        // extract the controller, action and route parameters if present
70
        // and fall back to defaults when not present
71 29
        $controller = 'index';
72 29
        $action = 'index';
73 29
        $this->routeParams = array();
74 29
        $routeInfo = $this->getRouteInfo($verb, $path);
75
        // ensure the path matches at least one of the routes
76 29
        if (Dispatcher::FOUND !== $routeInfo[0]) {
77 2
            return false;
78
        }
79
80 27
        if ($routeInfo[1] & self::MATCHES_CONTROLLER) {
81 27
            $controller = strtolower($routeInfo[2]['controller']);
82 27
            if ($routeInfo[1] & self::MATCHES_ACTION) {
83 14
                $action = strtolower($routeInfo[2]['action']);
84 14
                if ($routeInfo[1] & self::MATCHES_PARAMS) {
85 1
                    $this->routeParams = explode('/', $routeInfo[2]['params']);
86
                }
87
            }
88
        }
89
90
        // configure the default view encoder
91 27
        $this->configureViewEncoder($options, $controller, $action);
92
93
        // configure the request object
94 26
        $this->request = new HttpRequest(
95 26
            ucfirst($controller).'Controller',
96 26
            $action.'Action',
97 26
            $verb,
98 26
            'php://input'
99
        );
100
101 26
        $this->request->setQuery($query);
102 26
        $this->request->setPost($post);
103
104
        // return that we will handle this request
105 26
        return true;
106
    }
107
108
    /**
109
     * Performs the actual routing.
110
     * @return string Returns the result of the route.
111
     */
112 12
    public function performRoute()
113
    {
114 12
        $controller = null;
115 12
        $action = null;
116 12
        $this->determineControllerAndAction($controller, $action);
117 10
        $response = $this->invokeControllerAction($controller, $action);
118 8
        \Vectorface\SnappyRouter\http_response_code($response->getStatusCode());
119 8
        return $this->getEncoder()->encode($response);
120
    }
121
122
    /**
123
     * Returns a request object extracted from the request details (path, query, etc). The method
124
     * isAppropriate() must have returned true, otherwise this method should return null.
125
     * @return \Vectorface\SnappyRouter\Request\HttpRequest|null Returns a
126
     *         Request object or null if this handler is not appropriate.
127
     */
128 18
    public function getRequest()
129
    {
130 18
        return $this->request;
131
    }
132
133
    /**
134
     * Returns the active response encoder.
135
     * @return EncoderInterface Returns the response encoder.
136
     */
137 11
    public function getEncoder()
138
    {
139 11
        return $this->encoder;
140
    }
141
142
    /**
143
     * Sets the encoder to be used by this handler (overriding the default).
144
     * @param EncoderInterface $encoder The encoder to be used.
145
     * @return ControllerHandler Returns $this.
146
     */
147 2
    public function setEncoder(EncoderInterface $encoder)
148
    {
149 2
        $this->encoder = $encoder;
150 2
        return $this;
151
    }
152
153
    /**
154
     * Returns the new path with the base path extracted.
155
     * @param string $path The full path.
156
     * @param array $options The array of options.
157
     * @return string Returns the new path with the base path removed.
158
     */
159 29
    protected function extractPathFromBasePath($path, $options)
160
    {
161 29
        if (isset($options[self::KEY_BASE_PATH])) {
162 14
            $pos = strpos($path, $options[self::KEY_BASE_PATH]);
163 14 View Code Duplication
            if (false !== $pos) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
164 14
                $path = substr($path, $pos + strlen($options[self::KEY_BASE_PATH]));
165
            }
166
        }
167
        // ensure the path has a leading slash
168 29
        if (empty($path) || $path[0] !== '/') {
169 14
            $path = '/'.$path;
170
        }
171 29
        return $path;
172
    }
173
174
    /**
175
     * Determines the exact controller instance and action name to be invoked
176
     * by the request.
177
     * @param mixed $controller The controller passed by reference.
178
     * @param mixed $actionName The action name passed by reference.
179
     */
180 12
    private function determineControllerAndAction(&$controller, &$actionName)
181
    {
182 12
        $request = $this->getRequest();
183 12
        $this->invokePluginsHook(
184 12
            'beforeControllerSelected',
185 12
            array($this, $request)
186
        );
187
188 12
        $controllerDiKey = $request->getController();
189
        try {
190 12
            $controller = $this->getServiceProvider()->getServiceInstance($controllerDiKey);
191 1
        } catch (Exception $e) {
192 1
            throw new ResourceNotFoundException(sprintf(
193 1
                'No such controller found "%s".',
194 1
                $this->getRequest()->getController()
195
            ));
196
        }
197 11
        $actionName = $request->getAction();
198 11
        if (!method_exists($controller, $actionName)) {
199 1
            throw new ResourceNotFoundException(sprintf(
200 1
                '%s does not have method %s',
201 1
                $controllerDiKey,
202 1
                $actionName
203
            ));
204
        }
205 10
        $this->invokePluginsHook(
206 10
            'afterControllerSelected',
207 10
            array($this, $request, $controller, $actionName)
208
        );
209 10
        $controller->initialize($request, $this);
210 10
    }
211
212
    /**
213
     * Invokes the actual controller action and returns the response.
214
     * @param AbstractController $controller The controller to use.
215
     * @param string $action The action to invoke.
216
     * @return AbstractResponse Returns the response from the action.
217
     */
218 10
    protected function invokeControllerAction(AbstractController $controller, $action)
219
    {
220 10
        $this->invokePluginsHook(
221 10
            'beforeActionInvoked',
222 10
            array($this, $this->getRequest(), $controller, $action)
223
        );
224 10
        $response = $controller->$action($this->routeParams);
225 8
        if (null === $response) {
226
            // if the action returns null, we simply render the default view
227 2
            $response = array();
228 6
        } elseif (!is_string($response)) {
229
            // if they don't return a string, try to use whatever is returned
230
            // as variables to the view renderer
231 1
            $response = (array)$response;
232
        }
233
234
        // merge the response variables with the existing view context
235 8
        if (is_array($response)) {
236 3
            $response = array_merge($controller->getViewContext(), $response);
237
        }
238
239
        // whatever we have as a response needs to be encapsulated in an
240
        // AbstractResponse object
241 8
        if (!($response instanceof AbstractResponse)) {
242 8
            $response = new Response($response);
243
        }
244 8
        $this->invokePluginsHook(
245 8
            'afterActionInvoked',
246 8
            array($this, $this->getRequest(), $controller, $action, $response)
247
        );
248 8
        return $response;
249
    }
250
251
    /**
252
     * Configures the view encoder based on the current options.
253
     * @param array $options The current options.
254
     * @param string $controller The controller to use for the default view.
255
     * @param string $action The action to use for the default view.
256
     */
257 27
    private function configureViewEncoder($options, $controller, $action)
258
    {
259
        // configure the view encoder if they specify a view option
260 27
        if (isset($options[self::KEY_VIEWS])) {
261 8
            $this->encoder = new TwigViewEncoder(
262 8
                $options[self::KEY_VIEWS],
263 8
                sprintf('%s/%s.twig', $controller, $action)
264
            );
265
        } else {
266 19
            $this->encoder = new NullEncoder();
267
        }
268 26
    }
269
270
    /**
271
     * Returns the array of routes.
272
     * @return array The array of routes.
273
     */
274 19
    protected function getRoutes()
275
    {
276 19
        $c = self::ROUTE_PATTERN_CONTROLLER;
277 19
        $a = self::ROUTE_PATTERN_ACTION;
278 19
        $p = self::ROUTE_PATTERN_PARAMS;
279
        return array(
280 19
            "/" => self::MATCHES_NOTHING,
281 19
            "/$c" => self::MATCHES_CONTROLLER,
282 19
            "/$c/" => self::MATCHES_CONTROLLER,
283 19
            "/$c/$a" => self::MATCHES_CONTROLLER_AND_ACTION,
284 19
            "/$c/$a/" => self::MATCHES_CONTROLLER_AND_ACTION,
285 19
            "/$c/$a/$p" => self::MATCHES_CONTROLLER_ACTION_AND_PARAMS
286
        );
287
    }
288
}
289