📜 ⬆️ ⬇️

Routing in the CleverStyle Framework

Many aspects of the CleverStyle Framework have an alternative to most other frameworks, the implementation of the same things.

This article describes in some detail the device of the work of routing, examples of use, as well as examples of how to intervene in this mechanism, or, if desired, completely replace it with your own.

Main difference


The main difference between routing and implementations in popular frameworks like Symfony, Laravel or Yii is declarative instead of imperative.
')
This means that instead of specifying routes in a certain format and matching a route to a specific class, method or closure, we just describe the structure of the routes, and this structure is enough to understand what code will be executed depending on the route.

Such an approach of conventions instead of configurations is convenient in the sense that it requires less effort while writing code, and does not require viewing the configuration in order to understand what code will be called when opening a certain page, since this is evident from the agreement adopted in the framework.

Routing Basics


Any URL in the framework view is broken into several parts. At the very beginning, before any processing, the request parameters ( ? And everything after it) are deleted from the page path.

Next, we get the general format of the following path ( | used to divide the choice from several options, optional independent components of the path are grouped in [] ), the example is divided into several lines for convenience, before processing the path is divided into slashes and converted into an array of parts of the original path :

 [language/] [admin/|api/|cli/] [Module_name [/path [/sub_path [/id1 [/another_subpath [/id2] ] ] ] ] ] 

The number of nesting levels is unlimited.

First check the language prefix. It does not participate in routing (and may be absent), but if it does, it affects which language will be used on the page. The format depends on the languages ​​used and their number, it could be simple ( en , ru ), or take into account the region ( en_gb , ru_ua ).

The language is followed by an optional part defining the type of page. This can be an administration page ( $Request->admin_path === true ), a request to the API ( $Request->api_path === true ), a request to the CLI interface ( $Request->cli_path === true ), or a regular user page unless explicitly stated.

Next, the module that will process the page is determined. Subsequently, this module is available as $Request->current_module .

It is worth noting that the name of the module can be localized, for example, if the My_blog module has the pair "My_blog" : " " in translation "My_blog" : " " , then you can use _ as the module name, while all the same $Request->current_module === 'My_blog' .

The rest of the array elements after the module falls into $Request->route , which can be used by modules, for example, for custom routing.

Before proceeding to the next steps, 2 more arrays are filled.

$Request->route_ids contains elements from $Request->route , which are integers (it is assumed that they are identifiers), $Request->route_path contains all $Request->route elements except integers, and is used as a route inside the module.

How to get into the routing in the early stages


The developer has at his disposal a number of events that allow him to intervene at these stages and change the behavior at his own discretion.

The System/Request/routing_replace/before event is triggered immediately before determining the page language and allows you to somehow modify the source path as a string, the lowest level manipulations can be performed in this place.

The System/Request/routing_replace/after event fires after the formation of $Request->route_ids and $Request->route_path , allowing you to correct important parameters after they have been defined by the system.

An example of adding support for UUIDs as an alternative to standard integer identifiers:

 Event::instance()->on( 'System/Request/routing_replace/after', function ($data) { $route_path = []; $route_ids = []; foreach ($data['route'] as $item) { if (preg_match('/([af\d]{8}(?:-[af\d]{4}){3}-[af\d]{12}?)/i', $item)) { $route_ids[] = $item; } else { $route_path[] = $item; } } if ($route_ids) { $data['route_path'] = $route_path; $data['route_ids'] = $route_ids; } } ); 

Route structure


The route structure is a tree JSON, in which the key of each child level is a continuation of the parent, some final nodes may be empty if the neighboring ones have a deeper structure.

An example of the current API structure of a system module:

 { "admin" : { "about_server" : [], "blocks" : [], "databases" : [], "groups" : [ "_", "permissions" ], "languages" : [], "mail" : [], "modules" : [], "optimization" : [], "permissions" : [ "_", "for_item" ], "security" : [], "site_info" : [], "storages" : [], "system" : [], "themes" : [], "upload" : [], "users" : [ "_", "general", "groups", "permissions" ] }, "blank" : [], "languages" : [], "profile" : [], "profiles" : [], "timezones" : [] } 

Examples of (real) queries that fit this structure:

 GET api/System/blank GET api/System/admin/about_server SEARCH_OPTIONS api/System/admin/users SEARCH api/System/admin/users PATCH api/System/admin/users/42 GET api/System/admin/users/42/groups PUT api/System/admin/users/42/permissions 

Getting the final route


Getting a route out of the page path is only the first of two steps. The second stage takes into account the configuration of the current module and adjusts the final route accordingly.

What is it for? Suppose a user opens the /Blogs page, and the route structure is configured as follows ( modules/Blogs/index.json ):

 [ "latest_posts", "section", "post", "tag", "new_post", "edit_post", "drafts", "atom.xml" ] 

In this case, $Request->route_path === [] , but $App->controller_path === ['index', 'latest_posts'] .

index will be here regardless of the module and configuration, but the latest_posts depends on the configuration. The fact is that if the page is not an API and not a CLI request, then if you specify an incomplete route, the framework will select the first key from the configuration at each level until it reaches the end deep into the structure. That is, Blogs similar to Blogs/latest_posts .

For API and CLI requests, there is a difference in this sense - omitting parts of the route in this way is prohibited and allowed only if the structure uses _ as the first element at the appropriate level.

For example, for an API, we can have the following structure ( modules/Module_name/api/index.json ):

 { "_" : [] "comments" : [] } 

In this case, api/Module_name similar to api/Module_name/_ . This allows you to create an API with beautiful methods (remember that we have identifiers in a separate array):

 GET api/Module_name GET api/Module_name/42 POST api/Module_name PUT api/Module_name/42 DELETE api/Module_name/42 GET api/Module_name/42/comments GET api/Module_name/42/comments/13 POST api/Module_name/42/comments PUT api/Module_name/42/comments/13 DELETE api/Module_name/42/comments/13 

Location of files with route structure


The modules in the CleverStyle Framework store all of their contents inside the module folder (as opposed to frameworks, where all views are in one folder, all controllers are in another, all models are in third, all routes are in one file, and so on) for ease of maintenance.

Depending on the type of request, different configs are used in JSON format:


Route handlers are in the same folders.

Types of routing


There are two types of routing in the CleverStyle Framework: file-based (used extensively earlier) and controller-based (more used now).

Take the Blogs/latest_posts page from the example above and the final route is ['index', 'latest_posts'] .

In the case of file-based routing, the following files will be mounted in the specified order:

 modules/Blogs/index.php modules/Blogs/latest_posts.php 

If controller-based routing is used, then the cs\modules\Blogs\Controller class ( modules/Blogs/Controller.php file) must exist with the following public static methods:

 cs\modules\Blogs\Controller::index($Request, $Response) : mixed cs\modules\Blogs\Controller::latest_posts($Request, $Response) : mixed 

It is important that any file / method other than the last can be omitted, and this will not lead to an error.

Now let's take a more complex example, the GET api/Module_name/items/42/comments request.

Firstly, for API and CLI requests, besides the path, the HTTP method is also important.
Secondly, the sub-folder api will be used here.

In the case of file-based routing, the following files will be mounted in the specified order:

 modules/Module_name/api/index.php modules/Module_name/api/index.get.php modules/Module_name/api/items.php modules/Module_name/api/items.get.php modules/Module_name/api/items/comments.php modules/Module_name/api/items/comments.get.php 

If controller-based routing is used, then the class cs\modules\Blogs\api\Controller ( modules/Blogs/api/Controller.php file modules/Blogs/api/Controller.php must exist with the following public static methods:

 cs\modules\Blogs\api\Controller::index($Request, $Response) : mixed cs\modules\Blogs\api\Controller::index_get($Request, $Response) : mixed cs\modules\Blogs\api\Controller::items($Request, $Response) : mixed cs\modules\Blogs\api\Controller::items_get($Request, $Response) : mixed cs\modules\Blogs\api\Controller::items_comments($Request, $Response) : mixed cs\modules\Blogs\api\Controller::items_comments_get($Request, $Response) : mixed 

In this case, at least one of the last two files / controllers must exist.

As you can see, for API and CLI requests, an explicit separation of request processing code with different HTTP methods is used, while for ordinary pages and administration pages this is not taken into account.

Arguments in controllers and return value


$Request and $Response are nothing but instances of cs\Request and cs\Response .

The return value in simple cases is sufficient to set the content. Under the hood for API requests, the return value will be passed to cs\Page::json() , and for other requests to cs\Page::content() .

 public static function items_comments_get () { return []; } //   public static function items_comments_get () { Page::instance->json([]); } 

Non-existent HTTP method handlers


It may happen that there is no HTTP handler method that the user requests, in this case there are several scenarios.

API: if neither cs\modules\Blogs\api\Controller::items_comments() nor cs\modules\Blogs\api\Controller::items_comments_get() (or similar files) are cs\modules\Blogs\api\Controller::items_comments_get() , then:


CLI: Similar to the API, but instead of OPTIONS CLI is a special method, and instead of the Allow header, the available methods will be displayed in the console (if the called method was different from the CLI , then the output status will be changed to 245 ( 501 % 256 )).

Using your own routing system


If for some reason you don’t like the routing device in the framework, in each separate module you can create only the index.php file and connect the router to taste.

Since index.php does not require controllers and structure in index.json , you will index.json most of the routing system.

Access rights


Permissions are checked for each route level. Permissions in the framework have two key parameters: a group and a label.

The name of the module with an optional prefix for administration pages and API is used as a group when checking access permissions to the page; the route path is used as a label (without taking into account the index prefix).

For example, for the api/Module_name/items/comments page, the user rights for permissions will be checked (with a space of group label ):

 api/Module_name index api/Module_name items api/Module_name items/comments 

If at some level the user does not have access, the processing will end with a 403 Forbidden error, while the handlers of the previous levels will not be executed, since the access rights are determined at the stage of final formation of the route, before the handlers are launched.

At last


The implementation of query processing in the CleverStyle Framework is quite powerful and flexible, while being declarative.

The article describes the key stages of processing requests from the point of view of the routing system and its interest for the developer, but in fact, if you delve into the nuances, there is still something to learn.

I hope this guide is enough to not get lost. Now it should be clear why, in order to determine which code was called in response to a particular request, you do not even need to look into the configuration. It is enough to determine the type of routing used by the presence of Controller.php in the target folder and open the corresponding file.

The current version of the framework at the time of writing of article 5.29, in newer versions are subject to change, follow the release notes.

» GitHub repository
» Framework documentation

Constructive comments are usually welcome.

Source: https://habr.com/ru/post/307690/


All Articles