PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/controller/lib/controller.php

https://github.com/plank/Seed-Framework
PHP | 753 lines | 325 code | 153 blank | 275 comment | 59 complexity | 9c645c91429bd62889955df6b0d1e02b MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. * controller.php, part of the seed framework
  4. *
  5. * @author mateo murphy
  6. * @copyright mateo murphy
  7. * @license The MIT License
  8. * @package controller
  9. */
  10. /**
  11. * Factory for controller objects.
  12. *
  13. * Implemented as a singleton, the controller factory is responsible for creating Controller objects, as well as including
  14. * the files containing. By default, the factory looks in app/controller, but it's possible to register other locations
  15. * (for plug-ins, etc)
  16. *
  17. *
  18. * @author mateo murphy
  19. * @copyright mateo murphy
  20. * @license The MIT License
  21. * @package controller
  22. */
  23. class ControllerFactory {
  24. /**
  25. * An array of key-value pairs, where the key is a type, and the value is the file for that type
  26. *
  27. * @var array
  28. */
  29. var $mappings;
  30. /**
  31. * Constructor
  32. *
  33. * @return ControllerFactory
  34. */
  35. function ControllerFactory() {
  36. $this->mappings = array();
  37. }
  38. /**
  39. * Register a controller type to path mapping
  40. *
  41. * @param string $type
  42. * @param string $path
  43. */
  44. function register($type, $path) {
  45. $this->mappings[$type] = $path;
  46. }
  47. /**
  48. * Singleton method
  49. *
  50. * @static
  51. * @return ControllerFactory
  52. */
  53. function & get_instance() {
  54. static $instances;
  55. if (!isset($instances[0])) {
  56. $instances[0] = new ControllerFactory();
  57. }
  58. return $instances[0];
  59. }
  60. /**
  61. * Requires the file for the given type. This will first look for a mapping with that key, and failing that
  62. * will look in app/controllers. Triggers an error if the file isn't found.
  63. *
  64. * @param string $type The type of the controller i.e. news for NewsController
  65. * @return bool
  66. */
  67. function import($type) {
  68. $type = strtolower($type);
  69. if (isset($this->mappings[$type])) {
  70. $path = $this->mappings[$type];
  71. } else {
  72. $path = CONTROLLER_PATH.$type.".php";
  73. // allow client applications to overide Seed and SeedCMS controllers
  74. if(defined('CLIENT_APP_DIR') && file_exists(CLIENT_APP_DIR.'/controllers/'.$type.".php")) {
  75. // We don't want to override the admin cms controllers
  76. if(defined('REQUEST_URI') && !is_int(strpos(REQUEST_URI, 'admin')) ) {
  77. $path = CLIENT_APP_DIR.'/controllers/'.$type.".php";
  78. }
  79. }
  80. }
  81. if (!file_exists($path)) {
  82. trigger_error("Controller file '$path' does not exist", E_USER_ERROR);
  83. return false;
  84. }
  85. require_once($path);
  86. return true;
  87. }
  88. /**
  89. * Factory method, returns a controller for the given type
  90. *
  91. * @param string $type
  92. * @return Controller
  93. */
  94. function factory($type, $router = null) {
  95. $type = strtolower($type);
  96. ControllerFactory::import($type);
  97. $class_name = Inflector::camelize(basename($type)).'Controller';
  98. if (!class_exists($class_name)) {
  99. trigger_error("Controller file for '$type' exists, but doesn't contain controller class '$class_name'", E_USER_ERROR);
  100. return false;
  101. }
  102. $controller = new $class_name;
  103. if (!is_a($controller, 'Controller')) {
  104. trigger_error("Class '$class_name' doesn't extend Controller", E_USER_ERROR);
  105. return false;
  106. }
  107. $controller->full_type = $type;
  108. $controller->router = $router;
  109. return $controller;
  110. }
  111. }
  112. /**
  113. * Controller
  114. *
  115. * Controllers are the heart of seed requests. They are made up of one or more actions that are executed on requests, and then either
  116. * render a template or redirect to another action. Actions are defined as methods on the controller, and will be made accessible to the
  117. * web server via the routes.
  118. *
  119. * Action, by default, render a template in the app/views directory corresponding to the name of the controller and the action after
  120. * executing the code of the action.
  121. *
  122. * Actions can also redirect after performing their code, by returning a 302 Moved HTTP Response.
  123. *
  124. * @author mateo murphy
  125. * @copyright mateo murphy
  126. * @license The MIT License
  127. * @package controller
  128. */
  129. class Controller {
  130. /**
  131. * @var string
  132. */
  133. var $type;
  134. /**
  135. * @var string
  136. */
  137. var $full_type;
  138. /**
  139. * The type of model associated with the controller
  140. *
  141. * @var string
  142. */
  143. var $model = '';
  144. /**
  145. * DB object
  146. *
  147. * @var db
  148. */
  149. var $db;
  150. /**
  151. * The scaffolding object
  152. *
  153. * @var Scaffolding
  154. */
  155. var $scaffolding;
  156. /**
  157. * The name of the scaffolding class to use
  158. *
  159. * @var string
  160. */
  161. var $scaffolding_class = 'scaffolding';
  162. /**
  163. * The router object
  164. *
  165. * @var Router
  166. */
  167. var $router;
  168. /**
  169. * The request object
  170. *
  171. * @var Request
  172. */
  173. var $request;
  174. /**
  175. * The response object
  176. *
  177. * @var Response
  178. */
  179. var $response;
  180. /**
  181. * The template object
  182. *
  183. * @var Template
  184. */
  185. var $template;
  186. /**
  187. * The class to use for the template object
  188. *
  189. * @var string
  190. */
  191. var $template_type = '';
  192. /**
  193. * The flash object
  194. *
  195. * @var Flash
  196. */
  197. var $flash;
  198. /**
  199. * The filter chain object
  200. *
  201. * @var FilterChain
  202. */
  203. var $filter_chain;
  204. /**
  205. * The array of request parameters, which is the get and post variables merged
  206. *
  207. * @var array
  208. */
  209. var $params;
  210. /**
  211. * The name of the action to execute
  212. *
  213. * @var string
  214. */
  215. var $action_name;
  216. /**
  217. * The default layout to use with this controller
  218. *
  219. * @var string
  220. */
  221. var $layout;
  222. /**
  223. * Set to true once a template has been rendered
  224. *
  225. * @var bool
  226. */
  227. var $performed_render = false;
  228. /**
  229. * Set to true once a redirect has been performed
  230. *
  231. * @var bool
  232. */
  233. var $performed_redirect = false;
  234. /**
  235. * An array of methods that are protected from being run
  236. *
  237. * @var array
  238. */
  239. var $_hidden_methods = array(
  240. 'Controller',
  241. 'factory',
  242. 'get_type',
  243. 'has_performed',
  244. 'process',
  245. 'get_template_name',
  246. 'render',
  247. 'redirect'
  248. );
  249. /**
  250. * Constructor
  251. *
  252. * @return Controller
  253. */
  254. function Controller() {
  255. $this->db = & db::get_db();
  256. $this->flash = & Flash::get_flash();
  257. if (!isset($this->layout)) {
  258. $this->layout = $this->get_type();
  259. }
  260. if (!isset($this->model)) {
  261. $this->model = $this->get_type();
  262. }
  263. if (class_exists($this->scaffolding_class)) {
  264. $this->scaffolding = & new $this->scaffolding_class($this);
  265. }
  266. $this->filter_chain = & new FilterChain($this);
  267. $this->setup();
  268. }
  269. function setup() {
  270. }
  271. /**
  272. * Requires the file for the given type
  273. *
  274. * @param string $type
  275. * @return bool
  276. */
  277. function import($type) {
  278. $factory = ControllerFactory::get_instance();
  279. return $factory->import($type);
  280. }
  281. /**
  282. * Factory method, returns a controller for the given type
  283. *
  284. * @param string $type
  285. * @return Controller
  286. */
  287. function factory($type, $router = null) {
  288. $factory = ControllerFactory::get_instance();
  289. return $factory->factory($type, $router);
  290. }
  291. /**
  292. * Returns true if there's been a render or a redirect
  293. *
  294. * @return bool
  295. */
  296. function has_performed() {
  297. return $this->performed_render || $this->performed_redirect;
  298. }
  299. /**
  300. * Returns the type of the class. i.e. if the class is PageController, returns page
  301. *
  302. * @return string
  303. */
  304. function get_type() {
  305. if (isset($this->type)) {
  306. return $this->type;
  307. }
  308. if (get_class($this) === __CLASS__) {
  309. trigger_error('Tried to get type for base controller', E_USER_WARNING);
  310. return 'base';
  311. }
  312. return Inflector::underscore(str_replace('controller', '', strtolower(get_class($this))));
  313. }
  314. function _assign_shortcuts($request, $response) {
  315. $this->request = $request;
  316. $this->params = $request->parameters;
  317. $this->response = $response;
  318. if ($this->template_type) {
  319. $this->template = Template::factory($this->template_type);
  320. } else {
  321. $this->template = Template::factory($this->get_type());
  322. }
  323. if (!$this->template) {
  324. $this->template = & new ApplicationTemplate($this);
  325. }
  326. $this->template->controller = & $this;
  327. $this->template->params = $this->params;
  328. $this->template->request = $request;
  329. $this->template->flash = $this->flash;
  330. }
  331. /**
  332. * Processes the request and calls the appropriate method on the sub controller
  333. *
  334. * @param Request $request
  335. * @return bool
  336. */
  337. function process($request, $response) {
  338. $this->_assign_shortcuts($request, $response);
  339. if (isset($this->scaffolding)) {
  340. $this->scaffolding->controller = & $this;
  341. }
  342. $this->filter_chain->controller = & $this;
  343. // choose the action to perform
  344. $this->action_name = isset($this->params['action']) ? $this->params['action'] : 'index';
  345. Logger::log('dispatch', LOG_LEVEL_DEBUG, 'controller: '.$this->get_type().', action: '.$this->action_name);
  346. if (substr($this->action_name, 0, 1) == '_') {
  347. Logger::log('dispatch', LOG_LEVEL_WARNING, "Call to protected action '$this->action_name'");
  348. $this->response->status(404);
  349. return $this->response;
  350. }
  351. // run before filters
  352. $filter_result = $this->filter_chain->call_before($this->action_name);
  353. if (!$filter_result || $this->has_performed()) {
  354. Logger::log('dispatch', LOG_LEVEL_WARNING, "Filter chain returned false");
  355. return $this->response;
  356. }
  357. // import the model type
  358. if ($this->model) {
  359. Model::import($this->model);
  360. }
  361. if (in_array($this->action_name, $this->_hidden_methods) || substr($this->action_name, 0, 1) == '_') {
  362. // protected action, raise error
  363. trigger_error("Call to protected method", E_USER_ERROR);
  364. } else if (method_exists($this, $this->action_name)) {
  365. // normal action
  366. call_user_func(array(&$this, $this->action_name));
  367. } elseif (isset($this->scaffolding) && method_exists($this->scaffolding, $this->action_name)) {
  368. // scaffold action
  369. call_user_func(array(&$this->scaffolding, $this->action_name));
  370. } else {
  371. // action not found
  372. $this->action_not_found();
  373. }
  374. // render if it wasn't a manual render or redirect hasn't already happened
  375. if (!$this->has_performed()) {
  376. $this->render();
  377. }
  378. // call the after filter chain
  379. $this->filter_chain->call_after($this->action_name);
  380. return $this->response;
  381. }
  382. /**
  383. * This action is called when no other action matches the request
  384. *
  385. */
  386. function action_not_found() {
  387. trigger_error("Action '$this->action_name' not found in ".get_class($this), E_USER_ERROR);
  388. }
  389. /**
  390. * Includes a helper object into the template. It first looks for a helper containing the name
  391. * of the class, and if that isn't found, it looks for the ApplicationHelper
  392. *
  393. * @deprecated Helpers will be removed completely in the near future
  394. * @return bool Returns true if a helper was found and loaded, false if not
  395. */
  396. function include_helper() {
  397. $type = $this->get_type();
  398. $helper_file_name = HELPER_PATH.$type.'_helper.php';
  399. if (!file_exists($helper_file_name)) {
  400. $type = 'application';
  401. $helper_file_name = HELPER_PATH.$type.'_helper.php';
  402. }
  403. if (!file_exists($helper_file_name)) {
  404. return false;
  405. }
  406. require_once($helper_file_name);
  407. $helper_class_name = Inflector::camelize($type).'Helper';
  408. if (!class_exists($helper_class_name)) {
  409. return false;
  410. }
  411. $this->template->helper = new $helper_class_name($this->template);
  412. $this->template->helper->template = & $this->template;
  413. return true;
  414. }
  415. /**
  416. * Add all the user assigned object variables to the template
  417. */
  418. function add_variables_to_assigns() {
  419. $class_vars = get_class_vars(__CLASS__);
  420. $object_vars = get_object_vars($this);
  421. $vars = array_diff_by_key($object_vars, $class_vars);
  422. foreach($vars as $key => $value) {
  423. $this->template->$key = $value;
  424. }
  425. }
  426. /**
  427. * Renders the template for the current actions
  428. *
  429. * @param string $template_path
  430. */
  431. function render($template_path = null) {
  432. if (!is_a($this->template, 'Template')) {
  433. trigger_error("The template variable was overwritten", E_USER_ERROR);
  434. return false;
  435. }
  436. if ($this->has_performed()) {
  437. trigger_error("Double render error", E_USER_WARNING);
  438. return false;
  439. }
  440. if (!isset($template_path)) {
  441. $template_path = $this->get_template_name();
  442. }
  443. if (file_exists($template_path)) {
  444. $this->add_variables_to_assigns();
  445. $this->include_helper();
  446. // render template
  447. $this->template->layout = $this->layout;
  448. $this->render_text($this->template->render_file($template_path));
  449. } else {
  450. trigger_error("No template found in '$template_path'", E_USER_ERROR);
  451. }
  452. }
  453. function render_to_string($template_path = null) {
  454. $this->render($template_path);
  455. $result = $this->response->body;
  456. $this->erase_render_results();
  457. return $result;
  458. }
  459. /**
  460. * Renders a given string, with the given status code
  461. *
  462. * @param string $text
  463. * @param int $status
  464. */
  465. function render_text($text = '', $status = null) {
  466. $this->performed_render = true;
  467. if (isset($status)) {
  468. $this->response->response_code = $status;
  469. }
  470. if ($text) {
  471. $this->response->body = $text;
  472. }
  473. }
  474. function erase_render_results() {
  475. $this->performed_render = false;
  476. $this->response->response_code = 200;
  477. $this->response->body = '';
  478. }
  479. /**
  480. * Renders an empty string, with the given status code
  481. *
  482. * @param int $status
  483. */
  484. function render_nothing($status = null) {
  485. $this->render_text(' ', $status);
  486. }
  487. /**
  488. * Renders a component as a string
  489. *
  490. * @param array $options
  491. * @return string
  492. */
  493. function render_component_as_string($options = null) {
  494. static $stack = array();
  495. if (in_array($options, $stack)) {
  496. trigger_error('Recursion in render_component', E_USER_ERROR);
  497. return false;
  498. } else {
  499. $stack[] = $options;
  500. }
  501. if (!isset($options)) {
  502. $options = array();
  503. }
  504. if (isset($options['controller'])) {
  505. $controller = $options['controller'];
  506. } else {
  507. $controller = $this->controller->get_type();
  508. }
  509. $controller = Controller::factory($controller, $this->controller->router);
  510. if (!$controller) {
  511. return false;
  512. }
  513. // $controller->layout = '';
  514. $request = clone($this->request);
  515. if (isset($options)) {
  516. $request->parameters = array_merge($request->parameters, $options);
  517. $request->get = array_merge($request->get, $options);
  518. }
  519. return $controller->process($request, $this->response);
  520. }
  521. /**
  522. * Renders a component
  523. *
  524. * @param array $options
  525. */
  526. function render_component($options = null) {
  527. $this->render_text($this->render_component_as_string($options));
  528. }
  529. /**
  530. * Renders a partial. Useful when making ajax requests
  531. *
  532. * @param string $partial_name
  533. */
  534. function render_partial($partial_name) {
  535. $layout = $this->layout;
  536. $this->layout = '';
  537. $result = $this->render($this->get_template_name($partial_name));
  538. $this->layout = $layout;
  539. return $result;
  540. }
  541. /**
  542. * Returns the name of the template for the current action
  543. *
  544. * @param string $action_name
  545. * @return string
  546. */
  547. function get_template_name($action_name = null) {
  548. if (is_null($action_name)) {
  549. $action_name = $this->action_name;
  550. }
  551. if(defined('CLIENT_APP_DIR') && file_exists($template_name = CLIENT_APP_DIR.'views/'.$this->full_type.'/'.$action_name.'.php')) {
  552. $template_name = CLIENT_APP_DIR.'views/'.$this->full_type.'/'.$action_name.'.php';
  553. }
  554. else {
  555. $template_name = TEMPLATE_PATH.$this->full_type.'/'.$action_name.'.php';
  556. }
  557. return $template_name;
  558. }
  559. /**
  560. * Sets the response to redirect to the passed action or url
  561. *
  562. * @param mixed $options
  563. * @return bool
  564. */
  565. function redirect($options = null, $overwrite_options = null) {
  566. $request = & $this->request;
  567. if ($this->has_performed()) {
  568. trigger_error("Double render error", E_USER_WARNING);
  569. return false;
  570. }
  571. $options = $this->url_for($options, $overwrite_options);
  572. $this->response->redirect($options);
  573. $this->performed_redirect = true;
  574. return true;
  575. }
  576. /**
  577. * Returns a URL for a given set of options
  578. *
  579. * @param mixed $options
  580. * @param array $overwrite_options
  581. * @return string
  582. */
  583. function url_for($options = null, $overwrite_options = null) {
  584. $request = $this->request;
  585. if (is_array($options) || is_null($options)) {
  586. if (is_null($options)) {
  587. $current_options = $this->request->get;
  588. unset($current_options['url']);
  589. $overwrite_options = array_merge($current_options, $overwrite_options);
  590. }
  591. return APP_ROOT.$this->router->url_for($request->path, $options, $overwrite_options);
  592. } else {
  593. $options = $options.Route::build_query_string($overwrite_options);
  594. return $options;
  595. }
  596. }
  597. }
  598. ?>