Releases: nova-framework/framework
introduction to middleware
Rationale
In the Core\View exists a (possible) performance issue, as the associated Hooks are executed every time when a rendering Method is called, then for a typically triplet rendering, they will be called three times, even the results are the same.
What is wrong with that? When in the application exists a big number of Modules, they will be processed in the loop and modules files loaded, Controllers are instantiated and methods executed, possibly slowing down considerable the rendering.
The proposed solution give a single shoot to the Views associated Hooks, into Controller, before executing the requested Method, and store the resulted data in a Views shared storage, from where is loaded and added for rendering execution. This style avoid multiple calling of those Hooks, as in for every rendering call.
In the Core\View is introduced a new Method, called "share", permitting to set data variables which are shared by any called rendering method. For example, it is possible to have:
View::share('title', 'Shiny Title');
Making the variable 'title' available in any rendering command. This command is also very useful, as being used by design to setup the data coming from the Hooks execution.
In other hand, a generic pre-processing method called "before" and automatically executed before the requested action, is introduced and it by default execute the Views associated Hooks and share their result to Core\View. Yet, it doesn't introduce API changes and it can be safely ignored if there is no need to customize it.
Still, that pre-processing permit to fine tune the Controller behavior, for example hosting logic to check the user authentication or do whatever pre-processing, i.e. checking access codes, without requiring to add commands to every method, simplifying the code.
To note that in a "before" method are available the requested method name and the given parameters, in read-only mode, via two simple additional getter methods.
An complex example of its chained usage can be:
// Into a Base Controller
protected function before()
{
// Check if the User is authorized to use the requested Method.
switch($this->method()) {
case 'login':
case 'forgot':
case 'reset':
break;
default:
if (Session::get('loggedIn') == false) {
Url::redirect('login');
}
};
// Leave to parent's Method the execution decisions.
return parent::before(); // <-- there are loaded the Views associated Hooks.
}
// Into an Admin Controller
protected function before()
{
// Check the CSRF token for select Methods over POST, e.g: add/edit/delete.
switch($this->method()) {
case 'index':
case 'show':
break;
default:
if (Request::isPost() && ! Csrf::isTokenValid()) {
Url::redirect('login');
}
};
// Leave to parent's Method the execution decisions.
return parent::before(); // <-- there is checked the User Authorization.
}
Another more complex example, for a Controller for private files serving; with access authorization and time-bomb (limit the access time):
protected function before()
{
// Only the authorized Users can arrive to the Methods from this Controller.
if (Session::get('loggedIn') == false) {
Url::redirect('login');
}
//
if($this->method() == 'files') {
$params = $this->params();
if(count($params) != 3) {
header("HTTP/1.0 400 Bad Request");
return false;
}
// The current timestamp.
$timestamp = time();
// File access validation elements.
$validation = $params[0];
$time = $params[1];
$filename = $params[2];
$clientip = $this->getIpAddress();
$hash = hash('sha256', $clientip.$filename.$time.FILES_ACCESSKEY);
if((($timestamp - $time) > FILES_VALIDITY) || ($validation != $hash)) {
header("HTTP/1.0 403 Forbidden");
return false;
}
} else if (Request::isPost()) {
// All the POST requests should have a CSRF Token there.
if (! Csrf::isTokenValid()) {
Url::redirect('login');
}
}
// Leave to parent's Method the execution decisions.
return parent::before(); // <-- there are loaded the Views associated Hooks.
}
Also, the part of Method flow execution was moved into Core\Controller::execute(), presenting the big advantage to simplify the Routing and permitting to have the before/after methods protected, safe against their execution from outside.
Finally, into Core\Controller was introduced an new method called "trans", wrapping up the Language translation. Then, permitting commands like:
// Actual style
$data['title'] = $this->language->get('welcomeText');
$data['welcomeMessage'] = $this->language->get('welcomeMessage');
// New style
$data['title'] = $this->trans('welcomeText');
$data['welcomeMessage'] = $this->trans('welcomeMessage');
Again, to note that this pull request do NOT introduce any API break.
Someone asked about an example of using "View::share()"
public function index()
{
$title = $this->language->get('welcomeText');
$data['welcomeMessage'] = $this->language->get('welcomeMessage');
// Make $title available in all Views rendering
View::share('title', $title);
View::renderTemplate('header'); // <-- No need for $data in this case
View::render('Welcome/Welcome', $data);
View::renderTemplate('footer'); // <-- No need for $data in this case
}
What is difference between applying pre-processing in "__constructor()" and "before()" ?
Comparative with running checks in Class Constructor, the before() method is executed WHEN the requested Method is know being a valid one, both as in existing and being "callable", and it have at its disposition the requested Method name and the associated Parameters; both passed to by Routing. Then, before() can accurate tune the Controller behavior, depending on requested Method.
Is required to implement the methods "before()" and "after()" in every Controller ?
No. You can safely completely ignore their existence if you don't want to fine tune the Controller's behavior via that Middleware.
What if I want a base Controller which do NOT call the Views associated Hooks ?
Just override the before() like bellow in a Base Controller, e.g. App\Core\Controller
namespace App\Core;
use Core\Controller as BaseController
class Controller extends BaseController
{
public function __construct()
{
parent::__construct();
}
protected function before()
{
return true;
}
}
Then use this Class as base for your Controllers.
New fetch method for views
This pull request introduce a new composite method into Core\View, capable to fetch the View rendering and able to work also with Modules. Its usage is simple:
$content = View::fetch('Page/Show', $data, 'Pages');
echo $content;
OR
$data['content'] = View::fetch('Welcome/SubPage', $data);
View::renderTemplate('default', $data);
To note that the third parameter of the View::fetch() is an optional Module name, permitting a style similar with View::renderTemplate(), without specifying the full path into Module.
Using this new method permit even to use an alternative rendering style, using Layouts.
In other hand, this pull-request introduce a simple and non-invasive post-processing support into Controllers execution, having a new Method, called 'after', which is automatically executed when the current Action return a value different of null or boolean.
This post processing ability can be very useful in the RESTful Controllers, for example doing:
public function index()
{
$data = array(
'success' = true;
...
);
return $data;
}
public function show($id)
{
$data = array(
'success' = true;
...
);
return $data;
}
public function after($data)
{
header('Content-Type: application/json');
echo json_encode($data);
}
Also, this post processing can be very useful when it is used a Layout style rendering, to not write again and again the same triplets; as in example:
public function index()
{
$data['title'] = $this->language->get('welcomeText');
$data['welcomeMessage'] = $this->language->get('welcomeMessage');
// Render the View and fetch the output in a data variable.
$data['content'] = View::fetch('Welcome/Welcome', $data);
return $data;
}
public function subPage()
{
$data['title'] = $this->language->get('subpageText');
$data['welcomeMessage'] = $this->language->get('subpageMessage');
// Render the View and fetch the output in a data variable.
$data['content'] = View::fetch('Welcome/SubPage', $data);
return $data;
}
public function after($data)
{
View::renderTemplate('default', $data);
}
To note that the returned value of the current Action is passed to post-processing method as parameter and that this pull-request have no impact to current API of the framework.
Language patch
Fixed language not found when using aliases, adding a use statement ensures the language class can be found.
Cookie Patch
Fix cookie behavior and make it to properly delete the Cookies.
Ability to customise errors within app namespace
an error controller is now within app directory to allow changing the errors without needing to touch the system.
Error handling patch
Moved error handler above config call.
Internal restructure simply Config
Settings defined in Config whilst implementation happens within the system, introduced some global functions that can be used anywhere:
Return the site url:
site_url($path = '/')
Find string that starts with given key
str_starts_with($haystack, $needle)
Find string that ends with given key
str_ends_with($haystack, $needle)
run print_r wrapped inside pre tags
pr($data)
run var_dump
vd($data)
return string lengh
sl($data)
return uppercase
stu($data)
return lowercase
stl($data)
return each first char uppercase
ucw($data)
return a random key with specified lenght
createKey($length = 32)
Language Changer
Languages can now be changed with a route:
Router::any('language/(:any)', 'App\Controllers\Language@change');
Example of change language links:
<a href='<?=DIR;?>language/cs'>Czech</a>
<a href='<?=DIR;?>language/en'>English</a>
<a href='<?=DIR;?>language/de'>German</a>
<a href='<?=DIR;?>language/fr'>French</a>
<a href='<?=DIR;?>language/it'>Italian</a>
<a href='<?=DIR;?>language/nl'>Dutch</a>
<a href='<?=DIR;?>language/pl'>Polish</a>
<a href='<?=DIR;?>language/ro'>Romanian</a>
<a href='<?=DIR;?>language/ru'>Russian</a>
Patched deep routing within groups
Associated with the case presented by #715, right now the Routing doesn't support Deep Grouping, aka grouping the Routes until one of them come to have an empty "route" parameter. For example:
Router::group('admin', function() {
Router::any('', 'App\Controllers\Admin\Dashboard@index');
...
});
This PR fix the behavior and permit to have that Deep Grouping.
Ability to specify the **Prefix** and **Namespace** for the Route Groups and Resource(full) Routes.
This PR introduce the ability to specify the Prefix and Namespace for the Route Groups and Resource(full) Routes.
The new Router::group() accept an array as first parameter and permit commands like:
Router::group(['prefix' => 'admin', 'namespace' => 'App\Controllers\Admin'], function() {
Router::match('get', 'users', 'Users@index');
Router::match('get', 'users/create', 'Users@create');
Router::match('post', 'users', 'Users@store');
Router::match('get', 'users/(:any)', 'Users@show');
Router::match('get', 'users/(:any)/edit', 'Users@edit');
Router::match(['put', 'patch'], 'users/(:any)', 'Users@update');
Router::match('delete', 'users/(:any)', 'Users@destroy');
Router::match('get', 'categories', 'Categories@index');
Router::match('get', 'categories/create', 'Categories@create');
Router::match('post', 'categories', 'Categories@store');
Router::match('get', 'categories/(:any)', 'Categories@show');
Router::match('get', 'categories/(:any)/edit', 'Categories@edit');
Router::match(['put', 'patch'], 'categories/(:any)', 'Categories@update');
Router::match('delete', 'categories/(:any)', 'Categories@destroy');
Router::match('get', 'articles', 'Articles@index');
Router::match('get', 'articles/create', 'Articles@create');
Router::match('post', 'articles', 'Articles@store');
Router::match('get', 'articles/(:any)', 'Articles@show');
Router::match('get', 'articles/(:any)/edit', 'Articles@edit');
Router::match(['put', 'patch'], 'articles/(:any)', 'Articles@update');
Router::match('delete', 'articles/(:any)', 'Articles@destroy');
});
To note that while the prefix is the practically the group name, the active namespace is the last one for recursive groups and it will be composed with the Controller name into generated Route, e.g. App\Controllers\Admin\Categories
The new method Router::resource() introduce the ability to write in one command a group of resourceful routes, in the Laravel style, with the following specifications:
HTTP Method | Route | Controller Method |
---|---|---|
GET | /photo | index |
GET | /photo/create | create |
POST | /photo | store |
GET | /photo/(:any) | show |
GET | /photo/(:any)/edit | edit |
PUT/PATCH | /photo/(:any) | update |
DELETE | /photo/(:any) | destroy |
Practically,the previous code snippet can be written now also as:
Router::group(['prefix' => 'admin', 'namespace' => 'App\Controllers\Admin'], function() {
Router::resource('users', 'Users');
Router::resource('categories', 'Categories');
Router::resource('articles', 'Articles');
});
OR
Router::resource('admin/users', 'App\Controllers\Admin\Users');
Router::resource('admin/categories', 'App\Controllers\Admin\Categories');
Router::resource('admin/articles', 'App\Controllers\Admin\Articles');