OSDN Git Service

Router::match throws Exception when no match
[php-libraries/Router.git] / src / Router.php
1 <?php
2
3 namespace PHPRouter;
4
5 use Exception;
6 use Fig\Http\Message\RequestMethodInterface;
7
8 /**
9  * Routing class to match request URL's against given routes and map
10  * them to a controller action.
11  */
12 class Router
13 {
14     /**
15      * RouteCollection that holds all Route objects
16      *
17      * @var RouteCollection
18      */
19     private $routes = array();
20
21     /**
22      * Array to store named routes in, used for reverse routing.
23      * @var array
24      */
25     private $namedRoutes = array();
26
27     /**
28      * The base REQUEST_URI. Gets prepended to all route url's.
29      * @var string
30      */
31     private $basePath = '';
32
33     /**
34      * @param RouteCollection $collection
35      */
36     public function __construct(RouteCollection $collection)
37     {
38         $this->routes = $collection;
39
40         foreach ($this->routes->all() as $route) {
41             $name = $route->getName();
42             if (null !== $name) {
43                 $this->namedRoutes[$name] = $route;
44             }
45         }
46     }
47
48     private function getRequestUrlAndMethod()
49     {
50         $requestMethod = (
51             isset($_POST['_method']) &&
52             ($_method = strtoupper($_POST['_method'])) &&
53             in_array($_method, array(RequestMethodInterface::METHOD_PUT, RequestMethodInterface::METHOD_DELETE), true)
54         ) ? $_method : $_SERVER['REQUEST_METHOD'];
55
56         $requestUrl = $_SERVER['REQUEST_URI'];
57
58         // strip GET variables from URL
59         if (($pos = strpos($requestUrl, '?')) !== false) {
60             $requestUrl = substr($requestUrl, 0, $pos);
61         }
62
63         return array(
64             $requestMethod,
65             $requestUrl,
66         );
67     }
68
69     /**
70      * Set the base _url - gets prepended to all route _url's.
71      *
72      * @param $basePath
73      */
74     public function setBasePath($basePath)
75     {
76         $this->basePath = rtrim($basePath, '/');
77     }
78
79     /**
80      * check if the request has a valid route
81      *
82      * @return bool
83      */
84     public function requestHasValidRoute()
85     {
86         list($requestMethod, $requestUrl) = $this->getRequestUrlAndMethod();
87
88         /** @var Route $route */
89         list($route,) = $this->findRoute($requestUrl, $requestMethod);
90
91         if ($route === null) {
92             return false;
93         }
94
95         $controller = $route->getValidController();
96
97         return $controller !== null;
98     }
99
100     /**
101      * Matches the current request against mapped routes
102      */
103     public function matchCurrentRequest()
104     {
105         list($requestMethod, $requestUrl) = $this->getRequestUrlAndMethod();
106
107         return $this->match($requestUrl, $requestMethod);
108     }
109
110     private function findRoute($requestUrl, $requestMethod)
111     {
112         $currentDir = dirname($_SERVER['SCRIPT_NAME']);
113         $foundRoute = null;
114         $params     = array();
115
116         // must be unit testing
117         if ($currentDir === "." || "..") {
118             $currentDir = "";
119         }
120
121         foreach ($this->routes->all() as $routes) {
122             // compare server request method with route's allowed http methods
123             if (!in_array($requestMethod, (array)$routes->getMethods(), true)) {
124                 continue;
125             }
126
127             if ('/' !== $currentDir) {
128                 $requestUrl = str_replace($currentDir, '', $requestUrl);
129             }
130
131             $route   = rtrim($routes->getRegex(), '/');
132             $pattern = '@^' . preg_quote($this->basePath) . $route . '/?$@i';
133
134             if (!preg_match($pattern, $requestUrl, $matches)) {
135                 continue;
136             }
137
138             if (preg_match_all('/:([\w-%]+)/', $routes->getUrl(), $argument_keys)) {
139                 // grab array with matches
140                 $argument_keys = $argument_keys[1];
141
142                 // check arguments number
143
144                 if (count($argument_keys) !== (count($matches) - 1)) {
145                     continue;
146                 }
147
148                 // loop trough parameter names, store matching value in $params array
149                 foreach ($argument_keys as $key => $name) {
150                     if (isset($matches[$key + 1])) {
151                         $params[$name] = $matches[$key + 1];
152                     }
153                 }
154             }
155
156             $foundRoute = $routes;
157             break;
158         }
159
160         /** @var Route $foundRoute */
161         /** @var array $params */
162         return array(
163             $foundRoute,
164             $params,
165         );
166     }
167
168     public function getRequestRoute()
169     {
170         list($requestMethod, $requestUrl) = $this->getRequestUrlAndMethod();
171
172         return $this->getRoute($requestUrl, $requestMethod);
173     }
174
175     public function getRoute($requestUrl, $requestMethod = RequestMethodInterface::METHOD_GET)
176     {
177         /** @var Route $route */
178         list($route, $params) = $this->findRoute($requestUrl, $requestMethod);
179
180         if ($route !== null) {
181             $route->setParameters($params);
182
183             return $route;
184         } else {
185             return null;
186         }
187     }
188
189     /**
190      * Match given request _url and request method and see if a route
191      * has been defined for it If so, return route's target If called
192      * multiple times
193      *
194      * @param string $requestUrl
195      * @param string $requestMethod
196      *
197      * @return null|Route
198      */
199     public function match($requestUrl, $requestMethod = RequestMethodInterface::METHOD_GET)
200     {
201         /** @var Route $route */
202         $route = $this->getRoute($requestUrl, $requestMethod);
203
204         if ($route !== null) {
205             return $route->dispatch();
206         } else {
207             throw new \DomainException("No route found for $requestMethod '$requestUrl'");
208         }
209     }
210
211     /**
212      * Reverse route a named route
213      *
214      * @param       $routeName
215      * @param array $params Optional array of parameters to use in URL
216      *
217      * @throws Exception
218      *
219      * @return string The url to the route
220      */
221     public function generate($routeName, array $params = array())
222     {
223         // Check if route exists
224         if (!isset($this->namedRoutes[$routeName])) {
225             throw new Exception("No route with the name $routeName has been found.");
226         }
227
228         /** @var \PHPRouter\Route $route */
229         $route = $this->namedRoutes[$routeName];
230         $url   = $route->getUrl();
231
232         // replace route url with given parameters
233         if ($params && preg_match_all('/:(\w+)/', $url, $param_keys)) {
234             // grab array with matches
235             $param_keys = $param_keys[1];
236
237             // loop trough parameter names, store matching value in $params array
238             foreach ($param_keys as $key) {
239                 if (isset($params[$key])) {
240                     $url = preg_replace('/:' . preg_quote($key, '/') . '/', $params[$key], $url, 1);
241                 }
242             }
243         }
244
245         return $url;
246     }
247
248     /**
249      * Create routes by array, and return a Router object
250      *
251      * @param array $config provide by Config::loadFromFile()
252      *
253      * @return Router
254      */
255     public static function parseConfig(array $config)
256     {
257         $collection = new RouteCollection();
258         foreach ($config['routes'] as $name => $route) {
259             $collection->attachRoute(new Route($route[0], array(
260                 '_controller' => str_replace('.', '::', $route[1]),
261                 'methods'     => $route[2],
262                 'name'        => $name,
263             )));
264         }
265
266         $router = new Router($collection);
267         if (isset($config['base_path'])) {
268             $router->setBasePath($config['base_path']);
269         }
270
271         return $router;
272     }
273 }