diff options
Diffstat (limited to 'lib/trails/Controller.php')
| -rw-r--r-- | lib/trails/Controller.php | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/lib/trails/Controller.php b/lib/trails/Controller.php new file mode 100644 index 0000000..1918380 --- /dev/null +++ b/lib/trails/Controller.php @@ -0,0 +1,411 @@ +<?php +namespace Trails; + +use Flexi\Factory; +use Flexi\Template; +use Flexi\TemplateNotFoundException; +use Trails\Exceptions\DoubleRenderError; +use Trails\Exceptions\UnknownAction; + +/** + * A Controller is responsible for matching the unconsumed part of an URI + * to an action using the left over words as arguments for that action. The + * action is then mapped to method of the controller instance which is called + * with the just mentioned arguments. That method can send the #render_action, + * #render_template, #render_text, #render_nothing or #redirect method. + * Otherwise the #render_action is called with the current action as argument. + * If the action method sets instance variables during performing, they will be + * be used as attributes for the flexi-template opened by #render_action or + * #render_template. A controller's response's body is populated with the output + * of the #render_* methods. The action methods can add additional headers or + * change the status of that response. + * + * @package trails + * + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +class Controller +{ + protected Dispatcher $dispatcher; + protected Response $response; + protected bool $performed = false; + protected Template|string|null $layout = null; + private string $format = 'html'; + + /** + * @param Dispatcher $dispatcher the dispatcher who creates this instance + */ + public function __construct(Dispatcher $dispatcher) + { + $this->dispatcher = $dispatcher; + $this->erase_response(); + } + + /** + * Resets the response of the controller + * + * @return void + */ + public function erase_response() + { + $this->performed = false; + $this->response = new Response(); + } + + /** + * Return this controller's response + * + * @return Response the controller's response + */ + public function get_response() + { + return $this->response; + } + + /** + * This method extracts an action string and further arguments from it's + * parameter. The action string is mapped to a method being called afterwards + * using the said arguments. That method is called and a response object is + * generated, populated and sent back to the dispatcher. + * + * @param string $unconsumed + * + * @return Response + * @throws UnknownAction + */ + public function perform($unconsumed) + { + [$action, $args, $format] = $this->extract_action_and_args($unconsumed); + + $this->format = $format ?? 'html'; + + $before_filter_result = $this->before_filter($action, $args); + + # send action to controller + # TODO (mlunzena) shouldn't the after filter be triggered too? + if (!($before_filter_result === false || $this->performed)) { + + $callable = $this->map_action($action); + + if (is_callable($callable)) { + $callable(...$args); + } else { + $this->does_not_understand($action, $args); + } + + if (!$this->performed) { + $this->render_action($action); + } + + $this->after_filter($action, $args); + } + + return $this->response; + } + + /** + * Extracts action and args from a string. + * + * @param string $string the processed string + * @return array an array with two elements - a string containing the + * action and an array of strings representing the args + */ + public function extract_action_and_args($string) + { + if ('' === $string) { + return $this->default_action_and_args(); + } + + // find optional file extension + $format = null; + if (preg_match('/^(.*[^\/.])\.(\w+)$/', $string, $matches)) { + [, $string, $format] = $matches; + } + + // TODO this should possibly remove empty tokens + $args = explode('/', $string); + $action = array_shift($args); + return [$action, $args, $format]; + } + + /** + * Return the default action and arguments + * + * @return array containing the action, an array of args and the format + */ + public function default_action_and_args() + { + return ['index', [], null]; + } + + /** + * Maps the action to an actual method name. + * + * @param string $action + * @return array the mapped method name + */ + public function map_action($action) + { + return [&$this, $action . '_action']; + } + + /** + * Callback function being called before an action is executed. If this + * function does not return FALSE, the action will be called, otherwise + * an error will be generated and processing will be aborted. If this function + * already #rendered or #redirected, further processing of the action is + * withheld. + * + * @param string $action Name of the action to perform. + * @param array $args An array of arguments to the action. + * @return bool|void + */ + public function before_filter(&$action, &$args) + { + } + + /** + * Callback function being called after an action is executed. + * + * @param string $action Name of the action to perform. + * @param array $args An array of arguments to the action. + * @return void + */ + public function after_filter($action, $args) + { + } + + /** + * @param string $action + * @param array $args + * @return void + * @throws UnknownAction + */ + public function does_not_understand($action, $args) + { + throw new Exceptions\UnknownAction("No action responded to '$action'."); + } + + /** + * @param string $to + * + * @return void + * @throws DoubleRenderError + */ + public function redirect($to) + { + if ($this->performed) { + throw new Exceptions\DoubleRenderError(); + } + + $this->performed = true; + + # get uri; keep absolute URIs + $url = preg_match('#^(/|\w+://)#', $to) + ? $to + : $this->url_for($to); + + $this->response->add_header('Location', $url)->set_status(302); + } + + /** + * Renders the given text as the body of the response. + * + * @param string $text the text to be rendered + * @return void + * @throws DoubleRenderError + */ + public function render_text($text = ' ') + { + if ($this->performed) { + throw new Exceptions\DoubleRenderError(); + } + + $this->performed = true; + + $this->response->set_body($text); + } + + /** + * Renders the empty string as the response's body. + * + * @return void + * @throws DoubleRenderError + */ + public function render_nothing() + { + $this->render_text(''); + } + + /** + * Renders the template of the given action as the response's body. + * + * @param string $action the action + * @return void + */ + public function render_action($action) + { + $this->render_template( + $this->get_default_template($action), + $this->layout + ); + } + + public function get_default_template($action) + { + $controller_name = Inflector::underscore( + substr(static::class, 0, -10) + ); + return $controller_name . '/' . $action; + } + + /** + * Renders a template using an optional layout template. + * + * @param Template|string $template_name a flexi template + * @param Template|string|null $layout a flexi template which is used as layout + * + * @return void + * @throws DoubleRenderError + * @throws TemplateNotFoundException + */ + public function render_template($template_name, $layout = null) + { + $factory = $this->get_template_factory(); + $template = $factory->open($template_name); + + $template->set_attributes($this->get_assigned_variables()); + + if (isset($layout)) { + $template->set_layout($layout); + } + + $this->render_text($template->render()); + } + + /** + * Create and return a template factory for this controller. + * + * @return Factory + */ + public function get_template_factory() + { + return new Factory($this->dispatcher->trails_root . '/views/'); + } + + /** + * This method returns all the set instance variables to be used as attributes + * for a template. This controller is returned too as value for + * key 'controller'. + * + * @return array an associative array of variables for the template + */ + public function get_assigned_variables() + { + $assigns = []; + $protected = get_class_vars(static::class); + + foreach (get_object_vars($this) as $var => $value) { + if (!array_key_exists($var, $protected)) { + $assigns[$var] =& $this->$var; + } + } + + $assigns['controller'] = $this; + + return $assigns; + } + + /** + * Sets the layout to be used by this controller per default. + * + * @param Template|string|null $layout a flexi template to be used as layout + * @return void + */ + public function set_layout($layout) + { + $this->layout = $layout; + } + + /** + * Returns a URL to a specified route to your Trails application. + * + * Example: + * Your Trails application is located at 'http://example.com/dispatch.php'. + * So your dispatcher's trails_uri is set to 'http://example.com/dispatch.php' + * If you want the URL to your 'wiki' controller with action 'show' and + * parameter 'page' you should send: + * + * $url = $controller->url_for('wiki/show', 'page'); + * + * $url should then contain 'http://example.com/dispatch.php/wiki/show/page'. + * + * The first parameter is a string containing the controller and optionally an + * action: + * + * - "{controller}/{action}" + * - "path/to/controller/action" + * - "controller" + * + * This "controller/action" string is not url encoded. You may provide + * additional parameter which will be urlencoded and concatenated with + * slashes: + * + * $controller->url_for('wiki/show', 'page'); + * -> 'wiki/show/page' + * + * $controller->url_for('wiki/show', 'page', 'one and a half'); + * -> 'wiki/show/page/one+and+a+half' + * + * @param string $to a string containing a controller and optionally an action + * @return string a URL to this route + */ + public function url_for($to/*, ...*/) + { + # urlencode all but the first argument + $args = func_get_args(); + $args = array_map('urlencode', $args); + $args[0] = $to; + + return $this->dispatcher->trails_uri . '/' . implode('/', $args); + } + + /** + * @param int $status + * @return void + */ + public function set_status($status, $reason_phrase = null) + { + $this->response->set_status($status, $reason_phrase); + } + + /** + * Sets the content type of the controller's response. + * + * @param string $type the content type + * @return void + */ + public function set_content_type($type) + { + $this->response->add_header('Content-Type', $type); + } + + /** + * Exception handler called when the performance of an action raises an + * exception. + * + * @param \Throwable $exception the thrown exception + * @return Response a response object + */ + public function rescue($exception) + { + return $this->dispatcher->trails_error($exception); + } + + public function respond_to($ext) + { + return $this->format === $ext; + } +} |
