Paint the Web - a micro-Blog in .md about Dev, Web and more

Flood\Component: Route

The routing component is responsible for what request will execute what.

Given that task, the component need to get, for every app that should have routing, all information about protocols, domains, paths, query, SEO rules like www or no www/trailing slash or not.

Also it needs to know what should be done when which request is executed.

For using the individual callbacks, just assign dispatchers for the keys and triggers used.

Just searching for turning on debug?

<?php
\Flood\Component\Route\Route::$debug = true;

will enable output in exceptions of this component and other outputs like which routes are added

Storage Handler

A storage handler will know how to read and set the routes, and mostly how to build what should be executed.

Some storage namespaces are per default included in this component. These are reserved and/or added per default to the needed events:

  • hook: handles all routes to one hook and specify the locale based domain and url rules, built in
  • redirect: default redirector dispatcher
  • response: stores callback information, PHP callables with a JSON markup, built in
  • folder: cdn component reserved naming
  • file: cdn component reserved naming
{
  "<hook-id>": {
    "route": {
      "hook": {},
      "redirect": {},
      "response": {},
      "folder": {},
      "file": {}
    }
  }
}

Pseudo-code selector: hook.<hook-id>.route.<handler-key> | <hook-id>.route.<handler-key>

Register a new Handler

Handlers are the main objects for receiving and storing the data of each elements of the hook's route array.

The handlers for hook and response are built-in, but are registered like external handlers.

The built-in handler are using the special raw container syntax, the CDN component on the other hand only the simple object mode.

First the container syntax, it is possible to bypass protected and private when the method it is in an child of \Flood\Component\Route\Hook\Hook.

The executing routing object must be the correct one, when overwritten somewhere check the order and/or when was added what and to which container.

<?php
namespace Flood\Component\Route;

Container::i()->register('route-storage-object-hook', [Container::_hook(), 'addHookRoute']);
// Flood\Component\Route\Hook->addHookRoute($data['hook']);

Container::i()->register('route-storage-object-response', [Container::_hook(), 'addRouteResponse']);
// Flood\Component\Route\Hook->addRouteResponse($data['response']);

// Examples of other components:

// for: hook.<hook-id>.route.folder
Container::i()->register('route-storage-object-folder', function () {
    return new FloodCdn\Route\Folder();
});

// for: hook.<hook-id>.route.file
Container::i()->register('route-storage-object-file', function () {
    return new FloodCdn\Route\File();
});

Events

An event generates the routes at a given point, they must get their information from Flood\Component\Route\Container::_hook()->get('key').

Events are called in the execution of RouteGenerator, to each event just register the needed callback for each handler.

  • hook-list: calls the event with parameter $hook_id
  • locale-list: calls the event with parameter $hook_id, $locale

The events are saved in Flood\Component\Route\Hook\RouteGenerator, it is accessible from the container with Container::i()->_hookRouteGenerator(). They are executed in the order they where assigned.

Even the internal generateByLocalList is assigned to the event hook-list.

In the same style like that method and the built-in response handler you could extend those handlers. The hook namespace is handled as parent and there are no further event handlers.

The event handler needs to return an object of Symfony\Component\Routing\RouteCollection.

<?php
// hook-list
Container::i()->_hookRouteGenerator()->registerEventHandler(
    'hook-list',
    [Container::i()->_hookRouteGenerator(), 'generateByLocaleList']
);

// locale-list
Container::i()->_hookRouteGenerator()->registerEventHandler(
    'locale-list',
    function ($hook_id, $locale) {
        return Container::i()->_hookRouteGenerator()->generateResponse($hook_id, $locale);
    }
);

// Examples of other components:
Container::_hookRouteGenerator()->registerEventHandler('hook-list', function ($hook_id) {
    return Container::_hookStorage()->get($hook_id)->getStorageObject('folder')->generateByHook($hook_id);
});

Container::_hookRouteGenerator()->registerEventHandler('hook-list', function ($hook_id) {
    return Container::_hookStorage()->get($hook_id)->getStorageObject('file')->generateByHook($hook_id);
});

Callables

Everything is based on callables, these provide the methodology of executing and generating call sequences based on strings or saving the execution now in a variable and executing it later.

Basic PHP callables example:

<?php
// 1: function callback
call_user_func('the_function_name');

// 2: static method callback
call_user_func(['ClassName', 'theMethodName']);

// 3: object method call
$obj = new MyClass();
call_user_func([$obj, 'myCallbackMethod']);

// 4: static class method call (As of PHP 5.2.3)
call_user_func('MyClass::myCallbackMethod');

// 5: Relative static class method call (As of PHP 5.3.0)
class A {
    public static function who() {
        echo "A\n";
    }
}

class B extends A {
    public static function who() {
        echo "B\n";
    }
}

call_user_func(['B', 'parent::who']); // A

// 6: Objects implementing __invoke can be used as callables (since PHP 5.3)
class C {
    public function __invoke($name) {
        echo 'Hello ', $name, "\n";
    }
}

$c = new C();
call_user_func($c, 'PHP!');

But those callables need to be converted from JSON, what need some markup in JSON.

hook provides the basic information for all other callback extensions.

response is the default callback handler for executing locale based requests.

Markup of Callables in JSON

The markup:

hook

// todo

response

A sample markup, see the differences at response.<locale> for the two types lazy and extended. The type is determined through the locale scope, exists a 'locale' with class or call-list as index, type 'lazy' is used.

Lazy Markup

Markup Type Lazy Example

{
  "response":{
    "*": {
      "namespace": "Flood\\Hydro\\App",
      "register-autoload": true,
      "type": "object",
      "call-list": [
        "call",
        "handle",
        "respond"
      ]
    },
    "home": {
      "*": "/",
      "de-DE": "/",
      "en-US": "/",
      "class": "Home"
    },
    "blog": {
      "de-DE": "blog/{path-dynamic}",
      "en-US": "blog/{path-dynamic}",
      "class": "Blog"
    }
  }
}

Extended Markup

Markup Type Extended Example

{
  "response":{
    "*": {
      "namespace": "Flood\\Hydro\\App",
      "register-autoload": true,
      "type": "object",
      "call-list": [
        "call",
        "handle",
        "respond"
      ]
    },
    "home": {
      "*": {
        "structure-child": "/",
        "class": "HomeLocaleSwitch"
      },
      "de-DE": {
        "structure-child": "/",
        "class": "Home"
      },
      "en-US": {
        "structure-child": "/",
        "class": "Home"
      }
    },
    "blog": {
      "de-DE": "blog/{path-dynamic}",
      "en-US": "blog/{path-dynamic}",
      "class": "Blog"
    }
  }
}

Explained

The types could be mixed of cause, they only need to be persistent for one response element response.<id>.

response['*'] is the master ident, when used, values not defined in the other id's will be searched for here. It provides the data for Flood\Component\Route\Hook\Hook->route_response_build but will not be added to Flood\Component\Route\Hook\Hook->route_response.

When the value is also not found there, it will fetch getHookData as fallback.

Leading to the getter method getResponseData which is used to generate the response routes. This method has implemented the overwriting logic and serves as single getter for all information.

<?php
Container::i()->_hookStorage()->get($hook_id)->getResponseData('namespace', $response_id, $locale);
Container::i()->_hookStorage()->get($hook_id)->getResponseData('ssl', $response_id, $locale);
Container::i()->_hookStorage()->get($hook_id)->getResponseData('structure-child', $response_id, $locale);
Container::i()->_hookStorage()->get($hook_id)->getResponseData(['uri', 'path'], $response_id, $locale);

Example case in JSON:

  • namespace is saved in <hook-id>.response[*]
  • ssl is saved in <hook-id>.route.hook[*]
  • structure-child is saved in <hook-id>.route.response.<response_id>.<locale>
  • ['uri', 'path'] as selectors arrays are used to fetch the specific value, e.g. <hook-id>.route.hook.<locale>.uri.path

They of cause could also be saved in some other value of those that get overwritten/inherited.

General priority:

  • <hook-id>.route.response.<response_id>.<locale> will overwrite
  • <hook-id>.route.response[*] will overwrite
  • <hook-id>.route.hook.<locale> will overwrite
  • <hook-id>.route.hook[*]

Callback Call Types

The setting for responses consists of:

  • namespace: used to prefix class
  • controller-path: used for autoload, only needed at autoload
  • register-autoload: registers an spl autoload for the namespace and path
  • type: which type the callback is based on, see 'The Types'
  • call-list: which functions and methods should be executed
    • has reserved keys, see call-param
    • will execute in order they are defined, only respond will be called at the end no matter where it is placed
  • call-param: which parameteres should be added to which call
    • uses the call-list string as index
    • has reserved keys
      • __construct: will be added at specific types when a object is initiated
      • __invoke: will be added at specific types when a object is invoked
      • respond: the function/method will be called and returned at last

Types

There are four possible types object, static, invoke and function. These will use the other possible settings to run and require what need to be done.

object - initiates the object and calls all methods in call-list

{
  "response": {
    "*": {
      "namespace": "Flood\\Component\\Route\\DemoHook",
      "controller-path": "hook/demo/Controller/",
      "register-autoload": true,
      "type": "object",
      "call-list": [
        "call",
        "handle",
        "respond"
      ],
      "call-param": {
        "call": {
          "some-param": "value"
        }
      }
    },
    "home": {
      "*": "/",
      "en-US": "/",
      "class": "Home"
    }
  }
}

Would be this execution that will happen when the route matches home.'en-US'

<?php
$controller = function () {
    $obj = new \Flood\Component\Route\DemoHook\Home();
    call_user_func_array([$obj, 'call'], ['some-param' => 'value']);
    call_user_func([$obj, 'handle']);
    return call_user_func([$obj, 'respond']);
};
  • Supports: call-list, call-param, namespace, controller-path, register-autoload
  • Implements:
    • call-param.'__construct' - passes the call params as array to the constructor
    • call-list.'respond' - will be returned at last
  • Requires: call-list, namespace, class

static - calls all call-list as static methods of namespace.'\'.class

{
  "response": {
    "*": {
      "namespace": "Flood\\Component\\Route\\DemoHook",
      "controller-path": "hook/demo/Controller/",
      "register-autoload": true,
      "type": "static",
      "call-list": [
        "call",
        "handle",
        "respond"
      ]
    },
    "home": {
      "*": "/",
      "en-US": "/",
      "class": "Home"
    }
  }
}
<?php
$controller = function () {
    call_user_func(['Flood\Component\Route\DemoHook\Home', 'call']);
    call_user_func(['Flood\Component\Route\DemoHook\Home', 'handle']);
    return call_user_func(['Flood\Component\Route\DemoHook\Home', 'respond']);
};
  • Supports: call-list, call-param, namespace, controller-path, register-autoload
  • Implements:
    • call-list.'respond' - will be returned at last
  • Requires: call-list, namespace, class

invoke - initiates the object and calls it with __invoke

{
  "response": {
    "*": {
      "namespace": "Flood\\Component\\Route\\DemoHook",
      "controller-path": "hook/demo/Controller/",
      "register-autoload": true,
      "type": "invoke",
      "call-list": [
        "call",
        "handle",
        "respond"
      ]
    },
    "home": {
      "*": "/",
      "en-US": "/",
      "class": "Home"
    }
  }
}
<?php
$controller = function () {
    $obj = new \Flood\Component\Route\DemoHook\Home();
    call_user_func([$obj]);
    call_user_func([$obj, 'call']);
    call_user_func([$obj, 'handle']);
    return call_user_func([$obj, 'respond']);
};
  • Supports: call-list, call-param, namespace, controller-path, register-autoload
  • Implements:
    • call-param.'__construct' - passes the call params as array to the constructor
    • call-param.'__invoke' - passes the call params as params to __invoke
    • call-list.'respond' - will be returned at last
  • Requires: namespace, class

function - calls all call-list as functions

{
  "response": {
    "*": {
      "type": "function",
      "call-list": [
        "someFunction",
        "anotherFunction"
      ]
    },
    "home": {
      "*": "/",
      "en-US": "/",
      "call-list": [
        "someFunction",
        "anotherFunction"
      ]
    }
  }
}

At lazy, the functions need to be added to each response, but not the type when all of the responses use functions.

<?php
$controller = function () {
    call_user_func('someFunction');
    call_user_func('anotherFunction');
};
  • Supports: call-list, call-param
  • Implements:
    • call-list.'respond' - will be returned at last

Parameters

You could specify per call parameters which should be assigned:

{
  "blog": {
    "en-US": {
      "namespace": "",
      "type": "static",
      "call-list": [
        "staticCall",
        "staticHandle"
      ],
      "call-param": {
        "staticCall": {
          "test_param": "value-of-param"
        }
      },
      "class": "Home"
    }
  }
}

This will call \Home::staticCall('value-of-param').

The array call-param has indices that are used as foreign key to call-list, when a matching call is happening the array keys are used as parameters for the method call.

Possible Future:

All further indices will be used as parameter foreign keys. The result of getResponseData($param_key, $id, $locale) will be used as value.

Requirements, Options, Defaults for the Route

To each route could be added requirements, options and defaults.

They will be added to the symfony route like they exist, only at defaults the _controller will also be added.

Converting to Symfony Route

These builded callbacks, informations and more now need to be created as Symfony Routes,

structure to path

In Flood the part that controls which uri path will match is builded in a few steps. This builded string is added to the Symfony Route path property.

For a response the procedure is the following, the values could be overwritten like described previous:

  • get <hook-id>.route.response.<any>.structure if is string
  • merge/replace <hook-id>.route.response.<any>.structure-child into placeholder {structure-child} of previous result
  • <hook-id>.route.hook.<any>.uri.path into {hook-path} of previous result
  • <hook-id>.route.hook.<any>.locale-path into {locale-path} of previous result
  • replace the remaining placeholders dynamic if a value exists with getResponseDate and the value is a string or int

host to host

As host will be used <hook-id>.route.hook.<any>.uri.host and performed the following:

  • wildcard, if <hook-id>.route.hook.<any>.uri.path is * add a placeholder with {host} as the host
  • if <hook-id>.route.hook.<any>.www is true, prefix the host with www
  • host doesn't get other placeholders changed

Symfony Construct

The routes are added to a RouteCollection, thous each route will need to have an unique name, at responses the name is generated with the <hook-id> and the <hook-id>.route.response.<any> and the locale.

<?php
// `name` must be unique among all generated routes
$collection = new Symfony\Component\Routing\RouteCollection();
$collection->add(
    $hook_id . ':' . $response_id . ':' . $locale,
    new Symfony\Component\Routing\Route(
        (isset($response['path']) ? $response['path'] : ''),
        (isset($response['defaults']) ? $response['defaults'] : []),
        (isset($response['requirements']) ? $response['requirements'] : []),
        (isset($response['options']) ? $response['options'] : []),
        (isset($response['host']) ? $response['host'] : ''),
        (isset($response['schemes']) ? $response['schemes'] : []),
        (isset($response['methods']) ? $response['methods'] : []),
        (isset($response['condition']) ? $response['condition'] : '')
    )
);

Further Guides

Route - The Container