Prowect/Routing

View on GitHub
src/Router.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

/**
 * Created by Prowect
 * Author: Raffael Kessler
 * Date: 17.10.2015 - 23:30.
 * Copyright Prowect.
 */
namespace Drips\Routing;

use Exception;
use Drips\HTTP\Request;
use Drips\HTTP\Response;
use Drips\Utils\OutputBuffer;

/**
 * Class Router.
 *
 * Diese Klasse dient als Routing-System. Es können URLs (sogenannte Route)
 * registriert werden und eine dazugehörige Funktion. Wird die entsprechende URL
 * aufgerufen, so wird die verknüpfte Funktion ausgeführt.
 */
class Router
{
    /**
     * Beinhaltet alle registrierten Routen, sowie deren Eigenschaften.
     *
     * @var array
     */
    protected $routes = array();

    /**
     * Beinhaltet die aktuelle Route, die vom Router gefunden beziehungsweise
     * ausgeführt wurde.
     *
     * @var string
     */
    protected $current_route;

    /**
     * Beinhaltet die aufgerufene URL.
     *
     * @var string
     */
    protected $request_uri;

    /**
     * Beinhaltet den aktuellen Pfad, unter dem sich dieses Script befindet.
     *
     * @var string
     */
    protected $current_path;

    /**
     * Beinhaltet den virtuellen Root von Drips welcher vom Router bestimmt wurde.
     * Dieser ist ausgehend vom Routing-Script.
     *
     * @var string
     */
    protected $drips_root;

    /**
     * Beinhaltet die aufgerufene URL.
     *
     * @var string
     */
    protected $url;

    /**
     * Beinhaltet die Parameter-Informationen zur entsprechenden Route.
     *
     * @var array
     */
    protected $params = array();

    /**
     * Beinhaltet den eingegangen Request.
     *
     * @var Request
     */
    protected $request;

    /**
     * Beinhaltet die Router-Instanz
     *
     * @var Router
     */
    private static $instance;

    /**
     * Gibt die Router-Instanz zurück (Singleton)
     *
     * @return Router
     */
    public static function getInstance()
    {
        if(static::$instance === null){
            static::$instance = new Router;
        }

        return static::$instance;
    }

    /**
     * Erzeugt eine neue Router-Instanz.
     */
    private function __construct()
    {
        $this->request = Request::getInstance();
        $request_uri = $this->request->server->get('REQUEST_URI');
        $this->current_path = dirname($this->request->server->get('SCRIPT_FILENAME'));
        $this->drips_root = substr($this->current_path, strlen($this->request->server->get('DOCUMENT_ROOT'))).'/';
        if(defined('DRIPS_PUBLIC')){
            $this->drips_root = substr($this->drips_root, 0, strlen($this->drips_root) - strlen('/public'));
        }
        $this->request_uri = substr($request_uri, strlen($this->drips_root));
        if(strlen($this->drips_root) < 1 || $this->drips_root[0] != "/"){
            $this->drips_root = "/".$this->drips_root;
        }
        $parts = explode("#", $this->request_uri);
        $this->request_uri = $parts[0];
        $parts = explode("?", $this->request_uri);
        $this->request_uri = $parts[0];
    }

    private function __clone(){}

    /**
     * Diese Methode wird zum Registrieren neuer Routen verwendet.
     * Die $url kann ein regulärer Ausdruck sein, jedoch ohne Delimiter.
     * Außerdem kann die $url auch Platzhalter mit folgendem Format beinhalten:
     * {placeholder}
     * Optional können auch $options übergeben werden. Diese können das Routing
     * einschränken. Dafür gibt es folgende Möglichkeiten:
     *  - "https" ... bool - wenn TRUE, muss die aufgerufene URL über HTTPS aufgerufen worden sein - Standard: FALSE
     *  - "verb" ... string or array - schränkt die Request-Methode ein, also über welche Request-Methoden die Route erreichbar sein soll. - Standard: alle
     *  - "domain" ... string or array - Beschränkt die Route auf eine oder mehrere bestimmte Domains.
     *
     * Gibt zurück ob die Route erfolgreich hinzugefügt wurde oder nicht. (TRUE/FALSE)
     * Wenn der Name der Route bereits vergeben ist, kann die Route nicht registriert werden!

     * @param string $name     eindeutiger Name der Route
     * @param string $url      Routen-Definition - kann Platzhalter beinhalten
     * @param mixed  $callback Funktion, die aufgerufen wird, sobald die Route ausgeführt wird oder ein Controller (MVC)
     * @param array  $options  optional - ermöglicht Zusatzinformationen, wie z.B.: Einschränkungen für die Routen
     *
     * @return bool
     */
    public function add($name, $url, $callback, array $options = array())
    {
        if (!is_callable($callback) && !class_exists($callback)) {
            throw new Exception("Ungültiges Callback bei Route: $name");
        }

        if (!$this->has($name)) {
            $this->routes[$name] = array('url' => $url, 'callback' => $callback, 'options' => $options);
            if (!isset($this->current_route)) {
                if ($this->match($name)) {
                    $this->current_route = $name;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Liefert TRUE oder FALSE, je nachdem ob eine Route mit dem angebenen Namen
     * bereits existiert oder nicht.
     *
     * @param string $name Name der Route, die überprüft werden soll
     *
     * @return bool
     */
    public function has($name)
    {
        return array_key_exists($name, $this->routes);
    }

    /**
     * Führt die "gefundene" Route aus.
     * Gibt TRUE/FALSE zurück, je nachdem ob die Route ausgeführt werden konnte
     * oder nicht.
     * Wurde keine Route gefunden, wird eine Error404Exception geworfen.
     *
     * @return bool
     */
    public function route()
    {
        if (isset($this->current_route)) {
            return $this->exec($this->current_route);
        }

        throw new Error404Exception;
    }

    /**
     * Liefert den DocumentRoot des Routers.
     *
     * @return string
     */
    public function getRoot()
    {
        return $this->drips_root;
    }




    /**
     * Führt die Route aus, die unter dem Namen $name registriert ist.
     * Wurde die Route gefunden und ausgeführt wird TRUE zurückgeliefert, andernfalls
     * FALSE.
     *
     * @param string $name Name der Route, die ausgeführt werden soll.
     *
     * @return bool
     */
    protected function exec($name, array $params = array())
    {
        if ($this->has($name)) {
            if (empty($params)) {
                $params = $this->params;
            }
            $callback = $this->routes[$name]['callback'];
            if (is_callable($callback)) {
                $response = new Response();
                $buffer = new OutputBuffer();
                $buffer->start();
                echo call_user_func_array($callback, $params);
                $response->body = $buffer->end();
                $response->send();
            } elseif (class_exists($callback)) {
                $controller = new $callback($params);
            }

            return true;
        }

        return false;
    }

    /**
     * Gibt zurück ob die angegebene Route existiert und ob diese, falls erforderlich
     * auch mit HTTPS aufgerufen wurde.
     *
     * @param string $name Name der Route, die überprüft werden soll.
     *
     * @return bool
     */
    protected function isHTTPS($name)
    {
        if ($this->has($name)) {
            $route = $this->routes[$name];
            if (isset($route['options']['https'])) {
                $https = $route['options']['https'];
                $server_https = $this->request->server->get('HTTPS');
                if ($https === true && (empty($server_https) || $server_https == 'off')) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Gibt zurück ob die angegebene Route existiert und ob diese, falls erforderlich
     * auch über die richtige Request-Methode aufgerufen wurde.
     *
     * @param string $name Name der Route, die überprüft werden soll.
     *
     * @return bool
     */
    protected function isValidVerb($name)
    {
        if ($this->has($name)) {
            $route = $this->routes[$name];
            if (isset($route['options']['verb'])) {
                $verbs = $route['options']['verb'];
                $request_method = $this->request->server->get('REQUEST_METHOD');
                if (is_array($verbs) && !in_array(strtoupper($request_method), $verbs)) {
                    return false;
                } elseif (!is_array($verbs) && strtoupper($request_method) != $verbs) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Gibt zurück ob die angegebene Route existiert und ob diese, falls erforderlich
     * auch über die richtige Domain aufgerufen wurde.
     *
     * @param string $name Name der Route, die überprüft werden soll.
     *
     * @return bool
     */
    protected function isValidDomain($name)
    {
        if ($this->has($name)) {
            $route = $this->routes[$name];
            if (isset($route['options']['domain'])) {
                $domains = $route['options']['domain'];
                $http_host = $this->request->server('HTTP_HOST');
                if (is_array($domains) && !in_array($http_host, $domains)) {
                    return false;
                } elseif (!is_array($domains) && $http_host != $domains) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Gibt zurück ob die Route der aufgerufenen URL entspricht.
     *
     * @param string $name Name der Route, die überprüft werden soll.
     *
     * @return bool
     */
    protected function match($name)
    {
        if ($this->has($name)) {
            $route = $this->routes[$name];
            if (!$this->isHTTPS($name) || !$this->isValidVerb($name) || !$this->isValidDomain($name)) {
                return false;
            }
            $url = $this->findPlaceholders($route);

            return $this->getParams($url);
        }

        return false;
    }

    /**
     * Sucht nach Platzhaltern in der Routen-Definition und ersetzt diese durch
     * reguläre Ausdrücke.
     * Gibt die generierte URL zurück oder false wenn es sich um eine leere URL
     * bzw. um / handelt.
     *
     * @param array $route Routen-Objekt, wie es gespeichert wurde.
     *
     * @return string|false
     */
    protected function findPlaceholders($route)
    {
        $url = trim($route['url'], '/');
        if (empty($url) && trim($this->request_uri, '/') != '') {
            return false;
        }
        $matches = array();
        if (preg_match_all("/\{([\w-]+)\}/", $url, $matches) && isset($matches[1])) {
            foreach ($matches[1] as $match) {
                $replace = "([\w-]+)?";
                if (isset($route['options']['pattern'][$match])) {
                    $replace = $route['options']['pattern'][$match];
                }
                $url = str_replace('{'.$match.'}', $replace, $url);
            }
        }

        return $url;
    }

    /**
     * Speichert die Parameter der übergebenen URL als Array ($this->params)
     * anhand der aufgerufenen URL.
     *
     * @param string $url URL der die Parameter entnommen werden sollen
     *
     * @return bool
     */
    protected function getParams($url)
    {
        $request = trim($this->request_uri, '/');
        if(stripos($url, AUTO_ROUTE) !== false){
            $url = trim(str_replace('[auto]', '', $url), '/');
            if(preg_match("`^$url`", $request)){
                $this->params = explode('/', substr($request, strlen($url)));
                if(count($this->params) > 0 && empty($this->params[0])){
                    array_shift($this->params);
                }

                return true;
            }
        }

        $matches = array();
        $result = preg_match("`^$url$`", $request, $matches);
        if (count($matches) >= 2) {
            array_shift($matches);
            $this->params = $matches;
        }

        return $result;
    }

    /**
     * Liefert alle registrierten Routen zurück.
     *
     * @return array
     */
    public function getRoutes()
    {
        return $this->routes;
    }

    /**
     * Gibt zurück ob bereits Routen registriert wurden.
     *
     * @return bool
     */
    public function hasRoutes()
    {
        return !empty($this->routes);
    }

    /**
     * Generiert einen Link zu einer bestimmten Route.
     *
     * @param $name Name der Route oder URL
     *
     * @return string
     */
    public function link($name, array $params = array())
    {
        if ($this->has($name)) {
            $route = $this->routes[$name];
            $url = $route['url'];
            foreach ($params as $key => $val) {
                $url = str_replace('{'.$key.'}', $val, $url);
            }
            $url = preg_replace("/\{\w+\}/", '', $url);
            $name = ltrim(str_replace(AUTO_ROUTE, '', $url), '/');
        }

        return $this->asset($name);
    }

    /**
     * Führt eine Umleitung auf eine bestimmte Route oder URL durch.
     *
     * @param $name Name der Route oder URL
     * @param array $params Parameter für eine zugehörige Route
     */
    public function redirect($name, array $params = array())
    {
        $url = $this->link($name, $params);
        if ($url === null && filter_var($name, FILTER_VALIDATE_URL)) {
            $url = $name;
        }
        if (headers_sent()) {
            echo "<meta http-equiv='refresh' content='0, URL=$url'>";
        } else {
            header("Location: $url");
            exit();
        }
    }

    /**
     * Erzeugt einen absoluten Pfad für die aufgerufene URL eines bestimmten
     * Assets, beispielsweise einer CSS-Datei oder eines Bildes.
     *
     * @param $name
     *
     * @return string
     */
    public function asset($name)
    {
        return preg_replace('`/{2,}`', '/', $this->drips_root.$name);
    }

    /**
     * Liefert die aktuell gewählte Route zurück oder null, wenn keine Route
     * ausgewählt wurde.
     *
     * @return string
     */
    public function getCurrent()
    {
        return $this->current_route;
    }
}