Overview

Namespaces

  • Core
    • Auth
    • Exceptions
    • Helpers
    • Providers
  • Kernel
    • Dispatch
    • Providers
  • None
  • Vendor
    • ExampleVendor

Classes

  • Controller
  • Core\AbstractClass
  • Core\Auth\AuthenticateController
  • Core\Auth\PasswordModel
  • Core\ErrorController
  • Core\Exceptions\Exception
  • Core\Helpers\Flash
  • Core\Helpers\Hooks
  • Core\Language
  • Core\ParentController
  • Core\ParentModel
  • Core\Providers\Config
  • Core\Providers\Service
  • Core\Providers\Session
  • ErrorController
  • HomeController
  • HomeModel
  • Kernel\AppKernel
  • Kernel\Dispatch\Logger
  • Kernel\Dispatch\Mailer
  • Kernel\Dispatch\Mailer_PHP
  • Kernel\Providers\Permission
  • Kernel\Providers\Router
  • LoginController
  • LoginModel
  • Model
  • Vendor\ExampleVendor\ExampleVendorController
  • Vendor\ExampleVendor\ExampleVendorModel
  • Vendor\VendorController

Functions

  • _die
  • asset
  • clock_end
  • clock_start
  • clock_time
  • config
  • ddie
  • deleteDir
  • email_png
  • env
  • folder_action
  • folder_recurse
  • generateFile
  • getClientIP
  • getClientUserAgent
  • getVersion
  • isAjax
  • isAuth
  • isDev
  • isLocalServer
  • isProd
  • nocache
  • php
  • ppie
  • query
  • queryIndexed
  • save_ini_file
  • t
  • versioning
  • view
  • write_ini
  • Overview
  • Namespace
  • Class
  • Download
  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 
<?php

namespace Kernel\Providers;
use Core\Providers\Config;

/**
 * This file manages the routing for incoming URLs. Returns a call (<b>Controller->action()</b>) for the specific route.<br/>
 * To use in conjunction with <b>/src/config/routing.ini</b>, where all the desired routes must be present.
 * @usage
 * The [keys] MUST be unique throughout ALL routing.ini files.<br/>
 * The action omits the "Controller" part of the controller name. <br/>
 * Login@showLogin calls LoginController->showLogin(), \Core\Login@showLogin calls
 * \Core\LoginController->showLogin() and @test calls Controller->test()
 *
 * @example
 * <code>
 * [RoutingKey]
 *     path = <desired_path> ; no leading slash
 *     action = <Controller_Name>@<Action_Name> ; don't write "Controller", just its name
 *     after_login = 1 ; (or 0. OPTIONAL and 0 by default.<br/>
 *                   This means: if on first attempt it has no permission, can the user be auto-redirected after login?)
 *     method = "PUT|POST" ; (GET|PUT|POST|DELETE) OPTIONAL, "|" separated if multiple.
 *                   Will only enter if $_SERVER['REQUEST_METHOD'] matches
 *     condition = '$_SERVER["HTTP_HOST"] == "localhost"' ; OPTIONAL.
 *                   If set, the code will be evaluated (function: eval()) as a further filter
 *     default[animal] = dog
 *     default[sound] = bark ; This is optional! but /zoo/:animal/:sound will match for /zoo, /zoo/<any_animal> and
 *                             /zoo/<any_animal>/<any_sound> all in only one route! (use it for things like /blogpost/:page/:number
 *                             with :page = page and :number = 1 as defaults. Hence /blogpost is page 1 and /blogpost/page/2 is page 2)
 * </code>
 *
 * @package Kernel
 */
class Router {

    /**
     * @var string The request URI for this thread execution. (i.e., the URL in the browser)
     */
    public static $uri;

    /**
     * Searches for a URI match and returns the Controller + Action that dispatch it.
     * @return array ControllerName and Action for requested URI
     */
    public static function matchRoute() {        
        $config = Config::singleton();
        $routing = $config->get('Routing');

        self::$uri = substr(strtok($_SERVER['REQUEST_URI'] ?: '/', '?'), strlen(__PATH__) + 1);
        foreach ($routing as $routeKey => $route) {
            if (
                /* You can set a method = get|post|put|delete to further filter. See example */
                (!empty($route['method']) && !in_array($_SERVER['REQUEST_METHOD'], explode('|', strtoupper($route['method'])))) ||
                (!isset($route['path']) /* this means is the [404] Route */)) {
                continue;
            }

            /**
             * If there are defaults set, we replace it in the route.
             * This makes /blog, /blog/1, /blog/123 match the same route if default[page] is set:
             * -------
             * path = /blog/:page
             * default[page] = 1
             */
            $matchesDefault = FALSE;
            if (!empty($route['default'])) {
                $matchesDefault = self::checkForDefaultParams($route);
            }

            /* We replace the placeholders :id, or :name, or :foo for a valid RegEx */
            $pregRoute = preg_replace('(:[a-z0-9_]+)', '([^\/]+)', str_replace('/', '\/', $route['path']));
            $path = '/^' . $pregRoute . '\/?$/';

            if (preg_match($path, self::$uri, $matches) || $matchesDefault) {
                /* If PHP code is added to further filter routing, we evaluate it.
                   And if the condition doesn't match, we skip this route */
                if (isset($route['condition'])) {
                    $condition = FALSE;
                    eval('$condition = ' . $route['condition'] . ";");
                    if (!$condition) {
                        continue;
                    }
                }
                $routeMatches = $matchesDefault ?: $matches;

                /* Route found, so we set the Get params and return the proper Route (key) */
                self::setGetParams($route, $routeMatches);
                return $routeKey;
            }
        }

        /* Not found! Will go to NOT_FOUND_PAGE route */
        return 'NOT_FOUND_PAGE';
    }
    
    /**
     * Sets the $_GET superglobal with the GET parsed parameters. Just like .htaccess
     * @param array $route The route object from the routing file
     * @param array $matches The GET matches in the URL
     */
    private static function setGetParams($route, $matches) {
        /* The :placeholders get added to the $_GET global */
        if (count($matches) > 1) {
            preg_match_all('(:([a-z_]+))', str_replace('/', '\/', $route['path']), $params);
            foreach ($params[1] as $key => $param) {
                if (isset($matches[$key + 1])) {
                    $_GET[$param] = $matches[$key + 1];
                }
            }
        }
        /* The get[foo] parameters on the routing.ini get added to the $_GET superglobal */
        if (!empty($route['get'])) {
            foreach ($route['get'] as $get => $value) {
                $_GET[$get] = $value;
            }
        }
        /* The default values are added to the URL if they are not previously set */
        if (!empty($route['default'])) {
            foreach ($route['default'] as $default => $value) {
                if (!isset($_GET[$default])) {
                    $_GET[$default] = $value;
                }
            }
        }
    }

    /**
     * This function scans a routing rule that <em>contains</em> <b>default[&lt;foo&gt;]</b> parameters and tries to
     * match it against the current route by trying parameters iteratively.
     * Thanks to this function, the following many URIs can be controlled from the same path:
     * <code>
     * // The URIs: /zoo/cat/meow /zoo/cat and /zoo provide cat|meow, cat|bark and dog|bark from the $_GET global
     * //     $_GET['animal'] and $_GET['sound'] respectively
     * [Zoo]
     * path = zoo/:animal/:sound
     * default[animal] = dog
     * default[sound]  = bark
     * action = Foo@Zoo
     *
     * @param array $route The routing info for this iteration of all routing rules
     *
     * @note As you might imagine, adding default parameters slows down the loading a little. Avoid excessive use!
     *
     * @return array|FALSE The found matches (to later replace and add to $_GET) or FALSE if there are no matches
     */
    private static function checkForDefaultParams($route) {
        $fakeRoute = str_replace('/', '\/', $route['path']);
        preg_match_all("/:([a-z0-9_]+)/", $fakeRoute, $keyMatchesAux);
        $keyMatches = array_reverse($keyMatchesAux[1]);
        foreach ($keyMatches as $k) {
            // This means there are no more default values to check
            if (empty($route['default'][$k])) break;

            $fakeRoute = str_replace('\/:' . $k, '', $fakeRoute);
            $pregFakeRoute = '/^' . preg_replace('(:[a-z0-9_]+)', '([^\/]+)', $fakeRoute) . '\/?$/';
            if (preg_match($pregFakeRoute, self::$uri, $matches)) {
                /* Found a match, so we return WHICH parameters do match */
                return $matches;
            }
        }
        /* If the URL doesn't match this route with defaults, it means it's not the right one! */
        return FALSE;
    }
}
Ribosome API documentation generated by ApiGen