Controller.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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\Inflector;
  10. use lithium\action\DispatchException;
  11. use lithium\core\Libraries;
  12. /**
  13. * The `Controller` class is the fundamental building block of your application's request/response
  14. * cycle. Controllers are organized around a single logical entity, usually one or more model
  15. * classes (i.e. `lithium\data\Model`) and are tasked with performing operations against that
  16. * entity.
  17. *
  18. * Each controller has a series of 'actions' which are defined as class methods of the `Controller`
  19. * classes. Each action has a specific responsibility, such as listing a set of objects, updating an
  20. * object, or deleting an object.
  21. *
  22. * A controller object is instantiated by the `Dispatcher` (`lithium\action\Dispatcher`), and is
  23. * given an instance of the `lithium\action\Request` class, which contains all necessary request
  24. * state, including routing information, `GET` & `POST` data, and server variables. The controller
  25. * is then invoked (using PHP's magic `__invoke()` syntax), and the proper action is called,
  26. * according to the routing information stored in the `Request` object.
  27. *
  28. * A controller then returns a response (i.e. using `redirect()` or `render()`) which includes HTTP
  29. * headers, and/or a serialized data response (JSON or XML, etc.) or HTML webpage.
  30. *
  31. * For more information on returning serialized data responses for web services, or manipulating
  32. * template rendering from within your controllers, see the settings in `$_render` and the
  33. * `lithium\net\http\Media` class.
  34. *
  35. * @see lithium\net\http\Media
  36. * @see lithium\action\Dispatcher
  37. * @see lithium\action\Controller::$_render
  38. */
  39. class Controller extends \lithium\core\Object {
  40. /**
  41. * Contains an instance of the `Request` object with all the details of the HTTP request that
  42. * was dispatched to the controller object. Any parameters captured in routing, such as
  43. * controller or action name are accessible as properties of this object, i.e.
  44. * `$this->request->controller` or `$this->request->action`.
  45. *
  46. * @see lithium\action\Request
  47. * @var object
  48. */
  49. public $request = null;
  50. /**
  51. * Contains an instance of the `Response` object which aggregates the headers and body content
  52. * to be written back to the client (browser) when the result of the request is rendered.
  53. *
  54. * @see lithium\action\Response
  55. * @var object
  56. */
  57. public $response = null;
  58. /**
  59. * Lists the rendering control options for responses generated by this controller.
  60. *
  61. * - The `'type'` key is the content type that will be rendered by default, unless another is
  62. * explicitly specified (defaults to `'html'`).
  63. * - The `'data'` key contains an associative array of variables to be sent to the view,
  64. * including any variables created in `set()`, or if an action returns any variables (as an
  65. * associative array).
  66. * - When an action is invoked, it will by default attempt to render a response, set the
  67. * `'auto'` key to `false` to prevent this behavior.
  68. * - If you manually call `render()` within an action, the `'hasRendered'` key stores this
  69. * state, so that responses are not rendered multiple times, either manually or automatically.
  70. * - The `'layout'` key specifies the name of the layout to be used (defaults to `'default'`).
  71. * Typically, layout files are looked up as
  72. * `<app-path>/views/layouts/<layout-name>.<type>.php`. Based on the default settings, the
  73. * actual path would be `path-to-app/views/layouts/default.html.php`.
  74. * - Though typically introspected from the action that is executed, the `'template'` key can be
  75. * manually specified. This sets the template to be rendered, and is looked up (by default) as
  76. * `<app-path>/views/<controller>/<action>.<type>.php`, i.e.:
  77. * `path-to-app/views/posts/index.html.php`.
  78. * - To enable automatic content-type negotiation (i.e. determining the content type of the
  79. * response based on the value of the
  80. * [HTTP Accept header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)), set the
  81. * `'negotiate'` flag to `true`. Otherwise, the response will only be based on the `type`
  82. * parameter of the request object (defaulting to `'html'` if no type is present in the
  83. * `Request` parameters).
  84. *
  85. * Keep in mind that most of these settings may be passed to `Controller::render()` as well. To
  86. * change how these settings operate (i.e. template paths, default render settings for
  87. * individual media types), see the `Media` class.
  88. *
  89. * @var array
  90. * @see lithium\action\Controller::render()
  91. * @see lithium\net\http\Media::type()
  92. * @see lithium\net\http\Media::render()
  93. */
  94. protected $_render = array(
  95. 'type' => null,
  96. 'data' => array(),
  97. 'auto' => true,
  98. 'layout' => 'default',
  99. 'template' => null,
  100. 'hasRendered' => false,
  101. 'negotiate' => false
  102. );
  103. /**
  104. * Lists `Controller`'s class dependencies. For details on extending or replacing a class,
  105. * please refer to that class's API.
  106. *
  107. * @var array
  108. */
  109. protected $_classes = array(
  110. 'media' => 'lithium\net\http\Media',
  111. 'router' => 'lithium\net\http\Router',
  112. 'response' => 'lithium\action\Response'
  113. );
  114. /**
  115. * Auto configuration properties.
  116. *
  117. * @var array
  118. */
  119. protected $_autoConfig = array('render' => 'merge', 'classes' => 'merge');
  120. public function __construct(array $config = array()) {
  121. $defaults = array(
  122. 'request' => null, 'response' => array(), 'render' => array(), 'classes' => array()
  123. );
  124. parent::__construct($config + $defaults);
  125. }
  126. /**
  127. * Populates the `$response` property with a new instance of the `Response` class passing it
  128. * configuration, and sets some rendering options, depending on the incoming request.
  129. *
  130. * @return void
  131. */
  132. protected function _init() {
  133. parent::_init();
  134. $this->request = $this->request ?: $this->_config['request'];
  135. $this->response = $this->_instance('response', $this->_config['response']);
  136. if (!$this->request || $this->_render['type']) {
  137. return;
  138. }
  139. if ($this->_render['negotiate']) {
  140. $this->_render['type'] = $this->request->accepts();
  141. return;
  142. }
  143. $this->_render['type'] = $this->request->get('params:type') ?: 'html';
  144. }
  145. /**
  146. * Called by the Dispatcher class to invoke an action.
  147. *
  148. * @param object $request The request object with URL and HTTP info for dispatching this action.
  149. * @param array $dispatchParams The array of parameters that will be passed to the action.
  150. * @param array $options The dispatch options for this action.
  151. * @return object Returns the response object associated with this controller.
  152. * @filter This method can be filtered.
  153. */
  154. public function __invoke($request, $dispatchParams, array $options = array()) {
  155. $render =& $this->_render;
  156. $params = compact('request', 'dispatchParams', 'options');
  157. return $this->_filter(__METHOD__, $params, function($self, $params) use (&$render) {
  158. $dispatchParams = $params['dispatchParams'];
  159. $action = isset($dispatchParams['action']) ? $dispatchParams['action'] : 'index';
  160. $args = isset($dispatchParams['args']) ? $dispatchParams['args'] : array();
  161. $result = null;
  162. if (substr($action, 0, 1) === '_' || method_exists(__CLASS__, $action)) {
  163. throw new DispatchException('Attempted to invoke a private method.');
  164. }
  165. if (!method_exists($self, $action)) {
  166. throw new DispatchException("Action `{$action}` not found.");
  167. }
  168. $render['template'] = $render['template'] ?: $action;
  169. if ($result = $self->invokeMethod($action, $args)) {
  170. if (is_string($result)) {
  171. $self->render(array('text' => $result));
  172. return $self->response;
  173. }
  174. if (is_array($result)) {
  175. $self->set($result);
  176. }
  177. }
  178. if (!$render['hasRendered'] && $render['auto']) {
  179. $self->render();
  180. }
  181. return $self->response;
  182. });
  183. }
  184. /**
  185. * This method is used to pass along any data from the controller to the view and layout
  186. *
  187. * @param array $data sets of `<variable name> => <variable value>` to pass to view layer.
  188. * @return void
  189. */
  190. public function set($data = array()) {
  191. $this->_render['data'] = (array) $data + $this->_render['data'];
  192. }
  193. /**
  194. * Uses results (typically coming from a controller action) to generate content and headers for
  195. * a `Response` object.
  196. *
  197. * @see lithium\action\Controller::$_render
  198. * @param array $options An array of options, as follows:
  199. * - `'data'`: An associative array of variables to be assigned to the template. These
  200. * are merged on top of any variables set in `Controller::set()`.
  201. * - `'head'`: If true, only renders the headers of the response, not the body. Defaults
  202. * to `false`.
  203. * - `'template'`: The name of a template, which usually matches the name of the action.
  204. * By default, this template is looked for in the views directory of the current
  205. * controller, i.e. given a `PostsController` object, if template is set to `'view'`,
  206. * the template path would be `views/posts/view.html.php`. Defaults to the name of the
  207. * action being rendered.
  208. *
  209. * The options specified here are merged with the values in the `Controller::$_render`
  210. * property. You may refer to it for other options accepted by this method.
  211. * @return object Returns the `Response` object associated with this `Controller` instance.
  212. */
  213. public function render(array $options = array()) {
  214. $media = $this->_classes['media'];
  215. $class = get_class($this);
  216. $name = preg_replace('/Controller$/', '', substr($class, strrpos($class, '\\') + 1));
  217. $key = key($options);
  218. if (isset($options['data'])) {
  219. $this->set($options['data']);
  220. unset($options['data']);
  221. }
  222. $defaults = array(
  223. 'status' => null,
  224. 'location' => false,
  225. 'data' => null,
  226. 'head' => false,
  227. 'controller' => Inflector::underscore($name),
  228. 'library' => Libraries::get($class)
  229. );
  230. $options += $this->_render + $defaults;
  231. if ($key && $media::type($key)) {
  232. $options['type'] = $key;
  233. $this->set($options[$key]);
  234. unset($options[$key]);
  235. }
  236. $this->_render['hasRendered'] = true;
  237. $this->response->type($options['type']);
  238. $this->response->status($options['status']);
  239. $this->response->headers('Location', $options['location']);
  240. if ($options['head']) {
  241. return;
  242. }
  243. $response = $media::render($this->response, $this->_render['data'], $options + array(
  244. 'request' => $this->request
  245. ));
  246. return ($this->response = $response ?: $this->response);
  247. }
  248. /**
  249. * Creates a redirect response by calling `render()` and providing a `'location'` parameter.
  250. *
  251. * @see lithium\net\http\Router::match()
  252. * @see lithium\action\Controller::$response
  253. * @param mixed $url The location to redirect to, provided as a string relative to the root of
  254. * the application, a fully-qualified URL, or an array of routing parameters to be
  255. * resolved to a URL. Post-processed by `Router::match()`.
  256. * @param array $options Options when performing the redirect. Available options include:
  257. * - `'status'` _integer_: The HTTP status code associated with the redirect.
  258. * Defaults to `302`.
  259. * - `'head'` _boolean_: Determines whether only headers are returned with the
  260. * response. Defaults to `true`, in which case only headers and no body are
  261. * returned. Set to `false` to render a body as well.
  262. * - `'exit'` _boolean_: Exit immediately after rendering. Defaults to `false`.
  263. * Because `redirect()` does not exit by default, you should always prefix calls
  264. * with a `return` statement, so that the action is always immediately exited.
  265. * @return object Returns the instance of the `Response` object associated with this controller.
  266. * @filter This method can be filtered.
  267. */
  268. public function redirect($url, array $options = array()) {
  269. $router = $this->_classes['router'];
  270. $defaults = array('location' => null, 'status' => 302, 'head' => true, 'exit' => false);
  271. $options += $defaults;
  272. $params = compact('url', 'options');
  273. $this->_filter(__METHOD__, $params, function($self, $params) use ($router) {
  274. $options = $params['options'];
  275. $location = $options['location'] ?: $router::match($params['url'], $self->request);
  276. $self->render(compact('location') + $options);
  277. });
  278. if ($options['exit']) {
  279. $this->response->render();
  280. $this->_stop();
  281. }
  282. return $this->response;
  283. }
  284. }
  285. ?>