Added Canvas 1.1.0, originally not under SCM so no historical development records...
[canvas.git] / library / Controller.php
blobe8d3dd2c60d663242ef1d516df4a3f49c48847ac
1 <?php
2 // @role Controller abstract class
3 // @title Controller Abstract Class
4 // @author Matt Todd
5 // @date 2005-11-26 5:26PM
6 // @desc Handles performing the actions required by the requests.
8 include_once('stdexception.php');
10 // classes
11 abstract class Controller extends Canvas { // should implement the IController interface
12 // public variables
13 public $controller; // this controller
14 public $action; // the action // defaults to 'index'
15 public $id; // the action ID
17 public $properties;
18 public $response;
19 public $flash;
21 // protected variables
22 protected $request; // the request
23 protected $session; // the session
25 // action modification properties
26 protected $skip_authentication = array();
27 protected $authenticate = array();
29 // error variables
30 public $error; // the error object
32 // constructor // consider making this 'final'
33 public function __construct(Request $request) {
34 $this->request = $request;
35 $this->session = new Session();
36 $this->config = $this->load_config();
37 $this->response = new Response();
38 foreach($this->request->route->mapping() as $routing_symbol) {
39 $this->$routing_symbol = $this->request->route->$routing_symbol;
41 $this->environment = Config2::$environment;
44 // dispatcher
45 /* @note If the user defined 'dispatch' as a controller action, it could
46 be disasterous. Instead, make it final so that it is not able
47 to be overridden.
48 @consider Consider changing this from 'dispatch' to '_dispatch' or even
49 '__dispatch', to simulate the 'magic methods' for classes,
50 like '__get' and '__call'.
51 Consider handling action response (for view rendering) in a
52 different way.
53 @summary Dispatcher handles the majority of the overhead logic, such as
54 making sure the appropriate instance variables are set correctly,
55 executing the default handles/events/callbacks, and actually
56 calling the requested action's function (transparently).
57 Currently, the response of the $action is put in the $response
58 instance variable, within the array index of ['response'].
60 final public function dispatch($action, $id = null, $request = null) {
61 if(empty($action)) $action = "index";
62 $this->action = $action;
63 $this->id = $id;
65 // execute authentication & session handling event
66 $this->authenticate_request();
67 $this->handle_sessions();
69 $this->before_action(); // callback
71 try {
72 // dispatch action, calling the exception handler if an exception
73 // which is thrown is not caught
74 // $this->$action($id); // deprecated because it only supports one single param
75 call_user_func(array($this, $action), $this->request->params());
76 } catch(Exception $e) {
77 $this->handle_exception($e);
80 if($this->after_action_performed != true) {
81 $this->after_action(); // callback (if not already performed)
82 $this->after_action_performed = true;
85 // render (if not already rendered; also, consider layouts in views)
86 if(!$this->rendered) $this->render();
88 return $this->response;
91 // events
92 protected function authenticate_request() {
93 try { // check if the action is to be authenticated... skip it if it isn't
94 if(is_array($this->skip_authentication) && !empty($this->skip_authentication) && !in_array($this->action, $this->skip_authentication)) {
95 return $this->authenticate();
96 } elseif(is_array($this->authenticate) && !empty($this->authenticate) && in_array($this->action, $this->authenticate)) {
97 return $this->authenticate();
98 } else {
99 // dep // $this->redirect_to(array("controller"=>"admin")); // handled within 'authenticate()'
101 } catch(Exception $e) {
102 $this->handle_exception($e);
105 protected function authenticate() {
106 // this function is to be overwritten only if authentication is deemed necessary
107 return true;
110 protected function handle_exception($e) {
111 $exception_type = get_class($e); // will be the type of exception that was thrown
112 $handler = !empty($this->exception_handlers[$exception_type]) ? $this->exception_handlers[$exception_type] : $this->exception_handlers['*'];
113 if(!empty($handler)) {
114 $this->$handler($e);
115 } else {
116 Debug::generic_exception_handler($e);
119 // terminate execution on exception after handling exception
120 die();
123 protected function handle_sessions() {
124 // this function is to be overwritten only if specific session control is deemed necessary (rare!)
125 // this will require updating sessions and everything, particularly for flashes
128 public function __call($name, $params) {
129 // @consider Consider handling dynamic requests.
131 // catch-all function (throw an error)
132 throw new Exception("The <em>{$this->request->action}</em> action does not exist");
135 // renderers
136 protected function render($params = null) {
137 // if the after_action() hasn't been performed yet (by the action explicitly calling the render() method), call it now
138 if($this->after_action_performed != true) {
139 $this->after_action();
140 $this->after_action_performed = true;
143 $this->rendered = true; // so that multiple render calls aren't made (such as when the user calls render() and the dispatcher knows not to call render() too)
145 $this->before_render(); // callback
147 // so that if no $params were specified, it would still be an array and an access violation
148 // wouldn't occur below when $params['foo'] was accessed
149 if($params == null) $params = array();
151 // render results to screen
152 // passes important data to templates
153 // maybe just creates the View object and does these things? yeah
154 // or, even better, create Response and send it, from the main dispatcher, to a view object?
156 // actually, respond with the template file
157 // also should provide the a 'url' and the basics of the header/caller information as properties of the 'response' object/[array]
158 $this->response->layout = (!empty($params['layout'])) ? "{$params[layout]}.php" : (file_exists("views/{$this->controller}/layout.php") ? "layout.php" : '../layout.php');
159 if($params['layout'] == "null") $this->response->layout = null;
160 if(empty($this->response->template)) $this->response->template = (!empty($params['template'])) ? "{$params[template]}.php" : "{$this->action}.php"; // for smarty, took out 'views/{$this->controller}/' before because smarty keeps the template dir internally
161 $this->response->url = "{$this->controller}/{$this->action}/{$this->id}";
162 $this->response->controller = $this->controller;
163 $this->response->action = $this->action;
164 $this->response->id = $this->id;
165 $this->response->request = $this->request;
166 $this->response->flash = Session::flash();
168 View::render($this->response, $params);
170 $this->after_render(); // callback
172 protected function render_partial($partial, $no_layout = true) {
173 $this->render(array("template"=>$partial, "layout"=>($no_layout ? "null" : null)));
175 protected function render_template($template, $no_layout = false) {
176 $this->render(array("template"=>$template, "layout"=>($no_layout ? "null" : null)));
178 protected function render_layout($layout) {
179 $this->render(array("layout"=>$layout));
181 protected function redirect_to($request, $flash = null) {
182 $this->before_redirect(); // callback
184 // $this->response->flash = Session::flash();
185 $_SESSION['flash'] = $this->flash;
187 // if there's continuance to be done in the future, put it in the session data
188 if(!empty($request['continue_to'])) {
189 $this->session->continue_to = $request['continue_to'];
190 $this->session->continue_to($request['continue_to']);
193 // if the direct url hasn't been set, do normally... otherwise if it has been
194 if(empty($request['url'])) {
195 // redirect to the appropriate protocol if specified
196 if(!empty($request['protocol'])) $protocol = $request['protocol']; else $protocol = $this->request->protocol;
197 unset($request['protocol']);
199 // location
200 $location = "{$protocol}://{$this->request->host}/{$this->request->directory}";
202 // assemble route
203 $route = Router2::url_for($request);
204 $url = '%s%s';
205 $request = sprintf($url, $location, $route);
207 // set defaults if not set
208 // if(empty($controller)) $controller = $this->controller;
209 // shouldn't happen... should forward to default action // if(empty($action)) $action = $this->action; // probably won't happen... doesn't make sense, does it?
211 // assemble request
213 // $request = "{$controller}/{$action}/{$id}";
214 // if(empty($id)) $request = "{$controller}/{$action}";
215 // if(empty($action)) $request = "{$controller}/";
216 } else {
217 $request = $request['url'];
220 // handle session closing to prevent data loss
221 Session::flush();
223 // actually redirect
224 header("Location: {$request}");
226 // not sure if this will ever happen, but you never know
227 $this->after_redirect(); // callback
229 // log redirection
230 $execution_time = round(microtime(true) - $GLOBALS['dispatch_time_begin'], 5);
231 $execution_memory = round(memory_get_usage()/1024, 2);
232 Debug::log("Redirected {$_SERVER['PATH_INFO']} to {$path} ({$execution_time}sec/{$execution_memory}kb)", 'internal', 'notice', 'Controller');
234 // kill the rest of execution for this request
235 die("Action calls for redirection. Click <a href='{$request}'>here</a> if you are not automatically forwarded.");
238 // ajax response
239 protected function ajax_response($response) {
240 // log remote request
241 $execution_time = round(microtime(true) - $GLOBALS['dispatch_time_begin'], 5);
242 $execution_memory = round(memory_get_usage()/1024, 2);
243 Debug::log("Remote request {$_SERVER['PATH_INFO']} ({$execution_time}sec/{$execution_memory}kb)", 'internal', 'notice', 'Controller');
245 if(is_array($response)) {
246 // full on xml response
247 $this->response->response_id = $response['id'];
248 $this->response->units = $response['units'];
249 $this->render(array(
250 'template'=>'../templates/ajax_response',
251 'layout'=>'null',
253 } else {
254 // plain text response
255 die($response);
259 // property handler
260 // @desc These allow for developers to simply assign values to $this->foo
261 // and have them automatically sent to the Response object and,
262 // ultimately, the View object.
263 protected function __get($key) {
264 return $this->properties[$key];
265 return $this->response->$key;
267 protected function __set($key, $value) {
268 return $this->properties[$key] = $value;
269 return $this->response->$key = $value;
272 // flash function (which replaces just using $this->flash = array())
273 protected function flash($message, $class, $params = array()) {
274 // graft the message and the class into optionally-specified the $params array (whereby more data can be passed on)
275 $params['message'] = $message;
276 $params['class'] = $class;
277 $this->flash = $params;
280 // events/callbacks
281 protected function before_action() {}
282 protected function after_action() {}
283 protected function before_render() {} // @consider Consider moving this to the View class, or at least calling before_render() of the View as well... maybe
284 protected function after_render() {}
285 protected function before_redirect() {}
286 protected function after_redirect() {} // this is executed right before the header() call and the die()
288 // descructor
289 public function __destruct() {
290 // possibly put some serialization code in here?
292 $_SESSION['flash'] = $this->flash;
294 session_write_close();
296 // end;