Dispatcher.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\action;
  9. use lithium\util\String;
  10. use lithium\util\Inflector;
  11. use lithium\core\Libraries;
  12. use lithium\action\DispatchException;
  13. use lithium\core\ClassNotFoundException;
  14. /**
  15. * `Dispatcher` is the outermost layer of the framework, responsible for both receiving the initial
  16. * HTTP request and sending back a response at the end of the request's life cycle.
  17. *
  18. * After either receiving or instantiating a `Request` object instance, the `Dispatcher` passes that
  19. * instance to the `Router`, which produces the parameters necessary to dispatch the request
  20. * (unless no route matches, in which case an exception is thrown).
  21. *
  22. * Using these parameters, the `Dispatcher` loads and instantiates the correct `Controller` object,
  23. * and passes it the `Request` object instance. The `Controller` returns a `Response` object to the
  24. * `Dispatcher`, where the headers and content are rendered and sent to the browser.
  25. *
  26. * @see lithium\net\http\Router
  27. * @see lithium\action\Request
  28. * @see lithium\action\Response
  29. * @see lithium\action\Controller
  30. */
  31. class Dispatcher extends \lithium\core\StaticObject {
  32. /**
  33. * Fully-namespaced router class reference. Class must implement a `parse()` method,
  34. * which must return an array with (at a minimum) 'controller' and 'action' keys.
  35. *
  36. * @see lithium\net\http\Router::parse()
  37. * @var array
  38. */
  39. protected static $_classes = array(
  40. 'router' => 'lithium\net\http\Router'
  41. );
  42. /**
  43. * Contains pre-process format strings for changing Dispatcher's behavior based on 'rules'.
  44. *
  45. * Each key in the array represents a 'rule'; if a key that matches the rule is present (and
  46. * not empty) in a route, (i.e. the result of `lithium\net\http\Router::parse()`) then the
  47. * rule's value will be applied to the route before it is dispatched. When applying a rule, any
  48. * array elements of the flag which are present in the route will be modified
  49. * using a `lithium\util\String::insert()`-formatted string. Alternatively,
  50. * a callback can be used to do custom transformations other than the
  51. * default `lithium\util\String::insert()`.
  52. *
  53. * For example, to implement action prefixes (i.e. `admin_index()`), set a rule named 'admin',
  54. * with a value array containing a modifier key for the `action` element of a route, i.e.:
  55. * `array('action' => 'admin_{:action}')`. Now, if the `'admin'` key is present and not empty
  56. * in the parameters returned from routing, the value of `'action'` will be rewritten per the
  57. * settings in the rule.
  58. *
  59. * Here's another example. To support normalizing actions,
  60. * set a rule named 'action' with a value array containing a callback that uses
  61. * `lithium\util\Inflector` to camelize the action:
  62. *
  63. * {{{
  64. * use lithium\action\Dispatcher;
  65. * use lithium\util\Inflector;
  66. *
  67. * Dispatcher::config(array('rules' => array(
  68. * 'action' => array('action' => function($params) {
  69. * return Inflector::camelize(strtolower($params['action']), false);
  70. * })
  71. * )));
  72. * }}}
  73. *
  74. * The rules can be a callback as well:
  75. *
  76. * {{{
  77. * Dispatcher::config(array('rules' => function($params) {
  78. * if (isset($params['admin'])) {
  79. * return array('special' => array('action' => 'special_{:action}'));
  80. * }
  81. * return array();
  82. * }));
  83. * }}}
  84. *
  85. * @see lithium\action\Dispatcher::config()
  86. * @see lithium\util\String::insert()
  87. */
  88. protected static $_rules = array();
  89. /**
  90. * Used to set configuration parameters for the `Dispatcher`.
  91. *
  92. * @see lithium\action\Dispatcher::$_rules
  93. * @param array $config Possible key settings are `'classes'` which sets the class dependencies
  94. * for `Dispatcher` (i.e. `'request'` or `'router'`) and `'rules'`, which sets the
  95. * pre-processing rules for routing parameters. For more information on the
  96. * `'rules'` setting, see the `$_rules` property.
  97. * @return array If no parameters are passed, returns an associative array with the current
  98. * configuration, otherwise returns `null`.
  99. */
  100. public static function config(array $config = array()) {
  101. if (!$config) {
  102. return array('rules' => static::$_rules);
  103. }
  104. foreach ($config as $key => $val) {
  105. $key = "_{$key}";
  106. if (!is_array($val)) {
  107. static::${$key} = $val;
  108. continue;
  109. }
  110. if (isset(static::${$key})) {
  111. static::${$key} = $val + static::${$key};
  112. }
  113. }
  114. }
  115. /**
  116. * Dispatches a request based on a request object (an instance or subclass of
  117. * `lithium\net\http\Request`).
  118. *
  119. * @see lithium\action\Request
  120. * @see lithium\action\Response
  121. * @param object $request An instance of a request object (usually `lithium\action\Request`)
  122. * with HTTP request information.
  123. * @param array $options
  124. * @return mixed Returns the value returned from the callable object retrieved from
  125. * `Dispatcher::_callable()`, which is either a string or an instance of
  126. * `lithium\action\Response`.
  127. * @filter
  128. */
  129. public static function run($request, array $options = array()) {
  130. $router = static::$_classes['router'];
  131. $params = compact('request', 'options');
  132. return static::_filter(__FUNCTION__, $params, function($self, $params) use ($router) {
  133. $request = $params['request'];
  134. $options = $params['options'];
  135. if (($result = $router::process($request)) instanceof Response) {
  136. return $result;
  137. }
  138. $params = $self::applyRules($result->params);
  139. if (!$params) {
  140. throw new DispatchException('Could not route request.');
  141. }
  142. $callable = $self::invokeMethod('_callable', array($result, $params, $options));
  143. return $self::invokeMethod('_call', array($callable, $result, $params));
  144. });
  145. }
  146. /**
  147. * Attempts to apply a set of formatting rules from `$_rules` to a `$params` array, where each
  148. * formatting rule is applied if the key of the rule in `$_rules` is present and not empty in
  149. * `$params`. Also performs sanity checking against `$params` to ensure that no value
  150. * matching a rule is present unless the rule check passes.
  151. *
  152. * @param array $params An array of route parameters to which rules will be applied.
  153. * @return array Returns the `$params` array with formatting rules applied to array values.
  154. */
  155. public static function applyRules(&$params) {
  156. $result = array();
  157. $values = array();
  158. $rules = static::$_rules;
  159. if (!$params) {
  160. return false;
  161. }
  162. if (isset($params['controller']) && is_string($params['controller'])) {
  163. $controller = $params['controller'];
  164. if (strpos($controller, '.') !== false) {
  165. list($library, $controller) = explode('.', $controller);
  166. $controller = $library . '.' . Inflector::camelize($controller);
  167. $params += compact('library');
  168. } elseif (strpos($controller, '\\') === false) {
  169. $controller = Inflector::camelize($controller);
  170. if (isset($params['library'])) {
  171. $controller = "{$params['library']}.{$controller}";
  172. }
  173. }
  174. $values = compact('controller');
  175. }
  176. $values += $params;
  177. if (is_callable($rules)) {
  178. $rules = $rules($params);
  179. }
  180. foreach ($rules as $rule => $value) {
  181. if (!isset($values[$rule])) {
  182. continue;
  183. }
  184. foreach ($value as $k => $v) {
  185. if (is_callable($v)) {
  186. $result[$k] = $v($values);
  187. continue;
  188. }
  189. $match = preg_replace('/\{:\w+\}/', '@', $v);
  190. $match = preg_replace('/@/', '.+', preg_quote($match, '/'));
  191. if (preg_match('/' . $match . '/i', $values[$k])) {
  192. continue;
  193. }
  194. $result[$k] = String::insert($v, $values);
  195. }
  196. }
  197. return $result + $values;
  198. }
  199. /**
  200. * Accepts parameters generated by the `Router` class in `Dispatcher::run()`, and produces a
  201. * callable controller object. By default, this method uses the `'controller'` path lookup
  202. * configuration in `Libraries::locate()` to return a callable object.
  203. *
  204. * @param object $request The instance of the `Request` class either passed into or generated by
  205. * `Dispatcher::run()`.
  206. * @param array $params The parameter array generated by routing the request.
  207. * @param array $options Not currently implemented.
  208. * @return object Returns a callable object which the request will be routed to.
  209. * @filter
  210. */
  211. protected static function _callable($request, $params, $options) {
  212. $params = compact('request', 'params', 'options');
  213. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  214. $options = array('request' => $params['request']) + $params['options'];
  215. $controller = $params['params']['controller'];
  216. try {
  217. return Libraries::instance('controllers', $controller, $options);
  218. } catch (ClassNotFoundException $e) {
  219. throw new DispatchException("Controller `{$controller}` not found.", null, $e);
  220. }
  221. });
  222. }
  223. /**
  224. * Invokes the callable object returned by `_callable()`, and returns the results, usually a
  225. * `Response` object instance.
  226. *
  227. * @see lithium\action
  228. * @param object $callable Typically a closure or instance of `lithium\action\Controller`.
  229. * @param object $request An instance of `lithium\action\Request`.
  230. * @param array $params An array of parameters to pass to `$callable`, along with `$request`.
  231. * @return mixed Returns the return value of `$callable`, usually an instance of
  232. * `lithium\action\Response`.
  233. * @throws lithium\action\DispatchException Throws an exception if `$callable` is not a
  234. * `Closure`, or does not declare the PHP magic `__invoke()` method.
  235. * @filter
  236. */
  237. protected static function _call($callable, $request, $params) {
  238. $params = compact('callable', 'request', 'params');
  239. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  240. if (is_callable($callable = $params['callable'])) {
  241. return $callable($params['request'], $params['params']);
  242. }
  243. throw new DispatchException('Result not callable.');
  244. });
  245. }
  246. }
  247. ?>