strata-mvc/strata

View on GitHub
src/Router/RouteParser/Url/UrlRoute.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

namespace Strata\Router\RouteParser\Url;

use Strata\Router\RouteParser\Route;
use Strata\Controller\Controller;
use Strata\Controller\Request;
use Strata\Model\WordpressEntity;
use Strata\Utility\Inflector;
use Strata\Strata;
use AltoRouter;
use Exception;

/**
 * Handles routes generated from REST requests.
 */
class UrlRoute extends Route
{
    /**
     * @var string The hidden key for dynamically defined callback functions.
     */
    const DYNAMIC_PARSE = "__strata_dynamic_parse__";

    /**
     * Altorouter is the library that does the heavy lifting for us.
     * @var AltoRouter
     */
    private $altoRouter = null;

    /**
     * @var array Routes defined by the current application's configuration (3rd priority)
     */
    private $applicationRoutes = array();

    /**
     * @var array Routes defined by the current application's model (2nd priority)
     */
    private $modelRoutes = array();

    /**
     * @var array Routes defined automatically by Strata (1st priority)
     */
    private $automatedRoutes = array();

    /**
     * Configures the routing deamon from the routes that have
     * been set during bootstrap.
     */
    public function listen()
    {
        $this->altoRouter = new AltoRouter();
        $this->altoRouter->addRoutes($this->listRegisteredRoutes());
    }

    public function listRegisteredRoutes()
    {
        return $this->parseMethodTypes(array_merge(
            $this->modelRoutes,
            $this->automatedRoutes,
            $this->applicationRoutes
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function addPossibilities(array $routes)
    {
        foreach ($routes as $route) {
            $this->applicationRoutes[] = $this->parseRouteConfiguration($route);
        }
    }

    /**
     * Adds a resourced based route possibility based on a custom
     * post type or taxonomy.
     * @param WordpressEntity $model
     */
    public function addResourcePossibility(WordpressEntity $model)
    {
        $slug = null;

        if (property_exists($model, "routed") && is_array($model->routed) &&  array_key_exists("controller", $model->routed)) {
            $controllerName = $model->routed['controller'];
            $controllerObject = new $controllerName();
            $controller = $controllerObject->getShortName();
        } else {
            $controller = Controller::generateClassName($model->getShortName());
        }

        $i18n = Strata::i18n();
        if ($i18n->isLocalized()) {
            $currentLocale = $i18n->getCurrentLocale();
            if ($currentLocale && !$currentLocale->isDefault()) {
                $slugInfo = $model->extractConfig("i18n." . $currentLocale->getCode() . ".rewrite.slug");
                $slug = array_pop($slugInfo);

                if (!is_null($slug)) {
                    $this->automatedRoutes[] = array('*', "/$slug/page/[i:pageNumber]/?", "$controller#index");
                    $this->automatedRoutes[] = array('*', "/$slug/[:slug]/?", "$controller#show");
                    $this->automatedRoutes[] = array('*', "/$slug/?", "$controller#index");
                }
            }
        }

        $slugInfo = $model->extractConfig("rewrite.slug");
        $slug = array_pop($slugInfo);

        if (is_null($slug)) {
            $slug = $model->getWordpressKey();
        }

        $this->automatedRoutes[] = array('*', "/$slug/page/[i:pageNumber]/?", "$controller#index");
        $this->automatedRoutes[] = array('*', "/$slug/[:slug]/?", "$controller#show");
        $this->automatedRoutes[] = array('*', "/$slug/?", "$controller#index");
    }

    public function addModelPossibilities(array $routes)
    {
        foreach ($routes as $route) {
            $this->modelRoutes[] = $this->parseRouteConfiguration($route);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function start()
    {
        $this->logRouteStart();
    }

    /**
     * {@inheritdoc}
     */
    public function end()
    {
        $this->assignViewVars();
        $this->logRouteCompletion();
    }

    /**
     * This function is required to send variables in Wordpress' scope.
     * Unlike the templating using Controller#view->render() which allow
     * passing variables, Wordpress's load_template extracts variables in
     * $wp_query only.
     */
    protected function assignViewVars()
    {
        global $wp_query;

        if (!is_null($this->controller) && !is_null($this->controller->view)) {
            foreach ($this->controller->view->getVariables() as $key => $value) {
                $wp_query->set($key, $value);
                $GLOBALS[$key] = $value;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function process($url = null)
    {
        if ($url === "/index.php/") {
            $url = "/";
        }

        $match = $this->altoRouter->match($url);

        if (!is_array($match)) {
            return;
        }

        if (is_404() && $match["target"] === self::DYNAMIC_PARSE) {
            $match = array(
                'target' => 'PageController',
                'params' => [
                    'action' => 'notFound'
                ],
                'name' => null
            );
        }

        $this->handleRouterAnswer($match);
    }

    /**
     * Adds a new route configuration.
     * @param array $route
     */
    private function parseRouteConfiguration($route)
    {
        if (!is_array($route)) {
            throw new Exception("Strata configuration file contains an invalid route.");
        }

        return $this->isDynamic($route) ?
            $this->parseDynamicRoute($route) :
            $this->parseMatchedRoute($route);
    }

    /**
     * Defines whether the $route is dynamic based
     * on it's length
     * @param  array  $route
     * @return boolean
     */
    private function isDynamic($route)
    {
        return count($route) < 3;
    }

    /**
     * Adds a new dynamic route
     * @param array $route
     */
    private function parseDynamicRoute($route)
    {
        return array($route[0], $route[1], self::DYNAMIC_PARSE);
    }

    /**
     * Adds a matched route
     * @param array $route
     */
    private function parseMatchedRoute($route)
    {
        return array($route[0], $route[1], $route[2]);
    }

    /**
     * Analized the route match to see if it's a dynamic or
     * matched route.
     * @param  array $match An AltoRouter answer
     */
    private function handleRouterAnswer($match)
    {
        if ($match["target"] === self::DYNAMIC_PARSE) {
            $this->handleDynamicRouterAnswer($match);
        } else {
            $this->handleMatchedRouterAnswer($match);
        }

        if (!method_exists($this->controller, $this->action)) {
            $this->action = "noActionMatch";
        }
    }

    /**
     * Handles dynamic routes
     * @param  array $match An AltoRouter answer
     */
    private function handleDynamicRouterAnswer($match)
    {
        $this->controller   = $this->getControllerFromDynamicMatch($match);
        $this->action       = $this->getActionFromDynamicMatch($match);
        $this->arguments    = $this->getArgumentsFromDynamicMatch($match);
    }


    /**
     * Handles matched routes
     * @param  array $match An AltoRouter answer
     */
    private function handleMatchedRouterAnswer($match)
    {
        $this->controller   = $this->getControllerFromMatch($match);
        $this->action       = $this->getActionFromMatch($match);
        $this->arguments    = $this->getArgumentsFromMatch($match);
    }

    /**
     * Gets a Controller instance form the matched information
     * @param  array $match An AltoRouter answer
     * @return Controller A controller object
     */
    private function getControllerFromMatch($match = array())
    {
        try {
            $target = explode("#", $match["target"]);
            return Controller::factory($target[0]);
        } catch (Exception $e) {
            // The controller did not exist, we don't care at this point.
            // We'll just ignore the route.
        }

        return Controller::factory("App");
    }

    /**
     * Gets a Controller instance form the dynamic information
     * @param  array $match An AltoRouter answer
     * @return Controller A controller object
     */
    private function getControllerFromDynamicMatch($match = array())
    {
        try {
            if (array_key_exists("controller", $match["params"])) {
                return Controller::factory($match["params"]["controller"]);
            }
        } catch (Exception $e) {
            // The controller did not exist, we don't care at this point.
            // We'll just ignore the route.
        }

        return Controller::factory("App");
    }

    /**
     * Gets the action from the AltoRouter match.
     * @param  array $match An AltoRouter answer
     * @return string
     */
    private function getActionFromMatch($match = array())
    {
        $target = explode("#", $match["target"]);

        if (count($target) > 1) {
            return $target[1];
        }

        if (array_key_exists("action", $match["params"])) {
            return $this->getActionFromParam($match["params"]["action"]);
        }

        $this->controller->request = new Request();
        if (is_admin() && $this->controller->request->hasGet('page')) {
            return $this->controller->request->get('page');
            // When no method is sent, guesstimate from the action post value (ex: basic ajax)
        } elseif ($this->controller->request->hasPost('action')) {
            return $this->controller->request->post('action');
        }
    }

    /**
     * Gets the action from the dynamic match.
     * @param  array $match An AltoRouter answer
     * @return string
     */
    private function getActionFromDynamicMatch($match)
    {
        if (array_key_exists("action", $match["params"])) {
            return $this->getActionFromParam($match["params"]["action"]);
        }

        if (array_key_exists("controller", $match["params"]) && !array_key_exists("action", $match["params"])) {
            return "index";
        }

        return "noActionMatch";
    }

    /**
     * Formats the action based on the router's match
     * @param  string $action
     * @return string
     */
    private function getActionFromParam($action)
    {
        $action = str_replace("-", "_", $action);
        if (substr($action, -1) === "/") {
            $action = substr($action, 0, -1);
        }

        return lcfirst(Inflector::camelize($action));
    }

    /**
     * Gets the possible additional arguments from the route match.
     * @param  array $match An AltoRouter answer
     * @return array
     */
    private function getArgumentsFromMatch($match = array())
    {
        if (is_array($match['params']) && count($match['params'])) {
            $params = $match['params'];
            return is_array($params) ? $params : array($params);
        }

        return array();
    }

    /**
     * Gets the possible additional arguments from the dynamic match.
     * @param  array $match An AltoRouter answer
     * @return array
     */
    private function getArgumentsFromDynamicMatch($match = array())
    {
        if (array_key_exists("params", $match["params"])) {
            $params = $match["params"]["params"];
            return is_array($params) ? $params : array($params);
        }

        return array();
    }

    /** 
     * Strata allows a wildcard for global routes, replace that wildcard
     * with all the availlable possibilities explicitly.
     * @param  array $routes
     * @return array
     */
    private function parseMethodTypes($routes = array())
    {
        $parsed = array();

        foreach ($routes as $route) {
            if ($route[0] === "*") {
                $route[0] = "HEAD|GET|PUT|POST|UPDATE|DELETE|OPTIONS";
            }
            $parsed[] = $route;
        }

        return $parsed;
    }
}