Ubiquity User guide

Quick start with console

Note

If you do not like console mode, you can switch to quick-start with web tools (UbiquityMyAdmin).

Install Composer

ubiquity utilizes Composer to manage its dependencies. So, before using, you will need to make sure you have Composer installed on your machine.

Install Ubiquity-devtools

Download the Ubiquity-devtools installer using Composer.

composer global require phpmv/ubiquity-devtools

Test your recent installation by doing:

Ubiquity version
_images/ubi-version.png

You can get at all times help with a command by typing: Ubiquity help followed by what you are looking for.

Example :

Ubiquity help project

Project creation

Create the quick-start projet

Ubiquity new quick-start

Directory structure

The project created in the quick-start folder has a simple and readable structure:

the app folder contains the code of your future application:

app
 ├ cache
 ├ config
 ├ controllers
 ├ models
 └ views

Start-up

Go to the newly created folder quick-start and start the build-in php server:

Ubiquity serve

Check the correct operation at the address http://127.0.0.1:8090:

_images/quick-start-main.png

Note

If port 8090 is busy, you can start the server on another port using -p option.

Ubiquity serve -p=8095

Controller

The console application dev-tools saves time in repetitive operations. We go through it to create a controller.

Ubiquity controller DefaultController
_images/controller-creation.png

We can then edit app/controllers/DefaultController file in our favorite IDE:

app/controllers/DefaultController.php
1
2
3
4
5
6
7
namespace controllers;
 /**
  * Controller DefaultController
  */
class DefaultController extends ControllerBase{
     public function index(){}
}

Add the traditional message, and test your page at http://127.0.0.1:8090/DefaultController

app/controllers/DefaultController.php
class DefaultController extends ControllerBase{

    public function index(){
        echo 'Hello world!';
    }

}

For now, we have not defined routes,
Access to the application is thus made according to the following scheme:
controllerName/actionName/param

The default action is the index method, we do not need to specify it in the url.

Route

Important

The routing is defined with the attribute Route (with php>8) or the annotation @route and is not done in a configuration file:
it’s a design choice.

The automated parameter set to true allows the methods of our class to be defined as sub routes of the main route /hello.

With annotations:

app/controllers/DefaultController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
namespace controllers;
/**
 * Controller DefaultController
 * @route("/hello","automated"=>true)
 */
class DefaultController extends ControllerBase{

    public function index(){
        echo 'Hello world!';
    }

}

With attributes (php>8):

app/controllers/DefaultController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;
use Ubiquity\attributes\items\router\Route;

#[Route('/hello', automated: true)]
class DefaultController extends ControllerBase{

    public function index(){
        echo 'Hello world!';
    }

}

Router cache

Important

No changes on the routes are effective without initializing the cache.
Annotations are never read at runtime. This is also a design choice.

We can use the console for the cache re-initialization:

Ubiquity init-cache
_images/init-cache.png

Let’s check that the route exists:

Ubiquity info:routes
_images/info-routes1.png

We can now test the page at http://127.0.0.1:8090/hello

Action & route with parameters

We will now create an action (sayHello) with a parameter (name), and the associated route (to):
The route will use the parameter name of the action:

Ubiquity action DefaultController.sayHello -p=name -r=to/{name}/
_images/action-creation.png

After re-initializing the cache (init-cache command), the info:routes command should display:

_images/2-routes.png

Change the code in your IDE: the action must say Hello to somebody…

app/controllers/DefaultController.php
     /**
      * @route("to/{name}/")
      */
     public function sayHello($name){
             echo 'Hello '.$name.'!';
     }

and test the page at http://127.0.0.1:8090/hello/to/Mr SMITH

Action, route parameters & view

We will now create an action (information) with two parameters (title and message), the associated route (info), and a view to display the message:
The route will use the two parameters of the action.

Ubiquity action DefaultController.information -p=title,message='nothing' -r=info/{title}/{message} -v

Note

The -v (–view) parameter is used to create the view associated with the action.

After re-initializing the cache, we now have 3 routes:

_images/3-routes.png

Let’s go back to our development environment and see the generated code:

app/controllers/DefaultController.php
     /**
      * @route("info/{title}/{message}")
      */
     public function information($title,$message='nothing'){
             $this->loadView('DefaultController/information.html');
     }

We need to pass the 2 variables to the view:

/**
 * @route("info/{title}/{message}")
 */
public function information($title,$message='nothing'){
        $this->loadView('DefaultController/information.html',compact('title','message'));
}

And we use our 2 variables in the associated twig view:

app/views/DefaultController/information.html
     <h1>{{title}}</h1>
     <div>{{message | raw}}</div>

We can test your page at http://127.0.0.1:8090/hello/info/Quick start/Ubiquity is quiet simple
It’s obvious

_images/quiet-simple.png

Quick start with Webtools

Install Composer

ubiquity utilizes Composer to manage its dependencies. So, before using, you will need to make sure you have Composer installed on your machine.

Install Ubiquity-devtools

Download the Ubiquity-devtools installer using Composer.

composer global require phpmv/ubiquity-devtools

Test your recent installation by doing:

Ubiquity version
_images/ubi-version.png

You can get at all times help with a command by typing: Ubiquity help followed by what you are looking for.

Example :

Ubiquity help project

Project creation

Create the quick-start projet with Webtools interface (the -a option)

Ubiquity new quick-start -a

Directory structure

The project created in the quick-start folder has a simple and readable structure:

the app folder contains the code of your future application:

app
 ├ cache
 ├ config
 ├ controllers
 ├ models
 └ views

Start-up

Go to the newly created folder quick-start and start the build-in php server:

Ubiquity serve

Check the correct operation at the address http://127.0.0.1:8090:

_images/quick-start-main.png

Note

If port 8090 is busy, you can start the server on another port using -p option.

Ubiquity serve -p=8095

Controller

Goto admin interface by clicking on the button Webtools:

_images/ubi-my-admin-btn.png

Select the tools you need:

_images/ubi-my-admin-interface-0.png

The web application Webtools saves time in repetitive operations.

_images/ubi-my-admin-interface.png

We go through it to create a controller.

Go to the controllers part, enter DefaultController in the controllerName field and create the controller:

_images/create-controller-btn.png

The controller DefaultController is created:

_images/controller-created.png

We can then edit app/controllers/DefaultController file in our favorite IDE:

app/controllers/DefaultController.php
1
2
3
4
5
6
7
namespace controllers;
 /**
 * Controller DefaultController
 **/
class DefaultController extends ControllerBase{
     public function index(){}
}

Add the traditional message, and test your page at http://127.0.0.1:8090/DefaultController

app/controllers/DefaultController.php
     class DefaultController extends ControllerBase{

             public function index(){
                     echo 'Hello world!';
             }

     }

For now, we have not defined routes,
Access to the application is thus made according to the following scheme:
controllerName/actionName/param

The default action is the index method, we do not need to specify it in the url.

Route

Important

The routing is defined with the annotation @route and is not done in a configuration file:
it’s a design choice.

The automated parameter set to true allows the methods of our class to be defined as sub routes of the main route /hello.

app/controllers/DefaultController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
     namespace controllers;
      /**
      * Controller DefaultController
      * @route("/hello","automated"=>true)
      **/
     class DefaultController extends ControllerBase{

             public function index(){
                     echo 'Hello world!';
             }

     }

Router cache

Important

No changes on the routes are effective without initializing the cache.
Annotations are never read at runtime. This is also a design choice.

We can use the web tools for the cache re-initialization:

Go to the Routes section and click on the re-init cache button

_images/re-init-cache-btn.png

The route now appears in the interface:

_images/1-route.png

We can now test the page by clicking on the GET button or by going to the address http://127.0.0.1:8090/hello

Action & route with parameters

We will now create an action (sayHello) with a parameter (name), and the associated route (to):
The route will use the parameter name of the action:

Go to the Controllers section:

  • click on the + button associated with DefaultController,
  • then select Add new action in.. item.
_images/create-action-btn.png

Enter the action information in the following form:

_images/create-action.png

After re-initializing the cache with the orange button, we can see the new route hello/to/{name}:

_images/router-re-init-1.png

Check the route creation by going to the Routes section:

_images/router-re-init-2.png

We can now test the page by clicking on the GET button:

_images/test-action.png

We can see the result:

_images/test-action-result.png

We could directly go to http://127.0.0.1:8090/hello/to/Mr SMITH address to test

Action, route parameters & view

We will now create an action (information) with tow parameters (title and message), the associated route (info), and a view to display the message:
The route will use the two parameters of the action.

In the Controllers section, create another action on DefaultController:

_images/create-action-btn.png

Enter the action information in the following form:

_images/create-action-view.png

Note

The view checkbox is used to create the view associated with the action.

After re-initializing the cache, we now have 3 routes:

_images/create-action-view-result.png

Let’s go back to our development environment and see the generated code:

app/controllers/DefaultController.php
     /**
      *@route("info/{title}/{message}")
     **/
     public function information($title,$message='nothing'){
             $this->loadView('DefaultController/information.html');
     }

We need to pass the 2 variables to the view:

/**
 *@route("info/{title}/{message}")
**/
public function information($title,$message='nothing'){
        $this->loadView('DefaultController/information.html',compact('title','message'));
}

And we use our 2 variables in the associated twig view:

app/views/DefaultController/information.html
     <h1>{{title}}</h1>
     <div>{{message | raw}}</div>

We can test our page at http://127.0.0.1:8090/hello/info/Quick start/Ubiquity is quiet simple
It’s obvious

_images/quiet-simple.png

Ubiquity-devtools installation

Install Composer

ubiquity utilizes Composer to manage its dependencies. So, before using, you will need to make sure you have Composer installed on your machine.

Install Ubiquity-devtools

Download the Ubiquity-devtools installer using Composer.

composer global require phpmv/ubiquity-devtools

Make sure to place the ~/.composer/vendor/bin directory in your PATH so the Ubiquity executable can be located by your system.

Once installed, the simple Ubiquity new command will create a fresh Ubiquity installation in the directory you specify. For instance, Ubiquity new blog would create a directory named blog containing an Ubiquity project:

Ubiquity new blog

The semantic option adds Semantic-UI for the front end.

You can see more options about installation by reading the Project creation section.

Project creation

After installing Ubiquity-devtools installation, in your terminal, call the new command in the root folder of your web server :

Samples

A simple project

Ubiquity new projectName

A project with UbiquityMyAdmin interface

Ubiquity new projectName -a

A project with bootstrap and semantic-ui themes installed

Ubiquity new projectName --themes=bootstrap,semantic

Installer arguments

short name name role default Allowed values Since devtools
b dbName Sets the database name.      
s serverName Defines the db server address. 127.0.0.1    
p port Defines the db server port. 3306    
u user Defines the db server user. root    
w password Defines the db server password. ‘’    
h themes Install themes.   semantic,bootstrap,foundation  
m all-models Creates all models from db. false    
a admin Adds UbiquityMyAdmin interface. false    
i siteUrl Defines the site URL. http://127.0.0.1/{projectname}   1.2.6
e rewriteBase Sets the base for rewriting. /{projectname}/   1.2.6

Arguments usage

short names

Example of creation of the blog project, connected to the blogDb database, with generation of all models

Ubiquity new blog -b=blogDb -m=true

long names

Example of creation of the blog project, connected to the bogDb database, with generation of all models and integration of semantic theme

Ubiquity new blog --dbName=blogDb --all-models=true --themes=semantic

Running

To start the embedded web server and test your pages, run from the application root folder:

Ubiquity serve

The web server is started at 127.0.0.1:8090

Project configuration

Normally, the installer limits the modifications to be performed in the configuration files and your application is operational after installation

_images/firstProject.png

Main configuration

The main configuration of a project is localised in the app/conf/config.php file.

app/conf/config.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
return array(
             "siteUrl"=>"%siteUrl%",
             "database"=>[
                             "dbName"=>"%dbName%",
                             "serverName"=>"%serverName%",
                             "port"=>"%port%",
                             "user"=>"%user%",
                             "password"=>"%password%"
             ],
             "namespaces"=>[],
             "templateEngine"=>'Ubiquity\views\engine\twig\Twig',
             "templateEngineOptions"=>array("cache"=>false),
             "test"=>false,
             "debug"=>false,
             "di"=>[%injections%],
             "cacheDirectory"=>"cache/",
             "mvcNS"=>["models"=>"models","controllers"=>"controllers"]
);

Services configuration

Services loaded on startup are configured in the app/conf/services.php file.

app/conf/services.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
     use Ubiquity\controllers\Router;

     try{
             \Ubiquity\cache\CacheManager::startProd($config);
     }catch(Exception $e){
             //Do something
     }
     \Ubiquity\orm\DAO::startDatabase($config);
     Router::start();
     Router::addRoute("_default", "controllers\\IndexController");

Pretty URLs

Apache

The framework ships with an .htaccess file that is used to allow URLs without index.php. If you use Apache to serve your Ubiquity application, be sure to enable the mod_rewrite module.

.htaccess
AddDefaultCharset UTF-8
<IfModule mod_rewrite.c>
     RewriteEngine On
     RewriteBase /blog/
     RewriteCond %{REQUEST_FILENAME} !-f
     RewriteCond %{HTTP_ACCEPT} !(.*images.*)
     RewriteRule ^(.*)$ index.php?c=$1 [L,QSA]
</IfModule>

See Apache configuration for more.

Nginx

On Nginx, the following directive in your site configuration will allow “pretty” URLs:

location /{
      rewrite ^/(.*)$ /index.php?c=$1 last;
}

See NginX configuration for more.

Laravel Valet Driver

Valet is a php development environment for Mac minimalists. No Vagrant, no /etc/hosts file. You can even share your sites publicly using local tunnels.

Laravel Valet configures your Mac to always run Nginx in the background when your machine starts. Then, using DnsMasq, Valet proxies all requests on the *.test domain to point to sites installed on your local machine.

Get more info about Laravel Valet

Create UbiquityValetDriver.php under ~/.config/valet/Drivers/ add below php code and save it.

<?php

class UbiquityValetDriver extends BasicValetDriver{

        /**
        * Determine if the driver serves the request.
        *
        * @param  string  $sitePath
        * @param  string  $siteName
        * @param  string  $uri
        * @return bool
        */
        public function serves($sitePath, $siteName, $uri){
                if(is_dir($sitePath . DIRECTORY_SEPARATOR . '.ubiquity')) {
                        return true;
                }
                return false;
        }

        public function isStaticFile($sitePath, $siteName, $uri){
                if(is_file($sitePath . $uri)) {
                        return $sitePath . $uri;
                }
                return false;
        }

        /**
        * Get the fully resolved path to the application's front controller.
        *
        * @param  string  $sitePath
        * @param  string  $siteName
        * @param  string  $uri
        * @return string
        */
        public function frontControllerPath($sitePath, $siteName, $uri){
                $sitePath.='/public';
                $_SERVER['DOCUMENT_ROOT'] = $sitePath;
                $_SERVER['SCRIPT_NAME'] = '/index.php';
                $_SERVER['SCRIPT_FILENAME'] = $sitePath . '/index.php';
                $_SERVER['DOCUMENT_URI'] = $sitePath . '/index.php';
                $_SERVER['PHP_SELF'] = '/index.php';

                $_GET['c'] = '';

                if($uri) {
                        $_GET['c'] = ltrim($uri, '/');
                        $_SERVER['PHP_SELF'] = $_SERVER['PHP_SELF']. $uri;
                        $_SERVER['PATH_INFO'] = $uri;
                }

                $indexPath = $sitePath . '/index.php';

                if(file_exists($indexPath)) {
                        return $indexPath;
                }
        }
}

Devtools usage

Project creation

See Project creation to create a project.

Tip

For all other commands, you must be in your project folder or one of its subfolders.

Important

The .ubiquity folder created automatically with the project allows the devtools to find the root folder of the project.
If it has been deleted or is no longer present, you must recreate this empty folder.

Controller creation

Specifications

  • command : controller
  • Argument : controller-name
  • aliases : create-controller

Parameters

short name name role default Allowed values
v view Creates the associated view index. true true, false

Samples:

Creates the controller controllers\ClientController class in app/controllers/ClientController.php:

Ubiquity controller ClientController

Creates the controller controllers\ClientController class in app/controllers/ClientController.php and the associated view in app/views/ClientController/index.html:

Ubiquity controller ClientController -v

Action creation

Specifications

  • command : action
  • Argument : controller-name.action-name
  • aliases : new-action

Parameters

short name name role default Allowed values
p params The action parameters (or arguments).   a,b=5 or $a,$b,$c
r route The associated route path.   /path/to/route
v create-view Creates the associated view. false true,false

Samples:

Adds the action all in controller Users:

Ubiquity action Users.all

code result:

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;
/**
 * Controller Users
 */
class Users extends ControllerBase{

   public function index(){}

   public function all(){

   }

}

Adds the action display in controller Users with a parameter:

Ubiquity action Users.display -p=idUser

code result:

app/controllers/Users.php
1
2
3
4
5
6
7
8
class Users extends ControllerBase{

   public function index(){}

   public function display($idUser){

   }
}

Adds the action display with an associated route:

Ubiquity action Users.display -p=idUser -r=/users/display/{idUser}

code result:

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class Users extends ControllerBase{

   public function index(){}

   #[Route('/users/display/{idUser}')]
   public function display($idUser){

   }
}
app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

class Users extends ControllerBase{

   public function index(){}

   /**
    *@route("/users/display/{idUser}")
    */
   public function display($idUser){

   }
}

Adds the action search with multiple parameters:

Ubiquity action Users.search -p=name,address=''

code result:

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class Users extends ControllerBase{

   public function index(){}

   #[Route('/users/display/{idUser}')]
   public function display($idUser){

   }

   public function search($name,$address=''){

   }
}
app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace controllers;

class Users extends ControllerBase{

   public function index(){}

   /**
    * @route("/users/display/{idUser}")
    */
   public function display($idUser){

   }

   public function search($name,$address=''){

   }
}

Adds the action search and creates the associated view:

Ubiquity action Users.search -p=name,address -v

Model creation

Note

Optionally check the database connection settings in the app/config/config.php file before running these commands.

To generate a model corresponding to the user table in database:

Ubiquity model user

All models creation

For generating all models from the database:

Ubiquity all-models

Cache initialization

To initialize the cache for routing (based on annotations in controllers) and orm (based on annotations in models) :

Ubiquity init-cache

URLs

like many other frameworks, if you are using router with it’s default behavior, there is a one-to-one relationship between a URL string and its corresponding controller class/method. The segments in a URI normally follow this pattern:

example.com/controller/method/param
example.com/controller/method/param1/param2

Default method

When the URL is composed of a single part, corresponding to the name of a controller, the index method of the controller is automatically called :

URL :

example.com/Products
example.com/Products/index

Controller :

app/controllers/Products.php
1
2
3
4
5
class Products extends ControllerBase{
    public function index(){
        //Default action
    }
}

Required parameters

If the requested method requires parameters, they must be passed in the URL:

Controller :

app/controllers/Products.php
1
2
3
class Products extends ControllerBase{
    public function display($id){}
}

Valid Urls :

example.com/Products/display/1
example.com/Products/display/10/
example.com/Products/display/ECS

Optional parameters

The called method can accept optional parameters.

If a parameter is not present in the URL, the default value of the parameter is used.

Controller :

app/controllers/Products.php
class Products extends ControllerBase{
    public function sort($field, $order='ASC'){}
}

Valid Urls :

example.com/Products/sort/name (uses "ASC" for the second parameter)
example.com/Products/sort/name/DESC
example.com/Products/sort/name/ASC

Case sensitivity

On Unix systems, the name of the controllers is case-sensitive.

Controller :

app/controllers/Products.php
class Products extends ControllerBase{
    public function caseInsensitive(){}
}

Urls :

example.com/Products/caseInsensitive (valid)
example.com/Products/caseinsensitive (valid because the method names are case insensitive)
example.com/products/caseInsensitive (invalid since the products controller does not exist)

Routing customization

The Router and annotations/attributes in controller classes allow you to customize URLs.

Router

Routing can be used in addition to the default mechanism that associates controller/action/{parameters} with an url.

Dynamic routes

Dynamic routes are defined at runtime.
It is possible to define these routes in the app/config/services.php file.

Important

Dynamic routes should only be used if the situation requires it:

  • in the case of a micro-application
  • if a route must be dynamically defined

In all other cases, it is advisable to declare the routes with annotations, to benefit from caching.

Callback routes

The most basic Ubiquity routes accept a Closure.
In the context of micro-applications, this method avoids having to create a controller.

app/config/services.php
1
2
3
4
5
     use Ubiquity\controllers\Router;

     Router::get("foo", function(){
             echo 'Hello world!';
     });

Callback routes can be defined for all http methods with:

  • Router::post
  • Router::put
  • Router::delete
  • Router::patch
  • Router::options

Controller routes

Routes can also be associated more conventionally with an action of a controller:

app/config/services.php
1
2
3
     use Ubiquity\controllers\Router;

     Router::addRoute('bar', \controllers\FooController::class,'index');

The method FooController::index() will be accessible via the url /bar.

In this case, the FooController must be a class inheriting from Ubiquity\controllers\Controller or one of its subclasses, and must have an index method:

app/controllers/FooController.php
1
2
3
4
5
6
7
8
     namespace controllers;

     class FooController extends ControllerBase{

             public function index(){
                     echo 'Hello from foo';
             }
     }

Default route

The default route matches the path /.
It can be defined using the reserved path _default

app/config/services.php
1
2
3
     use Ubiquity\controllers\Router;

     Router::addRoute("_default", \controllers\FooController::class,'bar');

Static routes

Static routes are defined using annotation or with php native attributes since Ubiquity 2.4.0.

Note

These annotations or attributes are never read at runtime.
It is necessary to reset the router cache to take into account the changes made on the routes.

Creation

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{

    #[Route('products')]
    public function index(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

class ProductsController extends ControllerBase{

    /**
     * @route("products")
     */
    public function index(){}

}

The method Products::index() will be accessible via the url /products.

Note

The initial or terminal slash is ignored in the path. The following routes are therefore equivalent:
  • #[Route('products')]
  • #[Route('/products')]
  • #[Route('/products/')]

Route parameters

A route can have parameters:

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{
...
     #[Route('products/{value}')]
     public function search($value){
         // $value will equal the dynamic part of the URL
         // e.g. at /products/brocolis, then $value='brocolis'
         // ...
     }
}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

class ProductsController extends ControllerBase{
...
    /**
     * @route("products/{value}")
     */
     public function search($value){
        // $value will equal the dynamic part of the URL
        // e.g. at /products/brocolis, then $value='brocolis'
        // ...
     }
}

Route optional parameters

A route can define optional parameters, if the associated method has optional arguments:

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{
   ...
   #[Route('products/all/{pageNum}/{countPerPage}')]
   public function list($pageNum,$countPerPage=50){
      // ...
   }
}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

class ProductsController extends ControllerBase{
   ...
   /**
    * @route("products/all/{pageNum}/{countPerPage}")
    */
   public function list($pageNum,$countPerPage=50){
      // ...
   }
}

Route requirements

It is possible to add specifications on the variables passed in the url via the attribute requirements.

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{
   ...
   #[Route('products/all/{pageNum}/{countPerPage}',requirements: ["pageNum"=>"\d+","countPerPage"=>"\d?"])]
   public function list($pageNum,$countPerPage=50){
      // ...
   }
}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

class ProductsController extends ControllerBase{
   ...
   /**
    * @route("products/all/{pageNum}/{countPerPage}","requirements"=>["pageNum"=>"\d+","countPerPage"=>"\d?"])
    */
   public function list($pageNum,$countPerPage=50){
      // ...
   }
}
The defined route matches these urls:
  • products/all/1/20
  • products/all/5/
but not with that one:
  • products/all/test

Parameter typing

The route declaration takes into account the data types passed to the action, which avoids adding requirements for simple types (int, bool, float).

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{
   ...
   #[Route('products/{productNumber}')]
   public function one(int $productNumber){
      // ...
   }
}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

class ProductsController extends ControllerBase{
   ...
   /**
    * @route("products/{productNumber}")
    */
   public function one(int $productNumber){
      // ...
   }
}
The defined route matches these urls:
  • products/1
  • products/20
but not with that one:
  • products/test
Correct values by data type:
  • int: 1
  • bool: 0 or 1
  • float: 1 1.0

Route http methods

It is possible to specify the http method or methods associated with a route:

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{

   #[Route('products',methods: ['get','post'])]
   public function index(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

class ProductsController extends ControllerBase{

   /**
    * @route("products","methods"=>["get","post"])
    */
   public function index(){}

}

The methods attribute can accept several methods:
@route("testMethods","methods"=>["get","post","delete"])
#[Route('testMethods', methods: ['get','post','delete'])]

The @route annotation or Route attribute defaults to all HTTP methods.
There is a specific annotation for each of the existing HTTP methods:

  • @get => Get
  • @post => Post
  • @put => Put
  • @patch => Patch
  • @delete => Delete
  • @head => Head
  • @options => Options
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

use Ubiquity\attributes\items\router\Get;

class ProductsController extends ControllerBase{

   #[Get('products')]
   public function index(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

class ProductsController extends ControllerBase{

   /**
    * @get("products")
    */
   public function index(){}

}

Route name

It is possible to specify the name of a route, this name then facilitates access to the associated url.
If the name attribute is not specified, each route has a default name, based on the pattern controllerName_methodName.

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{

   #[Route('products',name: 'products.index')]
   public function index(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

class ProductsController extends ControllerBase{

   /**
    * @route("products","name"=>"products.index")
    */
   public function index(){}

}

URL or path generation

Route names can be used to generate URLs or paths.

Linking to Pages in Twig

<a href="{{ path('products.index') }}">Products</a>

Global route

The @route annotation can be used on a controller class :

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

use Ubiquity\attributes\items\router\Route;

#[Route('products')]
class ProductsController extends ControllerBase{
   ...
   #[Route('/all')]
   public function display(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;
/**
 * @route("/product")
 */
class ProductsController extends ControllerBase{

   ...
   /**
    * @route("/all")
    */
   public function display(){}

}

In this case, the route defined on the controller is used as a prefix for all controller routes :
The generated route for the action display is /product/all

automated routes

If a global route is defined, it is possible to add all controller actions as routes (using the global prefix), by setting the automated parameter :

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace controllers;

use Ubiquity\attributes\items\router\Route;

#[Route('/products',automated: true)]
class ProductsController extends ControllerBase{

   public function index(){}

   public function generate(){}

   public function display($id){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;
/**
 * @route("/product","automated"=>true)
 */
class ProductsController extends ControllerBase{

   public function index(){}

   public function generate(){}

   public function display($id){}

}
The automated attribute defines the 3 routes contained in ProductsController:
  • /product/(index/)?
  • /product/generate
  • /product/display/{id}
inherited routes

With the inherited attribute, it is also possible to generate the declared routes in the base classes, or to generate routes associated with base class actions if the automated attribute is set to true in the same time.

The base class:

app/controllers/ProductsBase.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 namespace controllers;

 use Ubiquity\attributes\items\router\Route;

 abstract class ProductsBase extends ControllerBase{

     #[Route('(index/)?')]
     public function index(){}

     #[Route('sort/{name}')]
     public function sortBy($name){}

 }
app/controllers/ProductsBase.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace controllers;

abstract class ProductsBase extends ControllerBase{

   /**
    *@route("(index/)?")
    */
   public function index(){}

   /**
    * @route("sort/{name}")
    */
   public function sortBy($name){}

}

The derived class using inherited members:

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace controllers;

use Ubiquity\attributes\items\router\Route;

#[Route('/product',inherited: true)]
class ProductsController extends ProductsBase{

   public function display(){}

}
app/controllers/ProductsController.php
1
2
3
4
5
6
7
8
9
namespace controllers;
/**
 * @route("/product","inherited"=>true)
 */
class ProductsController extends ProductsBase{

   public function display(){}

}
The inherited attribute defines the 2 routes defined in ProductsBase:
  • /products/(index/)?
  • /products/sort/{name}

If the automated and inherited attributes are combined, the base class actions are also added to the routes.

Global route parameters

The global part of a route can define parameters, which will be passed in all generated routes.
These parameters can be retrieved through a public data member:

app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace controllers;

use Ubiquity\attributes\items\router\Route;

#[Route('/foo/{bar}',automated: true)]
class FooController {

   public string $bar;

   public function display(){
       echo $this->bar;
   }

}
app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace controllers;

/**
 * @route("/foo/{bar}","automated"=>true)
 */
class FooController {

   public string $bar;

   public function display(){
       echo $this->bar;
   }

}

Accessing the url /foo/bar/display displays the contents of the bar member.

Route without global prefix

If the global route is defined on a controller, all the generated routes in this controller are preceded by the prefix.
It is possible to explicitly introduce exceptions on some routes, using the #/ prefix.

app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

use Ubiquity\attributes\items\router\Route;

#[Route('/foo',automated: true)]
class FooController {

   #[Route('#/noRoot')]
   public function noRoot(){}

}
app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

/**
 * @route("/foo","automated"=>true)
 */
class FooController {

  /**
   * @route("#/noRoot")
   */
   public function noRoot(){}

}

The controller defines the /noRoot url, which is not prefixed with the /foo part.

Route priority

The prority parameter of a route allows this route to be resolved in a priority order.

The higher the priority parameter, the more the route will be defined at the beginning of the stack of routes in the cache.

In the example below, the products/all route will be defined before the /products route.

app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace controllers;

use Ubiquity\attributes\items\router\Route;

class ProductsController extends ControllerBase{

   #[Route('products', priority: 1)]
   public function index(){}

   #[Route('products/all', priority: 10)]
   public function all(){}

}
app/controllers/ProductsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace controllers;

class ProductsController extends ControllerBase{

   /**
    * @route("products","priority"=>1)
    */
   public function index(){}

   /**
    * @route("products/all","priority"=>10)
    */
   public function all(){}

}

The default priority value is 0.

Routes response caching

It is possible to cache the response produced by a route:

In this case, the response is cached and is no longer dynamic.

#[Route('products/all', cache: true)]
public function all(){}
/**
 * @route("products/all","cache"=>true)
 */
public function all(){}

Cache duration

The duration is expressed in seconds, if it is omitted, the duration of the cache is infinite.

#[Route('products/all', cache: true, duration: 3600)]
public function all(){}
/**
 * @route("products/all","cache"=>true,"duration"=>3600)
 */
public function all(){}

Cache expiration

It is possible to force reloading of the response by deleting the associated cache.

Router::setExpired("products/all");

Dynamic routes caching

Dynamic routes can also be cached.

Important

This possibility is only useful if this caching is not done in production, but at the time of initialization of the cache.

Router::get("foo", function(){
   echo 'Hello world!';
});

Router::addRoute("string", \controllers\Main::class,"index");
CacheManager::storeDynamicRoutes(false);

Checking routes with devtools :

Ubiquity info:routes
_images/info-routes.png

Error management (404 & 500 errors)

Default routing system

With the default routing system (the controller+action couple defining a route), the error handler can be redefined to customize the error management.

In the configuration file app/config/config.php, add the onError key, associated to a callback defining the error messages:

"onError"=>function ($code, $message = null,$controller=null){
   switch($code){
      case 404:
         $init=($controller==null);
         \Ubiquity\controllers\Startup::forward('IndexController/p404',$init,$init);
         break;
   }
}

Implement the requested action p404 in the IndexController:

app/controllers/IndexController.php
...

public function p404(){
   echo "<div class='ui error message'><div class='header'>404</div>The page you are looking for doesn't exist!</div>";
}

Routage with annotations

It is enough in this case to add a last route disabling the default routing system, and corresponding to the management of the 404 error:

app/controllers/IndexController.php
...

#[Route('{url}', priority: -1000)]
public function p404($url){
   echo "<div class='ui error message'><div class='header'>404</div>The page `$url` you are looking for doesn't exist!</div>";
}
app/controllers/IndexController.php
...

/**
 * @route("{url}","priority"=>-1000)
 */
public function p404($url){
   echo "<div class='ui error message'><div class='header'>404</div>The page `$url` you are looking for doesn't exist!</div>";
}

Controllers

A controller is a PHP class inheriting from Ubiquity\controllers\Controller, providing an entry point in the application.
Controllers and their methods define accessible URLs.

Controller creation

The easiest way to create a controller is to do it from the devtools.

From the command prompt, go to the project folder.
To create the Products controller, use the command:

Ubiquity controller Products

The Products.php controller is created in the app/controllers folder of the project.

app/controllers/Products.php
1
2
3
4
5
6
7
8
9
namespace controllers;
/**
 * Controller Products
 */
class Products extends ControllerBase{

public function index(){}

}

It is now possible to access URLs (the index method is solicited by default):

example.com/Products
example.com/Products/index

Note

A controller can be created manually. In this case, he must respect the following rules:

  • The class must be in the app/controllers folder
  • The name of the class must match the name of the php file
  • The class must inherit from ControllerBase and be defined in the namespace controllers
  • and must override the abstract index method

Methods

public

The second segment of the URI determines which public method in the controller gets called.
The “index” method is always loaded by default if the second segment of the URI is empty.

app/controllers/First.php
1
2
3
4
5
6
7
8
namespace controllers;
class First extends ControllerBase{

   public function hello(){
      echo "Hello world!";
   }

}

The hello method of the First controller makes the following URL available:

example.com/First/hello

method arguments

the arguments of a method must be passed in the url, except if they are optional.

app/controllers/First.php
namespace controllers;
class First extends ControllerBase{

public function says($what,$who='world') {
   echo $what.' '.$who;
}

}

The hello method of the First controller makes the following URLs available:

example.com/First/says/hello (says hello world)
example.com/First/says/Hi/everyone (says Hi everyone)

private

Private or protected methods are not accessible from the URL.

Default controller

The default controller can be set with the Router, in the services.php file

app/config/services.php
Router::start();
Router::addRoute("_default", "controllers\First");

In this case, access to the example.com/ URL loads the controller First and calls the default index method.

views loading

loading

Views are stored in the app/views folder. They are loaded from controller methods.
By default, it is possible to create views in php, or with twig.
Twig is the default template engine for html files.

php view loading

If the file extension is not specified, the loadView method loads a php file.

app/controllers/First.php
namespace controllers;
class First extends ControllerBase{
   public function displayPHP(){
      //loads the view app/views/index.php
      $this->loadView('index');
   }
}
twig view loading

If the file extension is html, the loadView method loads an html twig file.

app/controllers/First.php
namespace controllers;
class First extends ControllerBase{
   public function displayTwig(){
      //loads the view app/views/index.html
      $this->loadView("index.html");
   }
}
Default view loading

If you use the default view naming method :
The default view associated to an action in a controller is located in views/controller-name/action-name folder:

views
     │
     └ Users
         └ info.html
app/controllers/Users.php
1
2
3
4
5
6
7
8
 namespace controllers;

 class Users extends BaseController{
   ...
   public function info(){
      $this->loadDefaultView();
   }
}

view parameters

One of the missions of the controller is to pass variables to the view.
This can be done at the loading of the view, with an associative array:

app/controllers/First.php
class First extends ControllerBase{
   public function displayTwigWithVar($name){
      $message="hello";
      //loads the view app/views/index.html
      $this->loadView('index.html', ['recipient'=>$name, 'message'=>$message]);
   }
}

The keys of the associative array create variables of the same name in the view.
Using of this variables in Twig:

app/views/index.html
<h1>{{message}} {{recipient}}</h1>

Variables can also be passed before the view is loaded:

//passing one variable
$this->view->setVar('title','Message');
//passing an array of 2 variables
$this->view->setVars(['message'=>$message,'recipient'=>$name]);
//loading the view that now contains 3 variables
$this->loadView('First/index.html');

view result as string

It is possible to load a view, and to return the result in a string, assigning true to the 3rd parameter of the loadview method :

$viewResult=$this->loadView("First/index.html",[],true);
echo $viewResult;

multiple views loading

A controller can load multiple views:

app/controllers/Products.php
namespace controllers;
class Products extends ControllerBase{
   public function all(){
      $this->loadView('Main/header.html', ['title'=>'Products']);
      $this->loadView('Products/index.html',['products'=>$this->products]);
      $this->loadView('Main/footer.html');
   }
}

Important

A view is often partial. It is therefore important not to systematically integrate the html and body tags defining a complete html page.

views organization

It is advisable to organize the views into folders. The most recommended method is to create a folder per controller, and store the associated views there.
To load the index.html view, stored in app/views/First:

$this->loadView("First/index.html");

initialize and finalize

The initialize method is automatically called before each requested action, the method finalize after each action.

Example of using the initialize and finalize methods with the base class automatically created with a new project:

app/controllers/ControllerBase.php
namespace controllers;

use Ubiquity\controllers\Controller;
use Ubiquity\utils\http\URequest;

/**
 * ControllerBase.
 */
abstract class ControllerBase extends Controller{
   protected $headerView = "@activeTheme/main/vHeader.html";
   protected $footerView = "@activeTheme/main/vFooter.html";

   public function initialize() {
      if (! URequest::isAjax ()) {
         $this->loadView ( $this->headerView );
      }
   }

   public function finalize() {
      if (! URequest::isAjax ()) {
         $this->loadView ( $this->footerView );
      }
   }
}

Access control

Access control to a controller can be performed manually, using the isValid and onInvalidControl methods.

The isValid method must return a boolean wich determine if access to the action passed as a parameter is possible:

In the following example, access to the actions of the IndexController controller is only possible if an activeUser session variable exists:

app/controllers/IndexController.php
class IndexController extends ControllerBase{
...
   public function isValid($action){
      return USession::exists('activeUser');
   }
}

If the activeUser variable does not exist, an unauthorized 401 error is returned.

The onInvalidControl method allows you to customize the unauthorized access:

app/controllers/IndexController.php
class IndexController extends ControllerBase{
   ...
   public function isValid($action){
      return USession::exists('activeUser');
   }

   public function onInvalidControl(){
      $this->initialize();
      $this->loadView('unauthorized.html');
      $this->finalize();
   }
}
app/views/unauthorized.html
<div class="ui container">
   <div class="ui brown icon message">
      <i class="ui ban icon"></i>
      <div class="content">
         <div class="header">
            Error 401
         </div>
         <p>You are not authorized to access to <b>{{app.getController() ~ "::" ~ app.getAction()}}</b>.</p>
      </div>
   </div>
</div>

It is also possible to automatically generate access control from AuthControllers

Forwarding

A redirection is not a simple call to an action of a controller.
The redirection involves the initialize and finalize methods, as well as access control.

The forward method can be invoked without the use of the initialize and finalize methods:

It is possible to redirect to a route by its name:

Dependency injection

See Dependency injection

namespaces

The controller namespace is defined by default to controllers in the app/config/config.php file.

Super class

Inheritance can be used to factorize controller behavior.
The BaseController class created with a new project is present for this purpose.

Specific controller base classes

Controller class role
Controller Base class for all controllers
SimpleViewController Base class associated with a php template engine (for using with micro-services)
SimpleViewAsyncController Base class associated with a php template engine for async servers

Events

Note

The Events module uses the static class EventsManager to manage events.

Framework core events

Ubiquity emits events during the different phases of submitting a request.
These events are relatively few in number, to limit their impact on performance.

Part Event name Parameters Occures when
ViewEvents BEFORE_RENDER viewname, parameters Before rendering a view
ViewEvents AFTER_RENDER viewname, parameters After rendering a view
DAOEvents GET_ALL objects, classname After loading multiple objects
DAOEvents GET_ONE object, classname After loading one object
DAOEvents BEFORE_UPDATE instance Before updating an object
DAOEvents AFTER_UPDATE instance, result After updating an object
DAOEvents BEFORE_INSERT instance Before inserting an object
DAOEvents AFTER_INSERT instance, result After inserting an object
RestEvents BEFORE_INSERT instance Before inserting an object
RestEvents BEFORE_UPDATE instance Before updating an object

Note

There is no BeforeAction and AfterAction event, since the initialize and finalize methods of the controller class perform this operation.

Listening to an event

Example 1 :

Adding an _updated property on modified instances in the database :

app/config/services.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use Ubiquity\events\EventsManager;
use Ubiquity\events\DAOEvents;

...

     EventsManager::addListener(DAOEvents::AFTER_UPDATE, function($instance,$result){
             if($result==1){
                     $instance->_updated=true;
             }
     });

Note

The parameters passed to the callback function vary according to the event being listened to.

Example 2 :

Modification of the view rendering

app/config/services.php
1
2
3
4
5
6
7
8
use Ubiquity\events\EventsManager;
use Ubiquity\events\ViewEvents;

...

     EventsManager::addListener(ViewEvents::AFTER_RENDER,function(&$render,$viewname,$datas){
             $render='<h1>'.$viewname.'</h1>'.$render;
     });

Creating your own events

Example :

Creating an event to count and store the number of displays per action :

app/eventListener/TracePageEventListener.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
     namespace eventListener;

     use Ubiquity\events\EventListenerInterface;
     use Ubiquity\utils\base\UArray;

     class TracePageEventListener implements EventListenerInterface {
             const EVENT_NAME = 'tracePage';

             public function on(&...$params) {
                     $filename = \ROOT . \DS . 'config\stats.php';
                     $stats = [ ];
                     if (file_exists ( $filename )) {
                             $stats = include $filename;
                     }
                     $page = $params [0] . '::' . $params [1];
                     $value = $stats [$page] ?? 0;
                     $value ++;
                     $stats [$page] = $value;
                     UArray::save ( $stats, $filename );
             }
     }

Registering events

Registering the TracePageEventListener event in services.php :

app/config/services.php
1
2
3
4
5
6
     use Ubiquity\events\EventsManager;
     use eventListener\TracePageEventListener;

     ...

     EventsManager::addListener(TracePageEventListener::EVENT_NAME, TracePageEventListener::class);

Triggering events

An event can be triggered from anywhere, but it makes more sense to do it here in the initialize method of the base controller :

app/controllers/ControllerBase.php
 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
     namespace controllers;

     use Ubiquity\controllers\Controller;
     use Ubiquity\utils\http\URequest;
     use Ubiquity\events\EventsManager;
     use eventListener\TracePageEventListener;
     use Ubiquity\controllers\Startup;

     /**
      * ControllerBase.
      **/
     abstract class ControllerBase extends Controller{
             protected $headerView = "@activeTheme/main/vHeader.html";
             protected $footerView = "@activeTheme/main/vFooter.html";
             public function initialize() {
                     $controller=Startup::getController();
                     $action=Startup::getAction();
                     EventsManager::trigger(TracePageEventListener::EVENT_NAME, $controller,$action);
                     if (! URequest::isAjax ()) {
                             $this->loadView ( $this->headerView );
                     }
             }
             public function finalize() {
                     if (! URequest::isAjax ()) {
                             $this->loadView ( $this->footerView );
                     }
             }
     }

The result in app/config/stats.php :

app/config/stats.php
return array(
             "controllers\\IndexController::index"=>5,
             "controllers\\IndexController::ct"=>1,
             "controllers\\NewController::index"=>1,
             "controllers\\TestUCookieController::index"=>1
     );

Events registering optimization

It is preferable to cache the registration of listeners, to optimize their loading time :

Create a client script, or a controller action (not accessible in production mode) :

use Ubiquity\events\EventsManager;

public function initEvents(){
        EventsManager::start();
        EventsManager::addListener(DAOEvents::AFTER_UPDATE, function($instance,$result){
                if($result==1){
                        $instance->_updated=true;
                }
        });
        EventsManager::addListener(TracePageEventListener::EVENT_NAME, TracePageEventListener::class);
        EventsManager::store();
}

After running, cache file is generated in app/cache/events/events.cache.php.

Once the cache is created, the services.php file just needs to have the line :

\Ubiquity\events\EventsManager::start();

Dependency injection

Note

For performance reasons, dependency injection is not used in the core part of the framework.

Dependency Injection (DI) is a design pattern used to implement IoC.
It allows the creation of dependent objects outside of a class and provides those objects to a class through different ways. Using DI, we move the creation and binding of the dependent objects outside of the class that depends on it.

Note

Ubiquity only supports property injection, so as not to require introspection at execution.
Only controllers support dependency injection.

Service autowiring

Service creation

Create a service

app/services/Service.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace services;

     class Service{
         public function __construct($ctrl){
             echo 'Service instanciation in '.get_class($ctrl);
         }

         public function do($someThink=""){
             echo 'do '.$someThink ."in service";
         }
     }

Autowiring in Controller

Create a controller that requires the service

app/services/Service.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
     namespace controllers;

      /**
      * Controller Client
      **/
     class ClientController extends ControllerBase{

             /**
              * @autowired
              * @var services\Service
              */
             private $service;

             public function index(){}

             /**
              * @param \services\Service $service
              */
             public function setService($service) {
                     $this->service = $service;
             }
     }

In the above example, Ubiquity looks for and injects $service when ClientController is created.

The @autowired annotation requires that:
  • the type to be instantiated is declared with the @var annotation
  • $service property has a setter, or whether declared public

As the annotations are never read at runtime, it is necessary to generate the cache of the controllers:

Ubiquity init-cache -t=controllers

It remains to check that the service is injected by going to the address /ClientController.

Service injection

Service

Let’s now create a second service, requiring a special initialization.

app/services/ServiceWithInit.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
     class ServiceWithInit{
             private $init;

             public function init(){
                     $this->init=true;
             }

             public function do(){
                     if($this->init){
                             echo 'init well initialized!';
                     }else{
                             echo 'Service not initialized';
                     }
             }
     }

Injection in controller

app/controllers/ClientController.php
 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
namespace controllers;

      /**
      * Controller Client
      **/
     class ClientController extends ControllerBase{

             /**
              * @autowired
              * @var \services\Service
              */
             private $service;

             /**
              * @injected
              */
             private $serviceToInit;

             public function index(){
                     $this->serviceToInit->do();
             }

             /**
              * @param \services\Service $service
              */
             public function setService($service) {
                     $this->service = $service;
             }

             /**
              * @param mixed $serviceToInit
              */
             public function setServiceToInit($serviceToInit) {
                     $this->serviceToInit = $serviceToInit;
             }

     }

Di declaration

In app/config/config.php, create a new key for serviceToInit property to inject in di part.

"di"=>["ClientController.serviceToInit"=>function(){
                        $service=new \services\ServiceWithInit();
                        $service->init();
                        return $service;
                }
        ]

generate the cache of the controllers:

Ubiquity init-cache -t=controllers

Check that the service is injected by going to the address /ClientController.

Note

If the same service is to be used in several controllers, use the wildcard notation :

"di"=>["*.serviceToInit"=>function(){
                        $service=new \services\ServiceWithInit();
                        $service->init();
                        return $service;
                }
        ]

Injection with a qualifier name

If the name of the service to be injected is different from the key of the di array, it is possible to use the name attribute of the @injected annotation

In app/config/config.php, create a new key for serviceToInit property to inject in di part.

"di"=>["*.service"=>function(){
                        $service=new \services\ServiceWithInit();
                        $service->init();
                        return $service;
                }
        ]
/**
 * @injected("service")
 */
private $serviceToInit;

Service injection at runtime

It is possible to inject services at runtime, without these having been previously declared in the controller classes.

app/services/RuntimeService.php
1
2
3
4
5
6
7
namespace services;

     class RuntimeService{
         public function __construct($ctrl){
             echo 'Service instanciation in '.get_class($ctrl);
         }
     }

In app/config/config.php, create the @exec key in di part.

"di"=>["@exec"=>"rService"=>function($ctrl){
                        return new \services\RuntimeService($ctrl);
                }
        ]

With this declaration, the $rService member, instance of RuntimeService, is injected into all the controllers.
It is then advisable to use the javadoc comments to declare $rService in the controllers that use it (to get the code completion on $rService in your IDE).

app/controllers/MyController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
namespace controllers;

      /**
      * Controller Client
      * property services\RuntimeService $rService
      **/
     class MyController extends ControllerBase{

             public function index(){
                     $this->rService->do();
             }
     }

CRUD Controllers

The CRUD controllers allow you to perform basic operations on a Model class:
  • Create
  • Read
  • Update
  • Delete

Note

Since version 2.4.6, Two types of CrudController exist:
  • ResourceCrudController associated with a model
  • MultiResourceCRUDController, displaying an index and allowing to navigate between models.

ResourceCrudController

Creation

In the admin interface (web-tools), activate the Controllers part, and choose create Resource Crud controller:
_images/speControllerBtn.png
Then fill in the form:
  • Enter the controller name
  • Select the associated model
  • Then click on the validate button
_images/createCrudForm1.png

Description of the features

The generated controller:

app/controllers/Products.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
namespace controllers;

 /**
 * CRUD Controller UsersController
 **/
class UsersController extends \Ubiquity\controllers\crud\CRUDController{

     public function __construct(){
             parent::__construct();
             $this->model= models\User::class;
     }

     public function _getBaseRoute():string {
             return 'UsersController';
     }
}

Test the created controller by clicking on the get button in front of the index action:

_images/getBtn.png
Read (index action)
_images/usersControllerIndex1.png

Clicking on a row of the dataTable (instance) displays the objects associated to the instance (details action):

_images/usersControllerIndex1-details.png

Using the search area:

_images/usersControllerSearch1.png
Create (newModel action)

It is possible to create an instance by clicking on the add button

_images/addNewModelBtn.png

The default form for adding an instance of User:

_images/usersControllerNew1.png
Update (update action)

The edit button on each row allows you to edit an instance

_images/editModelBtn.png

The default form for adding an instance of User:

_images/usersControllerEdit1.png
Delete (delete action)

The delete button on each row allows you to edit an instance

_images/deleteModelBtn.png

Display of the confirmation message before deletion:

_images/usersControllerDelete1.png

Customization

Create again a ResourceCrudController from the admin interface:

_images/createCrudForm2.png

It is now possible to customize the module using overriding.

Overview
_images/crud-schema.png
Classes overriding
ResourceCRUDController methods to override
Method Signification Default return
routes
index() Default page : list all objects  
edit($modal=”no”, $ids=””) Edits an instance  
newModel($modal=”no”) Creates a new instance  
display($modal=”no”,$ids=””) Displays an instance  
delete($ids) Deletes an instance  
update() Displays the result of an instance updating  
showDetail($ids) Displays associated members with foreign keys  
refresh_() Refreshes the area corresponding to the DataTable (#lv)  
refreshTable($id=null) //TO COMMENT  
ModelViewer methods to override
Method Signification Default return
index route
getModelDataTable($instances, $model,$totalCount,$page=1) Creates the dataTable and Adds its behavior DataTable
getDataTableInstance($instances,$model,$totalCount,$page=1) Creates the dataTable DataTable
recordsPerPage($model,$totalCount=0) Returns the count of rows to display (if null there’s no pagination) null or 6
getGroupByFields() Returns an array of members on which to perform a grouping []
getDataTableRowButtons() Returns an array of buttons to display for each row [“edit”,”delete”,”display”] [“edit”,”delete”]
onDataTableRowButton(HtmlButton $bt, ?string $name) To override for modifying the dataTable row buttons  
getCaptions($captions, $className) Returns the captions of the column headers all member names
detail route
showDetailsOnDataTableClick() To override to make sure that the detail of a clicked object is displayed or not true
onDisplayFkElementListDetails($element,$member,$className,$object) To modify for displaying each element in a list component of foreign objects  
getFkHeaderElementDetails($member, $className, $object) Returns the header for a single foreign object (issue from ManyToOne) HtmlHeader
getFkElementDetails($member, $className, $object) Returns a component for displaying a single foreign object (manyToOne relation) HtmlLabel
getFkHeaderListDetails($member, $className, $list) Returns the header for a list of foreign objects (oneToMany or ManyToMany) HtmlHeader
getFkListDetails($member, $className, $list) Returns a list component for displaying a collection of foreign objects (many) HtmlList
edit and newModel routes
getForm($identifier, $instance) Returns the form for adding or modifying an object HtmlForm
formHasMessage() Determines if the form has a message title true
getFormModalTitle($instance) Returns the form modal title instance class
onFormModalButtons($btOkay, $btCancel) Hook for updating modal buttons  
getFormTitle($form,$instance) Returns an associative array defining form message title with keys “icon”,”message”,”subMessage” HtmlForm
setFormFieldsComponent(DataForm $form,$fieldTypes) Sets the components for each field  
onGenerateFormField($field) For doing something when $field is generated in form  
isModal($objects, $model) Condition to determine if the edit or add form is modal for $model objects count($objects)>5
getFormCaptions($captions, $className, $instance) Returns the captions for form fields all member names
display route
getModelDataElement($instance,$model,$modal) Returns a DataElement object for displaying the instance DataElement
getElementCaptions($captions, $className, $instance) Returns the captions for DataElement fields all member names
delete route
onConfirmButtons(HtmlButton $confirmBtn,HtmlButton $cancelBtn) To override for modifying delete confirmation buttons  
CRUDDatas methods to override
Method Signification Default return
index route
_getInstancesFilter($model) Adds a condition for filtering the instances displayed in dataTable 1=1
getFieldNames($model) Returns the fields to display in the index action for $model all member names
getSearchFieldNames($model) Returns the fields to use in search queries all member names
edit and newModel routes
getFormFieldNames($model,$instance) Returns the fields to update in the edit and newModel actions for $model all member names
getManyToOneDatas($fkClass,$instance,$member) Returns a list (filtered) of $fkClass objects to display in an html list all $fkClass instances
getOneToManyDatas($fkClass,$instance,$member) Returns a list (filtered) of $fkClass objects to display in an html list all $fkClass instances
getManyToManyDatas($fkClass,$instance,$member) Returns a list (filtered) of $fkClass objects to display in an html list all $fkClass instances
display route
getElementFieldNames($model) Returns the fields to display in the display action for $model all member names
CRUDEvents methods to override
Method Signification Default return
index route
onConfDeleteMessage(CRUDMessage $message,$instance) Returns the confirmation message displayed before deleting an instance CRUDMessage
onSuccessDeleteMessage(CRUDMessage $message,$instance) RReturns the message displayed after a deletion CRUDMessage
onErrorDeleteMessage(CRUDMessage $message,$instance) Returns the message displayed when an error occurred when deleting CRUDMessage
edit and newModel routes
onSuccessUpdateMessage(CRUDMessage $message) Returns the message displayed when an instance is added or inserted CRUDMessage
onErrorUpdateMessage(CRUDMessage $message) Returns the message displayed when an error occurred when updating or inserting CRUDMessage
onNewInstance(object $instance) Triggered after the creation of a new instance  
onBeforeUpdate(object $instance, bool $isNew) Triggered before the instance update  
all routes
onNotFoundMessage(CRUDMessage $message,$ids) Returns the message displayed when an instance does not exists  
onDisplayElements($dataTable,$objects,$refresh) Triggered after displaying objects in dataTable  
CRUDFiles methods to override
Method Signification Default return
template files
getViewBaseTemplate() Returns the base template for all Crud actions if getBaseTemplate return a base template filename @framework/crud/baseTemplate.html
getViewIndex() Returns the template for the index route @framework/crud/index.html
getViewForm() Returns the template for the edit and newInstance routes @framework/crud/form.html
getViewDisplay() Returns the template for the display route @framework/crud/display.html
Urls
getRouteRefresh() Returns the route for refreshing the index route /refresh_
getRouteDetails() Returns the route for the detail route, when the user click on a dataTable row /showDetail
getRouteDelete() Returns the route for deleting an instance /delete
getRouteEdit() Returns the route for editing an instance /edit
getRouteDisplay() Returns the route for displaying an instance /display
getRouteRefreshTable() Returns the route for refreshing the dataTable /refreshTable
getDetailClickURL($model) Returns the route associated with a foreign key instance in list “”
Twig Templates structure
index.html
_images/template_index.png
form.html

Displayed in frm block

_images/template_form.png
display.html

Displayed in frm block

_images/template_display.png

MultiResourceCrudController

Note

The MultiResourceCRUDController displays an index allowing to navigate between the CRUDs of the models.

Creation

In the admin interface (web-tools), activate the Controllers part, and choose create Index Crud controller:
_images/speControllerBtn.png
Then fill in the form:
  • Enter the controller name
  • The route path (which must contain the variable part {resource})
  • Then click on the validate button
_images/createIndexCrudForm1.png

Description of the features

The generated controller:

app/controllers/CrudIndex.php
 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
<?php
namespace controllers;
use Ubiquity\attributes\items\router\Route;

#[Route(path: "/{resource}/crud",inherited: true,automated: true)]
class CrudIndex extends \Ubiquity\controllers\crud\MultiResourceCRUDController{

             #[Route(name: "crud.index",priority: -1)]
             public function index() {
                     parent::index();
             }

             #[Route(path: "#//home/crud",name: "crud.home",priority: 100)]
             public function home(){
                     parent::home();
             }

             protected function getIndexType():array {
                     return ['four link cards','card'];
             }

             public function _getBaseRoute():string {
                     return "/".$this->resource."/crud";
             }

}

Test the created controller at /home/crud url:

_images/indexCrudController.png

Customization

Create again a MultiResourceCrudController from the admin interface:

_images/createIndexCrudForm2.png

It is now possible to customize the module using overriding like the ResourceCRUDControllers.

Specific classes to override
MultiResourceCRUDController methods to override
Method Signification Default return
routes
home () Home page : list all models  
All routes from CRUDController  
Events
onRenderView(array &$data) On before home page rendering  
Configuration
hasNavigation() Returns True for displaying the navigation dropdown menu True
getIndexModels() Returns the list of available models to display models from default db
getIndexModelsDetails() Returns an associative array (title, icon url) for each model []
getIndexDefaultIcon(string $resource) Returns the icon for a model A random animal
getIndexDefaultTitle(string $resource) Returns the title for a model The resource name
getIndexDefaultDesc(string $modelClass) Returns the description for a model The complete classname
getIndexDefaultUrl(string $resource) Returns the url associated to a model The route path
getIndexDefaultMeta(string $modelClass) Returns the meta for a model  
getIndexType() Defines the index component css classes cards
getModelName() Returns the complete model name for $this->resource From default model NS
CRUDFiles methods to override
Method Signification Default return
template files
getViewHome() Returns the base template for the home view @framework/crud/home.html
getViewItemHome() Returns the template for an item in home route @framework/crud/itemHome.html
getViewNav() Returns the template for displaying models in a dropdown @framework/crud/nav.html

Note

All other methods of the CRUDController, CRUDFiles, CRUDEvents and CRUDDatas classes can be overridden as for the ResourceCRUDController.

Auth Controllers

The Auth controllers allow you to perform basic authentification with:
  • login with an account
  • account creation
  • logout
  • controllers with required authentication

Creation

In the admin interface (web-tools), activate the Controllers part, and choose create Auth controller:
_images/speControllerBtn.png
Then fill in the form:
  • Enter the controller name (BaseAuthController in this case)
_images/createAuthForm1.png

The generated controller:

app/controllers/BaseAuthController.php
 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
 /**
 * Auth Controller BaseAuthController
 **/
class BaseAuthController extends \Ubiquity\controllers\auth\AuthController{

     protected function onConnect($connected) {
             $urlParts=$this->getOriginalURL();
             USession::set($this->_getUserSessionKey(), $connected);
             if(isset($urlParts)){
                     Startup::forward(implode("/",$urlParts));
             }else{
                     //TODO
                     //Forwarding to the default controller/action
             }
     }

     protected function _connect() {
             if(URequest::isPost()){
                     $email=URequest::post($this->_getLoginInputName());
                     $password=URequest::post($this->_getPasswordInputName());
                     //TODO
                     //Loading from the database the user corresponding to the parameters
                     //Checking user creditentials
                     //Returning the user
             }
             return;
     }

     /**
      * {@inheritDoc}
      * @see \Ubiquity\controllers\auth\AuthController::isValidUser()
      */
     public function _isValidUser($action=null): bool {
             return USession::exists($this->_getUserSessionKey());
     }

     public function _getBaseRoute(): string {
             return 'BaseAuthController';
     }
}

Implementation of the authentification

Example of implementation with the administration interface : We will add an authentication check on the admin interface.

Authentication is based on verification of the email/password pair of a model User:

_images/model-user.png

BaseAuthController modification

app/controllers/BaseAuthController.php
 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
 /**
 * Auth Controller BaseAuthController
 **/
class BaseAuthController extends \Ubiquity\controllers\auth\AuthController{

     protected function onConnect($connected) {
             $urlParts=$this->getOriginalURL();
             USession::set($this->_getUserSessionKey(), $connected);
             if(isset($urlParts)){
                     Startup::forward(implode("/",$urlParts));
             }else{
                     Startup::forward("admin");
             }
     }

     protected function _connect() {
             if(URequest::isPost()){
                     $email=URequest::post($this->_getLoginInputName());
                     $password=URequest::post($this->_getPasswordInputName());
                     return DAO::uGetOne(User::class, "email=? and password= ?",false,[$email,$password]);
             }
             return;
     }

     /**
      * {@inheritDoc}
      * @see \Ubiquity\controllers\auth\AuthController::isValidUser()
      */
     public function _isValidUser($action=null): bool {
             return USession::exists($this->_getUserSessionKey());
     }

     public function _getBaseRoute(): string {
             return 'BaseAuthController';
     }
     /**
      * {@inheritDoc}
      * @see \Ubiquity\controllers\auth\AuthController::_getLoginInputName()
      */
     public function _getLoginInputName(): string {
             return "email";
     }
}

Admin controller modification

Modify the Admin Controller to use BaseAuthController:

app/controllers/Admin.php
1
2
3
4
5
6
class Admin extends UbiquityMyAdminBaseController{
     use WithAuthTrait;
     protected function getAuthController(): AuthController {
             return $this->_auth ??= new BaseAuthController($this);
     }
}

Test the administration interface at /admin:

_images/adminForbidden.png

After clicking on login:

_images/formLogin.png

If the authentication data entered is invalid:

_images/invalidCreditentials.png

If the authentication data entered is valid:

_images/adminWithAuth.png

Attaching the zone info-user

Modify the BaseAuthController controller:

app/controllers/BaseAuthController.php
1
2
3
4
5
6
7
8
9
 /**
 * Auth Controller BaseAuthController
 **/
class BaseAuthController extends \Ubiquity\controllers\auth\AuthController{
...
     public function _displayInfoAsString(): bool {
             return true;
     }
}

The _userInfo area is now present on every page of the administration:

_images/infoUserZone.png

It can be displayed in any twig template:

{{ _userInfo | raw }}

Description of the features

Customizing templates

index.html template

The index.html template manages the connection:

_images/template_authIndex.png

Example with the _userInfo area:

Create a new AuthController named PersoAuthController:

_images/createAuthForm2.png

Edit the template app/views/PersoAuthController/info.html

app/views/PersoAuthController/info.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends "@framework/auth/info.html" %}
{% block _before %}
     <div class="ui tertiary inverted red segment">
{% endblock %}
{% block _userInfo %}
     {{ parent() }}
{% endblock %}
{% block _logoutButton %}
     {{ parent() }}
{% endblock %}
{% block _logoutCaption %}
     {{ parent() }}
{% endblock %}
{% block _loginButton %}
     {{ parent() }}
{% endblock %}
{% block _loginCaption %}
     {{ parent() }}
{% endblock %}
{% block _after %}
             </div>
{% endblock %}

Change the AuthController Admin controller:

app/controllers/Admin.php
1
2
3
4
5
6
class Admin extends UbiquityMyAdminBaseController{
     use WithAuthTrait;
     protected function getAuthController(): AuthController {
             return $this->_auth ??= new PersoAuthController($this);
     }
}
_images/adminWithAuth2.png

Customizing messages

app/controllers/PersoAuthController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class PersoAuthController extends \controllers\BaseAuth{
...
 /**
  * {@inheritDoc}
  * @see \Ubiquity\controllers\auth\AuthController::badLoginMessage()
  */
 protected function badLoginMessage(\Ubiquity\utils\flash\FlashMessage $fMessage) {
     $fMessage->setTitle("Erreur d'authentification");
     $fMessage->setContent("Login ou mot de passe incorrects !");
     $this->_setLoginCaption("Essayer à nouveau");

 }
...
}

Self-check connection

app/controllers/PersoAuthController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PersoAuthController extends \controllers\BaseAuth{
...
 /**
  * {@inheritDoc}
  * @see \Ubiquity\controllers\auth\AuthController::_checkConnectionTimeout()
  */
 public function _checkConnectionTimeout() {
     return 10000;
 }
...
}

Limitation of connection attempts

app/controllers/PersoAuthController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PersoAuthController extends \controllers\BaseAuth{
...
 /**
  * {@inheritDoc}
  * @see \Ubiquity\controllers\auth\AuthController::attemptsNumber()
  */
 protected function attemptsNumber(): int {
     return 3;
 }
...
}

Account recovery

account recovery is used to reset the account password.
A password reset email is sent, to an email address corresponding to an active account.

_images/recoveryInit.png
app/controllers/PersoAuthController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PersoAuthController extends \controllers\BaseAuth{
...
 protected function hasAccountRecovery():bool{
     return true;
 }

 protected function _sendEmailAccountRecovery(string $email,string $validationURL,string $expire):bool {
     MailerManager::start();
     $mail=new AuthAccountRecoveryMail();
     $mail->to($connected->getEmail());
     $mail->setUrl($validationURL);
     $mail->setExpire($expire);
     return MailerManager::send($mail);
 }

 protected function passwordResetAction(string $email,string $newPasswordHash):bool {
     //To implement for modifying the user password
 }

 protected function isValidEmailForRecovery(string $email):bool {
     //To implement: return true if a valid account match with this email
 }
}
_images/recoveryForm.png

Note

By default, the link can only be used on the same machine, within a predetermined period of time (which can be modified by overriding the accountRecoveryDuration method).

Activation of MFA/2FA

Multi-factor authentication can be enabled conditionally, based on the pre-logged-in user’s information.

Note

Phase 2 of the authentication is done in the example below by sending a random code by email. The AuthMailerClass class is available in the Ubiquity-mailer package.

app/controllers/PersoAuthController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class PersoAuthController extends \controllers\BaseAuth{
...
 /**
  * {@inheritDoc}
  * @see \Ubiquity\controllers\auth\AuthController::has2FA()
  */
 protected function has2FA($accountValue=null):bool{
     return true;
 }

 protected function _send2FACode(string $code, $connected):void {
     MailerManager::start();
     $mail=new AuthMailerClass();
     $mail->to($connected->getEmail());
     $mail->setCode($code);
     MailerManager::send($mail);
 }
...
}
_images/2fa-code.png

Note

It is possible to customize the creation of the generated code, as well as the prefix used. The sample below is implemented with robthree/twofactorauth library.

protected function generate2FACode():string{
        $tfa=new TwoFactorAuth();
        return $tfa->createSecret();
}

protected function towFACodePrefix():string{
        return 'U-';
}

Account creation

The activation of the account creation is also optional:

_images/account-creation-available.png
app/controllers/PersoAuthController.php
1
2
3
4
5
6
7
class PersoAuthController extends \controllers\BaseAuth{
...
 protected function hasAccountCreation():bool{
     return true;
 }
...
}
_images/account-creation.png

In this case, the _create method must be overridden in order to create the account:

protected function _create(string $login, string $password): ?bool {
        if(!DAO::exists(User::class,'login= ?',[$login])){
                $user=new User();
                $user->setLogin($login);
                $user->setPassword($password);
                URequest::setValuesToObject($user);//for the others params in the POST.
                return DAO::insert($user);
        }
        return false;
}

You can check the validity/availability of the login before validating the account creation form:

protected function newAccountCreationRule(string $accountName): ?bool {
        return !DAO::exists(User::class,'login= ?',[$accountName]);
}
_images/account-creation-error.png

A confirmation action (email verification) may be requested from the user:

protected function hasEmailValidation(): bool {
        return true;
}

protected function _sendEmailValidation(string $email,string $validationURL,string $expire):void {
        MailerManager::start();
        $mail=new AuthEmailValidationMail();
        $mail->to($connected->getEmail());
        $mail->setUrl($validationURL);
        $mail->setExpire($expire);
        MailerManager::send($mail);
}

Note

It is possible to customize these parts by overriding the associated methods, or by modifying the interfaces in the concerned templates.

Database

The DAO class is responsible for loading and persistence operations on models :

Connecting to the database

Check that the database connection parameters are correctly entered in the configuration file:

Ubiquity config -f=database
_images/db-config.png

Transparent connection

Since Ubiquity 2.3.0, The connection to the database is done automatically the first time you request it:

use Ubiquity\orm\DAO;

$firstUser=DAO::getById(User::class,1);//Automatically start the database

This is the case for all methods in the DAO class used to perform CRUD operations.

Explicit connection

In some cases, however, it may be useful to make an explicit connection to the database, especially to check the connection.

use Ubiquity\orm\DAO;
use Ubiquity\controllers\Startup;
...
try{
    $config=\Ubiquity\controllers\Startup::getConfig();
    DAO::startDatabase($config);
    $users=DAO::getAll(User::class,'');
}catch(Exception $e){
    echo $e->getMessage();
}

Multiple connections

Adding a new connection

Ubiquity allows you to manage several connections to databases.

With Webtools

In the Models part, choose Add new connection button:

_images/add-new-co-btn.png

Define the connection configuration parameters:

_images/new-co.png

Generate models for the new connection:
The generated models include the @database annotation or the Database attribute mentioning their link to the connection.

<?php
namespace models\tests;
use Ubiquity\attributes\items\Database;
use Ubiquity\attributes\items\Table;

#[Database('tests')]
#[Table('groupe')]
class Groupe{
    ...
}
<?php
namespace models\tests;
/**
 * @database('tests')
 * @table('groupe')
 */
class Groupe{
    ...
}

Models are generated in a sub-folder of models.

With several connections, do not forget to add the following line to the services.php file:

\Ubiquity\orm\DAO::start();

The start method performs the match between each model and its associated connection.

Models generation

From existing database

ORM

Note

if you want to automatically generate the models, consult the generating models part.

A model class is just a plain old php object without inheritance.
Models are located by default in the app\models folder.
Object Relational Mapping (ORM) relies on member annotations or attributes (since PHP8) in the model class.

Models definition

A basic model

  • A model must define its primary key using the @id annotation on the members concerned
  • Serialized members must have getters and setters
  • Without any other annotation, a class corresponds to a table with the same name in the database, each member corresponds to a field of this table
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace models;

use Ubiquity\attributes\items\Id;

class User{

   #[Id]
   private $id;

   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 namespace models;

 class User{
   /**
    * @id
    */
   private $id;

   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
 }

Mapping

Table->Class

If the name of the table is different from the name of the class, the annotation @table allows to specify the name of the table.

app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace models;

use Ubiquity\attributes\items\Table;
use Ubiquity\attributes\items\Id;

#[Table('user')]
class User{

   #[Id]
   private $id;

   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace models;

/**
 * @table("name"=>"user")
 */
class User{
   /**
    * @id
    */
   private $id;

   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
}
Field->Member

If the name of a field is different from the name of a member in the class, the annotation @column allows to specify a different field name.

app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace models;

use Ubiquity\attributes\items\Table;
use Ubiquity\attributes\items\Id;
use Ubiquity\attributes\items\Column;

#[Table('user')
class User{

   #[Id]
   private $id;

   #[Column('column_name')]
   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace models;

/**
 * @table("user")
 */
class User{
   /**
    * @id
    */
   private $id;

   /**
    * column("user_name")
    */
   private $firstname;

   public function getFirstname(){
      return $this->firstname;
   }
   public function setFirstname($firstname){
      $this->firstname=$firstname;
   }
}

Associations

Note

Naming convention
Foreign key field names consist of the primary key name of the referenced table followed by the name of the referenced table whose first letter is capitalized.
Example
idUser for the table user whose primary key is id

ManyToOne

A user belongs to an organization:

_images/manyToOne.png
app/models/User.php
 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
 namespace models;

use Ubiquity\attributes\items\ManyToOne;
use Ubiquity\attributes\items\Id;
use Ubiquity\attributes\items\JoinColumn;

 class User{

   #[Id]
   private $id;

   private $firstname;

   #[ManyToOne]
   #[JoinColumn(className: \models\Organization::class, name: 'idOrganization', nullable: false)]
   private $organization;

   public function getOrganization(){
      return $this->organization;
   }

   public function setOrganization($organization){
      $this->organization=$organization;
   }
}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 namespace models;

 class User{
   /**
    * @id
    */
   private $id;

   private $firstname;

   /**
    * @manyToOne
    * @joinColumn("className"=>"models\\Organization","name"=>"idOrganization","nullable"=>false)
    */
   private $organization;

   public function getOrganization(){
      return $this->organization;
   }

   public function setOrganization($organization){
      $this->organization=$organization;
   }
}

The @joinColumn annotation or the JoinColumn attribute specifies that:

  • The member $organization is an instance of modelsOrganization
  • The table user has a foreign key idOrganization refering to organization primary key
  • This foreign key is not null => a user will always have an organization
OneToMany

An organization has many users:

_images/oneToMany.png
app/models/Organization.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace models;

use Ubiquity\attributes\items\OneToMany;
use Ubiquity\attributes\items\Id;

class Organization{

   #[Id]
   private $id;

   private $name;

   #[OneToMany(mappedBy: 'organization', className: \models\User::class)]
   private $users;
}
app/models/Organization.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace models;

class Organization{
   /**
    * @id
    */
   private $id;

   private $name;

   /**
    * @oneToMany("mappedBy"=>"organization","className"=>"models\\User")
    */
   private $users;
}

In this case, the association is bi-directional.
The @oneToMany annotation must just specify:

  • The class of each user in users array : modelsUser
  • the value of @mappedBy is the name of the association-mapping attribute on the owning side : $organization in User class
ManyToMany
  • A user can belong to groups.
  • A group consists of multiple users.
_images/manyToMany.png
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace models;

use Ubiquity\attributes\items\ManyToMany;
use Ubiquity\attributes\items\Id;
use Ubiquity\attributes\items\JoinTable;

class User{

   #[Id]
   private $id;

   private $firstname;

   #[ManyToMany(targetEntity: \models\Group::class, inversedBy: 'users')]
   #[JoinTable(name: 'groupusers')]
   private $groups;

}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

class User{
   /**
    * @id
    */
   private $id;

   private $firstname;

   /**
    * @manyToMany("targetEntity"=>"models\\Group","inversedBy"=>"users")
    * @joinTable("name"=>"groupusers")
    */
   private $groups;

}
app/models/Group.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace models;

use Ubiquity\attributes\items\ManyToMany;
use Ubiquity\attributes\items\Id;
use Ubiquity\attributes\items\JoinTable;

class Group{

   #[Id]
   private $id;

   private $name;

   #[ManyToMany(targetEntity: \models\User::class, inversedBy: 'groups')]
   #[JoinTable(name: 'groupusers')]
   private $users;

}
app/models/Group.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

class Group{
   /**
    * @id
    */
   private $id;

   private $name;

   /**
    * @manyToMany("targetEntity"=>"models\\User","inversedBy"=>"groups")
    * @joinTable("name"=>"groupusers")
    */
   private $users;

}

If the naming conventions are not respected for foreign keys,
it is possible to specify the related fields.

app/models/Group.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace models;

use Ubiquity\attributes\items\ManyToMany;
use Ubiquity\attributes\items\Id;
use Ubiquity\attributes\items\JoinTable;

class Group{

   #[Id]
   private $id;

   private $name;

   #[ManyToMany(targetEntity: \models\User::class, inversedBy: 'groupes')]
   #[JoinTable(name: 'groupeusers',
   joinColumns: ['name'=>'id_groupe','referencedColumnName'=>'id'],
   inverseJoinColumns: ['name'=>'id_user','referencedColumnName'=>'id'])]
   private $users;

}
app/models/Group.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
namespace models;

class Group{
   /**
    * @id
    */
   private $id;

   private $name;

   /**
    * @manyToMany("targetEntity"=>"models\\User","inversedBy"=>"groupes")
    * @joinTable("name"=>"groupeusers",
    * "joinColumns"=>["name"=>"id_groupe","referencedColumnName"=>"id"],
    * "inverseJoinColumns"=>["name"=>"id_user","referencedColumnName"=>"id"])
    */
   private $users;

}

ORM Annotations

Annotations for classes

@annotation role properties role
@database Defines the associated database offset (defined in config file)
@table Defines the associated table name.

Annotations for members

@annotation role properties role
@id Defines the primary key(s).
@column Specify the associated field characteristics. name Name of the associated field
nullable true if value can be null
dbType Type of the field in database
@transient Specify that the field is not persistent.

Associations

@annotation (extends) role properties [optional] role
@manyToOne Defines a single-valued association to another entity class.
@joinColumn (@column) Indicates the foreign key in manyToOne asso. className Class of the member
[referencedColumnName] Name of the associated column
@oneToMany Defines a multi-valued association to another entity class. className Class of the objects in member
[mappedBy] Name of the association-mapping attribute on the owning side
@manyToMany Defines a many-valued association with many-to-many multiplicity targetEntity Class of the objects in member
[inversedBy] Name of the association-member on the inverse-side
[mappedBy] Name of the association-member on the owning side
@joinTable Defines the association table for many-to-many multiplicity name The name of the association table
[joinColumns] @column => name and referencedColumnName for this side
[inverseJoinColumns] @column => name and referencedColumnName for the other side

DAO

The DAO class is responsible for loading and persistence operations on models :

Connecting to the database

Check that the database connection parameters are correctly entered in the configuration file:

Ubiquity config -f=database

Since 2.3.0 release

Database startup with DAO::startDatabase($config) in services.php file is useless, no need to start the database, the connection is made automatically at the first request. Use DAO::start() in app/config/services.php file when using several databases (with multi db feature)

Loading data

Loading an instance

Loading an instance of the models\User class with id 5

use Ubiquity\orm\DAO;
use models\User;

$user=DAO::getById(User::class, 5);

Loading an instance using a condition:

use Ubiquity\orm\DAO;
use models\User;

DAO::getOne(User::class, 'name= ?',false,['DOE']);
BelongsTo loading

By default, members defined by a belongsTo relationship are automatically loaded

Each user belongs to only one category:

$user=DAO::getById(User::class,5);
echo $user->getCategory()->getName();

It is possible to prevent this default loading ; the third parameter allows the loading or not of belongsTo members:

$user=DAO::getOne(User::class,5, false);
echo $user->getCategory();// NULL
HasMany loading

Loading hasMany members must always be explicit ; the third parameter allows the explicit loading of members.

Each user has many groups:

$user=DAO::getOne(User::class,5,['groupes']);
foreach($user->getGroupes() as $groupe){
    echo $groupe->getName().'<br>';
}
Composite primary key

Either the ProductDetail model corresponding to a product ordered on a command and whose primary key is composite:

app/models/ProductDetail.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 namespace models;

use Ubiquity\attributes\items\Id;

 class ProductDetail{

   #[Id]
   private $idProduct;

   #[Id]
   private $idCommand;

   ...
 }
app/models/ProductDetail.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 namespace models;

 class ProductDetail{
   /**
    * @id
    */
   private $idProduct;

   /**
    * @id
    */
   private $idCommand;

   ...
 }

The second parameter $keyValues can be an array if the primary key is composite:

$productDetail=DAO::getOne(ProductDetail::class,[18,'BF327']);
echo 'Command:'.$productDetail->getCommande().'<br>';
echo 'Product:'.$productDetail->getProduct().'<br>';

Loading multiple objects

Loading instances of the User class:

$users=DAO::getAll(User::class);
foreach($users as $user){
    echo $user->getName()."<br>";
}

Querying using conditions

Simple queries

The condition parameter is equivalent to the WHERE part of an SQL statement:

$users=DAO::getAll(User::class,'firstName like "bren%" and not suspended',false);

To avoid SQL injections and benefit from the preparation of statements, it is preferable to perform a parameterized query:

$users=DAO::getAll(User::class,'firstName like ? and suspended= ?',false,['bren%',false]);
UQueries

The use of U-queries allows to set conditions on associate members:

Selection of users whose organization has the domain lecnam.net:

$users=DAO::uGetAll(User::class,'organization.domain= ?',false,['lecnam.net']);

It is possible to view the generated request in the logs (if logging is enabled):

_images/uquery-users-log.png

The result can be verified by selecting all users in this organization:

$organization=DAO::getOne(Organization::class,'domain= ?',['users'],['lecnam.net']);
$users=$organization->getUsers();

The corresponding logs:

_images/uquery-users-orga-log.png

Counting

Existence testing
if(DAO::exists(User::class,'lastname like ?',['SMITH'])){
    //there's a Mr SMITH
}
Counting

To count the instances, what not to do, if users are not already loaded:

$users=DAO::getAll(User::class);
echo "there are ". \count($users) ." users";

What needs to be done:

$count=DAO::count(User::class);
echo "there are $count users";

With a condition:

$notSuspendedCount=DAO::count(User::class, 'suspended = ?', [false]);

with a condition on associated objects:

Number of users belonging to the OTAN named organization.

$count=DAO::uCount(User::class,'organization.name= ?',['OTAN']);

Modifying data

Adding an instance

Adding an organization:

$orga=new Organization();
$orga->setName('Foo');
$orga->setDomain('foo.net');
if(DAO::save($orga)){
  echo $orga.' added in database';
}

Adding an instance of User, in an organization:

$orga=DAO::getById(Organization::class, 1);
$user=new User();
$user->setFirstname('DOE');
$user->setLastname('John');
$user->setEmail('doe@bar.net');
$user->setOrganization($orga);
if(DAO::save($user)){
  echo $user.' added in database in '.$orga;
}

Updating an instance

First, the instance must be loaded:

$orga=DAO::getOne(Organization::class,'domain= ?',false,['foo.net']);
$orga->setAliases('foo.org');
if(DAO::save($orga)){
  echo $orga.' updated in database';
}

Deleting an instance

If the instance is loaded from database:

$orga=DAO::getById(Organization::class,5,false);
if(DAO::remove($orga)){
  echo $orga.' deleted from database';
}

If the instance is not loaded, it is more appropriate to use the delete method:

if(DAO::delete(Organization::class,5)){
  echo 'Organization deleted from database';
}

Deleting multiple instances

Deletion of multiple instances without prior loading:

if($res=DAO::deleteAll(models\User::class, 'id in (?,?,?)',[1,2,3])){
    echo "$res elements deleted";
}

Bulk queries

Bulk queries allow several operations (insertion, modification or deletion) to be performed in a single query, which contributes to improved performance.

Bulk inserts

Insertions example:

$u = new User();
$u->setName('Martin1');
DAO::toInsert($u);
$u = new User();
$u->setName('Martin2');
DAO::toInsert($u);
//Perform inserts
DAO::flushInserts();

Bulk updates

Updates example:

$users = DAO::getAll(User::class, 'name like ?', false, [
   'Martin%'
]);
foreach ($users as $user) {
   $user->setName(\strtoupper($user->getName()));
   DAO::toUpdate($user);
}
DAO::flushUpdates();

Bulk deletes

Deletions example

$users = DAO::getAll(User::class, 'name like ?', false, [
     'BULK%'
]);
DAO::toDeletes($users);
DAO::flushDeletes();

The DAO::flush() method can be called if insertions, updates or deletions are pending.

Transactions

Explicit transactions

All DAO operations can be inserted into a transaction, so that a series of changes can be atomized:

try{
   DAO::beginTransaction();
   $orga=new Organization();
   $orga->setName('Foo');
   DAO::save($orga);

   $user=new User();
   $user->setFirstname('DOE');
   $user->setOrganization($orga);
   DAO::save($user);
   DAO::commit();
}catch (\Exception $e){
   DAO::rollBack();
}

In case of multiple databases defined in the configuration, transaction-related methods can take the database offset defined in parameter.

DAO::beginTransaction('db-messagerie');
//some DAO operations on messagerie models
DAO::commit('db-messagerie');

Implicit transactions

Some DAO methods implicitly use transactions to group together insert, update or delete operations.

$users=DAO::getAll(User::class);
foreach ($users as $user){
    $user->setSuspended(true);
    DAO::toUpdate($user);
}
DAO::updateGroups();//Perform updates in a transaction

SDAO class

The SDAO class accelerates CRUD operations for the business classes without relationships.

Models must in this case declare public members only, and not respect the usual encapsulation.

app/models/Product.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 namespace models;
 class Product{
   /**
    * @id
    */
   public $id;

   public $name;

   ...
 }

The SDAO class inherits from DAO and has the same methods for performing CRUD operations.

use Ubiquity\orm\DAO;

$product=DAO::getById(Product::class, 5);

Prepared DAO queries

Preparing certain requests can improve performance with Swoole, Workerman or Roadrunner servers.
This preparation initializes the objects that will then be used to execute the query.
This initialization is done at server startup, or at the startup of each worker, if such an event exists.

Swoole sample

Preparation
app/config/swooleServices.php
$swooleServer->on('workerStart', function ($srv) use (&$config) {
   \Ubiquity\orm\DAO::startDatabase($config);
   \Ubiquity\orm\DAO::prepareGetById('user', User::class);
   \Ubiquity\orm\DAO::prepareGetAll('productsByName', Product::class,'name like ?');
});
Usage
app/controllers/UsersController.php
public function displayUser($idUser){
   $user=DAO::executePrepared('user',[1]);
   echo $user->getName();
}

public function displayProducts($name){
   $products=DAO::executePrepared('productsByName',[$name]);
   ...
}

Request

Note

For all Http features, Ubiquity uses technical classes containing static methods. This is a design choice to avoid dependency injection that would degrade performances.

The URequest class provides additional functionality to more easily manipulate native $_POST and $_GET php arrays.

Retrieving data

From the get method

The get method returns the null value if the key name does not exist in the get variables.

use Ubiquity\utils\http\URequest;

$name=URequest::get("name");

The get method can be called with the optional second parameter returning a value if the key does not exist in the get variables.

$name=URequest::get("name",1);

From the post method

The post method returns the null value if the key name does not exist in the post variables.

use Ubiquity\utils\http\URequest;

$name=URequest::post("name");

The post method can be called with the optional second parameter returning a value if the key does not exist in the post variables.

$name=URequest::post("name",1);

The getPost method applies a callback to the elements of the $_POST array and return them (default callback : htmlEntities) :

$protectedValues=URequest::getPost();

Retrieving and assigning multiple data

It is common to assign the values of an associative array to the members of an object.
This is the case for example when validating an object modification form.

The setValuesToObject method performs this operation :

Consider a User class:

class User {
     private $id;
     private $firstname;
     private $lastname;

     public function setId($id){
             $this->id=$id;
     }
     public function getId(){
             return $this->id;
     }

     public function setFirstname($firstname){
             $this->firstname=$firstname;
     }
     public function getFirstname(){
             return $this->firstname;
     }

     public function setLastname($lastname){
             $this->lastname=$lastname;
     }
     public function getLastname(){
             return $this->lastname;
     }
}

Consider a form to modify a user:

<form method="post" action="Users/update">
 <input type="hidden" name="id" value="{{user.id}}">
     <label for="firstname">Firstname:</label>
     <input type="text" id="firstname" name="firstname" value="{{user.firstname}}">
     <label for="lastname">Lastname:</label>
     <input type="text" id="lastname" name="lastname" value="{{user.lastname}}">
     <input type="submit" value="validate modifications">
</form>

The update action of the Users controller must update the user instance from POST values.
Using the setPostValuesToObject method avoids the assignment of variables posted one by one to the members of the object.
It is also possible to use setGetValuesToObject for the get method, or setValuesToObject to assign the values of any associative array to an object.

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 namespace controllers;

 use Ubiquity\orm\DAO;
 use Uniquity\utils\http\URequest;

 class Users extends BaseController{
     ...
     public function update(){
             $user=DAO::getOne("models\User",URequest::post("id"));
             URequest::setPostValuesToObject($user);
             DAO::update($user);
     }
 }

Note

SetValuesToObject methods use setters to modify the members of an object. The class concerned must therefore implement setters for all modifiable members.

Testing the request

isPost

The isPost method returns true if the request was submitted via the POST method:
In the case below, the initialize method only loads the vHeader.html view if the request is not an Ajax request.

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 namespace controllers;

 use Ubiquity\orm\DAO;
 use Ubiquity\utils\http\URequest;

 class Users extends BaseController{
     ...
     public function update(){
             if(URequest::isPost()){
                     $user=DAO::getOne("models\User",URequest::post("id"));
                     URequest::setPostValuesToObject($user);
                     DAO::update($user);
             }
     }
 }

isAjax

The isAjax method returns true if the query is an Ajax query:

app/controllers/Users.php
1
2
3
4
5
6
7
 ...
     public function initialize(){
             if(!URequest::isAjax()){
                     $this->loadView("main/vHeader.html");
             }
     }
     ...

isCrossSite

The isCrossSite method verifies that the query is not cross-site.

Response

Note

For all Http features, Ubiquity uses technical classes containing static methods. This is a design choice to avoid dependency injection that would degrade performances.

The UResponse class handles only the headers, not the response body, which is conventionally provided by the content displayed by the calls used to output data (echo, print …).

The UResponse class provides additional functionality to more easily manipulate response headers.

Adding or modifying headers

use Ubiquity\utils\http\UResponse;
$animal='camel';
UResponse::header('Animal',$animal);

Forcing multiple header of the same type:

UResponse::header('Animal','monkey',false);

Forces the HTTP response code to the specified value:

UResponse::header('Messages',$message,false,500);

Defining specific headers

content-type

Setting the response content-type to application/json:

UResponse::asJSON();

Setting the response content-type to text/html:

UResponse::asHtml();

Setting the response content-type to plain/text:

UResponse::asText();

Setting the response content-type to application/xml:

UResponse::asXml();

Defining specific encoding (default value is always utf-8):

UResponse::asHtml('iso-8859-1');

Cache

Forcing the disabling of the browser cache:

UResponse::noCache();

Accept

Define which content types, expressed as MIME types, the client is able to understand.
See Accept default values

UResponse::setAccept('text/html');

CORS responses headers

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let your web application running at one origin (domain) have permission to access selected resources from a server at a different origin.

Access-Control-Allow-Origin

Setting allowed origin:

UResponse::setAccessControlOrigin('http://myDomain/');

Access-Control-Allow-methods

Defining allowed methods:

UResponse::setAccessControlMethods('GET, POST, PUT, DELETE, PATCH, OPTIONS');

Access-Control-Allow-headers

Defining allowed headers:

UResponse::setAccessControlHeaders('X-Requested-With, Content-Type, Accept, Origin, Authorization');

Global CORS activation

enabling CORS for a domain with default values:

  • allowed methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
  • allowed headers: X-Requested-With, Content-Type, Accept, Origin, Authorization
UResponse::enableCors('http://myDomain/');

Testing response headers

Checking if headers have been sent:

if(!UResponse::isSent()){
     //do something if headers are not send
}

Testing if response content-type is application/json:

Important

This method only works if you used the UResponse class to set the headers.

if(UResponse::isJSON()){
     //do something if response is a JSON response
}

Session

Note

For all Http features, Ubiquity uses technical classes containing static methods. This is a design choice to avoid dependency injection that would degrade performances.

The USession class provides additional functionality to more easily manipulate native $_SESSION php array.

Starting the session

The Http session is started automatically if the sessionName key is populated in the app/config.php configuration file:

<?php
return array(
             ...
             "sessionName"=>"key-for-app",
             ...
 );

If the sessionName key is not populated, it is necessary to start the session explicitly to use it:

use Ubiquity\utils\http\USession;
...
USession::start("key-for-app");

Note

The name parameter is optional but recommended to avoid conflicting variables.

Creating or editing a session variable

use Ubiquity\utils\http\USession;

USession::set("name","SMITH");
USession::set("activeUser",$user);

Retrieving data

The get method returns the null value if the key name does not exist in the session variables.

use Ubiquity\utils\http\USession;

$name=USession::get("name");

The get method can be called with the optional second parameter returning a value if the key does not exist in the session variables.

$name=USession::get("page",1);

Note

The session method is an alias of the get method.

The getAll method returns all session vars:

$sessionVars=USession::getAll();

Testing

The exists method tests the existence of a variable in session.

if(USession::exists("name")){
     //do something when name key exists in session
}

The isStarted method checks the session start

if(USession::isStarted()){
     //do something if the session is started
}

Deleting variables

The delete method remove a session variable:

USession::delete("name");

Explicit closing of the session

The terminate method closes the session correctly and deletes all session variables created:

USession::terminate();

Views

Ubiquity uses Twig as the default template engine (see Twig documentation).
The views are located in the app/views folder. They must have the .html extension for being interpreted by Twig.

Ubiquity can also be used with a PHP view system, to get better performance, or simply to allow the use of php in the views.

Loading

Views are loaded from controllers:

app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{
     ...
     public function index(){
                     $this->loadView("index.html");
             }
     }
 }

Default view loading

If you use the default view naming method :
The default view associated to an action in a controller is located in views/controller-name/action-name folder:

views
     │
     └ Users
         └ info.html
app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{
     ...
     public function info(){
                     $this->loadDefaultView();
             }
     }
 }

Loading and passing variables

Variables are passed to the view with an associative array. Each key creates a variable of the same name in the view.

app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{
     ...
     public function display($message,$type){
                     $this->loadView("users/display.html",["message"=>$message,"type"=>$type]);
             }
     }
 }

In this case, it is usefull to call Compact for creating an array containing variables and their values :

app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{
     ...
     public function display($message,$type){
                     $this->loadView("users/display.html",compact("message","type"));
             }
     }
 }

Displaying in view

The view can then display the variables:

users/display.html
 <h2>{{type}}</h2>
 <div>{{message}}</div>

Variables may have attributes or elements you can access, too.

You can use a dot (.) to access attributes of a variable (methods or properties of a PHP object, or items of a PHP array), or the so-called “subscript” syntax ([]):

{{ foo.bar }}
{{ foo['bar'] }}

Ubiquity extra functions

Global app variable provides access to predefined Ubiquity Twig features:

  • app is an instance of Framework and provides access to public methods of this class.

Get framework installed version:

{{ app.version() }}

Return the active controller and action names:

{{ app.getController() }}
{{ app.getAction() }}

Return global wrapper classes :

For request:

{{ app.getRequest().isAjax() }}

For session :

{{ app.getSession().get('homePage','index') }}

see Framework class in API for more.

PHP view loading

Disable if necessary Twig in the configuration file by deleting the templateEngine key.

Then create a controller that inherits from SimpleViewController, or SimpleViewAsyncController if you use Swoole or Workerman:

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 namespace controllers;

 use Ubiquity\controllers\SimpleViewController;

 class Users extends SimpleViewController{
     ...
     public function display($message,$type){
                     $this->loadView("users/display.php",compact("message","type"));
             }
     }
 }

Note

In this case, the functions for loading assets and themes are not supported.

Assets

Assets correspond to javascript files, style sheets, fonts, images to include in your application.
They are located from the public/assets folder.
It is preferable to separate resources into sub-folders by type.

public/assets
     ├ css
     │   ├ style.css
     │   └ semantic.min.css
     └ js
         └ jquery.min.js

Integration of css or js files :

{{ css('css/style.css') }}
{{ css('css/semantic.min.css') }}

{{ js('js/jquery.min.js') }}
{{ css('https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css') }}

{{ js('https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js') }}

CDN with extra parameters:

{{ css('https://cdn.jsdelivr.net/npm/foundation-sites@6.5.3/dist/css/foundation.min.css',{crossorigin: 'anonymous',integrity: 'sha256-/PFxCnsMh+...'}) }}

Themes

Note

The themes are totally useless if you only have one presentation to apply.

Ubiquity support themes wich can have it’s own assets and views according to theme template to be rendered by controller. Each controller action can render a specific theme, or they can use the default theme configured at config.php file in templateEngineOptions => array("activeTheme" => "semantic").

Ubiquity is shipped with 3 default themes : Bootstrap, Foundation and Semantic-UI.

Installing a theme

With devtools, run :

Ubiquity install-theme bootstrap

The installed theme is one of bootstrap, foundation or semantic.

With webtools, you can do the same, provided that the devtools are installed and accessible (Ubiquity folder added in the system path) :

_images/themesManager-install-theme.png

Creating a new theme

With devtools, run :

Ubiquity create-theme myTheme

Creating a new theme from Bootstrap, Semantic…

With devtools, run :

Ubiquity create-theme myBootstrap -x=bootstrap

With webtools :

_images/themesManager-create-theme.png

Theme functioning and structure

Structure

Theme view folder

The views of a theme are located from the app/views/themes/theme-name folder

app/views
        └ themes
               ├ bootstrap
               │         └ main
               │              ├ vHeader.html
               │              └ vFooter.html
               └ semantic
                        └ main
                             ├ vHeader.html
                             └ vFooter.html

The controller base class is responsible for loading views to define the header and footer of each page :

app/controllers/ControllerBase.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     <?php
     namespace controllers;

     use Ubiquity\controllers\Controller;
     use Ubiquity\utils\http\URequest;

     /**
      * ControllerBase.
      **/
     abstract class ControllerBase extends Controller{
             protected $headerView = "@activeTheme/main/vHeader.html";
             protected $footerView = "@activeTheme/main/vFooter.html";

             public function initialize() {
                     if (! URequest::isAjax ()) {
                             $this->loadView ( $this->headerView );
                     }
             }
             public function finalize() {
                     if (! URequest::isAjax ()) {
                             $this->loadView ( $this->footerView );
                     }
             }
     }

Theme assets folder

The assets of a theme are created inside public/assets/theme-name folder.

The structure of the assets folder is often as follows :

public/assets/bootstrap
                                ├ css
                                │   ├ style.css
                                │   └ all.min.css
                                ├ scss
                                │   ├ myVariables.scss
                                │   └ app.scss
                                ├ webfonts
                                │
                                └ img

Change of the active theme

Persistent change

activeTheme is defined in app/config/config.php with templateEngineOptions => array("activeTheme" => "semantic")

The active theme can be changed with devtools :

Ubiquity config:set --templateEngineOptions.activeTheme=bootstrap

It can also be done from the home page, or with webtools :

From the home page :

_images/change-theme-home.png

From the webtools :

_images/change-theme-webtools.png

This change can also be made at runtime :

From a controller :

ThemeManager::saveActiveTheme('bootstrap');

Non-persistent local change

To set a specific theme for all actions within a controller, the simplest method is to override the controller’s initialize method :

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 namespace controllers;

 use \Ubiquity\themes\ThemesManager;

 class Users extends BaseController{

         public function initialize(){
             parent::intialize();
             ThemesManager::setActiveTheme('bootstrap');
         }
     }

Or if the change should only concern one action :

app/controllers/Users.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 namespace controllers;

 use \Ubiquity\themes\ThemesManager;

 class Users extends BaseController{

         public function doStuff(){
             ThemesManager::setActiveTheme('bootstrap');
             ...
         }
     }

Conditional theme change, regardless of the controller :

Example with a modification of the theme according to a variable passed in the URL

app/config/services.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use Ubiquity\themes\ThemesManager;
use Ubiquity\utils\http\URequest;

...

ThemesManager::onBeforeRender(function(){
             if(URequest::get("th")=='bootstrap'){
                     ThemesManager::setActiveTheme("bootstrap");
             }
     });

Mobile device support

Add a mobile device detection tool.
Installing MobileDetect:

composer require mobiledetect/mobiledetectlib

It is generally easier to create different views per device.

Create a specific theme for the mobile part (by creating a folder views/themes/mobile and putting the views specific to mobile devices in it).
It is important in this case to use the same file names for the mobile and non-mobile part.

It is also advisable in this case that all view loadings use the @activeTheme namespace:

$this->loadView("@activeTheme/index.html");

index.html must be available in this case in the folders views and views/themes/mobile.

Global mobile detection (from services.php)
app/config/services.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use Ubiquity\themes\ThemesManager;

...

ThemesManager::onBeforeRender(function () {
     $mb = new \Mobile_Detect();
     if ($mb->isMobile()) {
             ThemesManager::setActiveTheme('mobile');
     }
});
Locale detection (from a controller)
app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use Ubiquity\themes\ThemesManager;

...

     public function initialize() {
             $mb = new \Mobile_Detect();
             if ($mb->isMobile()) {
                     ThemesManager::setActiveTheme('mobile');
             }
             parent::initialize();
     }

View and assets loading

Views

For loading a view from the activeTheme folder, you can use the @activeTheme namespace :

app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{

         public function action(){
             $this->loadView('@activeTheme/action.html');
             ...
         }
     }

If the activeTheme is bootstrap, the loaded view is app/views/themes/bootstrap/action.html.

DefaultView

If you follow the Ubiquity view naming model, the default view loaded for an action in a controller when a theme is active is : app/views/themes/theme-name/controller-name/action-name.html.

For example, if the activeTheme is bootstrap, the default view for the action display in the Users controller must be loacated in app/views/themes/bootstrap/Users/display.html.

app/controllers/Users.php
1
2
3
4
5
6
7
8
9
 namespace controllers;

 class Users extends BaseController{

         public function display(){
             $this->loadDefaultView();
             ...
         }
     }

Note

The devtools commands to create a controller or an action and their associated view use the @activeTheme folder if a theme is active.

Ubiquity controller Users -v

Ubiquity action Users.display -v

Assets loading

The mechanism is the same as for the views : @activeTheme namespace refers to the public/assets/theme-name/ folder

{{ css('@activeTheme/css/style.css') }}

{{ js('@activeTheme/js/scripts.js') }}

{{ img('@activeTheme/img/image-name.png', {alt: 'Image Alt Name', class: 'css-class'}) }}

If the bootstrap theme is active,
the assets folder is public/assets/bootstrap/.

Css compilation

For Bootstrap or foundation, install sass:

npm install -g sass

Then run from the project root folder:

For bootstrap:

ssass public/assets/bootstrap/scss/app.scss public/assets/bootstrap/css/style.css --load-path=vendor

For foundation:

ssass public/assets/foundation/scss/app.scss public/assets/foundation/css/style.css --load-path=vendor

jQuery Semantic-UI

By default, Ubiquity uses the phpMv-UI library for the client-rich part.
PhpMv-UI allows to create components based on Semantic-UI or Bootstrap and to generate jQuery scripts in PHP.

This library is used for the webtools administration interface.

Integration

By default, a $jquery variable is injected in controllers at runtime.

This operation is done using dependency injection, in app/config.php:

app/config.php
...
"di"=>array(
             "@exec"=>array(
                             "jquery"=>function ($controller){
                                     return \Ajax\php\ubiquity\JsUtils::diSemantic($controller);
                                     }
                             )
             )
...

So there’s nothing to do,
but to facilitate its use and allow code completion in a controller, it is recommended to add the following code documentation:

app/controllers/FooController.php
 /**
 * Controller FooController
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 **/
class FooController extends ControllerBase{

     public function index(){}
}

jQuery

Href to ajax requests

Create a new Controller and its associated view, then define the folowing routes:

app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace controllers;

class FooController extends ControllerBase {

     public function index() {
             $this->loadview("FooController/index.html");
     }

     /**
      *
      *@get("a","name"=>"action.a")
      */
     public function aAction() {
             echo "a";
     }

     /**
      *
      *@get("b","name"=>"action.b")
      */
     public function bAction() {
             echo "b";
     }
}

The associated view:

app/views/FooController/index.html
     <a href="{{path('action.a')}}">Action a</a>
     <a href="{{path('action.b')}}">Action b</a>

Initialize router cache:

Ubiquity init:cache -t=controllers

Test this page in your browser at http://127.0.0.1:8090/FooController.

Transformation of requests into Ajax requests

The result of each ajax request should be displayed in an area of the page defined by its jQuery selector (.result span)

app/controllers/FooController.php
namespace controllers;

/**
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 */
class FooController extends ControllerBase {

     public function index() {
             $this->jquery->getHref('a','.result span');
             $this->jquery->renderView("FooController/index.html");
     }
     ...
}
app/views/FooController/index.html
     <a href="{{path('action.a')}}">Action a</a>
     <a href="{{path('action.b')}}">Action b</a>
<div class='result'>
     Selected action:
     <span>No One</span>
</div>
{{ script_foot | raw }}

Note

The script_foot variable contains the generated jquery script produced by the renderView method. The raw filter marks the value as being “safe”, which means that in an environment with automatic escaping enabled this variable will not be escaped.

Let’s add a little css to make it more professional:

app/views/FooController/index.html
<div class="ui buttons">
     <a class="ui button" href="{{path('action.a')}}">Action a</a>
     <a class="ui button" href="{{path('action.b')}}">Action b</a>
</div>
<div class='ui segment result'>
     Selected action:
     <span class="ui label">No One</span>
</div>
{{ script_foot | raw }}

If we want to add a new link whose result should be displayed in another area, it is possible to specify it via the data-target attribute

The new action:

app/controllers/FooController.php
namespace controllers;

class FooController extends ControllerBase {
     ...
     /**
      *@get("c","name"=>"action.c")
      */
     public function cAction() {
             echo \rand(0, 1000);
     }
}

The associated view:

app/views/FooController/index.html
<div class="ui buttons">
     <a class="ui button" href="{{path('action.a')}}">Action a</a>
     <a class="ui button" href="{{path('action.b')}}">Action b</a>
     <a class="ui button" href="{{path('action.c')}}" data-target=".result p">Action c</a>
</div>
<div class='ui segment result'>
     Selected action:
     <span class="ui label">No One</span>
     <p></p>
</div>
{{ script_foot | raw }}
_images/fooController.png
Definition of the ajax request attributes:

In the folowing example, the parameters passed to the attributes variable of the getHref method:

  • remove the history of the navigation,
  • make the ajax loader internal to the clicked button.
app/controllers/FooController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
namespace controllers;

/**
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 */
class FooController extends ControllerBase {

     public function index() {
             $this->jquery->getHref('a','.result span', [
                     'hasLoader' => 'internal',
                     'historize' => false
             ]);
             $this->jquery->renderView("FooController/index.html");
     }
     ...
}

Note

It is possible to use the postHref method to use the POST http method.

Classical ajax requests

For this example, create the following database:

CREATE DATABASE `uguide` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `uguide`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `firstname` varchar(30) NOT NULL,
  `lastname` varchar(30) NOT NULL,
  `password` varchar(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` (`id`, `firstname`, `lastname`) VALUES
(1, 'You', 'Evan'),
(2, 'Potencier', 'Fabien'),
(3, 'Otwell', 'Taylor');

ALTER TABLE `user` ADD PRIMARY KEY (`id`);
ALTER TABLE `user`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;

Connect the application to the database, and generate the User class:

With devtools:

Ubiquity config:set --database.dbName=uguide
Ubiquity all-models

Create a new Controller UsersJqueryController

Ubiquity controller UsersJqueryController -v

Create the folowing actions in UsersJqueryController:

_images/UsersJqueryControllerStructure.png
Index action

The index action must display a button to obtain the list of users, loaded via an ajax request:

app/controllers/UsersJqueryController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace controllers;

/**
 * Controller UsersJqueryController
 *
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 * @route("users")
 */
class UsersJqueryController extends ControllerBase {

     /**
      *
      * {@inheritdoc}
      * @see \Ubiquity\controllers\Controller::index()
      * @get
      */
     public function index() {
             $this->jquery->getOnClick('#users-bt', Router::path('display.users'), '#users', [
                     'hasLoader' => 'internal'
             ]);
             $this->jquery->renderDefaultView();
     }
}

The default view associated to index action:

app/views/UsersJqueryController/index.html
<div class="ui container">
     <div id="users-bt" class="ui button">
             <i class="ui users icon"></i>
             Display <b>users</b>
     </div>
     <p></p>
     <div id="users">
     </div>
</div>
{{ script_foot | raw }}
displayUsers action

All users are displayed, and a click on a user must display the user details via a posted ajax request:

app/controllers/UsersJqueryController.php
 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
namespace controllers;

/**
 * Controller UsersJqueryController
 *
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 * @route("users")
 */
class UsersJqueryController extends ControllerBase {
...
     /**
      *
      * @get("all","name"=>"display.users","cache"=>true)
      */
     public function displayUsers() {
             $users = DAO::getAll(User::class);
             $this->jquery->click('#close-bt', '$("#users").html("");');
             $this->jquery->postOnClick('li[data-ajax]', Router::path('display.one.user', [
                     ""
             ]), '{}', '#user-detail', [
                     'attr' => 'data-ajax',
                     'hasLoader' => false
             ]);
             $this->jquery->renderDefaultView([
                     'users' => $users
             ]);
     }

The view associated to displayUsers action:

app/views/UsersJqueryController/displayUsers.html
<div class="ui top attached header">
     <i class="users circular icon"></i>
     <div class="content">Users</div>
</div>
<div class="ui attached segment">
     <ul id='users-content'>
     {% for user in users %}
             <li data-ajax="{{user.id}}">{{user.firstname }} {{user.lastname}}</li>
     {% endfor %}
     </ul>
     <div id='user-detail'></div>
</div>
<div class="ui bottom attached inverted segment">
<div id="close-bt" class="ui inverted button">Close</div>
</div>
{{ script_foot | raw }}
displayOneUser action
app/controllers/UsersJqueryController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace controllers;

/**
 * Controller UsersJqueryController
 *
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 * @route("users")
 */
class UsersJqueryController extends ControllerBase {
...
     /**
      *
      * @post("{userId}","name"=>"display.one.user","cache"=>true,"duration"=>3600)
      */
     public function displayOneUser($userId) {
             $user = DAO::getById(User::class, $userId);
             $this->jquery->hide('#users-content', '', '', true);
             $this->jquery->click('#close-user-bt', '$("#user-detail").html("");$("#users-content").show();');
             $this->jquery->renderDefaultView([
                     'user' => $user
             ]);
     }

The view associated to displayOneUser action:

app/views/UsersJqueryController/displayUsers.html
<div class="ui label">
     <i class="ui user icon"></i>
     Id
     <div class="detail">{{user.id}}</div>
</div>
<div class="ui label">
     Firstname
     <div class="detail">{{user.firstname}}</div>
</div>
<div class="ui label">
     Lastname
     <div class="detail">{{user.lastname}}</div>
</div>
<p></p>
<div id="close-user-bt" class="ui black button">
     <i class="ui users icon"></i>
     Return to users
</div>
{{ script_foot | raw }}

Semantic components

Next, we are going to make a controller implementing the same functionalities as before, but using PhpMv-UI components (Semantic part).

HtmlButton sample

Create a new Controller UsersJqueryController

Ubiquity controller UsersCompoController -v
app/controllers/UsersJqueryController.php
 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
namespace controllers;

use Ubiquity\controllers\Router;

/**
 * Controller UsersCompoController
 *
 * @property \Ajax\php\ubiquity\JsUtils $jquery
 * @route("users-compo")
 */
class UsersCompoController extends ControllerBase {

     private function semantic() {
             return $this->jquery->semantic();
     }

     /**
      *
      * @get
      */
     public function index() {
             $bt = $this->semantic()->htmlButton('users-bt', 'Display users');
             $bt->addIcon('users');
             $bt->getOnClick(Router::path('display.compo.users'), '#users', [
                     'hasLoader' => 'internal'
             ]);
             $this->jquery->renderDefaultView();
     }

Note

Calling renderView or renderDefaultView on the JQuery object performs the compilation of the component, and generates the corresponding HTML and JS.

The associated view integrates the button component with the q array available in the view :

app/views/UsersCompoController/index.html
<div class="ui container">
     {{ q['users-bt'] | raw }}
     <p></p>
     <div id="users">
     </div>
</div>
{{ script_foot | raw }}

//todo DataTable sample +++++++++++++++++

Normalizers

Note

The Normalizer module uses the static class NormalizersManager to manage normalization.

Validators

Note

The Validators module uses the static class ValidatorsManager to manage validation.

Validators are used to check that the member datas of an object complies with certain constraints.

Adding validators

Either the Author class that we want to use in our application :

app/models/Author.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
     namespace models;

     class Author {
             /**
              * @var string
              * @validator("notEmpty")
              */
             private $name;

             public function getName(){
                     return $this->name;
             }

             public function setName($name){
                     $this->name=$name;
             }
     }

We added a validation constraint on the name member with the @validator annotation, so that it is not empty.

Generating cache

Run this command in console mode to create the cache data of the Author class :

Ubiquity init-cache -t=models

Validator cache is generated in app/cache/contents/validators/models/Author.cache.php.

Validating instances

an instance

public function testValidateAuthor(){
        $author=new Author();
        //Do something with $author
        $violations=ValidatorsManager::validate($author);
        if(sizeof($violations)>0){
                echo implode('<br>', ValidatorsManager::validate($author));
        }else{
                echo 'The author is valid!';
        }
}

if the name of the author is empty, this action should display:

name : This value should not be empty

The validate method returns an array of ConstraintViolation instances.

multiple instances

public function testValidateAuthors(){
        $authors=DAO::getAll(Author::class);
        $violations=ValidatorsManager::validateInstances($author);
        foreach($violations as $violation){
                echo $violation.'<br>';
        }
}

Models generation with default validators

When classes are automatically generated from the database, default validators are associated with members, based on the fields’ metadatas.

Ubiquity create-model User
app/models/Author.php
 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
     namespace models;
     class User{
             /**
              * @id
              * @column("name"=>"id","nullable"=>false,"dbType"=>"int(11)")
              * @validator("id","constraints"=>array("autoinc"=>true))
             **/
             private $id;

             /**
              * @column("name"=>"firstname","nullable"=>false,"dbType"=>"varchar(65)")
              * @validator("length","constraints"=>array("max"=>65,"notNull"=>true))
             **/
             private $firstname;

             /**
              * @column("name"=>"lastname","nullable"=>false,"dbType"=>"varchar(65)")
              * @validator("length","constraints"=>array("max"=>65,"notNull"=>true))
             **/
             private $lastname;

             /**
              * @column("name"=>"email","nullable"=>false,"dbType"=>"varchar(255)")
              * @validator("email","constraints"=>array("notNull"=>true))
              * @validator("length","constraints"=>array("max"=>255))
             **/
             private $email;

             /**
              * @column("name"=>"password","nullable"=>true,"dbType"=>"varchar(255)")
              * @validator("length","constraints"=>array("max"=>255))
             **/
             private $password;

             /**
              * @column("name"=>"suspended","nullable"=>true,"dbType"=>"tinyint(1)")
              * @validator("isBool")
             **/
             private $suspended;
     }

These validators can then be modified.
Modifications must always be folowed by a re-initialization of the model cache.

Ubiquity init-cache -t=models

Models validation informations can be displayed with devtools :

Ubiquity info:validation -m=User
_images/info-validation-devtools.png

Gets validators on email field:

Ubiquity info:validation email -m=User
_images/info-validation-email-devtools.png

Validation informations are also accessible from the models part of the webtools:

_images/info-validation-webtools.png

Validator types

Basic

Validator Roles Constraints Accepted values
isBool Check if value is a boolean   true,false,0,1
isEmpty Check if value is empty   ‘’,null
isFalse Check if value is false   false,’false’,0,’0’
isNull Check if value is null   null
isTrue Check if value is true   true,’true’,1,’1’
notEmpty Check if value is not empty   !null && !’’
notNull Check if value is not null   !null
type Check if value is of type {type} {type}  

Comparison

Dates

Multiples

Strings

Transformers

Note

The Transformers module uses the static class TransformersManager to manage data transformations.

Transformers are used to transform datas after loading from the database, or before displaying in a view.

Adding transformers

Either the Author class that we want to use in our application :

app/models/Author.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

use Ubiquity\attributes\items\Transformer;

class Author {

   #[Transformer('upper')]
   private $name;

   public function getName(){
      return $this->name;
   }

   public function setName($name){
      $this->name=$name;
   }
}
app/models/Author.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

class Author {
   /**
    * @var string
    * @transformer("upper")
    */
   private $name;

   public function getName(){
      return $this->name;
   }

   public function setName($name){
      $this->name=$name;
   }
}

We added a transformer on the name member with the @transformer annotation, in order to capitalize the name in the views.

Generating cache

Run this command in console mode to create the cache data of the Author class :

Ubiquity init-cache -t=models

transformer cache is generated with model metadatas in app/cache/models/Author.cache.php.

Transformers informations can be displayed with devtools :

Ubiquity info:model -m=Author -f=#transformers
_images/trans-info.png

Using transformers

Start the TransformersManager in the file app/config/services.php:

app/config/services.php
\Ubiquity\contents\transformation\TransformersManager::startProd();

You can test the result in the administration interface:

_images/trans-upper.png

or by creating a controller:

app/controllers/Authors.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace controllers;

class Authors {

   public function index(){
      DAO::transformersOp='toView';
      $authors=DAO::getAll(Author::class);
      $this->loadDefaultView(['authors'=>$authors]);
   }

}
app/views/Authors/index.html
<ul>
   {% for author in authors %}
      <li>{{ author.name }}</li>
   {% endfor %}
</ul>

Transformer types

transform

The transform type is based on the TransformerInterface interface. It is used when the transformed data must be converted into an object.
The DateTime transformer is a good example of such a transformer:

  • When loading the data, the Transformer converts the date from the database into an instance of php DateTime.
  • Its reverse method performs the reverse operation (php date to database compatible date).

toView

The toView type is based on the TransformerViewInterface interface. It is used when the transformed data must be displayed in a view.

toForm

The toForm type is based on the TransformerFormInterface interface. It is used when the transformed data must be used in a form.

Transformers usage

Transform on data loading

If ommited, default transformerOp is transform

$authors=DAO::getAll(Author::class);

Set transformerOp to toView

DAO::transformersOp='toView';
$authors=DAO::getAll(Author::class);

Transform after loading

Return the transformed member value:

TransformersManager::transform($author, 'name','toView');

Return a transformed value:

TransformersManager::applyTransformer($author, 'name','john doe','toView');

Transform an instance by applying all defined transformers:

TransformersManager::transformInstance($author,'toView');

Existing transformers

Transformer Type(s) Description
datetime transform, toView, toForm Transform a database datetime to a php DateTime object
upper toView Make the member value uppercase
lower toView Make the member value lowercase
firstUpper toView Make the member value first character uppercase
password toView Mask the member characters
md5 toView Hash the value with md5

Create your own

Creation

Create a transformer to display a user name as a local email address:

app/transformers/toLocalEmail.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
namespace transformers;
use Ubiquity\contents\transformation\TransformerViewInterface;

class ToLocalEmail implements TransformerViewInterface{

   public static function toView($value) {
      if($value!=null) {
         return sprintf('%s@mydomain.local',strtolower($value));
      }
   }

}

Registration

Register the transformer by executing the following script:

TransformersManager::registerClassAndSave('localEmail',\transformers\ToLocalEmail::class);

Usage

app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

use Ubiquity\attributes\items\Transformer;

class User {

   #[Transformer('localEmail')]
   private $name;

   public function getName(){
      return $this->name;
   }

   public function setName($name){
      $this->name=$name;
   }
}
app/models/User.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace models;

class User {
   /**
    * @var string
    * @transformer("localEmail")
    */
   private $name;

   public function getName(){
      return $this->name;
   }

   public function setName($name){
      $this->name=$name;
   }
}
DAO::transformersOp='toView';
$user=DAO::getOne(User::class,"name='Smith'");
echo $user->getName();

Smith user name will be displayed as smith@mydomain.local.

Translation module

Note

The Translation module uses the static class TranslatorManager to manage translations.

Module structure

Translations are grouped by domain, within a locale :

In the translation root directory (default app/translations):

  • Each locale corresponds to a subfolder.
  • For each locale, in a subfolder, a domain corresponds to a php file.
translations
     ├ en_EN
     │     ├ messages.php
     │     └ blog.php
     └ fr_FR
           ├ messages.php
           └ blog.php
  • each domain file contains an associative array of translations key-> translation value
  • Each key can be associated with
    • a translation
    • a translation containing variables (between % and %)
    • an array of translations for handle pluralization
app/translations/en_EN/messages.php
return [
     'okayBtn'=>'Okay',
     'cancelBtn'=>'Cancel',
     'deleteMessage'=>['No message to delete!','1 message to delete.','%count% messages to delete.']
];

Starting the module

Module startup is logically done in the services.php file.

app/config/services.php
1
2
Ubiquity\cache\CacheManager::startProd($config);
Ubiquity\translation\TranslatorManager::start();

With no parameters, the call of the start method uses the locale en_EN, without fallbacklocale.

Important

The translations module must be started after the cache has started.

Setting the locale

Changing the locale when the manager starts:

app/config/services.php
1
2
Ubiquity\cache\CacheManager::startProd($config);
Ubiquity\translation\TranslatorManager::start('fr_FR');

Changing the locale after loading the manager:

TranslatorManager::setLocale('fr_FR');

Setting the fallbackLocale

The en_EN locale will be used if fr_FR is not found:

app/config/services.php
1
2
Ubiquity\cache\CacheManager::startProd($config);
Ubiquity\translation\TranslatorManager::start('fr_FR','en_EN');

Defining the root translations dir

If the rootDir parameter is missing, the default directory used is app/translations.

app/config/services.php
1
2
Ubiquity\cache\CacheManager::startProd($config);
Ubiquity\translation\TranslatorManager::start('fr_FR','en_EN','myTranslations');

Make a translation

With php

Translation of the okayBtn key into the default locale (specified when starting the manager):

$okBtnCaption=TranslatorManager::trans('okayBtn');

With no parameters, the call of the trans method uses the default locale, the domain messages.

Translation of the message key using a variable:

$okBtnCaption=TranslatorManager::trans('message',['user'=>$user]);

In this case, the translation file must contain a reference to the user variable for the key message:

app/translations/en_EN/messages.php
['message'=>'Hello %user%!',...];

In twig views:

Translation of the okayBtn key into the default locale (specified when starting the manager):

{{ t('okayBtn') }}

Translation of the message key using a variable:

{{ t('message',parameters) }}

Security

Guiding principles

Forms validation

Client-side validation

It is preferable to perform an initial client-side validation to avoid submitting invalid data to the server.

Example of the creation of a form in the action of a controller (this part could be located in a dedicated service for a better separation of layers):

app/controllers/UsersManagement.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 public function index(){
     $frm=$this->jquery->semantic()->dataForm('frm-user',new User());
     $frm->setFields(['login','password','connection']);
     $frm->fieldAsInput('login',
         ['rules'=>'empty']
     );
     $frm->fieldAsInput('password',
         [
             'inputType'=>'password',
             'rules'=>['empty','minLength[6]']
         ]
     );
     $frm->setValidationParams(['on'=>'blur','inline'=>true]);
     $frm->fieldAsSubmit('connection','fluid green','/submit','#response');
     $this->jquery->renderDefaultView();
 }

The Associated View:

app/views/UsersManagement/index.html
 {{ q['frm-user'] | raw }}
 {{ script_foot | raw }}
 <div id="response"></div>
_images/frm-user.png

Note

The CRUD controllers automatically integrate this client-side validation using the Validators attached to the members of the models.

#[Column(name: "password",nullable: true,dbType: "varchar(255)")]
#[Validator(type: "length",constraints: ["max"=>20,"min"=>6])]
#[Transformer(name: "password")]
private $password;
Server-side validation

It is preferable to restrict the URLs allowed to modify data.
Beforehand, by specifying the Http method in the routes, and by testing the request :

#[Post(path: "/submit")]
public function submitUser(){
   if(!URequest::isCrossSite() && URequest::isAjax()){
      $datas=URequest::getPost();//post with htmlEntities
      //Do something with $datas
   }
}

Note

The Ubiquity-security module offers additional control to avoid cross-site requests.

After modifying an object, it is possible to check its validity, given the validators attached to the members of the associated Model:

#[Post(path: "/submit")]
public function submitUser(){
   if(!URequest::isCrossSite()){
      $datas=URequest::getPost();//post with htmlEntities
      $user=new User();
      URequest::setValuesToObject($user,$datas);

      $violations=ValidatorsManager::validate($user);
      if(\count($violations)==0){
         //do something with this valid user
      } else {
         //Display violations...
      }
   }
}

DAO operations

It is always recommended to use parameterized queries, regardless of the operations performed on the data:
  • To avoid SQL injections.
  • To allow the use of prepared queries, speeding up processing.
$googleUsers=DAO::getAll(User::class,'email like ?',false,['%@gmail.com']);
$countActiveUsers=DAO::count(User::class,'active= ?',[true]);

Note

DAO operations that take objects as parameters use this mechanism by default.

DAO::save($user);

Passwords management

The Password Transformer allows a field to be of the password type when displayed in an automatically generated CRUD form.

#[Transformer(name: "password")]
private $password;

After submission from a form, it is possible to encrypt a password from the URequest class:

$encryptedPassword=URequest::password_hash('password');
$user->setPassword($encryptedPassword);
DAO::save($user);

The algorithm used in this case is defined by the php PASSWORD_DEFAULT.

It is also possible to check a password entered by a user in the same way, to compare it to a hash:

if(URequest::password_verify('password', $existingPasswordHash)){
   //password is ok
}

Important

Set up Https to avoid sending passwords in clear text.

Security module/ ACL management

In addition to these few rules, you can install if necessary:

Security module

Installation

Install the Ubiquity-security module from the command prompt or from the Webtools (Composer part).

composer require phpmv/ubiquity-security

Then activate the display of the Security part in the Webtools:

_images/display-security.png

Session CSRF

The session is by default protected against CSRF attacks via the VerifyCsrfToken class (even without the Ubiquity-security module).
A token instance (CSRFToken) is generated at the session startup. The validity of the token is then checked via a cookie at each request.

_images/security-part.png

This protection can be customized by creating a class implementing the VerifySessionCsrfInterface.

app/session/MyCsrfProtection.php
class MyCsrfProtection implements VerifySessionCsrfInterface {
   private AbstractSession $sessionInstance;

   public function __construct(AbstractSession $sessionInstance) {
      $this->sessionInstance = $sessionInstance;
   }

   public function init() {
      //TODO when the session starts
   }

   public function clear() {
      //TODO when the session ends
   }

   public function start() {
      //TODO When the session starts or is resumed
   }

   public static function getLevel() {
      return 1; //An integer to appreciate the level of security
   }
}

Starting the custom protection in services:

app/config/services.php
use Ubiquity\utils\http\session\PhpSession;
use Ubiquity\controllers\Startup;
use app\session\MyCsrfProtection;

Startup::setSessionInstance(new PhpSession(new MyCsrfProtection()));

Deactivating the protection

If you do not need to protect your session against Csrf attacks, start the session with the NoCsrfProtection class.

app/config/services.php
use Ubiquity\utils\http\session\PhpSession;
use Ubiquity\controllers\Startup;
use Ubiquity\utils\http\session\protection\NoCsrfProtection;

Startup::setSessionInstance(new PhpSession(new NoCsrfProtection()));

CSRF manager

The CsrfManager service can be started directly from the webtools interface.
Its role is to provide tools to protect sensitive routes from Csrf attacks (the ones that allow the validation of forms for example).

_images/csrf-manager-started.png
  • The service is started in the services.php file.
app/config/services.php
 \Ubiquity\security\csrf\CsrfManager::start();

Example of form protection:

The form view:

<form id="frm-bar" action='/submit' method='post'>
   {{ csrf('frm-bar') }}
   <input type='text' id='sensitiveData' name='sensitiveData'>
</form>

The csrf method generates a token for the form (By adding a hidden field in the form corresponding to the token.).

The form submitting in a controller:

use Ubiquity\security\csrf\UCsrfHttp;

#[Post('/submit')]
public function submit(){
   if(UCsrfHttp::isValidPost('frm-bar')){
      //Token is valid! => do something with post datas
   }
}

Note

It is also possible to manage this protection via cookie.

Example of protection with ajax:

The meta field csrf-token is generated on all pages.

app/controllers/BaseController.php
abstract class ControllerBase extends Controller{
   protected $headerView = "@activeTheme/main/vHeader.html";
   protected $footerView = "@activeTheme/main/vFooter.html";

   public function initialize() {
      if (! URequest::isAjax ()) {
         $meta=UCsrfHttp::getTokenMeta('postAjax');
         $this->loadView ( $this->headerView,['meta'=>$meta] );
      }
   }
}

This field is added in the headerView:

app/views/main/vHeader.html
{% block header %}
   <base href="{{config["siteUrl"]}}">
   <meta charset="UTF-8">
   <link rel="icon" href="data:;base64,iVBORw0KGgo=">
   {{meta | raw}}
   <title>Tests</title>
{% endblock %}

Example with a button posting data via ajax. The parameter csrf is set to true. So when the request is posted, the csrf-token is sent in the request headers.

#[Get(path: "/ajax")]
public function ajax(){
   $this->jquery->postOnClick('#bt','/postAjax','{id:55}','#myResponse',['csrf'=>true]);
   $this->jquery->renderDefaultView();
}

The submitting route can check the presence and validity of the token:

#[Post(path: "postAjax")]
public function postAjax(){
   if(UCsrfHttp::isValidMeta('postAjax')){
      var_dump($_POST);
   }else{
      echo 'invalid or absent meta csrf-token';
   }
}

Encryption manager

The EncryptionManager service can be started directly from the webtools interface.

  • In this case, a key is generated in the configuration file app/config/config.php.
  • The service is started in the services.php file.
app/config/services.php
 \Ubiquity\security\data\EncryptionManager::start($config);

Note

By default, encryption is performed in AES-128.

_images/encryption-manager-started.png

Changing the cipher:

Upgrade to AES-256:

app/config/services.php
\Ubiquity\security\data\EncryptionManager::startProd($config, Encryption::AES256);

Generate a new key:

Ubiquity new:key 256

The new key is generated in the app/config/config.php file.

Model data encryption

The Crypt transformer can also be used on the members of a model:

app/models/User.php
 class Foo{
     #[Transformer(name: "crypt")]
     private $secret;
     ...
 }

Usage:

$o=new Foo();
$o->setSecret('bar');
TransformersManager::transformInstance($o);// secret member is encrypted
Generic Data encryption

Strings encryption:

$encryptedBar=EncryptionManager::encryptString('bar');

To then decrypt it:

echo EncryptionManager::decryptString($encryptedBar);

It is possible to encrypt any type of data:

$encryptedUser=EncryptionManager::encrypt($user);

To then decrypt it, with possible serialisation/deserialisation if it is an object:

$user=EncryptionManager::decrypt($encryptedUser);

Content Security Policies manager

The ContentSecurityManager service can be started directly from the webtools interface.

  • The service is started in the services.php file.
app/config/services.php
\Ubiquity\security\csp\ContentSecurityManager::start(reportOnly: true,onNonce: function($name,$value){
     if($name==='jsUtils') {
             \Ubiquity\security\csp\ContentSecurityManager::defaultUbiquityDebug()->addNonce($value, \Ubiquity\security\csp\CspDirectives::SCRIPT_SRC)->addHeaderToResponse();
     }
});

Note

With this default configuration, a nonce is added to jquery scripts generated with phpmv-ui. CSP control is done in Report-only mode..

_images/csp-manager-started.png

Adding a nonce

Example of adding nonce on the header and footer pages:

Updating the base controller
app/controllers/ControllerBase.php
namespace controllers;

use Ubiquity\controllers\Controller;
use Ubiquity\security\csp\ContentSecurityManager;
use Ubiquity\utils\http\URequest;

/**
 * controllers$ControllerBase
 */
abstract class ControllerBase extends Controller {

     protected $headerView = "@activeTheme/main/vHeader.html";

     protected $footerView = "@activeTheme/main/vFooter.html";

     protected $nonce;

     public function initialize() {
             $this->nonce=ContentSecurityManager::getNonce('jsUtils');
             if (! URequest::isAjax()) {
                     $this->loadView($this->headerView,['nonce'=>$this->nonce]);
             }
     }

     public function finalize() {
             if (! URequest::isAjax()) {
                     $this->loadView($this->footerView,['nonce'=>$this->nonce]);
             }
     }
}

Password management

Users token

ACL management

Installation

Install the Ubiquity-acl module from the command prompt or from the Webtools (Composer part).

composer require phpmv/ubiquity-acl

Then activate the display of the Acl part in the Webtools:

_images/display-acl.png

ACL interface in webtools:

_images/acl-part.png

Acl Rules

ACLs are used to define access to an Ubiquity application. They are defined according to the following principles:

An Ubiquity application is composed of :
  • Resources (possibly controllers, or actions of these controllers)
  • Roles, possibly assigned to users. Each Role can inherit parent roles.
  • Permissions, which correspond to a right to do. Each permission has a level (represented by an integer value).
Additional rules:
  • An AclElement (Allow) grants Permission to a Role on a Resource.
  • Each role inherits authorisations from its parents, in addition to its own.
  • If a role has a certain level of access permission on a resource, it will also have all the permissions of a lower level on that resource.
  • The association of a resource and a permission to a controller or a controller action defines a map element.
_images/acl-diagram.png
Naming tips:
  • Role, in capital letters, beginning with an arobase (@USER, @ADMIN, @ALL…).
  • Permissions, in upper case, named using a verb (READ, WRITE, OPEN…).
  • Resource, capitalized on the first letter (Products, Customers…)

ACL Starting

The AclManager service can be started directly from the webtools interface, in the Security part.

  • The service is started in the services.php file.
app/config/services.php
 \Ubiquity\security\acl\AclManager::startWithCacheProvider();

ACLCacheProvider

This default provider allows you to manage ACLs defined through attributes or annotations.

AclController

An AclController enables automatic access management based on ACLs to its own resources.
It is possible to create them automatically from webtools.

_images/new-acl-controller.png

But it is just a basic controller, using the AclControllerTrait feature.

This controller just goes to redefine the _getRole method, so that it returns the role of the active user, for example.

app/controllers/BaseAclController.php
<?php
namespace controllers;

use Ubiquity\controllers\Controller;
use Ubiquity\security\acl\controllers\AclControllerTrait;
use Ubiquity\attributes\items\acl\Allow;

class BaseAclController extends Controller {
use AclControllerTrait;

   #[Allow('@ME')]
   public function index() {
      $this->loadView("BaseAclController/index.html");
   }

   public function _getRole() {
      $_GET['role']??'@ME';//Just for testing: logically, this is the active user's role
   }

   /**
    * {@inheritdoc}
    * @see \Ubiquity\controllers\Controller::onInvalidControl()
    */
   public function onInvalidControl() {
      echo $this->_getRole() . ' is not allowed!';
   }
}
Authorisation has been granted for the resource:
  • Without specifying the resource, the controller’s actions are defined as a resource.
  • Without specifying the permission, the ALL permission is used.
_images/me-allow.png

And this association is present in the Acls map:

_images/me-map.png
AclController with authentication

Note

The use of both WithAuthTrait and AclControllerTrait requires to remove the ambiguity about the isValid method.

app/controllers/BaseAclController.php
class BaseAclController extends Controller {
   use AclControllerTrait,WithAuthTrait{
      WithAuthTrait::isValid insteadof AclControllerTrait;
      AclControllerTrait::isValid as isValidAcl;
   }

   public function isValid($action){
        return parent::isValid($action)&& $this->isValidAcl($action);
   }
}
Allow with Role, resource and permission

Allow without prior creation:

@USER is allowed to access to Foo resource with READ permission.

app/controllers/BaseAclController.php
use Ubiquity\attributes\items\acl\Allow;

class BaseAclController extends Controller {
use AclControllerTrait;
   ...

   #[Allow('@USER','Foo', 'READ')]
   public function foo(){
      echo 'foo page allowed for @USER and @ME';
   }
}

Note

The role, resource and permission are automatically created as soon as they are invoked with Allow.

Allow with explicit creation:

app/controllers/BaseAclController.php
use Ubiquity\attributes\items\acl\Allow;
use Ubiquity\attributes\items\acl\Permission;

class BaseAclController extends Controller {
use AclControllerTrait;
   ...

   #[Permission('READ',500)]
   #[Allow('@USER','Foo', 'READ')]
   public function foo(){
      echo 'foo page allowed for @USER and @ME';
   }
}
Adding ACL at runtime

Whether in a controller or in a service, it is possible to add Roles, Resources, Permissions and Authorizations at runtime:

For example :\ Adding a Role @USER inheriting from @GUEST.

use Ubiquity\security\acl\AclManager;

AclManager::addRole('@GUEST');
AclManager::addRole('@USER',['@GUEST']);
Defining ACLs with Database

The ACLs defined in the database are additional to the ACLs defined via annotations or attributes.

Initializing

The initialization allows to create the tables associated to the ACLs (Role, Resource, Permission, AclElement). It needs to be done only once, and in dev mode only.

To place for example in app/config/bootstrap.php file:

use Ubiquity\controllers\Startup;
use Ubiquity\security\acl\AclManager;

$config=Startup::$config;
AclManager::initializeDAOProvider($config, 'default');

Starting

In app/config/services.php file :

use Ubiquity\security\acl\AclManager;
use Ubiquity\security\acl\persistence\AclCacheProvider;
use Ubiquity\security\acl\persistence\AclDAOProvider;
use Ubiquity\orm\DAO;

DAO::start();//Optional, to use only if dbOffset is not default

AclManager::start();
AclManager::initFromProviders([
    new AclCacheProvider(), new AclDAOProvider($config)
]);

Strategies for defining ACLs

With few resources:

Defining authorisations for each controller’s action or action group:

Resources logically correspond to controllers, and permissions to actions. But this rule may not be respected, and an action may be defined as a resource, as required.

The only mandatory rule is that a Controller/action pair can only correspond to one Resource/permission pair (not necessarily unique).

app/controllers/BaseAclController.php
namespace controllers;

use Ubiquity\controllers\Controller;
use Ubiquity\security\acl\controllers\AclControllerTrait;
use Ubiquity\attributes\items\acl\Permission;
use Ubiquity\attributes\items\acl\Resource;

#[Resource('Foo')]
#[Allow('@ADMIN')]
class FooController extends Controller {
   use AclControllerTrait;

   #[Allow('@NONE')]
   public function index() {
      echo 'index';
   }

   #[Allow('@USER')]
   public function read() {
      echo 'read';
   }

   #[Allow('@USER')]
   public function write() {
      echo 'write';
   }

   public function admin() {
      echo 'admin';
   }

   public function _getRole() {
      return $_GET['role']??'@NONE';
   }

   /**
    * {@inheritdoc}
    * @see \Ubiquity\controllers\Controller::onInvalidControl()
    */
   public function onInvalidControl() {
      echo $this->_getRole() . ' is not allowed!';
   }

}

With more resources:

app/controllers/BaseAclController.php
namespace controllers;

use Ubiquity\controllers\Controller;
use Ubiquity\security\acl\controllers\AclControllerTrait;
use Ubiquity\attributes\items\acl\Permission;
use Ubiquity\attributes\items\acl\Resource;

#[Resource('Foo')]
class FooController extends Controller {
   use AclControllerTrait;

   #[Permission('INDEX',1)]
   public function index() {
      echo 'index';
   }

   #[Permission('READ',2)]
   public function read() {
      echo 'read';
   }

   #[Permission('WRITE',3)]
   public function write() {
      echo 'write';
   }

   #[Permission('ADMIN',10)]
   public function admin() {
      echo 'admin';
   }

   public function _getRole() {
      return $_GET['role']??'NONE';
   }

   /**
    * {@inheritdoc}
    * @see \Ubiquity\controllers\Controller::onInvalidControl()
    */
   public function onInvalidControl() {
      echo $this->_getRole() . ' is not allowed!';
   }

}

Rest

The REST module implements a basic CRUD,
with an authentication system, directly testable in the administration part.

REST and routing

The router is essential to the REST module, since REST (Respresentation State Transfer) is based on URLs and HTTP methods.

Note

For performance reasons, REST routes are cached independently of other routes.
It is therefore necessary to start the router in a particular way to activate the REST routes and not to obtain a recurring 404 error.

The router is started in services.php.

Without activation of REST routes:

app/config/services.php
...
Router::start();

To enable REST routes in an application that also has a non-REST part:

app/config/services.php
...
Router::startAll();

To activate only Rest routes:

Router::startRest();

It is possible to start routing conditionally (this method will only be more efficient if the number of routes is large in either part):

app/config/services.php
...
     if($config['isRest']()){
             Router::startRest();
     }else{
             Router::start();
     }

Resource REST

A REST controller can be directly associated with a model.

Note

If you do not have a mysql database on hand, you can download this one: messagerie.sql

Creation

With devtools:

Ubiquity rest RestUsersController -r=User -p=/rest/users

Or with webtools:

Go to the REST section and choose Add a new resource:

_images/addNewResource.png

The created controller :

app/controllers/RestUsersController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
     namespace controllers;

     /**
      * Rest Controller RestUsersController
      * @route("/rest/users","inherited"=>true,"automated"=>true)
      * @rest("resource"=>"models\\User")
      */
     class RestUsersController extends \Ubiquity\controllers\rest\RestController {

     }

Since the attributes automated and inherited of the route are set to true, the controller has the default routes of the parent class.

Test interface

Webtools provide an interface for querying datas:

_images/createdResource.png
Getting an instance

A user instance can be accessed by its primary key (id):

_images/getOneResource.png

Inclusion of associated members: the organization of the user

_images/getOneResourceInclude.png

Inclusion of associated members: organization, connections and groups of the user

_images/getOneResourceIncludeAll.png
Getting multiple instances

Getting all instances:

_images/getAllOrgas.png

Setting a condition:

_images/condition-orgas.png

Including associated members:

_images/include-orgas.png
Adding an instance

The datas are sent by the POST method, with a content type defined at application/x-www-form-urlencoded:

Add name and domain parameters by clicking on the parameters button:

_images/post-parameters.png

The addition requires an authentication, so an error is generated, with the status 401:

_images/unauthorized-post.png

The administration interface allows you to simulate the default authentication and obtain a token, by requesting the connect method:

_images/connect.png

The token is then automatically sent in the following requests.
The record can then be inserted.

_images/added.png
Updating an instance

The update follows the same scheme as the insertion.

Deleting an instance
_images/delete-instance.png

Customizing

Routes

It is of course possible to customize and simplify the routes.
In this case, it is preferable to use inheritance from the RestBaseController class, and not to enable automatic routes.

app/controllers/RestOrgas.php
 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
namespace controllers;

use models\Organization;

/**
 * Rest Controller for organizations
 *
 * @route("/orgas")
 * @rest
 */
class RestOrgas extends \Ubiquity\controllers\rest\RestBaseController {

     public function initialize() {
             $this->model = Organization::class;
             parent::initialize();
     }

     /**
      *
      * @get
      */
     public function index() {
             $this->_get();
     }

     /**
      *
      * @get("{keyValues}")
      */
     public function get($keyValues) {
             $this->_getOne($keyValues);
     }

     /**
      *
      * @post("/")
      */
     public function add() {
             $this->_add();
     }

     /**
      *
      * @patch("{keyValues}")
      */
     public function update(...$keyValues) {
             $this->_update(...$keyValues);
     }

     /**
      *
      * @delete("{keyValues}")
      */
     public function delete(...$keyValues) {
             $this->_delete(...$keyValues);
     }
}

After re-initializing the cache, the test interface shows the accessible routes:

_images/custom-orgas.png
Modification of sent data

By overriding

It is possible to modify the data sent to the update and add methods, in order to add, modify or delete the value of fields before sending.
Either by overdefining the method getDatas:

app/controllers/RestOrgas.php
...

     protected function getDatas() {
             $datas = parent::getDatas();
             unset($datas['aliases']);// Remove aliases field
             return $datas;
     }

With events

Either in a more global way by acting on the rest events:

app/config/services.php
use Ubiquity\events\EventsManager;
use Ubiquity\events\RestEvents;
use Ubiquity\controllers\rest\RestBaseController;

...

EventsManager::addListener(RestEvents::BEFORE_INSERT, function ($o, array &$datas, RestBaseController $resource) {
     unset($datas['aliases']);// Remove aliases field
});

Authentification

Ubiquity REST implements an Oauth2 authentication with Bearer tokens.
Only methods with @authorization annotation require the authentication, these are the modification methods (add, update & delete).

             /**
              * Update an instance of $model selected by the primary key $keyValues
              * Require members values in $_POST array
              * Requires an authorization with access token
              *
              * @param array $keyValues
              * @authorization
              * @route("methods"=>["patch"])
              */
             public function update(...$keyValues) {
                     $this->_update ( ...$keyValues );
             }

The connect method of a REST controller establishes the connection and returns a new token.
It is up to the developer to override this method to manage a possible authentication with login and password.

_images/token.png

Simulation of a connection with login

In this example, the connection consists simply in sending a user variable by the post method.
If the user is provided, the connect method of $server instance returns a valid token that is stored in session (the session acts as a database here).

app/controllers/RestOrgas.php
 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
     namespace controllers;

     use Ubiquity\utils\http\URequest;
     use Ubiquity\utils\http\USession;

     /**
      * Rest Controller RestOrgas
      * @route("/rest/orgas","inherited"=>true,"automated"=>true)
      * @rest("resource"=>"models\\Organization")
      */
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             /**
              * This method simulate a connection.
              * Send a <b>user</b> variable with <b>POST</b> method to retreive a valid access token
              * @route("methods"=>["post"])
              */
             public function connect(){
                     if(!URequest::isCrossSite()){
                             if(URequest::isPost()){
                                     $user=URequest::post("user");
                                     if(isset($user)){
                                             $tokenInfos=$this->server->connect ();
                                             USession::set($tokenInfos['access_token'], $user);
                                             $tokenInfos['user']=$user;
                                             echo $this->_format($tokenInfos);
                                             return;
                                     }
                             }
                     }
                     throw new \Exception('Unauthorized',401);
             }
     }

For each request with authentication, it is possible to retrieve the connected user (it is added here in the response headers) :

app/controllers/RestOrgas.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     namespace controllers;

     use Ubiquity\utils\http\URequest;
     use Ubiquity\utils\http\USession;

     /**
      * Rest Controller RestOrgas
      * @route("/rest/orgas","inherited"=>true,"automated"=>true)
      * @rest("resource"=>"models\\Organization")
      */
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             public function isValid($action){
                     $result=parent::isValid($action);
                     if($this->requireAuth($action)){
                             $key=$this->server->_getHeaderToken();
                             $user=USession::get($key);
                             $this->server->_header('active-user',$user,true);
                     }
                     return $result;
             }
     }

Use the webtools interface to test the connection:

_images/connected-user.png

Customizing

Api tokens

It is possible to customize the token generation, by overriding the getRestServer method:

app/controllers/RestOrgas.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
     namespace controllers;

     use Ubiquity\controllers\rest\RestServer;
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             protected function getRestServer(): RestServer {
                     $srv= new RestServer($this->config);
                     $srv->setTokenLength(32);
                     $srv->setTokenDuration(4800);
                     return $srv;
             }
     }

Allowed origins and CORS

Cross-Origin Resource Sharing (CORS)

If you access your api from another site, it is necessary to set up CORS.

In this case, for requests of type PATCH, PUT, DELETE, your api must define a route allowing CORS to carry out its control pre-request using the OPTIONS method.

app/controllers/RestOrgas.php
1
2
3
4
5
6
7
8
9
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             /**
              * @options('{url}')
              */
             public function options($url='') {}
     }
Allowed origins

Allowed origins allow to define the clients that can access the resource in case of a cross domain request by defining The Access-Control-Allow-Origin response header.
This header field is returned by the OPTIONS method.

app/controllers/RestOrgas.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             protected function getRestServer(): RestServer {
                     $srv= new RestServer($this->config);
                     $srv->setAllowOrigin('http://mydomain/');
                     return $srv;
             }
     }

It is possible to authorize several origins:

app/controllers/RestOrgas.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             protected function getRestServer(): RestServer {
                     $srv= new RestServer($this->config);
                     $srv->setAllowOrigins(['http://mydomain1/','http://mydomain2/']);
                     return $srv;
             }
     }

Response

To change the response format, it is necessary to create a class inheriting from ResponseFormatter.
We will take inspiration from HAL, and change the format of the responses by:

  • adding a link to self for each resource
  • adding an _embedded attribute for collections
  • removing the data attribute for unique resources
app/controllers/RestOrgas.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     namespace controllers\rest;

     use Ubiquity\controllers\rest\ResponseFormatter;
     use Ubiquity\orm\OrmUtils;

     class MyResponseFormatter extends ResponseFormatter {

             public function cleanRestObject($o, &$classname = null) {
                     $pk = OrmUtils::getFirstKeyValue ( $o );
                     $r=parent::cleanRestObject($o);
                     $r["links"]=["self"=>"/rest/orgas/get/".$pk];
                     return $r;
             }

             public function getOne($datas) {
                     return $this->format ( $this->cleanRestObject ( $datas ) );
             }

             public function get($datas, $pages = null) {
                     $datas = $this->getDatas ( $datas );
                     return $this->format ( [ "_embedded" => $datas,"count" => \sizeof ( $datas ) ] );
             }
     }

Then assign MyResponseFormatter to the REST controller by overriding the getResponseFormatter method:

app/controllers/RestOrgas.php
1
2
3
4
5
6
7
8
     class RestOrgas extends \Ubiquity\controllers\rest\RestController {

             ...

             protected function getResponseFormatter(): ResponseFormatter {
                     return new MyResponseFormatter();
             }
     }

Test the results with the getOne and get methods:

_images/getOneFormatted.png _images/getFormatted.png

APIs

Unlike REST resources, APIs controllers are multi-resources.

SimpleRestAPI

JsonApi

Ubiquity implements the jsonApi specification with the class JsonApiRestController.
JsonApi is used by EmberJS and others.
see https://jsonapi.org/ for more.

Creation

With devtools:

Ubiquity restapi JsonApiTest -p=/jsonapi

Or with webtools:

Go to the REST section and choose Add a new resource:

_images/jsonapi-creation.png

Test the api in webtools:

_images/jsonapi-admin.png
Getting an array of objects

By default, all associated members are included:

_images/getAll.png
Including associated members

you need to use the include parameter of the request:

URL Description
/jsonapi/user?include=false No associated members are included
/jsonapi/user?include=organization Include the organization
/jsonapi/user?include=organization,connections Include the organization and the connections
/jsonapi/user?include=groupes.organization Include the groups and their organization
Filtering instances

you need to use the filter parameter of the request,
filter parameter corresponds to the where part of an SQL statement:

URL Description
/jsonapi/user?1=1 No filtering
/jsonapi/user?firstname='Benjamin' Returns all users named Benjamin
/jsonapi/user?filter=firstname like 'B*' Returns all users whose first name begins with a B
/jsonapi/user?filter=suspended=0 and lastname like 'ca*' Returns all suspended users whose lastname begins with ca
Adding an instance

The datas, contained in data[attributes], are sent by the POST method, with a content type defined at application/json; charset=utf-8.

Add your parameters by clicking on the parameters button:

_images/add-parameters.png

The addition requires an authentication, so an error is generated, with the status 401 if the token is absent or expired.

_images/add-response.png
Deleting an instance

Deletion requires the DELETE method, and the use of the id of the object to be deleted:

_images/delete-response.png

Webtools

Note

Webtools allow you to manage an Ubiquity application via a web interface. Since Ubiquity 2.2.0, webtools are in a separate repository.

Installation

Update the devtools if necessary to get started:

composer global update

At the project creation

Create a projet with webtools (-a option)

Ubiquity new quick-start -a

In an existing project

In a console, go to the project folder and execute:

Ubiquity admin

Starting

Start the embedded web server, from the project folder:

Ubiquity serve

go to the address: http://127.0.0.1:8090/Admin

_images/interface.png

Customizing

Click on customize to display only the tools you use:

_images/customizing.png _images/customized.png

Webtools modules

Routes

_images/routes.png

Displays default (non REST) routes.

Operations:

  • Filter routes
  • Test routes (GET, POST…)
  • Initialize router cache

Controllers

_images/controllers.png

Displays non REST controllers.

Operations:

  • Create a controller (and optionally the view associated to the default index action)
  • Create an action in a controller (optionally the associated view, the associated route)
  • Create a special controller (CRUD or Auth)
  • Test an action (GET, POST…)

Models

_images/models.png

Displays the metadatas of the models, allows to browse the entities.

Operations:

  • Create models from database
  • Generate models cache
  • Generate database script from existing models
  • Performs CRUD operations on models

Rest

_images/rest.png

Displays an manage REST services.

Operations:

  • Re-initialize Rest cache and routes
  • Create a new Service (using an api)
  • Create a new resource (associated to a model)
  • Test and query a web service using http methods
  • Performs CRUD operations on models

Cache

_images/cache.png

Displays cache files.

Operations:

  • Delete or re-initialize models cache
  • Delete or re-initialize controllers cache
  • Delete other cache files

Maintenance

_images/maintenance.png

Allows to manage maintenance modes.

Operations:

  • Create or update a maintenance mode
  • De/Activate a maintenance mode
  • Delete a maintenance mode

Config

_images/config.png

Allows the display and modification of the app configuration.

Git

_images/git.png

Synchronizes the project using git.

Operations:

  • Configuration with external repositories
  • Commit
  • Push
  • Pull
_images/themes.png

Manages Css themes.

Operations:

  • Install an existing theme
  • Activate a theme
  • Create a new theme (eventually base on an existing theme)

Contributing

System requirements

Before working on Ubiquity, setup your environment with the following software:

  • Git
  • PHP version 7.1 or above.

Get Ubiquity source code

On Ubiquity github repository :

  • Click Fork Ubiquity project
  • Clone your fork locally:
git clone git@github.com:USERNAME/ubiquity.git

Work on your Patch

Note

Before you start, you must know that all the patches you are going to submit must be released under the Apache 2.0 license, unless explicitly specified in your commits.

Create a Topic Branch

Note

Use a descriptive name for your branch:

  • issue_xxx where xxx is the issue number is a good convention for bug fixes
  • feature_name is a good convention for new features
git checkout -b NEW_BRANCH_NAME master

Work on your Patch

Work on your code and commit as much as you want, and keep in mind the following:

  • Read about the Ubiquity coding standards;

  • Add unit, fonctional or acceptance tests to prove that the bug is fixed or that the new feature actually works;

  • Do atomic and logically separate commits (use git rebase to have a clean and logical history);

  • Write good commit messages (see the tip below).

  • Increase the version numbers in any modified files, respecting semver rules:

    Given a version number MAJOR.MINOR.PATCH, increment the:

    • MAJOR version when you make incompatible API changes,
    • MINOR version when you add functionality in a backwards-compatible manner, and
    • PATCH version when you make backwards-compatible bug fixes.

Submit your Patch

Update the [Unrelease] part of the CHANGELOG.md file by integrating your changes into the appropriate parts:

  • Added
  • Changed
  • Removed
  • Fixed

Eventualy rebase your Patch
Before submitting, update your branch (needed if it takes you a while to finish your changes):

git checkout master
git fetch upstream
git merge upstream/master
git checkout NEW_BRANCH_NAME
git rebase master

Make a Pull Request

You can now make a pull request on Ubiquity github repository .

Coding guide

Note

Although the framework is very recent, please note some early Ubiquity classes do not fully follow this guide and have not been modified for backward compatibility reasons.
However all new codes must follow this guide.

Design choices

Fetching and using Services

Dependency injections

Avoid using dependency injection for all parts of the framework, internally.
Dependency injection is a resource-intensive mechanism:

  • it needs to identify the element to instantiate ;
  • then to proceed to its instantiation ;
  • to finally assign it to a variable.
Getting services from a container

Also avoid public access to services registered in a service container.
This type of access involves manipulating objects whose return type is unknown, not easy to handle for the developer.

For example, It’s hard to manipulate the untyped return of $this->serviceContainer->get('translator'), as some frameworks allow, and know which methods to call on it.

When possible, and when it does not reduce flexibility too much, the use of static classes is suggested:

For a developer, the TranslatorManager class is accessible from an entire project without any object instantiation.
It exposes its public interface and allows code completion:

  • The translator does not need to be injected to be used;
  • It does not need to be retrieved from a service container.

The use of static classes inevitably creates a strong dependency and affects flexibility.
But to come back to the Translator example, there is no reason to change it if it is satisfying.
It is not desirable to want to provide flexibility at all costs when it is not necessary, and then for the user to see that its application is a little slow.

Optimization

Execution of each line of code can have significant performance implications.
Compare and benchmark implementation solutions, especially if the code is repeatedly called:

Code quality

Ubiquity use Scrutinizer-CI for code quality.

  • For classes and methods :
    • A or B evaluations are good
    • C is acceptable, but to avoid if possible
    • The lower notes are to be prohibited

Code complexity

  • Complex methods must be split into several, to facilitate maintenance and allow reuse;
  • For complex classes , do an extract-class or extract-subclass refactoring and split them using Traits;

Code duplications

Absolutely avoid duplication of code, except if duplication is minimal, and is justified by performance.

Bugs

Try to solve all the bugs reported as you go, without letting them accumulate.

Tests

Any bugfix that doesn’t include a test proving the existence of the bug being fixed, may be suspect.
Ditto for new features that can’t prove they actually work.

It is also important to maintain an acceptable coverage, which may drop if a new feature is not tested.

Code Documentation

The current code is not yet fully documented, feel free to contribute in order to fill this gap.

Coding standards

Ubiquity coding standards are mainly based on the PSR-1 , PSR-2 and PSR-4 standards, so you may already know most of them.
The few intentional exceptions to the standards are normally reported in this guide.

Naming Conventions

  • Use camelCase for PHP variables, members, function and method names, arguments (e.g. $modelsCacheDirectory, isStarted());
  • Use namespaces for all PHP classes and UpperCamelCase for their names (e.g. CacheManager);
  • Prefix all abstract classes with Abstract except PHPUnit BaseTests;
  • Suffix interfaces with Interface;
  • Suffix traits with Trait;
  • Suffix exceptions with Exception;
  • Suffix core classes manager with Manager (e.g. CacheManager, TranslatorManager);
  • Prefix Utility classes with U (e.g. UString, URequest);
  • Use UpperCamelCase for naming PHP files (e.g. CacheManager.php);
  • Use uppercase for constants (e.g. const SESSION_NAME=’Ubiquity’).

Indentation, tabs, braces

  • Use Tabs, not spaces; (!PSR-2)
  • Use brace always on the same line; (!PSR-2)
  • Use braces to indicate control structure body regardless of the number of statements it contains;

Classes

  • Define one class per file;
  • Declare the class inheritance and all the implemented interfaces on the same line as the class name;
  • Declare class properties before methods;
  • Declare private methods first, then protected ones and finally public ones;
  • Declare all the arguments on the same line as the method/function name, no matter how many arguments there are;
  • Use parentheses when instantiating classes regardless of the number of arguments the constructor has;
  • Add a use statement for every class that is not part of the global namespace;

Operators

  • Use identical comparison and equal when you need type juggling;

Example

<?php
namespace Ubiquity\namespace;

use Ubiquity\othernamespace\Foo;

/**
 * Class description.
 * Ubiquity\namespace$Example
 * This class is part of Ubiquity
 *
 * @author authorName <authorMail>
 * @version 1.0.0
 * @since Ubiquity x.x.x
 */
class Example {
        /**
         * @var int
         *
         */
        private $theInt = 1;

        /**
         * Does something from **a** and **b**
         *
         * @param int $a The a
         * @param int $b The b
         */
        function foo($a, $b) {
                switch ($a) {
                        case 0 :
                                $Other->doFoo ();
                                break;
                        default :
                                $Other->doBaz ();
                }
        }

        /**
         * Adds some values
         *
         * @param param V $v The v object
         */
        function bar($v) {
                for($i = 0; $i < 10; $i ++) {
                        $v->add ( $i );
                }
        }
}

Important

You can import this standardization files that integrates all these rules in your IDE:

If your preferred IDE is not listed, you can submit the associated standardization file by creating a new PR.

Documenting guide

Ubiquity has two main sets of documentation:

  • the guides, which help you learn about manipulations or concepts ;
  • and the API, which serves as a reference for coding.

You can help improve the Ubiquity guides by making them more coherent, consistent, or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest Ubiquity version.

To do so, make changes to Ubiquity guides source files (located here on GitHub). Then open a pull request to apply your changes to the master branch.

When working with documentation, please take into account the guidelines.

Servers configuration

Important

Since version 2.4.5, for security and simplification reasons, the root of an Ubiquity application is located in the public folder.

Apache2

mod_php/PHP-CGI

Apache 2.2
mydomain.conf
     <VirtualHost *:80>
         ServerName mydomain.tld

         DocumentRoot /var/www/project/public
         DirectoryIndex /index.php

         <Directory /var/www/project/public>
             # enable the .htaccess rewrites
             AllowOverride All
             Order Allow,Deny
             Allow from All
         </Directory>

         ErrorLog /var/log/apache2/project_error.log
         CustomLog /var/log/apache2/project_access.log combined
     </VirtualHost>
mydomain.conf
     <VirtualHost *:80>
         ServerName mydomain.tld

         DocumentRoot /var/www/project/public
         DirectoryIndex /index.php

         <Directory /var/www/project/public>
             AllowOverride None

             # Copy .htaccess contents here

         </Directory>

         ErrorLog /var/log/apache2/project_error.log
         CustomLog /var/log/apache2/project_access.log combined
     </VirtualHost>
Apache 2.4

In Apache 2.4, Order Allow,Deny has been replaced by Require all granted.

mydomain.conf
     <VirtualHost *:80>
         ServerName mydomain.tld

         DocumentRoot /var/www/project/public
         DirectoryIndex /index.php

         <Directory /var/www/project/public>
             # enable the .htaccess rewrites
             AllowOverride All
             Require all granted
         </Directory>

         ErrorLog /var/log/apache2/project_error.log
         CustomLog /var/log/apache2/project_access.log combined
     </VirtualHost>
index.php relocation in public folder

If you created your project with a version prior to 2.4.5, you have to modify index.php and move the index.php and .htaccess files to the public folder.

public/index.php
<?php
define('DS', DIRECTORY_SEPARATOR);
//Updated with index.php in public folder
define('ROOT', __DIR__ . DS . '../app' . DS);
$config = include_once ROOT . 'config/config.php';
require_once ROOT . './../vendor/autoload.php';
require_once ROOT . 'config/services.php';
\Ubiquity\controllers\Startup::run($config);

PHP-FPM

Make sure the libapache2-mod-fastcgi and php7.x-fpm packages are installed (replace x with php version number).

php-pm configuration:

php-pm.conf
;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;

; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]

user = www-data
group = www-data

; use a unix domain socket
listen = /var/run/php/php7.4-fpm.sock

; or listen on a TCP socket
listen = 127.0.0.1:9000

Apache 2.4 configuration:

mydomain.conf
<VirtualHost *:80>
...
   <FilesMatch \.php$>
        SetHandler proxy:fcgi://127.0.0.1:9000
        # for Unix sockets, Apache 2.4.10 or higher
        # SetHandler proxy:unix:/path/to/fpm.sock|fcgi://localhost/var/www/
    </FilesMatch>
 </VirtualHost>

nginX

nginX configuration:

nginx.conf
upstream fastcgi_backend {
    server unix:/var/run/php/php7.4-fpm.sock;
    keepalive 50;
}
server {
    server_name mydomain.tld www.mydomain.tld;
    root /var/www/project/public;
    index index.php;
    listen 8080;

 location / {
     # try to serve file directly, fallback to index.php
     try_files $uri @rewrites;
 }

 location @rewrites {
     rewrite ^/(.*)$ /index.php?c=$1 last;
 }

    location = /index.php{
        fastcgi_pass fastcgi_backend;
        fastcgi_keep_conn on;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param SCRIPT_FILENAME  $document_root/index.php;
        include /etc/nginx/fastcgi_params;
    }

    # return 404 for all other php files not matching the front controller
    # this prevents access to other php files you don't want to be accessible.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;
}

Swoole

Swoole configuration:

.ubiquity/swoole-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "options"=>[
        "worker_num" => \swoole_cpu_num() * 2,
            "reactor_num" => \swoole_cpu_num() * 2
        ]
);

Workerman

Workerman configuration:

.ubiquity/workerman-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "socket"=>[
        "count" => 4,
        "reuseport" =>true
    ]
);

RoadRunner

RoadRunner configuration:

.ubiquity/.rr.yml
http:
  address:         ":8090"
  workers.command: "php-cgi ./.ubiquity/rr-worker.php"
  workers:
    pool:
      # Set numWorkers to 1 while debugging
      numWorkers: 10
      maxJobs:    1000

# static file serving. remove this section to disable static file serving.
static:
  # root directory for static file (http would not serve .php and .htaccess files).
  dir:   "."

  # list of extensions for forbid for serving.
  forbid: [".php", ".htaccess", ".yml"]

  always: [".ico", ".html", ".css", ".js"]

Ubiquity optimization

Ubiquity is fast, but can be even faster by optimizing a few elements.

Note

The integrated test server (accessible by Ubiquity serve) uses its own configuration and launch files (in the .ubiquity folder of your project).
It should therefore not be used to assess the results of the changes made.

Test your pages using a software and hardware configuration similar to the one used in production.
Use a benchmark tool to assess your changes as they happen (Apache bench for example).

Cache

System

Choose and test among the different cache systems (ArrayCache, PhpFastCache, MemCached).
The cache system is defined in the configuration file:

app/config/config.php
     "cache" => [
             "directory" => "cache/",
             "system" => "Ubiquity\\cache\\system\\ArrayCache",
             "params" => []
     ]

default ArrayCache is often the most optimized solution.

Generation

Generate the router and ORM cache (Think that annotations are never used at runtime):

Ubiquity init-cache

Static contents

If your application has pages that are being generated by PHP but that actually rarely change, you can cache:

  • The query results (using DAO methods)
  • The route response (with @route annotation)

index file

Remove the line defining error reporting at runtime, and make sure that error display is disabled in php.ini.

index.php
error_reporting(\E_ALL);//To be removed

Config optimization

The configuration is accessible from the app/config/config.php file.

Keep only those items that are essential to your application.

key role Optimization
siteUrl Used by Ajax methods, and by Twig’s url and path functions To be removed if these functions are not used
database Used by Ubiquity ORM To be removed if the ORM is not used
sessionName If assigned, starts or retrieves the php session for each request To be removed if the session is useless
templateEngine If assigned, instanciates a new Engine object for each request To be removed if the views are not used
templateEngineOptions Options assigned to the template engine instance set the cache option to true if Twig is used
test To remove (deprecated)  
debug Enables or disables logs Set to false in production
logger Defines the logger instance To remove in production
di Defines the services to be injected Only the @exec key is read at runtime
cache Defines the cache path and base class of the cache, used by models, router, dependency injection  
mvcNS Defines the paths or namespaces used by Rest controllers, models and controllers  
isRest Defines the condition to detect if a path corresponds to a controller Rest To be removed if you do not explicitly use this condition in your code

Example of configuration without session, and without dependency injection:

app/config/config.php
1
2
3
4
5
6
7
8
<?php
return array(
             "templateEngine"=>'Ubiquity\\views\\engine\\Twig',
             "templateEngineOptions"=>array("cache"=>true),
             "debug"=>false,
             "cache"=>["directory"=>"cache/","system"=>"Ubiquity\\cache\\system\\ArrayCache","params"=>[]],
             "mvcNS"=>["models"=>"models","controllers"=>"controllers","rest"=>""]
);

Services optimization

The loaded services are accessibles from the app/config/services.php file.

As for the configuration file, keep only those items that are essential to your application.

Lines Role
\Ubiquity\cache\CacheManager::startProd($config) Starts the cache for ORM, database, router, dependency injection
UbiquityormDAO::start() To be used only with multiple databases
Router::start() To be used only if the routes are defined with annotations
Router::addRoute(“_default”, “controllers\IndexController”) Defines the default route (to remove in production)
\Ubiquity\assets\AssetsManager::start($config) Assigns the variable siteUrl to the ThemeManager, to be used only if the css and js functions of twig are used

Example of a Services file with a database and starting the router :

app/config/services.php
1
2
3
<?php
\Ubiquity\cache\CacheManager::startProd($config);
\Ubiquity\controllers\Router::start();

Autoloader optimization

In production, remove dependencies used only in development, and generate the optimized class map file:

composer install --no-dev --classmap-authoritative

If the dependencies used have already been removed and you only want to update the map file (after adding or removing a class):

composer dump-autoload -o  --classmap-authoritative

Note

The --no-dev parameter removes the ubiquity-dev dependency required by webtools. If you use webtools in production, add the phpmv/ubiquity-dev dependency:

composer require phpmv/ubiquity-dev

PHP optimization

Please note that other applications can use the modified values on the same server.

OP-Cache

OPcache improves PHP performance by storing precompiled script bytecode in shared memory, thereby removing the need for PHP to load and parse scripts on each request.

php.ini
[opcache]
; Determines if Zend OPCache is enabled
opcache.enable=1
php.ini
; The OPcache shared memory storage size.
opcache.memory_consumption=256

; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 1000000 are allowed.
opcache.max_accelerated_files=10000

; When disabled, you must reset the OPcache manually or restart the
; webserver for changes to the filesystem to take effect.
opcache.validate_timestamps=0

; Allow file existence override (file_exists, etc.) performance feature.
opcache.enable_file_override=1

; Enables or disables copying of PHP code (text segment) into HUGE PAGES.
; This should improve performance, but requires appropriate OS configuration.
opcache.huge_code_pages=1

If you use ubiquity-swoole web server:

php.ini
; Determines if Zend OPCache is enabled for the CLI version of PHP
opcache.enable_cli=1

To complete

Remember that the framework used does not do everything. You also need to optimize your own code.

Ubiquity commands

Note

This part is accessible from the webtools, so if you created your project with the -a option or with the create-project command..

Commands

From the webtools, activate the commands part,

_images/commands-elm.png

or go directly to http://127.0.0.1:8090/Admin/commands.

Commands list

Activate the Commands tab to get the list of existing devtools commands.

_images/commands-list.png

Command info

It is possible to get help on a command (which produces a result equivalent to Ubiquity help cmdName).

_images/command-help.png

Command execution

Clicking on the run button of a command displays a form to enter the parameters (or executes it directly if it takes none).

_images/command-run.png

After entering the parameters, the execution produces a result.

_images/command-exec.png

Commands suite

Return to My commands tab: It is possible to save a sequence of commands (with stored parameters), and then execute the same sequence:

Suite creation

Click on the add command suite

_images/new-suite-btn.png

Add the desired commands and modify the parameters:

_images/new-commands-suite.png

The validation generates the suite:

_images/commands-suite-created.png

Commands suite execution

Clicking on the run button of the suite executes the list of commands it contains:

_images/commands-suite-exec.png

Custom command creation

Click on the Create devtools command button.

_images/create-devtools-command-btn.png

Enter the characteristics of the new command:

  • The command name
  • The command value: name of the main argument
  • The command parameters: In case of multiple parameters, use comma as separator
  • The command description
  • The command aliases: In case of multiple aliases, use comma as separator
_images/create-command.png

Note

Custom commands are created in the app/commands folder of the project.

_images/custom-command-exec.png

The generated class:

app/commands/CreateArray.php
 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
namespace commands;

use Ubiquity\devtools\cmd\commands\AbstractCustomCommand;
use Ubiquity\devtools\cmd\ConsoleFormatter;
use Ubiquity\devtools\cmd\Parameter;

class CreateArray extends AbstractCustomCommand {

     protected function getValue(): string {
             return 'jsonValue';
     }

     protected function getAliases(): array {
             return array("createarray","arrayFromJson");
     }

     protected function getName(): string {
             return 'createArray';
     }

     protected function getParameters(): array {
             return ['f' => Parameter::create('fLongName', 'The f description.', [])];
     }

     protected function getExamples(): array {
             return ['Sample use of createArray'=>'Ubiquity createArray jsonValue'];
     }

     protected function getDescription(): string {
             return 'Creates an array from JSON and save to file';
     }

     public function run($config, $options, $what, ...$otherArgs) {
             //TODO implement command behavior
             echo ConsoleFormatter::showInfo('Run createArray command');
     }
}

The CreateArray command implemented:

app/commands/CreateArray.php
 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
namespace commands;

use Ubiquity\devtools\cmd\commands\AbstractCustomCommand;
use Ubiquity\devtools\cmd\ConsoleFormatter;
use Ubiquity\devtools\cmd\Parameter;
use Ubiquity\utils\base\UFileSystem;

class CreateArray extends AbstractCustomCommand {

     protected function getValue(): string {
             return 'jsonValue';
     }

     protected function getAliases(): array {
             return array(
                     "createarray",
                     "arrayFromJson"
             );
     }

     protected function getName(): string {
             return 'createArray';
     }

     protected function getParameters(): array {
             return [
                     'f' => Parameter::create('filename', 'The filename to create.', [])
             ];
     }

     protected function getExamples(): array {
             return [
                     'Save an array in test.php' => "Ubiquity createArray \"{\\\"created\\\":true}\" -f=test.php"
             ];
     }

     protected function getDescription(): string {
             return 'Creates an array from JSON and save to file';
     }

     public function run($config, $options, $what, ...$otherArgs) {
             echo ConsoleFormatter::showInfo('Run createArray command');
             $array = \json_decode($what, true);
             $error = \json_last_error();
             if ($error != 0) {
                     echo ConsoleFormatter::showMessage(\json_last_error_msg(), 'error');
             } else {
                     $filename = self::getOption($options, 'f', 'filename');
                     if ($filename != null) {
                             UFileSystem::save($filename, "<?php\nreturn " . var_export($array, true) . ";\n");
                             echo ConsoleFormatter::showMessage("$filename succefully created!", 'success', 'CreateArray');
                     } else {
                             echo ConsoleFormatter::showMessage("Filename must have a value!", 'error');
                     }
             }
     }
}

Custom command execution

The new command is accessible from the devtools, as long as it is in the project:

Ubiquity help createArray
_images/custom-command-help.png
Ubiquity createArray "{\"b\":true,\"i\":5,\"s\":\"string\"}" -f=test.php
_images/custom-command-devtools.png

Composer management

Note

This part is accessible from the webtools, so if you created your project with the -a option or with the create-project command..

Access

From the webtools, activate the composer part,

_images/composer-elm.png

or go directly to http://127.0.0.1:8090/Admin/composer.

Dependencies list

The interface displays the list of already installed dependencies, and those that are directly installable.

_images/composer-dependencies.png

Dependency installation

Among the listed dependencies:

Click on the add button of the dependencies you want to add.

_images/composer-add-1.png

Then click on the Generate composer update button:

_images/composer-add-2.png

The validation generates the update.

For non listed dependencies:

Click on the Add dependency button :

_images/composer-add-dependency.png
  • Enter a vendor name (provider) ;
  • Select a package in the list ;
  • Select eventually a version (if none, the last stable version will be installed).

Dependency removal

Click on the remove button of the dependencies you want to add.

_images/composer-remove-1.png

Then click on the Generate composer update button, and validate the update.

Note

It is possible to perform several addition or deletion operations and validate them simultaneously.

Composer optimization

Click on the Optimize autoloader button.

This optimize composer autoloading with an authoritative classmap.

Ubiquity Caching

Ubiquity dependencies

  • php >=7.4
  • phpmv/ubiquity => Ubiquity core

In production

Templating

Twig is required if it is used as a template engine, which is not a requirement.

  • twig/twig => Template engine

Security

  • phpmv/ubiquity-security => Csrf, Csp…
  • phpmv/ubiquity-acl => Access Control List management

In development

Webtools

  • phpmv/ubiquity-dev => dev classes for webtools and devtools since v2.3.0
  • phpmv/php-mv-ui => Front library
  • mindplay/annotations => Annotations library, required for generating models, cache…
  • monolog/monolog => Logging
  • czproject/git-php => Git operations (+ require git console)

Devtools

  • phpmv/ubiquity-devtools => Cli console
  • phpmv/ubiquity-dev => dev classes for webtools and devtools since v2.3.0
  • mindplay/annotations => Annotations library, required for generating models, cache…

Testing

  • codeception/codeception => Tests
  • codeception/c3 => C3 integration
  • phpmv/ubiquity-codeception => Codeception for Ubiquity

OAuth2 client module

Note

This part is accessible from the webtools, so if you created your project with the -a option or with the create-project command. The OAuth module is not installed by default. It uses HybridAuth library.

Installation

In the root of your project:

composer require phpmv/ubiquity-oauth

Note

It is also possible to add the ubiquity-oauth dependency using the Composer part of the administration module.

_images/composer-add-1.png

OAuth configuration

Global configuration

_images/oauth-part-0.png

Click on the Global configuration button, and modify the callback URL, which corresponds to the local callback url after a successful connection.

_images/oauth-part-callback.png

OAuth controller

Click on the Create Oauth controller button and assign to the route the value previously given to the callback:

_images/create-oauth-controller.png

Validate and reset the router cache:

_images/create-oauth-controller-created.png

Providers

Note

For an OAuth authentication, it is necessary to create an application at the provider beforehand, and to take note of the keys of the application (id and secret).

Click on the Add provider button and select Google:

_images/provider-config.png

Check the connection by clicking on the Check button:

_images/google-check.png

Post Login Information:

_images/google-check-infos.png

OAuthController customization

The controller created is the following:

app/controllers/OAuthTest.php
namespace controllers;
use Hybridauth\Adapter\AdapterInterface;
/**
 * Controller OAuthTest
 */
class OAuthTest extends \Ubiquity\controllers\auth\AbstractOAuthController{

   public function index(){
   }

   /**
    * @get("oauth/{name}")
    */
   public function _oauth(string $name):void {
      parent::_oauth($name);
   }

   protected function onConnect(string $name,AdapterInterface $provider){
      //TODO
   }
}
  • The _oauth method corresponds to the callback url
  • The onConnect method is triggered on connection and can be overridden.

Example :

  • Possible retrieval of an associated user in the database
  • or creation of a new user
  • Adding the logged-in user and redirection
app/controllers/OAuthTest.php
   protected function onConnect(string $name, AdapterInterface $provider) {
      $userProfile = $provider->getUserProfile();
      $key = md5($name . $userProfile->identifier);
      $user = DAO::getOne(User::class, 'oauth= ?', false, [
         $key
      ]);
      if (! isset($user)) {
         $user = new User();
         $user->setOauth($key);
         $user->setLogin($userProfile->displayName);
         DAO::save($user);
      }
      USession::set('activeUser', $user);
      \header('location:/');
     }

Async platforms

Note

Ubiquity supports multiple platforms : Swoole, Workerman, RoadRunner, PHP-PM, ngx_php.

Swoole

Install the Swoole extension on your system (linux) or in your Docker image :

#!/bin/bash
pecl install swoole

Run Ubiquity Swoole (for the first time, ubiquity-swoole package will be installed):

Ubiquity serve -t=swoole

Server configuration

.ubiquity/swoole-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "options"=>[
        "worker_num" => \swoole_cpu_num() * 2,
            "reactor_num" => \swoole_cpu_num() * 2
        ]
);

The port can also be changed at server startup:

Ubiquity serve -t=swoole -p=8999

Services optimization

Startup of services will be done only once, at server startup.

app/config/services.php
\Ubiquity\cache\CacheManager::startProd($config);
\Ubiquity\orm\DAO::setModelsDatabases([
     'models\\Foo' => 'default',
     'models\\Bar' => 'default'
]);

\Ubiquity\cache\CacheManager::warmUpControllers([
     \controllers\IndexController::class,
     \controllers\FooController::class
]);

$swooleServer->on('workerStart', function ($srv) use (&$config) {
     \Ubiquity\orm\DAO::startDatabase($config, 'default');
     \controllers\IndexController::warmup();
     \controllers\FooController::warmup();
});
The warmUpControllers method:
  • instantiates the controllers
  • performs dependency injection
  • prepares the call of the initialize and finalize methods (initialization of call constants)

At the start of each Worker, the warmup method of the controllers can for example initialize prepared DAO queries:

app/controllers/FooController.php
     public static function warmup() {
             self::$oneFooDao = new DAOPreparedQueryById('models\\Foo');
             self::$allFooDao = new DAOPreparedQueryAll('models\\Foo');
     }

Workerman

Workerman does not require any special installation (except for libevent to be used in production for performance reasons).

Run Ubiquity Workerman (for the first time, ubiquity-workerman package will be installed):

Ubiquity serve -t=workerman

Server configuration

.ubiquity/workerman-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "socket"=>[
        "count" => 4,
        "reuseport" =>true
    ]
);

The port can also be changed at server startup:

Ubiquity serve -t=workerman -p=8999

Services optimization

Startup of services will be done only once, at server startup.

app/config/services.php
\Ubiquity\cache\CacheManager::startProd($config);
\Ubiquity\orm\DAO::setModelsDatabases([
     'models\\Foo' => 'default',
     'models\\Bar' => 'default'
]);

\Ubiquity\cache\CacheManager::warmUpControllers([
     \controllers\IndexController::class,
     \controllers\FooController::class
]);

$workerServer->onWorkerStart = function () use ($config) {
     \Ubiquity\orm\DAO::startDatabase($config, 'default');
     \controllers\IndexController::warmup();
     \controllers\FooController::warmup();
});

ngx_php

//TODO

Roadrunner

//TODO

Indices and tables