polyfony-inc/polyfony

View on GitHub
Private/Polyfony/Router.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

namespace Polyfony;

class Router {
    
    // all the routes
    protected static $_routes = [];
    // the currently matched route
    protected static $_match = null;
    // the currently instanciated controller
    protected static $_controller = null;
    
    public static function init() {

        // find a matching route
        self::route();
        
    }

    private static function canWeUseCachedRoutes() :bool {
        return 
            Config::isProd() && 
            Cache::has('Routes') && 
            Config::get('router', 'cache');
    }
    
    private static function restoreCachedRoutes() :void {
        // restore the routes from the cache
        foreach(Cache::get('Routes') as $route_name => $route) {
            // reinstanciate a route (maybe there's a better way, this looks very much like a hack)
            $route_object = new Route($route_name);
            // for each attribute
            foreach($route as $key => $value) {
                // re-set it
                $route_object->{$key} = $value;
            }
            // add the route
            self::$_routes[$route_name] = $route_object;
        }
    }

    public static function includeBundlesRoutes($bundles_routes_files) :void {
        
        // if we are in prod, and allowed to cache routes, and have a cache file available
        if(self::canWeUseCachedRoutes()) {
            // we can restore the routes from the cache
            self::restoreCachedRoutes();
        }
        // then we have to include the routes files
        else {
            // for each of those files
            foreach($bundles_routes_files as $file) {
                // include it
                include($file);
            }
            // as we have build the routes objet, we should cache them
            Cache::put('Routes', self::$_routes, true);
        }

    }
    
    // new syntax for mapping routes
    public static function map(
        string $url = null, 
        string $destination, 
        string $route_name = null, 
        string $method = null
    ) :Route {
        // We cannot allow duplicate route names for reversing reasons
        if($route_name && self::hasRoute($route_name)) {
            // throw an exception
            throw new Exception("Router::map() The route {$route_name} has already been declared");
        }
        // create a new route
        $route = new Route($route_name);
        // configure the route
        $route
            ->setUrl($url)
            ->setDestination($destination)
            ->setMethod($method);
        // add it to the list of known routes
        self::$_routes[$route->name] = $route;
        // return the route for finer tuning
        return self::$_routes[$route->name];
    }

    // new syntax for mapping routes
    public static function get(
        string $url, 
        string $destination, 
        $route_name = null
    ) :Route {
        return self::map($url, $destination, $route_name, 'get');
    }

    // new syntax for mapping routes
    public static function post(
        string $url, 
        string $destination, 
        $route_name = null
    ) :Route {    
        return self::map($url, $destination, $route_name, 'post');
    }

    // new syntax for mapping routes
    public static function delete(
        string $url, 
        string $destination, 
        $route_name = null
    ) :Route {    
        return self::map($url, $destination, $route_name, 'delete');
    }

    // new syntax for mapping routes
    public static function put(
        string $url, 
        string $destination, 
        $route_name = null
    ) :Route {    
        return self::map($url, $destination, $route_name, 'put');
    }

    // new syntax for quick redirects
    public static function redirect(
        string $source_url, 
        string $redirection_url, 
        int $status_code=301
    )  {
        // create a new route
        $route = new Route();
        // add it to the list of known routes
        self::$_routes[$route->name] = $route
            ->setUrl($source_url)
            ->setRedirect($redirection_url, $status_code);
        // return the route for finer tuning
        return self::$_routes[$route->name];
    }

    // check if the route exists
    public static function hasRoute(string $route_name) :bool {
        // true if route exists, false otherwise
        return isset(self::$_routes[$route_name]);
    }
    
    // get a specific route
    public static function getRoute(string $route_name) {
        // return the route of false
        return self::hasRoute($route_name) ? self::$_routes[$route_name] : false;
    }
    
    // update the current route after forwarding
    public static function setCurrentRoute(Route $route) :void {
        // update the matched route
        self::$_match = $route;
    }
    
    // get the current route
    public static function getCurrentRoute() {
        // returned the matched route
        return self::$_match ? self::$_match : null;
    }
    
    // find the proper route
    public static function route() :void {

        // marker
        Profiler::setMarker('Router.route', 'framework');
        // get the requested url
        $request_url = Request::getUrl();
        // get the requested method
        $request_method = Request::getMethod();
        // Loop over each route and test to see if they are valid
        foreach(self::$_routes as $route) {
            // if the route matches
            if(self::routeMatch($request_url, $request_method, $route)) {
                // get it
                $matching_route = $route;
                // stop trying
                break;
            }
        }
        // marker
        Profiler::releaseMarker('Router.route');
        // if no match is found and we don't have an error route to fallback on
        if(!isset($matching_route)) {
            // throw a native exception since there is no cleaner alternative
            Throw new Exception('Router::route() no matching route', 404);
        }
        // send the matching route to the dispatcher
        self::forward($matching_route);    
        
    }


    // Test to see if this route is valid against the URL.
    private static function routeMatch(
        string $request_url, 
        string $request_method, 
        Route $route
    ) :bool {

        // if the method is set for that route, and it doesn't match
        if(!$route->hasMethod($request_method)) {
            return false;
        }
        // if the url doesn't begin with the static segment or that route
        if(!$route->hasStaticSegment($request_url)) {
            return false;
        }
        // if we've got a redirect, let's go for it
        $route->redirectIfAny();
        // get a list of current request parameters, by numerical position (index)
        $request_parameters_indexed_by_position = Request::getUrlIndexedParameters(
            $route->staticSegment
        );
        // if restricttion against url parameters don't match
        if(!$route->hasValidParameters($request_parameters_indexed_by_position)) {
            return false;
        }
        // if the url's signature doesn't match
        if(!$route->hasValidSignature($request_parameters_indexed_by_position)) {
            return false;
        }
        // set route as matching
        $route->setAsMatching($request_parameters_indexed_by_position);
        // check if they match defined constraints
        return true;

    }

    // Generate an URL from an known route and url parameters
    public static function reverse(
        string $route_name, 
        array $parameters = [], 
        bool $is_absolute = false, 
        bool $force_tls = false
    ) :string {
        // if the specified route doesn't exist
        if(!self::hasRoute($route_name)) {
            // we cannot reverse a route that does not exist
            Throw new Exception("Router::reverse() : The [{$route_name}] route does not exist");
        }
        // if the route is to be signed
        if(self::$_routes[$route_name]->signing) {
            // if the route is to expire
            if(self::$_routes[$route_name]->expiring) {
                // generate the expiration timestamp
                $parameters[Config::get('router','url_parameters_expiration')] = time() + self::$_routes[$route_name]->expiringTtl;
            }
            // generate a signature of the parameters and add it after existing parameters
            $parameters[Config::get('router','signature_parameter_name')] = Hashs::get([$route_name,$parameters]);
        }
        // return the reversed url
        return self::$_routes[$route_name]->getAssembledSegments(
            $parameters, 
            $is_absolute, 
            $force_tls
        );
    }
    
    public static function forward(Route $route) :void {

        // get the full destination
        list(
            $script, 
            $class, 
            $method
        ) = $route->getDestination();
        // if script is missing from the bundle
        if(!file_exists($script)) {
            // new polyfony exception
            Throw new Exception(
                "Dispatcher::forward() : Controller file [{$script}] does not exist", 
                500
            );    
        }
        // include the controller's file
        require_once($script);
        // if the class is missing from the controller
        if(!class_exists($class,false)) {
            // new polyfony exception
            Throw new Exception(
                "Dispatcher::forward() : Controller class [{$class}] does not exist in [{$script}]", 
                500
            );    
        }
        // update the current route
        self::setCurrentRoute($route);
        // apply rate-limiting restrictions 
        $route->throttleIfAny();
        // instanciate
        self::$_controller = new $class;
        // if the method does not exist in the controller
        if(!method_exists($class,$method)) {
            // new polyfony exception 
            // Method name removed from error message for safety reason (method is a user input).
            // DO NOT ADD IT BACK IN. 
            Throw new Exception(
                "Dispatcher::forward() : Method not implemented in [{$class}]", 501);    
        }
        // marker
        $id_pre_marker = Profiler::setMarker("{$route->controller}.before", "controller");
        // before action
        self::$_controller->before();
        // marker
        Profiler::releaseMarker($id_pre_marker);
        // marker
        $id_marker = Profiler::setMarker("{$route->controller}.{$method}", "controller");
        // call the method
        self::$_controller->$method();
        // marker
        Profiler::releaseMarker($id_marker);
        // marker
        $id_post_marker = Profiler::setMarker("{$route->controller}.after", "controller");
        // after action
        self::$_controller->after();
        // marker
        Profiler::releaseMarker($id_post_marker);
        
    }
    
}


?>