diff options
Diffstat (limited to 'lib/trails')
| -rw-r--r-- | lib/trails/Controller.php | 411 | ||||
| -rw-r--r-- | lib/trails/Dispatcher.php | 262 | ||||
| -rw-r--r-- | lib/trails/Exception.php | 43 | ||||
| -rw-r--r-- | lib/trails/Exceptions/DoubleRenderError.php | 13 | ||||
| -rw-r--r-- | lib/trails/Exceptions/MissingFile.php | 10 | ||||
| -rw-r--r-- | lib/trails/Exceptions/RoutingError.php | 10 | ||||
| -rw-r--r-- | lib/trails/Exceptions/SessionRequiredException.php | 11 | ||||
| -rw-r--r-- | lib/trails/Exceptions/UnknownAction.php | 10 | ||||
| -rw-r--r-- | lib/trails/Exceptions/UnknownController.php | 10 | ||||
| -rw-r--r-- | lib/trails/Flash.php | 184 | ||||
| -rw-r--r-- | lib/trails/Inflector.php | 44 | ||||
| -rw-r--r-- | lib/trails/Response.php | 185 |
12 files changed, 1193 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; + } +} diff --git a/lib/trails/Dispatcher.php b/lib/trails/Dispatcher.php new file mode 100644 index 0000000..efa90c7 --- /dev/null +++ b/lib/trails/Dispatcher.php @@ -0,0 +1,262 @@ +<?php +namespace Trails; + +use Trails\Exceptions\MissingFile; +use Trails\Exceptions\RoutingError; +use Trails\Exceptions\UnknownController; + +/** + * The Dispatcher is used to map an incoming HTTP request to a Controller + * producing a response which is then rendered. To initialize an instance of + * class Dispatcher you have to give three configuration settings: + * + * trails_root - the absolute file path to a directory containing the + * applications controllers, views etc. + * trails_uri - the URI to which routes to mapped Controller/Actions + * are appended + * default_controller - the route to a controller, that is used if no + * controller is given, that is the route is equal to '/' + * + * After instantiation of a dispatcher you have to call method #dispatch with + * the request uri to be mapped to a controller/action pair. + * + * @package trails + * + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +class Dispatcher +{ + # TODO (mlunzena) Konfiguration muss anders geschehen + + /** + * This is the absolute file path to the trails application directory. + */ + public string $trails_root; + + /** + * This is the URI to which routes to controller/actions are appended. + */ + public string $trails_uri; + + /** + * This variable contains the route to the default controller. + */ + public string $default_controller; + + /** + * @param string $trails_root absolute file path to a directory containing the + * applications controllers, views etc. + * @param string $trails_uri the URI to which routes to mapped Controller/Actions + * are appended + * @param string $default_controller the route to a controller, that is used if no + * controller is given, that is the route is equal to '/' + */ + public function __construct( + string $trails_root, + string $trails_uri, + string $default_controller + ) { + $this->trails_root = $trails_root; + $this->trails_uri = $trails_uri; + $this->default_controller = $default_controller; + } + + /** + * Maps a string to a response which is then rendered. + * + * @param string $uri The requested URI. + */ + public function dispatch($uri) + { + # E_USER_ERROR|E_USER_WARNING|E_USER_NOTICE|E_RECOVERABLE_ERROR = 5888 + $old_handler = set_error_handler([$this, 'error_handler'], 5888); + + ob_start(); + $level = ob_get_level(); + + $this->map_uri_to_response($this->clean_request_uri((string) $uri))->output(); + + while (ob_get_level() >= $level) { + ob_end_flush(); + } + + if (isset($old_handler)) { + set_error_handler($old_handler); + } + } + + /** + * Maps an URI to a response by figuring out first what controller to + * instantiate, then delegating the unconsumed part of the URI to the + * controller who returns an appropriate response object or throws an + * Exception. + * + * @param string $uri the URI string + * @return Response a response object + */ + public function map_uri_to_response($uri) + { + try { + [$controller_path, $unconsumed] = '' === $uri ? $this->default_route() : $this->parse($uri); + + $controller = $this->load_controller($controller_path); + + $response = $controller->perform($unconsumed); + } catch (Exception $e) { + $response = isset($controller) ? $controller->rescue($e) : $this->trails_error($e); + } + + return $response; + } + + /** + * @return array an array containing the default controller and an + * empty unconsumed route + * @throws MissingFile + */ + public function default_route() + { + if (!$this->file_exists($this->default_controller . '.php')) { + throw new Exceptions\MissingFile( + "Default controller '{$this->default_controller}' not found'" + ); + } + return [$this->default_controller, '']; + } + + public function trails_error($exception) + { + ob_clean(); + + # show details for local requests + $detailed = @$_SERVER['REMOTE_ADDR'] === '127.0.0.1'; + + $body = sprintf('<html><head><title>Trails Error</title></head>' . + '<body><h1>%s</h1><pre>%s</pre></body></html>', + htmlentities($exception->__toString()), + $detailed + ? htmlentities($exception->getTraceAsString()) + : ''); + + if ($exception instanceof Exception) { + $response = new Response( + $body, + $exception->getHeaders(), + $exception->getCode(), + $exception->getMessage() + ); + } else { + $response = new Response( + $body, + [], + 500, + $exception->getMessage() + ); + } + + return $response; + } + + /** + * Clean up URI string by removing the query part and leading slashes. + * + * @param string $uri an URI string + * @return string the cleaned string + */ + public function clean_request_uri($uri) + { + $pos = strpos($uri, '?'); + if ($pos !== false) { + $uri = substr($uri, 0, $pos); + } + return ltrim($uri, '/'); + } + + /** + * @param string $unconsumed + * @param string $controller + * @return array + * @throws RoutingError + */ + public function parse($unconsumed, $controller = null) + { + [$head, $tail] = $this->split_on_first_slash($unconsumed); + + if (!preg_match('/^\w+$/', $head)) { + throw new RoutingError("No route matches '$head'"); + } + + $controller = (isset($controller) ? $controller . '/' : '') . $head; + + if ($this->file_exists($controller . '.php')) { + return [$controller, $tail]; + } + + if ($this->file_exists($controller)) { + return $this->parse($tail, $controller); + } + + throw new RoutingError("No route matches '$head'"); + } + + /** + * @param string $str + * @return array + */ + public function split_on_first_slash($str) + { + preg_match(":([^/]*)(/+)?(.*):", $str, $matches); + return [$matches[1], $matches[3]]; + } + + /** + * @param string $path + * @return bool + */ + public function file_exists($path) + { + return file_exists("{$this->trails_root}/controllers/$path"); + } + + /** + * Loads the controller file for a given controller path and return an + * instance of that controller. If an error occures, an exception will be + * thrown. + * + * @param string $controller the relative controller path + * @return Controller an instance of that controller + * @throws UnknownController + */ + public function load_controller($controller) + { + require_once "{$this->trails_root}/controllers/{$controller}.php"; + $class = Inflector::camelize($controller) . 'Controller'; + if (!class_exists($class)) { + throw new UnknownController("Controller missing: '$class'"); + } + return new $class($this); + } + + /** + * This method transforms E_USER_* and E_RECOVERABLE_ERROR to + * Exceptions. + * + * @param integer $errno the level of the error raised + * @param string $string the error message + * @param string $file the filename that the error was raised in + * @param integer $line the line number the error was raised at + * + * @return bool + * @throws Exception + * + */ + public function error_handler($errno, $string, $file, $line) + { + if (!(5888 & $errno)) { + return false; + } + throw new Exception(500, $string); + } +} diff --git a/lib/trails/Exception.php b/lib/trails/Exception.php new file mode 100644 index 0000000..fd92b35 --- /dev/null +++ b/lib/trails/Exception.php @@ -0,0 +1,43 @@ +<?php + +namespace Trails; + +/** + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +class Exception extends \Exception +{ + protected array $headers; + + /** + * @param int $status the status code to be set in the response + * @param string|null $reason a human readable presentation of the status code + * @param array $headers a hash of additional headers to be set in the response + */ + public function __construct(int $status = 500, string $reason = null, array $headers = []) + { + parent::__construct( + $reason ?? Response::get_reason($status), + $status + ); + + $this->setHeaders($headers); + } + + public function setHeaders(array $headers): void + { + $this->headers = $headers; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function __toString(): string + { + return "{$this->code} {$this->message}"; + } +} diff --git a/lib/trails/Exceptions/DoubleRenderError.php b/lib/trails/Exceptions/DoubleRenderError.php new file mode 100644 index 0000000..c2dfe9e --- /dev/null +++ b/lib/trails/Exceptions/DoubleRenderError.php @@ -0,0 +1,13 @@ +<?php +namespace Trails\Exceptions; + +class DoubleRenderError extends \Trails\Exception +{ + public function __construct() + { + $message = 'Render and/or redirect were called multiple times in this '; + $message .= 'action. Please note that you may only call render OR '; + $message .= 'redirect, and at most once per action.'; + parent::__construct(500, $message); + } +} diff --git a/lib/trails/Exceptions/MissingFile.php b/lib/trails/Exceptions/MissingFile.php new file mode 100644 index 0000000..59e8480 --- /dev/null +++ b/lib/trails/Exceptions/MissingFile.php @@ -0,0 +1,10 @@ +<?php +namespace Trails\Exceptions; + +class MissingFile extends \Trails\Exception +{ + public function __construct(string $message) + { + parent::__construct(500, $message); + } +} diff --git a/lib/trails/Exceptions/RoutingError.php b/lib/trails/Exceptions/RoutingError.php new file mode 100644 index 0000000..e62459e --- /dev/null +++ b/lib/trails/Exceptions/RoutingError.php @@ -0,0 +1,10 @@ +<?php +namespace Trails\Exceptions; + +class RoutingError extends \Trails\Exception +{ + public function __construct(string $message) + { + parent::__construct(400, $message); + } +} diff --git a/lib/trails/Exceptions/SessionRequiredException.php b/lib/trails/Exceptions/SessionRequiredException.php new file mode 100644 index 0000000..b77a344 --- /dev/null +++ b/lib/trails/Exceptions/SessionRequiredException.php @@ -0,0 +1,11 @@ +<?php +namespace Trails\Exceptions; + +class SessionRequiredException extends \Trails\Exception +{ + public function __construct() + { + $message = 'Tried to access a non existing session.'; + parent::__construct(500, $message); + } +} diff --git a/lib/trails/Exceptions/UnknownAction.php b/lib/trails/Exceptions/UnknownAction.php new file mode 100644 index 0000000..f749a51 --- /dev/null +++ b/lib/trails/Exceptions/UnknownAction.php @@ -0,0 +1,10 @@ +<?php +namespace Trails\Exceptions; + +class UnknownAction extends \Trails\Exception +{ + public function __construct(string $message) + { + parent::__construct(404, $message); + } +} diff --git a/lib/trails/Exceptions/UnknownController.php b/lib/trails/Exceptions/UnknownController.php new file mode 100644 index 0000000..420c238 --- /dev/null +++ b/lib/trails/Exceptions/UnknownController.php @@ -0,0 +1,10 @@ +<?php +namespace Trails\Exceptions; + +class UnknownController extends \Trails\Exception +{ + public function __construct(string $message) + { + parent::__construct(404, $message); + } +} diff --git a/lib/trails/Flash.php b/lib/trails/Flash.php new file mode 100644 index 0000000..eede7af --- /dev/null +++ b/lib/trails/Flash.php @@ -0,0 +1,184 @@ +<?php + +namespace Trails; + +use Trails\Exceptions\SessionRequiredException; + +/** + * The flash provides a way to pass temporary objects between actions. + * Anything you place in the flash will be exposed to the very next action and + * then cleared out. This is a great way of doing notices and alerts, such as + * a create action that sets + * <tt>$flash->set('notice', "Successfully created")</tt> + * before redirecting to a display action that can then expose the flash to its + * template. + * + * @package trails + * + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +final class Flash implements \ArrayAccess +{ + private array $flash = []; + private array $used = []; + + /** + * @return self + * @throws SessionRequiredException + */ + public static function instance() + { + if (!isset($_SESSION)) { + throw new Exceptions\SessionRequiredException(); + } + + if (!isset($_SESSION[self::class])) { + $_SESSION[self::class] = new self(); + } + return $_SESSION[self::class]; + } + + public function offsetExists($offset): bool + { + return isset($this->flash[$offset]); + } + + public function offsetGet($offset): mixed + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + $this->set($offset, $value); + } + + public function offsetUnset($offset): void + { + unset($this->flash[$offset], $this->used[$offset]); + } + + /** + * Used internally by the <tt>keep</tt> and <tt>discard</tt> methods + * use() # marks the entire flash as used + * use('msg') # marks the "msg" entry as used + * use(null, false) # marks the entire flash as unused + * # (keeps it around for one more action) + * use('msg', false) # marks the "msg" entry as unused + * # (keeps it around for one more action) + * + */ + private function use(?string $key = null, bool $isUsed = true): void + { + if ($key) { + $this->used[$key] = $isUsed; + } else { + foreach (array_keys($this->used) as $key) { + $this->use($key, $isUsed); + } + } + } + + /** + * Marks the entire flash or a single flash entry to be discarded by the end + * of the current action. + * + * $flash->discard() # discards entire flash + * # (it'll still be available for the + * # current action) + * $flash->discard('warning') # discard the "warning" entry + * # (it'll still be available for the + * # current action) + */ + public function discard(?string $key = null): void + { + $this->use($key); + } + + /** + * Returns the value to the specified key. + * + * @return mixed the key's value. + */ + public function &get(string $key): mixed + { + $return = null; + if (isset($this->flash[$key])) { + $return =& $this->flash[$key]; + } + return $return; + } + + /** + * Keeps either the entire current flash or a specific flash entry available + * for the next action: + * + * $flash->keep() # keeps the entire flash + * $flash->keep('notice') # keeps only the "notice" entry, the rest of + * # the flash is discarded + */ + public function keep(?string $key = null): void + { + $this->use($key, false); + } + + /** + * Sets a key's value. + */ + public function set(string $key, mixed $value): void + { + $this->keep($key); + $this->flash[$key] = $value; + } + + /** + * Sets a key's value by reference. + */ + public function set_ref(string $key, mixed &$value): void + { + $this->keep($key); + $this->flash[$key] =& $valze; + } + + /** + * Removes all used values + */ + public function sweep(): void + { + foreach (array_keys($this->flash) as $k) { + if ($this->used[$k]) { + unset($this->flash[$k], $this->used[$k]); + } else { + $this->use($k); + } + } + } + + public function __toString() + { + $values = []; + foreach ($this->flash as $key => $value) { + $values[] = sprintf( + "'%s': [%s, '%s']", + $key, + var_export($value, true), + !empty($this->used[$key]) ? 'used' : 'unused' + ); + } + return '{' . implode(', ', $values) . '}'. "\n"; + } + + public function __sleep(): array + { + $this->sweep(); + return ['flash', 'used']; + } + + + public function __wakeUp() + { + $this->discard(); + } +} diff --git a/lib/trails/Inflector.php b/lib/trails/Inflector.php new file mode 100644 index 0000000..bbc6b58 --- /dev/null +++ b/lib/trails/Inflector.php @@ -0,0 +1,44 @@ +<?php +namespace Trails; + +/** + * The Inflector class is a namespace for inflections methods. + * + * @package trails + * + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +class Inflector +{ + /** + * Returns a camelized string from a lower case and underscored string by + * replacing slash with underscore and upper-casing each letter preceded + * by an underscore. + * + * @param string $word String to camelize. + * @return string Camelized string. + */ + public static function camelize(string $word): string + { + $parts = explode('/', $word); + foreach ($parts as $key => $part) { + $parts[$key] = strtopascalcase($part); + } + return implode('_', $parts); + } + + /** + * @param string $word + * @return string + */ + public static function underscore(string $word): string + { + $parts = explode('_', $word); + foreach ($parts as $key => $part) { + $parts[$key] = strtosnakecase($part); + } + return implode('/', $parts); + } +} diff --git a/lib/trails/Response.php b/lib/trails/Response.php new file mode 100644 index 0000000..b3f14d8 --- /dev/null +++ b/lib/trails/Response.php @@ -0,0 +1,185 @@ +<?php +namespace Trails; + +/** + * This class represents a response returned by a controller that was asked to + * perform for a given request. A Response contains the body, status and + * additional headers which can be renderer back to the client. + * + * @package trails + * + * @author mlunzena + * @copyright (c) Authors + * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ + */ +class Response +{ + public $body = ''; + public $status; + public $reason; + public $headers = []; + + /** + * Constructor. + * + * @param string $body the body of the response defaulting to '' + * @param array $headers an array of additional headers defaulting to an + * empty array + * @param int|null $status the status code of the response defaulting to a + * regular 200 + * @param string|null $reason the descriptional reason for a status code defaulting to + * the standard reason phrases defined in RFC 2616 + */ + public function __construct( + string $body = '', + array $headers = [], + ?int $status = null, + ?string $reason = null + ) { + $this->set_body($body); + + $this->headers = $headers; + + if (isset($status)) { + $this->set_status($status, $reason); + } + } + + /** + * Sets the body of the response. + * + * @param string $body the body + * @return static this response object. Useful for cascading method calls. + */ + public function set_body($body) + { + $this->body = $body; + return $this; + } + + /** + * Sets the status code and an optional custom reason. If none is given, the + * standard reason phrase as of RFC 2616 is used. + * + * @param integer $status the status code + * @param string $reason the custom reason, defaulting to the one given in RFC 2616 + * @return static this response object. Useful for cascading method calls. + */ + public function set_status($status, $reason = null) + { + $this->status = $status; + $this->reason = $reason ?? self::get_reason($status); + return $this; + } + + /** + * Returns the reason phrase of this response according to RFC2616. + * + * @param int $status the response's status + * @return string the reason phrase for this response's status + */ + public static function get_reason($status) + { + return match ($status) { + 100 => 'Continue', + 101 => 'Switching Protocols', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + + default => '', + }; + } + + /** + * Adds an additional header to the response. + * + * @param string $key the left hand key part + * @param string $value the right hand value part + * @return static this response object. Useful for cascading method calls. + */ + public function add_header($key, $value) + { + $this->headers[$key] = $value; + return $this; + } + + /** + * Outputs this response to the client using "echo" and "header". + * @return void + */ + public function output() + { + if (isset($this->status)) { + $this->send_header( + "HTTP/1.1 {$this->status} {$this->reason}", + true, + $this->status + ); + } + + foreach ($this->headers as $k => $v) { + $this->send_header("{$k}: {$v}"); + } + + echo $this->body; + } + + /** + * Internally used function to actually send headers + * + * @param string $header the HTTP header + * @param bool $replace optional; TRUE if previously sent header should be + * replaced - FALSE otherwise (default) + * @param integer $status optional; the HTTP response code + * @return void + */ + public function send_header($header, $replace = false, $status = null) + { + if (isset($status)) { + header($header, $replace, $status); + } else { + header($header, $replace); + } + } +} |
