aboutsummaryrefslogtreecommitdiff
path: root/lib/trails/Dispatcher.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/trails/Dispatcher.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/trails/Dispatcher.php')
-rw-r--r--lib/trails/Dispatcher.php262
1 files changed, 262 insertions, 0 deletions
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);
+ }
+}