Guide d’utilisation Ubiquity

Quick start avec la console

Note

Si vous n’aimez pas le mode console, vous pouvez passer au quick-start avec les webtools (UbiquityMyAdmin).

Composer installation

ubiquity utilise Composer pour gérer ses dépendances. Vous devrez donc vous assurer que vous avez Composer installé sur votre machine.

Installation d’Ubiquity devtools

Télécharger l’installeur Ubiquity-devtools avec Composer.

composer global require phpmv/ubiquity-devtools

Testez votre installation en faisant :

Ubiquity version
_images/ubi-version.png

Vous pouvez obtenir à tout moment de l’aide sur une commande en tapant : Ubiquity help suivi de ce que vous cherchez.

Exemple :

Ubiquity help project

Création de projet

Créer le projet quick-start

Ubiquity new quick-start

Structure des dossiers

Le projet créé dans le dossier quick-start a une structure simple et lisible :

Le dossier app contient le code de votre future application :

app
  cache
  config
  controllers
  models
  views

Démarrage

Allez dans le dossier nouvellement créé quick-start et démarrez le serveur php intégré :

Ubiquity serve

Vérifier l’opération à l’adresse http://127.0.0.1:8090 :

_images/quick-start-main.png

Note

Si le port 8090 est occupé, vous pouvez démarrer le serveur en utilisant un autre port en utilisant l’option -p.

Ubiquity serve -p=8095

Contrôleur

L’application en mode console devtools permet de gagner du temps dans les opérations répétitives. Utilisons la pour créer un contrôleur.

Ubiquity controller DefaultController
_images/controller-creation.png

Vous pouvez éditer le fichier app/controllers/DefaultController dans votre IDE :

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

Ajouter le message traditionnel, et tester votre page à l’adresse http://127.0.0.1:8090/DefaultController

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

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

}

Pour l’instant, nous n’avons pas défini de routes,
L’accès à l’application se fait donc selon le schéma suivant :
controllerName/actionName/param

L’action par défaut est la méthode index, elle n’a pas besoin d’être spécifiée dans l’url.

Route

Important

Le routage est défini avec l’attribut Route (avec php>8) ou l’annotation @route et n’est pas fait dans un fichier de configuration :
c’est un choix de conception.

Le paramètre automated mis à true permet aux méthodes de notre classe d’être définies comme des sous routes de la route principale /hello.

Avec annotations :

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

Avec attributs (php>8) :

app/controllers/DefaultController.php
 1namespace controllers;
 2use Ubiquity\attributes\items\router\Route;
 3
 4#[Route('/hello', automated: true)]
 5class DefaultController extends ControllerBase{
 6
 7    public function index(){
 8        echo 'Hello world!';
 9    }
10
11}

Cache du routeur

Important

Aucune modification sur les routes n’est effective sans initialiser le cache.
Les annotations ne sont jamais lues au moment de l’exécution. C’est également un choix de conception.

Nous pouvons utiliser la console pour ré-initialiser le cache :

Ubiquity init-cache
_images/init-cache.png

Vérifions que la route existe :

Ubiquity info:routes
_images/info-routes1.png

Nous pouvons maintenant tester la page http://127.0.0.1:8090/hello

Action et route avec paramètres

Nous allons maintenant créer une action (sayHello) avec un paramètre (name), et la route associée (to) :
La route utilisera le paramètre name de l’action :

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

Après ré-initialisation du cache (commande init-cache), la commande info:routes devrait afficher :

_images/2-routes.png

Changer le code dans votre IDE : l’action doit dire hello à quelqu’un (somebody)…

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

et tester la page http://127.0.0.1:8090/hello/to/Mr SMITH

Action, route, paramètres et vue

Nous allons maintenant créer une action (information) avec deux paramètres (titre et message), la route associée (info), et une vue pour afficher le message :
La route utilisera les deux paramètres de l’action.

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

Note

Le paramètre -v (–view) est utiliser pour créer la vue associée à l’action.

Après ré-initialisation du cache, nous devrions avoir 3 routes :

_images/3-routes.png

Retournons à notre environnement de développement pour voir le code généré :

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

Nous devons passer les 2 variables à la vue :

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

Et utiliser ces 2 variables dans la vue twig associée :

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

Nous pouvons tester votre page à l’addresse http://127.0.0.1:8090/hello/info/Quick start/Ubiquity est super simple
une évidence.

_images/quiet-simple.png

Quick start avec les Webtools

Installation de Composer

ubiquity utilise Composer pour gérer ses dépendances. Vous devrez donc vous assurer que vous avez Composer installé sur votre machine.

Installation d’Ubiquity-devtools

Télécharger l’installeur Ubiquity-devtools avec Composer.

composer global require phpmv/ubiquity-devtools

Testez votre installation en faisant :

Ubiquity version
_images/ubi-version.png

Vous pouvez obtenir à tout moment de l’aide sur une commande en tapant : Ubiquity help suivi de ce que vous cherchez.

Exemple :

Ubiquity help project

Création de projet

Créer le projet quick-start incluant les Webtools (avec l’option -a)

Ubiquity new quick-start -a

Structure des dossiers

Le projet créé dans le dossier quick-start a une structure simple et lisible :

Le dossier app contient le code de votre future application :

app
  cache
  config
  controllers
  models
  views

Démarrage

Allez dans le dossier nouvellement créé quick-start et démarrez le serveur php intégré :

Ubiquity serve

Vérifier l’opération à l’adresse http://127.0.0.1:8090 :

_images/quick-start-main.png

Note

Si le port 8090 est occupé, vous pouvez démarrer le serveur en utilisant un autre port en utilisant l’option -p.

Ubiquity serve -p=8095

Contrôleur

Naviguer vers l’interface d’administration en cliquant sur le bouton Webtools :

_images/ubi-my-admin-btn.png

Sélectionner les outils dont vous avez besoin :

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

L’application web Webtools permet de gagner du temps sur les opérations répétitives.

_images/ubi-my-admin-interface.png

Nous allons les utiliser pour créer un contrôleur.

Aller dans la partie Controllers, saisir DefaultController dans la zone controllerName et créer le contrôleur :

_images/create-controller-btn.png

Le contrôleur DefaultController est créé :

_images/controller-created.png

Vous pouvez éditer le fichier app/controllers/DefaultController dans votre IDE :

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

Ajouter le traditionnel Hello world, et tester votre page à l’adresse http://127.0.0.1:8090/DefaultController

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

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

     }

Pour l’instant, nous n’avons pas défini de routes,
L’accès à l’application se fait donc selon le schéma suivant :
controllerName/actionName/param

L’action par défaut est la méthode index, elle n’a pas besoin d’être spécifiée dans l’url.

Route

Important

Le routage est défini avec l’attribut Route (avec php>8) ou l’annotation @route et n’est pas fait dans un fichier de configuration :
c’est un choix de conception.

Le paramètre automated mis à true permet aux méthodes de notre classe d’être définies comme des sous routes de la route principale /hello.

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

Cache du routeur

Important

Aucune modification sur les routes n’est effective sans initialiser le cache.
Les annotations ne sont jamais lues au moment de l’exécution. C’est également un choix de conception.

Nous pouvons utiliser les webtools pour ré-initialiser le cache :

Allez dans la partie Routes et cliquer sur le bouton re-init cache

_images/re-init-cache-btn.png

La route est maintenant listée dans l’interface :

_images/1-route.png

Nous pouvons maintenant tester la page en cliquant sur le bouton GET ou en allant à l’adresse http://127.0.0.1:8090/hello.

Action & route avec paramètres

Nous allons maintenant créer une action (sayHello) avec un paramètre (name), et la route associée (to) :
La route utilisera le paramètre name de l’action :

Aller à la section Controllers :

  • cliquer sur le bouton + associé à DefaultController,

  • puis sélectionner l’élément Add new action in..,

_images/create-action-btn.png

Saisir les caractéristiques de l’action dans le formulaire suivant :

_images/create-action.png

Après avoir réinitialisé le cache avec le bouton orange, nous pouvons voir la nouvelle route hello/to/{name} :

_images/router-re-init-1.png

Vérifiez la création de la route en allant dans la section Routes :

_images/router-re-init-2.png

Nous pouvons maintenant tester la page en cliquant sur le bouton GET :

_images/test-action.png

Nous pouvons voir le résultat :

_images/test-action-result.png

Aller directement à l’adresse « http://127.0.0.1:8090/hello/to/Mr SMITH » pour tester

Action, paramètres de route & vue

Nous allons maintenant créer une action (information) avec deux paramètres (titre et message), la route associée (info), et une vue pour afficher le message :
La route utilisera les deux paramètres de l’action.

Dans la section Contrôleurs, créer une autre action dans DefaultController :

_images/create-action-btn.png

Saisir les caractéristiques de l’action dans le formulaire suivant :

_images/create-action-view.png

Note

La case à cocher view d’activer la création de la vue associée à l’action.

Après avoir réinitialisé le cache, nous avons maintenant 3 routes :

_images/create-action-view-result.png

Retournons dans notre environnement de développement et voyons le code généré :

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

Nous devons passer les 2 variables à la vue :

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

Et nous utilisons nos 2 variables dans la vue twig associée :

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

Nous pouvons tester notre page à l’adresse http://127.0.0.1:8090/hello/info/Quick start/Ubiquity is quiet simple
;-)

_images/quiet-simple.png

Installation d’ubiquity-devtools

Composer installation

ubiquity utilise Composer pour gérer ses dépendances. Vous devrez donc vous assurer que vous avez Composer installé sur votre machine.

Installer Ubiquity-devtools

Téléchargez l’installateur Ubiquity-devtools en utilisant Composer.

composer global require phpmv/ubiquity-devtools

Assurez-vous de placer le répertoire ~/.composer/vendor/bin dans votre PATH afin que l’exécutable Ubiquity puisse être localisé par votre système.

Une fois installé, la commande Ubiquity new crée une nouvelle installation d’Ubiquity dans le répertoire que vous avez spécifié. Par exemple, Ubiquity new blog crée un répertoire nommé blog contenant un projet Ubiquity :

Ubiquity new blog

L’option semantic ajoute Semantic-UI pour le front-end.

Vous pouvez voir plus d’options sur l’installation en lisant la section Création de projet.

Création de projet

Après avoir installé Installation d’ubiquity-devtools, dans votre terminal, utilisez la commande new depuis le dossier racine de votre serveur web :

Exemples

Un simple projet

Ubiquity new projectName

Un projet avec les webtools

Ubiquity new projectName -a

Un projet avec les thèmes Bootstrap et Semantic-ui installés

Ubiquity new projectName --themes=bootstrap,semantic

Arguments de l’installeur

nom court

nom

rôle

valeur par défaut

valeurs autorisées

Depuis les devtools

b

dbName

Définit le nom de la base de données

s

serverName

Définit l’adresse du serveur de base de données

127.0.0.1

p

port

Définit le port de la base de données

3306

u

user

Définit l’utilisateur de la base de données

root

w

password

Définit le mot de passe d’accès à la base de données

“”

h

themes

Installe les thèmes.

semantic,bootstrap,foundation

m

all-models

Crée l’ensemble des modèles depuis la base de données.

false

a

admin

Ajoute les webtools.

false

i

siteUrl

Définit l’URL du site.

http://127.0.0.1/{projectname}

1.2.6

e

rewriteBase

Définit la base de la ré-écriture d’urls.

/{projectname}/

1.2.6

Usage des arguments

noms courts

Exemple de création du projet blog, connecté à la base de données blogDb, avec génération de tous les modèles

Ubiquity new blog -b=blogDb -m=true

noms longs

Exemple de création du projet blog, connecté à la base de données bogDb, avec génération de tous les modèles et intégration du thème sémantic

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

Lancement

Pour démarrer le serveur web intégré et tester vos pages, exécutez à partir du dossier racine de l’application :

Ubiquity serve

Le serveur web est démarré à l’adresse 127.0.0.1:8090

Configuration de projet

Normalement, le programme d’installation limite les modifications à apporter aux fichiers de configuration et votre application est opérationnelle après l’installation.

_images/firstProject.png

Configuration principale

La configuration principale d’un projet est localisée dans le fichier app/conf/config.php.

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

Configuration des services

Les services chargés au démarrage sont configurés dans le fichier app/conf/services.php.

app/conf/services.php
 1     use Ubiquity\controllers\Router;
 2
 3     try{
 4             \Ubiquity\cache\CacheManager::startProd($config);
 5     }catch(Exception $e){
 6             //Do something
 7     }
 8     \Ubiquity\orm\DAO::startDatabase($config);
 9     Router::start();
10     Router::addRoute("_default", "controllers\\IndexController");

URLs sympas

Apache

Le framework est livré avec un fichier .htaccess qui est utilisé pour autoriser les URLs sans index.php. Si vous utilisez Apache comme serveur pour votre application Ubiquity, assurez-vous d’activer le module mod_rewrite.

.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>

Voir Apache configuration pour des détails supplémentaires.

Nginx

Avec Nginx, la directive suivante dans la configuration de votre site autorisera les URL « sympas » :

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

Voir NginX configuration pour des informations supplémentaires.

Driver Laravel Valet

Valet est un environnement de développement php pour les adeptes Mac. Pas de Vagrant, pas de fichier /etc/hosts. Vous pouvez même partager vos sites publiquement en utilisant des tunnels locaux.

Laravel Valet configure votre Mac pour qu’il exécute toujours Nginx en arrière-plan au démarrage de votre machine. Ensuite, en utilisant DnsMasq, Valet renvoie toutes les requêtes sur le domaine *.test pour pointer vers des sites installés sur votre machine locale.

Plus d’informations sur Laravel Valet

Créez UbiquityValetDriver.php sous ~/.config/valet/Drivers/ ajoutez le code php ci-dessous et sauvegardez-le.

<?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;
                }
        }
}

Utilisation des devtools

Création de projet

Voir Création de projet pour créer un projet.

Astuce

Pour toutes les autres commandes, vous devez vous trouver dans le dossier de votre projet ou dans l’un de ses sous-dossiers.

Important

Le dossier .ubiquity créé automatiquement avec le projet permet aux devtools de trouver le dossier racine du projet.
S’il a été supprimé ou n’est plus présent, vous devez recréer ce dossier vide.

Création de contrôleur

Spécifications

  • commande : controller

  • Argument : controller-name

  • aliases : create-controller

Paramètres

nom court

nom

rôle

valeur par défaut

valeurs autorisées

v

view

Crée la vue index associée.

true

true, false

Exemples :

Crée la classe contrôleur controllers\ClientController dans app/controllers/ClientController.php :

Ubiquity controller ClientController

Crée la classe contrôleur controllers\ClientController dans app/controllers/ClientController.php et la vue associée dans app/views/ClientController/index.html :

Ubiquity controller ClientController -v

Création d’action

Spécifications

  • commande : action

  • Argument : controller-name.action-name

  • aliases : new-action

Paramètres

nom court

nom

rôle

valeur par défaut

valeurs autorisées

p

params

Les paramètres (ou arguments) de l’action.

a,b=5 or $a,$b,$c

r

route

Le path de route associé

/path/to/route

v

create-view

Crée la vue associée.

false

true,false

Exemples :

Ajoute l’action « all » dans le contrôleur « Users » :

Ubiquity action Users.all

Résultat :

app/controllers/Users.php
 1namespace controllers;
 2/**
 3 * Controller Users
 4 */
 5class Users extends ControllerBase{
 6
 7   public function index(){}
 8
 9   public function all(){
10
11   }
12
13}

Ajoute l’action display dans le contrôleur Users avec un paramètre :

Ubiquity action Users.display -p=idUser

Résultat :

app/controllers/Users.php
1class Users extends ControllerBase{
2
3   public function index(){}
4
5   public function display($idUser){
6
7   }
8}

Ajoute l’action « display » avec une route associée :

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

Résultat :

app/controllers/Users.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class Users extends ControllerBase{
 6
 7   public function index(){}
 8
 9   #[Route('/users/display/{idUser}')]
10   public function display($idUser){
11
12   }
13}

Ajoute l’action search avec plusieurs paramètres :

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

Résultat :

app/controllers/Users.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class Users extends ControllerBase{
 6
 7   public function index(){}
 8
 9   #[Route('/users/display/{idUser}')]
10   public function display($idUser){
11
12   }
13
14   public function search($name,$address=''){
15
16   }
17}

Ajoute l’action search et crée la vue associée :

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

Création de modèle

Note

Vous pouvez vérifier les paramètres de connexion à la base de données dans le fichier app/config/config.php avant d’exécuter ces commandes.

Pour générer un modèle correspondant à la table user dans la base de données :

Ubiquity model user

Création de tous les modèles

Pour générer tous les modèles à partir de la base de données :

Ubiquity all-models

Initialisation du cache

Pour initialiser le cache du routeur (basé sur les annotations dans les contrôleurs) et de l’ORM (basé sur les annotations dans les modèles) :

Ubiquity init-cache

URLs

Comme de nombreux autres frameworks, si vous utilisez le routeur avec son comportement par défaut, il existe une relation biunivoque entre une chaîne d’URL et la classe/méthode de contrôleur correspondante. Les segments d’une URI suivent normalement ce modèle : : :

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

Méthode par défaut

Lorsque l’URL est composée d’une seule partie, correspondant au nom d’un contrôleur, la méthode index du contrôleur est automatiquement appelée :

URL :

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

Controller :

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

Paramètres requis

Si la méthode sollicitée a des paramètres obligatoires, ils doivent être passés dans l’URL :

Controller :

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

Urls valides :

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

Paramètres optionnels

La méthode appelée peut avoir des paramètres optionnels.

Si un paramètre n’est pas présent dans l’URL, la valeur par défaut du paramètre est utilisée.

Controller :

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

Urls valides :

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

Sensibilité à la casse

Sur les systèmes Unix, le nom des contrôleurs est sensible à la casse.

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)

Personnalisation du routage

Le Routeur et les attributs ou annotations dans les classes contrôleur permettent de personnaliser les URLs.

Routeur

Le routage peut être utilisé en plus du mécanisme par défaut qui associe controller/action/{paramètres} à une url.

Routes dynamiques

Les routes dynamiques sont définies à l’exécution.
Il est possible de les déclarer dans le fichier app/config/services.php.

Important

Les routes dynamiques ne devraient être utilisées que si la situation l’exige :

  • Dans le cas d’une micro-application

  • Si une route doit être définie dynamiquement

Dans tous les autres cas, il est conseillé de déclarer les routes avec des annotations ou attributs, afin de bénéficier du cache.

Callback routes

Les routes Ubiquity les plus basiques sont définies à partir d’une Closure.
Dans le contexte des micro-applications, cette méthode évite de devoir créer un contrôleur.

app/config/services.php
1     use Ubiquity\controllers\Router;
2
3     Router::get("foo", function(){
4             echo 'Hello world!';
5     });

Les routes de type callback peuvent être définies pour toutes les méthodes http :

  • Router::post

  • Router::put

  • Router::delete

  • Router::patch

  • Router::options

Controller routes

Les routes peuvent aussi être définies de manière plus conventionnelle à partir de l’action d’un contrôleur :

app/config/services.php
1     use Ubiquity\controllers\Router;
2
3     Router::addRoute('bar', \controllers\FooController::class,'index');

La méthode FooController::index() sera accessible via l’url /bar.

Dans ce cas, le contrôleur FooController doit hériter de Ubiquity\controllers\Controller ou l’un de ces dérivés, et doit implémenter la méthode index :

app/controllers/FooController.php
1     namespace controllers;
2
3     class FooController extends ControllerBase{
4
5             public function index(){
6                     echo 'Hello from foo';
7             }
8     }

Route par défaut

La route par défaut correspond au path /.
Elle peut être définie en utilisant le path réservé _default

app/config/services.php
1     use Ubiquity\controllers\Router;
2
3     Router::addRoute("_default", \controllers\FooController::class,'bar');

Routes statiques

Les routes statiques sont définies via annotations ou en utilisant les attribut php natifs depuis Ubiquity 2.4.0.

Note

Ces annotations ou attributs ne sont jamais lus à l’exécution.
Il est nécessaire de ré-initialiser le cache du router pour prendre en compte les changements effectués sur les routes.

Création

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6
 7    #[Route('products')]
 8    public function index(){}
 9
10}

La méthode Products::index() sera accessible par l’url /products.

Note

Les slash initial ou terminal sont ignorés dans le path. Les routes suivantes sont donc équivalentes :
  • #[Route('products')]

  • #[Route('/products')]

  • #[Route('/products/')]

Paramètres de routes

Une route peut avoir des paramètres :

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

Paramètres optionnels des routes

Une route peut définir des paramètres optionnels, si la méthode associée a des arguments optionnels :

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

Route requirements

Il est possible d’ajouter des spécifications sur les variables passées dans l’url via l’attribut requirements.

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6   ...
 7   #[Route('products/all/{pageNum}/{countPerPage}',requirements: ["pageNum"=>"\d+","countPerPage"=>"\d?"])]
 8   public function list($pageNum,$countPerPage=50){
 9      // ...
10   }
11}
La route définie correspond à ces urls :
  • products/all/1/20

  • products/all/5/

mais pas avec celle-ci :
  • products/all/test

Types des paramètres

La déclaration de route prend en compte les types de données passés à l’action, ce qui évite d’ajouter des requirements pour les types simples (int, bool, float).

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6   ...
 7   #[Route('products/{productNumber}')]
 8   public function one(int $productNumber){
 9      // ...
10   }
11}
La route définie correspond à ces urls :
  • products/1

  • products/20

mais pas avec celle-ci :
  • products/test

Valeurs possibles par type de données :
  • int: 1

  • bool: 0 or 1

  • float: 1 1.0

Méthodes http des routes

Il est possible de spécifier la ou les méthodes http associées à une route :

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6
 7   #[Route('products',methods: ['get','post'])]
 8   public function index(){}
 9
10}

L’attribut methods peut accepter plusieurs méthodes :
@route("testMethods","methods"=>["get","post","delete"])
#[Route('testMethods', methods: ['get','post','delete'])]

L’annotation @route ou l’attribut Route correspondent à l’ensemble des méthodes http.
Une annotation spécifique existe pour chaque méthode http :

  • @get => Get

  • @post => Post

  • @put => Put

  • @patch => Patch

  • @delete => Delete

  • @head => Head

  • @options => Options

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Get;
 4
 5class ProductsController extends ControllerBase{
 6
 7   #[Get('products')]
 8   public function index(){}
 9
10}

Nom de route

Il est possible de spécifier le nom name d’une route pour faciliter l’accès à l’url associée.
Si l’attribut name n’est pas spécifié, chaque route a un nom par défaut, basé sur le pattern controllerName.methodName.

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6
 7   #[Route('products',name: 'products.index')]
 8   public function index(){}
 9
10}

Génération d’URL

Les routes names peuvent être utilisés pour générer les URLs.

Liens vers une route en Twig

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

Route globale

L’annotation @route peut être utilisée sur une classe contrôleur :

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route('products')]
 6class ProductsController extends ControllerBase{
 7   ...
 8   #[Route('/all')]
 9   public function display(){}
10
11}

Dans ce cas, la route définie sur le contrôleur est utilisée en tant que préfixe pour toutes les routes du contrôleur :
La route générée pour l’action display est /product/all

Routes automatiques

Si une route globale est définie, il est possible d’ajouter toutes les actions du contrôleur en tant que routes (en utilisant le préfixe global), en spécifiant le paramètre automated :

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route('/products',automated: true)]
 6class ProductsController extends ControllerBase{
 7
 8   public function index(){}
 9
10   public function generate(){}
11
12   public function display($id){}
13
14}
L’attribut automated définit 3 routes depuis ProductsController :
  • /product/(index/)?

  • /product/generate

  • /product/display/{id}

Routes héritées

Avec l’attribut inherited, il est possible de générer les routes déclarées dans la classe de base, ou de générer automatiquement les routes associées aux actions de la classe de base, si l’attribut automated est mis à true en même temps.

Classe de base :

app/controllers/ProductsBase.php
 1 namespace controllers;
 2
 3 use Ubiquity\attributes\items\router\Route;
 4
 5 abstract class ProductsBase extends ControllerBase{
 6
 7     #[Route('(index/)?')]
 8     public function index(){}
 9
10     #[Route('sort/{name}')]
11     public function sortBy($name){}
12
13 }

La classe dérivée utilisant les membres hérités :

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route('/product',inherited: true)]
 6class ProductsController extends ProductsBase{
 7
 8   public function display(){}
 9
10}
L’attribut inherited définit les 2 routes de ProductsBase:
  • /products/(index/)?

  • /products/sort/{name}

Si les attributs automated et inherited sont utilisés en conjonction, les actions de la classe de base sont également ajoutées aux routes.

Paramètres globaux de routes

La partie globale d’une route peut définir des paramètres, qui seront passés dans toutes les routes générées.
Ces paramètres peuvent être récupérés par le biais d’un membre de données public :

app/controllers/FooController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route('/foo/{bar}',automated: true)]
 6class FooController {
 7
 8   public string $bar;
 9
10   public function display(){
11       echo $this->bar;
12   }
13
14}

L’accès à l’url /foo/bar/display affiche le contenu du membre bar.

Routes avec préfixe global

Si la route globale est définie sur un contrôleur, toutes les routes générées dans ce contrôleur sont précédées du préfixe.
Il est possible d’introduire explicitement des exceptions sur certaines routes, en utilisant le préfixe #/.

app/controllers/FooController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route('/foo',automated: true)]
 6class FooController {
 7
 8   #[Route('#/noRoot')]
 9   public function noRoot(){}
10
11}

Le contrôleur définit l’url /noRoot url, qui n’est pas préfixée par /foo.

Priorité des routes

Le paramètre prority d’une route permet à celle ci d’être résolue avec une plus ou moins grande priorité.

Plus le paramètre de priorité est élevé, plus la route sera définie au début de la pile des routes en cache.

Dans l’exemple ci-dessous, la route produits/all sera définie avant la route /produits.

app/controllers/ProductsController.php
 1namespace controllers;
 2
 3use Ubiquity\attributes\items\router\Route;
 4
 5class ProductsController extends ControllerBase{
 6
 7   #[Route('products', priority: 1)]
 8   public function index(){}
 9
10   #[Route('products/all', priority: 10)]
11   public function all(){}
12
13}

La valeur par défaut pour priority est 0.

Mise en cache de réponse d’une route

Il est possible de mettre en cache la réponse produite par une route :

Dans ce cas, la réponse est en cache et n’est plus générée dynamiquement.

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

Durée du cache

La duration est exprimée en secondes, si elle est omise, la validité du cache est infinie.

#[Route('products/all', cache: true, duration: 3600)]
public function all(){}

Expiration du cache

Il est possible de forcer le rechargement de la réponse en supprimant le cache associé.

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

Mise en cache des routes dynamiques

Les routes dynamiques peuvent également être mises en cache.

Important

Cette possibilité n’a d’intérêt que si cette mise en cache n’est pas faite en production, mais au moment de l’initialisation du cache.

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

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

Vérification des routes avec les devtools :

Ubiquity info:routes
_images/info-routes.png

Gestion des erreurs (404 & 500)

Routage par défaut

Avec le système de routage par défaut (le couple contrôleur+action définissant une route), un gestionnaire d’erreurs peut être redéfini pour personnaliser la gestion des erreurs.

Dans le fichier de configuration app/config/config.php, ajoutez la clé onError, associée à un callback définissant les messages d’erreur :

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

Implémente l’action sollicitée p404 dans 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 avec annotations

Il suffit dans ce cas d’ajouter une dernière route désactivant le système de routage par défaut, et correspondant à la gestion de l’erreur 404 :

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>";
}

Contrôleurs

Un contrôleur est une classe PHP héritant de Ubiquity\controllers\Controller et fournissant un point d’entrée dans l’application.
Les contrôleurs et leurs méthodes définissent les URLs accessibles.

Création de contrôleur

La méthode la plus facile pour créer un contrôleur est de le faire depuis les devtools.

A partir de l’invite de commande, aller dans le dossier du projet.
Pour créer le contrôleur Products, utiliser la commande :

Ubiquity controller Products

Le contrôleur Products.php créé dans le dossier app/controllers du projet.

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

Il est maintenant possible d’accéder à l’URL (la méthode index est sollicitée par défaut) :

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

Note

Un contrôleur peut être créé manuellement. Dans ce cas, il doit respecter les règles suivantes :

  • La classe doit être définie dans le dossier app/controllers

  • Le nom de la classe doit correspondre au nom du fichier php

  • La classe doit hériter de ControllerBase et doit être définie dans le namespace controllers

  • Elle doit surdéfinir la méthode abstraite index

Méthodes

public

Le second segment de l’URL détermine la méthode publique du contrôleur sollicitée.
La méthode « index » est appelée par défaut, si le second segment est vide.

app/controllers/First.php
1namespace controllers;
2class First extends ControllerBase{
3
4   public function hello(){
5      echo "Hello world!";
6   }
7
8}

La méthode hello du contrôleur First met à disposition les URL suivantes :

example.com/First/hello

Arguments de méthode

Les arguments d’une méthode doivent être passés dans l’url, excepté s’ils sont optionnels.

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

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

}

La méthode hello du contrôleur First met à disposition les URL suivantes :

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

private

Les méthodes privées ou protégées ne sont pas accessibles depuis l’URL.

Contrôleur par défaut

Le contrôleur par défaut peut être défini par le biais du Router, dans le fichier services.php

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

Dans ce cas, l’accès à l’URL « exemple.com/ » charge le contrôleur First et appelle la méthode par défaut index.

Chargement des vues

chargement

Les vues sont stockées dans le dossier app/views. Elles sont chargées à partir des méthodes du contrôleur.
Par défaut, il est possible de créer des vues en php, ou avec twig.
Twig est le moteur de template par défaut pour les fichiers html.

Chargement des vues php

Si l’extension du fichier n’est pas spécifiée, la méthode loadView charge un fichier php.

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

Si l’extension du fichier est html, la méthode loadView charge un fichier twig html.

app/controllers/First.php
namespace controllers;
class First extends ControllerBase{
   public function displayTwig(){
      //loads the view app/views/index.html
      $this->loadView("index.html");
   }
}
Chargement par défaut de vue

Si vous utilisez la méthode de dénomination des vues par défaut :
La vue par défaut associée à une action dans un contrôleur est située dans le dossier views/controller-name/action-name :

views
           Users
          info.html
app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4   ...
5   public function info(){
6      $this->loadDefaultView();
7   }
8}

Variables associées à une vue

Une des missions du contrôleur est de passer des variables à la vue.
Ceci peut être fait au chargement de la vue, à partir d’un tableau associatif :

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]);
   }
}

Les clés du tableau associatif créent des variables du même nom dans la vue.
Utilisation de ces variables dans Twig :

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

Les variables peuvent également être passées avant le chargement de la vue :

//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');

retour de l’affichage d’une vue dans une chaîne

Il est possible de charger une vue, et de retourner le résultat dans une chaîne, en mettant à true le 3ème paramètre de la méthode loadview :

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

chargement de plusieurs vues

Une action peut charger plusieurs vues

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

Une vue est souvent partielle. Il est donc important de ne pas intégrer systématiquement les balises html et body définissant une page html complète.

organisation des vues

Il est conseillé d’organiser les vues en dossiers. La méthode la plus recommandée est de créer un dossier par contrôleur, et d’y stocker les vues associées.
Pour charger la vue index.html, stockée dans app/views/First :

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

initialize et finalize

La méthode initialize est automatiquement appelée avant chaque action sollicitée, la méthode finalize après chaque action.

Exemple d’utilisation des méthodes initialize et finalize créées automatiquement au sein d’un nouveau projet :

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 );
      }
   }
}

Contrôle d’accès

Le contrôle d’accès à un contrôleur peut être effectué manuellement, à l’aide des méthodes isValid et onInvalidControl.

La méthode isValid doit retourner un booléen permettant de définir si l’action sollicitée passée en paramètre est accessible.

Dans l’exemple suivant, l’accès aux actions du contrôleur IndexController n’est possible que si une variable de session activeUser existe :

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

Si la variable activeUser n’existe pas, une erreur unauthorized 401 est renvoyée.

La méthode onInvalidControl permet de personnalisé le comportement de l’accès non autorisé :

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>

Il est aussi possible de gérer le contrôle d’accès à partir des AuthControllers

Redirection

Une redirection n’est pas un simple appel à une action d’un contrôleur.
La redirection sollicite également les méthodes initialize et finalize, ainsi que le contrôle d’accès.

La méthode forward peut être invoquée sans la sollicitation des méthodes initialize et finalize.

Il est possible d’effectuer une redirection vers une route par son nom.

Injection de dépendances

Voir Dependency injection

namespaces

Le namespace des contrôleurs est défini par défaut à controllers dans le fichier app/config/config.php.

Classe de base

L’héritage peut être utiliser pour factoriser le comportement des contrôleurs.
La classe BaseController est créée par défaut au sein des nouveaux projets dans cette logique.

Classe de base contrôleur spécifiques

Classe contrôleur

rôle

Contrôleur

Classe de base pour tous les contrôleurs

SimpleViewController

Classe de base associée à un moteur de template php basique (pour utilisation avec des micro-services)

SimpleViewAsyncController

Classe de base associée avec un moteur de template php pour les serveurs asynchrones

Evènements

Note

Le module Events utilise la classe statique EventsManager pour gérer les évènements.

Framework core events

Ubiquity émet des événements lors des différentes phases de soumission d’une requête.
Ces événements sont relativement peu nombreux, afin de limiter leur impact sur les performances.

Partie

Nom d’évènement

Paramètres

Se produit quand

ViewEvents

BEFORE_RENDER

viewname, parameters

Avant d’afficher une vue

ViewEvents

AFTER_RENDER

viewname, parameters

Après l’affichage d’une vue

DAOEvents

GET_ALL

objects, classname

Après le chargement d’un ensemble d’objets

DAOEvents

GET_ONE

object, classname

Après le chargement d’un objet

DAOEvents

BEFORE_UPDATE

instance

Avant la mise à jour d’un objet

DAOEvents

AFTER_UPDATE

instance, result

Après mise à jour d’un objet

DAOEvents

BEFORE_INSERT

instance

Avant l’insertion d’un objet

DAOEvents

AFTER_INSERT

instance, result

Après l’insertion d’un objet

RestEvents

BEFORE_INSERT

instance

Avant l’insertion d’un objet

RestEvents

BEFORE_UPDATE

instance

Avant la mise à jour d’un objet

Note

Il n’y a pas d’événement BeforeAction et AfterAction, puisque les méthodes initialize et finalize de la classe contrôleur effectuent ces opérations.

Ecouter un évènement

Exemple 1 :

Ajout d’une propriété _updated sur les instances modifiées dans la base de données :

app/config/services.php
 1use Ubiquity\events\EventsManager;
 2use Ubiquity\events\DAOEvents;
 3
 4...
 5
 6     EventsManager::addListener(DAOEvents::AFTER_UPDATE, function($instance,$result){
 7             if($result==1){
 8                     $instance->_updated=true;
 9             }
10     });

Note

Les paramètres passés à la fonction de callback varient en fonction de l’évènement écouté.

Exemple 2 :

Modification de l’affichage de la vue

app/config/services.php
1use Ubiquity\events\EventsManager;
2use Ubiquity\events\ViewEvents;
3
4...
5
6     EventsManager::addListener(ViewEvents::AFTER_RENDER,function(&$render,$viewname,$datas){
7             $render='<h1>'.$viewname.'</h1>'.$render;
8     });

Créer ses propres évènements

Exemple :

Création d’un évènement pour compter et mémoriser le nombre d’affichages par action :

app/eventListener/TracePageEventListener.php
 1     namespace eventListener;
 2
 3     use Ubiquity\events\EventListenerInterface;
 4     use Ubiquity\utils\base\UArray;
 5
 6     class TracePageEventListener implements EventListenerInterface {
 7             const EVENT_NAME = 'tracePage';
 8
 9             public function on(&...$params) {
10                     $filename = \ROOT . \DS . 'config\stats.php';
11                     $stats = [ ];
12                     if (file_exists ( $filename )) {
13                             $stats = include $filename;
14                     }
15                     $page = $params [0] . '::' . $params [1];
16                     $value = $stats [$page] ?? 0;
17                     $value ++;
18                     $stats [$page] = $value;
19                     UArray::save ( $stats, $filename );
20             }
21     }

Enregistrement d’évènements

Enregistrement de l’évènement TracePageEventListener dans services.php :

app/config/services.php
1     use Ubiquity\events\EventsManager;
2     use eventListener\TracePageEventListener;
3
4     ...
5
6     EventsManager::addListener(TracePageEventListener::EVENT_NAME, TracePageEventListener::class);

Déclencher des évènements

Un évènement peut être déclenché depuis n’importe quel contexte, mais il est plus logique de le faire depuis la méthode initialize du contrôleur de base.

app/controllers/ControllerBase.php
 1     namespace controllers;
 2
 3     use Ubiquity\controllers\Controller;
 4     use Ubiquity\utils\http\URequest;
 5     use Ubiquity\events\EventsManager;
 6     use eventListener\TracePageEventListener;
 7     use Ubiquity\controllers\Startup;
 8
 9     /**
10      * ControllerBase.
11      **/
12     abstract class ControllerBase extends Controller{
13             protected $headerView = "@activeTheme/main/vHeader.html";
14             protected $footerView = "@activeTheme/main/vFooter.html";
15             public function initialize() {
16                     $controller=Startup::getController();
17                     $action=Startup::getAction();
18                     EventsManager::trigger(TracePageEventListener::EVENT_NAME, $controller,$action);
19                     if (! URequest::isAjax ()) {
20                             $this->loadView ( $this->headerView );
21                     }
22             }
23             public function finalize() {
24                     if (! URequest::isAjax ()) {
25                             $this->loadView ( $this->footerView );
26                     }
27             }
28     }

Le résultat dans 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
     );

Optimisation de l’enregistrement d’évènements

Il est préférable de mettre en cache l’enregistrement des écouteurs, afin d’optimiser leur temps de chargement :

Créer un script client, ou une action contrôleur (non accessible en mode production) :

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();
}

Après exécution, le cache est généré dans le fichier app/cache/events/events.cache.php.

Une fois le cache généré, le fichier services.php doit juste inclure cette ligne :

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

Injection de dépendances

Note

Pour des raisons de performances, l’injection de dépendances n’est pas utilisée dans le cœur du framework.

L’injection de dépendances (DI) est un modèle de conception utilisé pour mettre en œuvre l’IoC.
Il permet de créer des objets dépendants en dehors d’une classe et de fournir ces objets à une classe de différentes manières. En utilisant l’injection de dépendances, nous déplaçons la création et la liaison des objets dépendants en dehors de la classe qui en dépend.

Note

Ubiquity ne supporte que l’injection de propriétés, afin de ne pas recourir à l’introspection à l’exécution.
Seuls les contrôleurs supportent l’injection de dépendances.

Autowiring de service

Création de service

Créer un service

app/services/Service.php
 1namespace services;
 2
 3     class Service{
 4         public function __construct($ctrl){
 5             echo 'Service instanciation in '.get_class($ctrl);
 6         }
 7
 8         public function do($someThink=""){
 9             echo 'do '.$someThink ."in service";
10         }
11     }

Autowiring dans un contrôleur

Créer un contrôleur utilisant le service

app/services/Service.php
 1     namespace controllers;
 2
 3      /**
 4      * Controller Client
 5      **/
 6     class ClientController extends ControllerBase{
 7
 8             /**
 9              * @autowired
10              * @var services\Service
11              */
12             private $service;
13
14             public function index(){}
15
16             /**
17              * @param \services\Service $service
18              */
19             public function setService($service) {
20                     $this->service = $service;
21             }
22     }

Dans l’exemple ci-dessus, Ubiquity recherche et injecte $service lorsque ClientController est créé.

L’annotation @autowired nécessite que :
  • Le type à instancier soit déclaré avec l’annotation @var

  • La propriété $service ait un setteur, ou soit déclarée publique

Etant donné que les annotations ne sont jamais lues à l’exécution, il est nécessaire de générer le cache des contrôleurs :

Ubiquity init-cache -t=controllers

Reste à vérifier que le service est bien injecté, en allant à l’adresse /ClientController.

Injection de service

Service

Créons maintenant un deuxième service, nécessitant une initialisation spéciale.

app/services/ServiceWithInit.php
 1     class ServiceWithInit{
 2             private $init;
 3
 4             public function init(){
 5                     $this->init=true;
 6             }
 7
 8             public function do(){
 9                     if($this->init){
10                             echo 'init well initialized!';
11                     }else{
12                             echo 'Service not initialized';
13                     }
14             }
15     }

Injection dans le contrôleur

app/controllers/ClientController.php
 1namespace controllers;
 2
 3      /**
 4      * Controller Client
 5      **/
 6     class ClientController extends ControllerBase{
 7
 8             /**
 9              * @autowired
10              * @var \services\Service
11              */
12             private $service;
13
14             /**
15              * @injected
16              */
17             private $serviceToInit;
18
19             public function index(){
20                     $this->serviceToInit->do();
21             }
22
23             /**
24              * @param \services\Service $service
25              */
26             public function setService($service) {
27                     $this->service = $service;
28             }
29
30             /**
31              * @param mixed $serviceToInit
32              */
33             public function setServiceToInit($serviceToInit) {
34                     $this->serviceToInit = $serviceToInit;
35             }
36
37     }

Di déclaration

Dans app/config/config.php, créez une nouvelle clé pour la propriété serviceToInit à injecter dans la partie di.

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

générer le cache des contrôleurs :

Ubiquity init-cache -t=controllers

Vérifier que le service est bien injecté en allant à l’adresse /ClientController.

Note

Si le même service doit être utilisé dans plusieurs contrôleurs, utilisez la notation avec joker :

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

Injection avec un nom

Si le nom du service à injecter est différent de la clé du tableau di, il est possible d’utiliser l’attribut name de l’annotation @injected.

Dans app/config/config.php, créez une nouvelle clé pour la propriété serviceToInit à injecter dans la partie di.

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

Injection de service à l’exécution

Il est possible d’injecter des services à l’exécution, sans qu’ils aient été précédemment déclarés dans une classe contrôleur.

app/services/RuntimeService.php
1namespace services;
2
3     class RuntimeService{
4         public function __construct($ctrl){
5             echo 'Service instanciation in '.get_class($ctrl);
6         }
7     }

Dans app/config/config.php, ajouter la clé @exec dans la partie di.

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

Avec cette déclaration, le membre $rService, instance de RuntimeService, est injecté dans tous les contrôleurs.
Il est donc conseillé d’utiliser les commentaires de javadoc pour déclarer $rService dans les contrôleurs qui l’utilisent (pour obtenir la complétion de code sur $rService dans votre IDE).

app/controllers/MyController.php
 1namespace controllers;
 2
 3      /**
 4      * Controller Client
 5      * property services\RuntimeService $rService
 6      **/
 7     class MyController extends ControllerBase{
 8
 9             public function index(){
10                     $this->rService->do();
11             }
12     }

Contrôleurs CRUD

Les contrôleurs CRUD vous permettent d’effectuer des opérations de base sur une classe Modèle :
  • Create

  • Read

  • Update

  • Delete

Note

Depuis la version 2.4.6, deux types de CrudController existent :

  • ResourceCrudController associé à un modèle

  • MultiResourceCRUDController, affichant un index et permettant de naviguer entre les modèles.

ResourceCrudController

Création

Dans l’interface d’administration (webtools), activez la partie Controllers, et choisissez Resource Crud controller :

_images/speControllerBtn.png
Remplissez ensuite le formulaire :
  • Entrez le nom du contrôleur

  • Sélectionnez le modèle associé

  • Cliquez ensuite sur le bouton de validation

_images/createCrudForm1.png

Description des caractéristiques

Le contrôleur généré :

app/controllers/Products.php
 1<?php
 2namespace controllers;
 3
 4 /**
 5 * CRUD Controller UsersController
 6 **/
 7class UsersController extends \Ubiquity\controllers\crud\CRUDController{
 8
 9     public function __construct(){
10             parent::__construct();
11             $this->model= models\User::class;
12     }
13
14     public function _getBaseRoute():string {
15             return 'UsersController';
16     }
17}

Testez le contrôleur créé en cliquant sur le bouton « get » devant l’action index :

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

En cliquant sur une ligne de la dataTable (instance), on affiche les objets associés à l’instance (action details) :

_images/usersControllerIndex1-details.png

Utilisation de la zone de recherche :

_images/usersControllerSearch1.png
Création (action newModel)

Il est possible de créer une instance en cliquant sur le bouton « add ».

_images/addNewModelBtn.png

Le formulaire par défaut pour ajouter une instance de User :

_images/usersControllerNew1.png
Mise à jour (action update)

Le bouton d’édition sur chaque ligne vous permet d’éditer une instance.

_images/editModelBtn.png

Le formulaire par défaut pour ajouter une instance de User :

_images/usersControllerEdit1.png
Suppression (action delete)

Le bouton de suppression sur chaque ligne vous permet de supprimer une instance.

_images/deleteModelBtn.png

Affichage du message de confirmation avant la suppression :

_images/usersControllerDelete1.png

Personnalisation

Créez à nouveau un ResourceCrudController à partir de l’interface d’administration :

_images/createCrudForm2.png

Il est désormais possible de personnaliser le module en utilisant la surdéfinition.

Aperçu
_images/crud-schema.png
Surdéfinition des classes
Méthodes de ResourceCRUDController à surdéfinir

Méthode

Signification

Retour par défaut

routes

index()

Default page : liste de toutes les instances

edit($modal= »no », $ids= » »)

Edite une instance

newModel($modal= »no »)

Crée une instance

display($modal= »no »,$ids= » »)

Affiche une instance

delete($ids)

Supprime une instance

update()

Affiche le résultat de la mise à jour d’une instance

showDetail($ids)

Affiche les membres associés avec des clés étrangères

refresh_()

Rafraîchit la zone correspondant à la DataTable (#lv)

refreshTable($id=null)

//TO COMMENT

Méthodes de ModelViewer à surdéfinir

Méthode

Signification

Retour par défaut

route index

getModelDataTable($instances, $model,$totalCount,$page=1)

Crée la dataTable et ajoute son comportement

DataTable

getDataTableInstance($instances,$model,$totalCount,$page=1)

Crée la dataTable

DataTable

recordsPerPage($model,$totalCount=0)

Retourne le nombre de lignes à afficher (si null, il n’y a pas de pagination).

null ou 6

getGroupByFields()

Renvoie un tableau de membres sur lequel on peut effectuer un regroupement.

[]

getDataTableRowButtons()

Retourne un tableau de boutons à afficher pour chaque ligne [« edit », »delete », »display »]

[« edit », »delete »]

onDataTableRowButton(HtmlButton $bt, ?string $name)

À surdéfinir pour modifier les boutons des lignes de la table de données.

getCaptions($captions, $className)

Retourne les légendes des en-têtes de colonne

tous les noms des membres

route detail

showDetailsOnDataTableClick()

À remplacer pour s’assurer que le détail d’un objet cliqué est affiché ou non.

true

onDisplayFkElementListDetails($element,$member,$className,$object)

A modifier pour l’affichage de chaque élément dans un composant liste d’objets étrangers

getFkHeaderElementDetails($member, $className, $object)

Renvoie l’en-tête d’un objet étranger (issue de ManyToOne)

HtmlHeader

getFkElementDetails($member, $className, $object)

Renvoie un composant permettant d’afficher un objet étranger (relation manyToOne).

HtmlLabel

getFkHeaderListDetails($member, $className, $list)

Renvoie l’en-tête d’une liste d’objets étrangers (oneToMany ou ManyToMany).

HtmlHeader

getFkListDetails($member, $className, $list)

Renvoie un composant liste permettant d’afficher une collection d’objets étrangers (many).

HtmlList

routes edit et newModel

getForm($identifier, $instance)

Renvoie le formulaire d’ajout ou de modification d’un objet

HtmlForm

formHasMessage()

Détermine si le formulaire a un titre de type message

true

getFormModalTitle($instance)

Renvoie le titre de la modale du formulaire

instance class

onFormModalButtons($btOkay, $btCancel)

Hook pour modifier les boutons modaux

getFormTitle($form,$instance)

Renvoie un tableau associatif définissant le titre du message du formulaire avec les clés « icon », « message », « subMessage ».

HtmlForm

setFormFieldsComponent(DataForm $form,$fieldTypes)

Définit le composant pour chaque champ

onGenerateFormField($field)

Pour faire quelque chose lorsque $field est généré dans le formulaire

isModal($objects, $model)

Condition pour déterminer si le formulaire de modification ou d’ajout est modal pour les objets $model

count($objects)>5

getFormCaptions($captions, $className, $instance)

Renvoie les légendes des champs de formulaire

tous les noms des membres

route display

getModelDataElement($instance,$model,$modal)

Renvoie un objet DataElement pour l’affichage de l’instance.

DataElement

getElementCaptions($captions, $className, $instance)

Renvoie les légendes des champs du DataElement

tous les noms des membres

route delete

onConfirmButtons(HtmlButton $confirmBtn,HtmlButton $cancelBtn)

A surdéfinir pour modifier les boutons de confirmation de suppression

Méthodes CRUDDatas à surdéfinir

Méthode

Signification

Retour par défaut

route index

_getInstancesFilter($model)

Ajoute une condition pour filtrer les instances affichées dans dataTable

1=1

getFieldNames($model)

Retourne les champs à afficher dans l’action index pour $model

tous les noms des membres

getSearchFieldNames($model)

Renvoie les champs à utiliser dans les requêtes de recherche.

tous les noms des membres

routes edit et newModel

getFormFieldNames($model,$instance)

Renvoie les champs à mettre à jour dans les actions edit et newModel pour $model.

tous les noms des membres

getManyToOneDatas($fkClass,$instance,$member)

Renvoie une liste (filtrée) d’objets $fkClass à afficher dans une liste html

toutes les instances de $fkClass

getOneToManyDatas($fkClass,$instance,$member)

Renvoie une liste (filtrée) d’objets $fkClass à afficher dans une liste html

toutes les instances de $fkClass

getManyToManyDatas($fkClass,$instance,$member)

Renvoie une liste (filtrée) d’objets $fkClass à afficher dans une liste html

toutes les instances de $fkClass

route display

getElementFieldNames($model)

Retourne les champs à afficher dans l’action display pour $model

tous les noms des membres

Méthodes CRUDEvents à surdéfinir

Méthode

Signification

Retour par défaut

route index

onConfDeleteMessage(CRUDMessage $message,$instance)

Renvoie le message de confirmation affiché avant la suppression d’une instance.

CRUDMessage

onSuccessDeleteMessage(CRUDMessage $message,$instance)

Renvoie le message affiché après une suppression

CRUDMessage

onErrorDeleteMessage(CRUDMessage $message,$instance)

Renvoie le message affiché lorsqu’une erreur s’est produite lors de la suppression.

CRUDMessage

routes edit et newModel

onSuccessUpdateMessage(CRUDMessage $message)

Renvoie le message affiché lorsqu’une instance est ajoutée ou insérée.

CRUDMessage

onErrorUpdateMessage(CRUDMessage $message)

Renvoie le message affiché lorsqu’une erreur s’est produite lors de la mise à jour ou de l’insertion.

CRUDMessage

onNewInstance(object $instance)

Déclenché après la création d’une nouvelle instance

onBeforeUpdate(object $instance, bool $isNew)

Déclenché avant la mise à jour de l’instance

Toutes les routes

onNotFoundMessage(CRUDMessage $message,$ids)

Renvoie le message affiché lorsqu’une instance n’existe pas.

onDisplayElements($dataTable,$objects,$refresh)

Déclenché après l’affichage des objets dans le dataTable

Méthodes CRUDFiles à surdéfinir

Méthode

Signification

Retour par défaut

Fichiers template

getViewBaseTemplate()

Renvoie le template de base pour toutes les actions Crud si getBaseTemplate renvoie un nom de fichier de template de base.

@framework/crud/baseTemplate.html

getViewIndex()

Retourne le template pour la route index.

@framework/crud/index.html

getViewForm()

Retourne le modèle pour les routes edit et newInstance.

@framework/crud/form.html

getViewDisplay()

Retourne le modèle pour la route display.

@framework/crud/display.html

Urls

getRouteRefresh()

Retourne la route pour rafraîchir la route index.

/refresh_

getRouteDetails()

Renvoie la route de la route detail, lorsque l’utilisateur clique sur une ligne du dataTable.

/showDetail

getRouteDelete()

Retourne la route pour la suppression d’une instance.

/delete

getRouteEdit()

Retourne la route pour éditer une instance

/edit

getRouteDisplay()

Retourne la route pour l’affichage d’une instance

/display

getRouteRefreshTable()

Renvoie la route pour rafraîchir le dataTable.

/refreshTable

getDetailClickURL($model)

Retourne la route associée à une instance de clé étrangère dans la liste

« « 

Structure des templates Twig
index.html
_images/template_index.png
form.html

Affiché dans le bloc frm

_images/template_form.png
display.html

Affiché dans le bloc frm

_images/template_display.png

MultiResourceCrudController

Note

MultiResourceCRUDController affiche un index permettant de naviguer entre les CRUDs des modèles.

Création

Dans l’interface d’administration (webtools), activez la partie Controllers, et choisissez Index Crud controller :

_images/speControllerBtn.png
Remplissez ensuite le formulaire :
  • Entrez le nom du contrôleur

  • Le chemin de la route (qui doit contenir la partie variable {resource})

  • Cliquez ensuite sur le bouton de validation

_images/createIndexCrudForm1.png

Description des caractéristiques

Le contrôleur généré :

app/controllers/CrudIndex.php
 1<?php
 2namespace controllers;
 3use Ubiquity\attributes\items\router\Route;
 4
 5#[Route(path: "/{resource}/crud",inherited: true,automated: true)]
 6class CrudIndex extends \Ubiquity\controllers\crud\MultiResourceCRUDController{
 7
 8             #[Route(name: "crud.index",priority: -1)]
 9             public function index() {
10                     parent::index();
11             }
12
13             #[Route(path: "#//home/crud",name: "crud.home",priority: 100)]
14             public function home(){
15                     parent::home();
16             }
17
18             protected function getIndexType():array {
19                     return ['four link cards','card'];
20             }
21
22             public function _getBaseRoute():string {
23                     return "/".$this->resource."/crud";
24             }
25
26}

Testez le contrôleur créé à l’url /home/crud :

_images/indexCrudController.png

Personnalisation

Créez à nouveau un MultiResourceCrudController à partir de l’interface d’administration :

_images/createIndexCrudForm2.png

Il est maintenant possible de personnaliser le module en utilisant la surdéfinition comme pour les ResourceCRUDControllers.

Classes spécifiques à surdéfinir
Méthodes de MultiResourceCRUDController à surdéfinir

Méthode

Signification

Retour par défaut

routes

home ()

Page d’accueil : liste des modèles

Toutes les routes de « CRUDController ».

Evènements

onRenderView(array &$data)

Avant le rendu de la page d’accueil

Configuration

hasNavigation()

Renvoie la valeur True pour l’affichage du menu déroulant de navigation.

True

getIndexModels()

Renvoie la liste des modèles disponibles à afficher

modèles à partir de la base de données par défaut

getIndexModelsDetails()

Retourne un tableau associatif (titre, icon, url) pour chaque modèle.

[]

getIndexDefaultIcon(string $resource)

Renvoie l’icône d’un modèle

Un animal au hasard

getIndexDefaultTitle(string $resource)

Retourne le titre d’un modèle

Le nom de la ressource

getIndexDefaultDesc(string $modelClass)

Renvoie la description d’un modèle

Le nom complet de la classe

getIndexDefaultUrl(string $resource)

Retourne l’url associée à un modèle

Le chemin de la route

getIndexDefaultMeta(string $modelClass)

Retourne la partie méta pour un modèle

getIndexType()

Définit les classes css du composant de l’index

cards

getModelName()

Renvoie le nom complet du modèle pour $this->resource

A partir du NS par défaut des modèles

Méthodes CRUDFiles à surdéfinir

Méthode

Signification

Retour par défaut

Fichiers template

getViewHome()

Retourne le template de base pour la vue d’accueil

@framework/crud/home.html

getViewItemHome()

Renvoie le template d’un élément de la route home.

@framework/crud/itemHome.html

getViewNav()

Renvoie le template pour l’affichage des modèles dans une liste déroulante.

@framework/crud/nav.html

Note

Toutes les autres méthodes des classes CRUDController, CRUDFiles, CRUDEvents et CRUDDatas peuvent être surdéfinies comme pour le ResourceCRUDController.

Auth Contrôleurs

Les contrôleurs Auth vous permettent d’effectuer une authentification de base avec :
  • se connecter avec un compte

  • création de compte

  • déconnexion

  • contrôleurs avec l’authentification requise

Création

Dans l’interface d’administration (webtools), activez la partie Controllers, et choisissez de créer un Auth controller :

_images/speControllerBtn.png
Remplissez ensuite le formulaire :
  • Entrez le nom du contrôleur (BaseAuthController dans ce cas)

_images/createAuthForm1.png

Le contrôleur généré :

app/controllers/BaseAuthController.php
 1 /**
 2 * Auth Controller BaseAuthController
 3 **/
 4class BaseAuthController extends \Ubiquity\controllers\auth\AuthController{
 5
 6     protected function onConnect($connected) {
 7             $urlParts=$this->getOriginalURL();
 8             USession::set($this->_getUserSessionKey(), $connected);
 9             if(isset($urlParts)){
10                     Startup::forward(implode("/",$urlParts));
11             }else{
12                     //TODO
13                     //Forwarding to the default controller/action
14             }
15     }
16
17     protected function _connect() {
18             if(URequest::isPost()){
19                     $email=URequest::post($this->_getLoginInputName());
20                     $password=URequest::post($this->_getPasswordInputName());
21                     //TODO
22                     //Loading from the database the user corresponding to the parameters
23                     //Checking user creditentials
24                     //Returning the user
25             }
26             return;
27     }
28
29     /**
30      * {@inheritDoc}
31      * @see \Ubiquity\controllers\auth\AuthController::isValidUser()
32      */
33     public function _isValidUser($action=null): bool {
34             return USession::exists($this->_getUserSessionKey());
35     }
36
37     public function _getBaseRoute(): string {
38             return 'BaseAuthController';
39     }
40}

Implémentation de l’authentification

Exemple d’implémentation avec l’interface d’administration : Nous allons ajouter un contrôle d’authentification sur l’interface d’administration.

L’authentification est basée sur la vérification du couple email/password sur le modèle User :

_images/model-user.png

Modification de BaseAuthController

app/controllers/BaseAuthController.php
 1 /**
 2 * Auth Controller BaseAuthController
 3 **/
 4class BaseAuthController extends \Ubiquity\controllers\auth\AuthController{
 5
 6     protected function onConnect($connected) {
 7             $urlParts=$this->getOriginalURL();
 8             USession::set($this->_getUserSessionKey(), $connected);
 9             if(isset($urlParts)){
10                     Startup::forward(implode("/",$urlParts));
11             }else{
12                     Startup::forward("admin");
13             }
14     }
15
16     protected function _connect() {
17             if(URequest::isPost()){
18                     $email=URequest::post($this->_getLoginInputName());
19                     $password=URequest::post($this->_getPasswordInputName());
20                     return DAO::uGetOne(User::class, "email=? and password= ?",false,[$email,$password]);
21             }
22             return;
23     }
24
25     /**
26      * {@inheritDoc}
27      * @see \Ubiquity\controllers\auth\AuthController::isValidUser()
28      */
29     public function _isValidUser($action=null): bool {
30             return USession::exists($this->_getUserSessionKey());
31     }
32
33     public function _getBaseRoute(): string {
34             return 'BaseAuthController';
35     }
36     /**
37      * {@inheritDoc}
38      * @see \Ubiquity\controllers\auth\AuthController::_getLoginInputName()
39      */
40     public function _getLoginInputName(): string {
41             return "email";
42     }
43}

Modification du contrôleur Admin

Modifiez le contrôleur d’administration pour utiliser BaseAuthController :

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

Tester l’interface d’administration à l’adresse /admin:

_images/adminForbidden.png

Après avoir cliqué sur login :

_images/formLogin.png

Si les données d’authentification sont invalides :

_images/invalidCreditentials.png

Si les données d’authentification sont valides :

_images/adminWithAuth.png

Attachement de la zone info-user

Modifier le contrôleur BaseAuthController :

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

La zone _userInfo est désormais présente sur toutes les pages de l’administration :

_images/infoUserZone.png

Elle peut être affichée dans n’importe quelle vue twig :

{{ _userInfo | raw }}

Description des caractéristiques

Personnalisation des templates

template index.html

Le template index.html gère la connexion :

_images/template_authIndex.png

Exemple avec la zone _userInfo :

Créez un nouveau AuthController nommé PersoAuthController :

_images/createAuthForm2.png

Modifiez le template app/views/PersoAuthController/info.html.

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

Changez le contrôleur AuthController Admin :

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

Personnalisation des messages

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

Auto-vérification

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

Limitation du nombre de tentatives

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

Récupération de compte

La récupération de compte se fait en réinitialisant le mot de passe du compte.
Un e-mail de réinitialisation du mot de passe est envoyé, à une adresse e-mail correspondant à un compte actif.

_images/recoveryInit.png
app/controllers/PersoAuthController.php
 1class PersoAuthController extends \controllers\BaseAuth{
 2...
 3 protected function hasAccountRecovery():bool{
 4     return true;
 5 }
 6
 7 protected function _sendEmailAccountRecovery(string $email,string $validationURL,string $expire):bool {
 8     MailerManager::start();
 9     $mail=new AuthAccountRecoveryMail();
10     $mail->to($connected->getEmail());
11     $mail->setUrl($validationURL);
12     $mail->setExpire($expire);
13     return MailerManager::send($mail);
14 }
15
16 protected function passwordResetAction(string $email,string $newPasswordHash):bool {
17     //To implement for modifying the user password
18 }
19
20 protected function isValidEmailForRecovery(string $email):bool {
21     //To implement: return true if a valid account match with this email
22 }
23}
_images/recoveryForm.png

Note

Par défaut, le lien ne peut être utilisé que sur la même machine, dans une période de temps prédéterminée (qui peut être modifiée en surchargeant la méthode accountRecoveryDuration).

Activation de MFA/2FA

L’authentification multi-facteurs peut être activée de manière conditionnelle, sur la base des informations de l’utilisateur préalablement connecté.

Note

La phase 2 de l’authentification est réalisée dans l’exemple ci-dessous en envoyant un code aléatoire par email. La classe AuthMailerClass est disponible dans le paquet Ubiquity-mailer.

app/controllers/PersoAuthController.php
 1class PersoAuthController extends \controllers\BaseAuth{
 2...
 3 /**
 4  * {@inheritDoc}
 5  * @see \Ubiquity\controllers\auth\AuthController::has2FA()
 6  */
 7 protected function has2FA($accountValue=null):bool{
 8     return true;
 9 }
10
11 protected function _send2FACode(string $code, $connected):void {
12     MailerManager::start();
13     $mail=new AuthMailerClass();
14     $mail->to($connected->getEmail());
15     $mail->setCode($code);
16     MailerManager::send($mail);
17 }
18...
19}
_images/2fa-code.png

Note

Il est possible de personnaliser la création du code généré, ainsi que le préfixe utilisé. L’exemple ci-dessous est implémenté avec la bibliothèque robthree/twofactorauth.

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

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

Création de compte

L’activation de la création du compte est également optionnelle :

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

Dans ce cas, la méthode _create doit être surchargée afin de créer le compte :

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;
}

Vous pouvez vérifier la validité/disponibilité du login avant de valider le formulaire de création de compte :

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

Une action de confirmation (vérification par courriel) peut être demandée à l’utilisateur :

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

Il est possible de personnaliser ces parties en surchargeant les méthodes associées, ou en modifiant les interfaces dans les modèles concernés.

Base de données

La classe DAO est en charge des opérations de chargement et de persistance des modèles :

Connexion à la base de données

Vérifiez que les paramètres de connexion à la base de données sont correctement renseignés dans le fichier de configuration :

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

Connexion transparente

Depuis Ubiquity 2.3.0, la connexion à la base de données se fait automatiquement à la première requête :

use Ubiquity\orm\DAO;

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

C’est le cas de toutes les méthodes de la classe DAO utilisées pour effectuer des opérations CRUD.

Connexion explicite

Dans certains cas, cependant, il peut être utile d’établir une connexion explicite à la base de données, notamment pour vérifier la connexion.

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();
}

Connexions multiples

Ajout d’une nouvelle connexion

Ubiquity vous permet de gérer plusieurs connexions à des bases de données.

Avec les webtools

Dans la partie Modèles, choisissez le bouton Add new connection :

_images/add-new-co-btn.png

Définir les paramètres de configuration de la connexion :

_images/new-co.png

Générer des modèles pour la nouvelle connexion:
Les modèles générés incluent l’annotation @database ou l’attribut Database mentionnant leur lien avec la connexion.

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

#[Database('tests')]
#[Table('groupe')]
class Groupe{
    ...
}

Les modèles sont générés dans un sous-dossier de models.

Avec plusieurs connexions, n’oubliez pas d’ajouter la ligne suivante au fichier services.php :

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

La méthode start effectue la correspondance entre chaque modèle et la connexion associée.

Génération des modèles

A partir d’une base de données existante

A partir de zéro

ORM

Note

si vous voulez générer automatiquement les modèles, consultez la partie génération des modèles .

Une classe de type modèle est juste un bon vieil objet php sans héritage.
Les modèles sont situés par défaut dans le dossier appmodels.
Le mappage relationnel d’objets (ORM) repose sur les annotations ou les attributs des membres (depuis PHP8) dans la classe du modèle.

Définition de modèles

Un modèle basique

  • Un modèle doit définir sa clé primaire en utilisant l’annotation @id sur les membres concernés.

  • Les membres sérialisés doivent avoir des getters et setters.

  • Sans autre annotation, une classe correspond à une table du même nom dans la base de données, chaque membre correspond à un champ de cette table.

app/models/User.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\Id;
 4
 5class User{
 6
 7   #[Id]
 8   private $id;
 9
10   private $firstname;
11
12   public function getFirstname(){
13      return $this->firstname;
14   }
15   public function setFirstname($firstname){
16      $this->firstname=$firstname;
17   }
18}

Mappage

Table->Classe

Si le nom de la table est différent du nom de la classe, l’annotation @table permet de préciser le nom de la table.

app/models/User.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\Table;
 4use Ubiquity\attributes\items\Id;
 5
 6#[Table('user')]
 7class User{
 8
 9   #[Id]
10   private $id;
11
12   private $firstname;
13
14   public function getFirstname(){
15      return $this->firstname;
16   }
17   public function setFirstname($firstname){
18      $this->firstname=$firstname;
19   }
20}
Champ->Membre

Si le nom d’un champ est différent du nom du membre de la classe associé, l’annotation @column permet de spécifier un nom de champ différent.

app/models/User.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\Table;
 4use Ubiquity\attributes\items\Id;
 5use Ubiquity\attributes\items\Column;
 6
 7#[Table('user')
 8class User{
 9
10   #[Id]
11   private $id;
12
13   #[Column('column_name')]
14   private $firstname;
15
16   public function getFirstname(){
17      return $this->firstname;
18   }
19   public function setFirstname($firstname){
20      $this->firstname=$firstname;
21   }
22}

Associations

Note

Convention de nommage
Les noms des champs des clés étrangères sont constitués du nom de la clé primaire de la table référencée suivi du nom de la table référencée dont la première lettre est en majuscule.
Exemple : idUser pour la table user dont la clé primaire est id.

ManyToOne

Un utilisateur appartient à une organisation :

_images/manyToOne.png
app/models/User.php
 1 namespace models;
 2
 3use Ubiquity\attributes\items\ManyToOne;
 4use Ubiquity\attributes\items\Id;
 5use Ubiquity\attributes\items\JoinColumn;
 6
 7 class User{
 8
 9   #[Id]
10   private $id;
11
12   private $firstname;
13
14   #[ManyToOne]
15   #[JoinColumn(className: \models\Organization::class, name: 'idOrganization', nullable: false)]
16   private $organization;
17
18   public function getOrganization(){
19      return $this->organization;
20   }
21
22   public function setOrganization($organization){
23      $this->organization=$organization;
24   }
25}

L’annotation @joinColumn ou l’attribut JoinColumn le spécifie :

  • Le membre $organisation est une instance de modelsOrganization.

  • La table user possède une clé étrangère idOrganization faisant référence à la clé primaire de l’organisation.

  • Cette clé étrangère n’est pas nulle => un utilisateur aura toujours une organisation.

OneToMany

Une organisation a de nombreux utilisateurs :

_images/oneToMany.png
app/models/Organization.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\OneToMany;
 4use Ubiquity\attributes\items\Id;
 5
 6class Organization{
 7
 8   #[Id]
 9   private $id;
10
11   private $name;
12
13   #[OneToMany(mappedBy: 'organization', className: \models\User::class)]
14   private $users;
15}

Dans ce cas, l’association est bidirectionnelle.
L’annotation @oneToMany doit juste préciser :

  • La classe de chaque utilisateur dans le tableau users : modelsUser.

  • la valeur de @mappedBy est le nom de l’attribut association-mapping du côté propriétaire : $organisation dans la classe User.

ManyToMany
  • Un utilisateur peut appartenir à des groupes.

  • Un groupe se compose de plusieurs utilisateurs.

_images/manyToMany.png
app/models/User.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\ManyToMany;
 4use Ubiquity\attributes\items\Id;
 5use Ubiquity\attributes\items\JoinTable;
 6
 7class User{
 8
 9   #[Id]
10   private $id;
11
12   private $firstname;
13
14   #[ManyToMany(targetEntity: \models\Group::class, inversedBy: 'users')]
15   #[JoinTable(name: 'groupusers')]
16   private $groups;
17
18}
app/models/Group.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\ManyToMany;
 4use Ubiquity\attributes\items\Id;
 5use Ubiquity\attributes\items\JoinTable;
 6
 7class Group{
 8
 9   #[Id]
10   private $id;
11
12   private $name;
13
14   #[ManyToMany(targetEntity: \models\User::class, inversedBy: 'groups')]
15   #[JoinTable(name: 'groupusers')]
16   private $users;
17
18}

Si les conventions de nommage ne sont pas respectées pour les clés étrangères,
il est possible de spécifier les champs associés.

app/models/Group.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\ManyToMany;
 4use Ubiquity\attributes\items\Id;
 5use Ubiquity\attributes\items\JoinTable;
 6
 7class Group{
 8
 9   #[Id]
10   private $id;
11
12   private $name;
13
14   #[ManyToMany(targetEntity: \models\User::class, inversedBy: 'groupes')]
15   #[JoinTable(name: 'groupeusers',
16   joinColumns: ['name'=>'id_groupe','referencedColumnName'=>'id'],
17   inverseJoinColumns: ['name'=>'id_user','referencedColumnName'=>'id'])]
18   private $users;
19
20}

Annotations ORM

Annotations pour les classes

@annotation

rôle

propriétés

rôle

@database

Définit l’offset de la base de données associée (défini dans le fichier de configuration)

@table

Définit le nom de la table associée.

Annotations pour les membres

@annotation

rôle

propriétés

rôle

@id

Définit la ou les clés primaires.

@column

Spécifie les caractéristiques du champ associé.

name

Nom du champ associé

nullable

true si la valeur peut être nulle

dbType

Type du champ dans la base de données

@transient

Indique que le champ n’est pas persistant.

Associations

@annotation (extends)

rôle

properties [optional]

rôle

@manyToOne

Définit une association à valeur unique avec une autre classe d’entité.

@joinColumn (@column)

Indique la clé étrangère dans l’association manyToOne.

className

Classe du membre

[referencedColumnName]

Nom de la colonne associée

@oneToMany

Définit une association à valeurs multiples avec une autre classe d’entité.

className

Classe des objets du membre

[mappedBy]

Nom de l’attribut de mappage d’association du côté propriétaire.

@manyToMany

Définit une association à valeurs multiples avec une multiplicité de plusieurs à plusieurs.

targetEntity

Classe des objets du membre

[inversedBy]

Nom du membre associé du côté inverse

[mappedBy]

Nom du membre associé du côté propriétaire

@joinTable

Définit la table d’association pour l’association many-to-many.

name

Le nom de la table d’association

[joinColumns]

@column => name et referencedColumnName pour ce côté

[inverseJoinColumns]

@column => name et referencedColumnName pour l’autre côté

DAO

La classe DAO est en charge des opérations de chargement et de persistance des modèles :

Connexion à la base de données

Vérifiez que les paramètres de connexion à la base de données sont correctement renseignés dans le fichier de configuration :

Ubiquity config -f=database

Depuis la version 2.3.0

Le démarrage de la base de données avec DAO::startDatabase($config) dans le fichier services.php est inutile, nul besoin de démarrer la base de données, la connexion est faite automatiquement à la première requête. Utilisez DAO::start() dans le fichier app/config/services.php lorsque vous utilisez plusieurs bases de données (avec la fonctionnalité multi db).

Chargement de données

Chargement d’une instance

Chargement d’une instance de la classe modelsUser avec l’identifiant 5.

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

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

Chargement d’une instance en utilisant une condition :

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

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

Par défaut, les membres définis par une relation belongsTo sont automatiquement chargés.

Chaque utilisateur n’appartient qu’à une seule catégorie :

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

Il est possible d’empêcher ce chargement par défaut ; le troisième paramètre permet de charger ou non les membres belongsTo :

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

Le chargement des membres hasMany doit toujours être explicite ; le troisième paramètre permet le chargement explicite des membres.

Chaque utilisateur fait partie de plusieurs groupes :

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

Soit le modèle ProductDetail correspondant à un produit commandé sur une commande et dont la clé primaire est composite :

app/models/ProductDetail.php
 1 namespace models;
 2
 3use Ubiquity\attributes\items\Id;
 4
 5 class ProductDetail{
 6
 7   #[Id]
 8   private $idProduct;
 9
10   #[Id]
11   private $idCommand;
12
13   ...
14 }

Le deuxième paramètre $keyValues peut être un tableau si la clé primaire est composite :

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

Chargement de plusieurs objets

Chargement d’instances de la classe User :

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

Requêtage utilisant des conditions

Requêtes simples

Le paramètre condition est équivalent à la partie WHERE d’une instruction SQL :

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

Pour éviter les injections SQL et bénéficier de la préparation des statements, il est préférable d’effectuer une requête paramétrée :

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

L’utilisation de U-queries permet de poser des conditions sur les membres associés :

Sélection des utilisateurs dont l’organisation possède le domaine lecnam.net :

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

Il est possible de visualiser la requête générée dans les logs (si le logging est activé) :

_images/uquery-users-log.png

Le résultat peut être vérifié en sélectionnant tous les utilisateurs de cette organisation :

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

Les logs correspondants :

_images/uquery-users-orga-log.png

Comptage

Test de l’existance
if(DAO::exists(User::class,'lastname like ?',['SMITH'])){
    //there's a Mr SMITH
}
Comptage

Pour compter les instances, ce qu’il ne faut pas faire, si les utilisateurs ne sont pas déjà chargés :

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

Ce qui doit être fait :

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

Avec une condition :

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

Avec une condition sur les objets associés :

Nombre d’utilisateurs appartenant à l’organisation nommée OTAN.

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

Modification de données

Ajout d’une instance

Ajout d’une organisation :

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

Ajout d’une instance d’utilisateur, dans une organisation :

$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;
}

Mise à jour d’une instance

Dans un premier temps, l’instance doit être chargée :

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

Suppression d’une instance

Si l’instance est déjà chargée depuis la base de données :

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

Si l’instance n’est pas chargée, il est plus approprié d’utiliser la méthode delete :

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

Suppression de plusieurs instances

Suppression de plusieurs instances sans chargement préalable :

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

Requêtes en masse (bulk)

Les requêtes en masse permettent d’effectuer plusieurs opérations (insertion, modification ou suppression) en une seule requête, ce qui contribue à améliorer les performances.

Insertions en masse

Exemple d’insertion :

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

Mises à jour en masse

Exemple de mise à jour :

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

Suppressions en masse

Exemple de suppression :

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

La méthode DAO::flush() peut être appelée si des insertions, des mises à jour ou des suppressions sont en attente.

Transactions

Transactions explicites

Toutes les opérations DAO peuvent être insérées dans une transaction, ce qui permet d’atomiser une série de changements :

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();
}

En cas de bases de données multiples définies dans la configuration, les méthodes liées aux transactions peuvent prendre l’offset de base de données défini en paramètre.

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

Transactions implicites

Certaines méthodes DAO utilisent implicitement les transactions pour regrouper les opérations d’insertion, de mise à jour ou de suppression.

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

Classe SDAO

La classe SDAO accélère les opérations CRUD pour les classes métier sans relations.

Les modèles doivent dans ce cas déclarer uniquement des membres publics, et ne pas respecter l’encapsulation habituelle.

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

La classe SDAO hérite de DAO et possède les mêmes méthodes pour effectuer des opérations CRUD.

use Ubiquity\orm\DAO;

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

Requêtes DAO préparées

La préparation de certaines requêtes peut améliorer les performances avec les serveurs Swoole, Workerman ou Roadrunner.
Cette préparation initialise les objets qui seront ensuite utilisés pour exécuter la requête.
Cette initialisation est effectuée au démarrage du serveur, ou au démarrage de chaque worker, si un tel événement existe.

Exemple Swoole

Préparation
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 ?');
});
Utilisation
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]);
   ...
}

Requête

Note

Pour toutes les fonctionnalités Http, Ubiquity utilise des classes techniques contenant des méthodes statiques. C’est un choix de conception pour éviter l’injection de dépendances qui dégraderait les performances.

La classe URequest offre des fonctionnalités supplémentaires permettant de manipuler plus facilement les tableaux php natifs $_POST et $_GET.

Récupération de données

A partir de la méthode get

La méthode get renvoie la valeur null si la clé name n’existe pas dans les variables get.

use Ubiquity\utils\http\URequest;

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

La méthode get peut être appelée avec le second paramètre facultatif qui renvoie une valeur si la clé n’existe pas dans les variables get.

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

A partir de la méthode post

La méthode post renvoie la valeur null si la clé name n’existe pas dans les variables post.

use Ubiquity\utils\http\URequest;

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

La méthode post peut être appelée avec le second paramètre facultatif qui renvoie une valeur si la clé n’existe pas dans les variables post.

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

La méthode getPost applique un callback aux éléments du tableau $_POST et les retourne (callback par défaut : htmlEntities) :

$protectedValues=URequest::getPost();

Récupération et affectation de données multiples

Il est courant d’affecter les valeurs d’un tableau associatif aux membres d’un objet.
C’est le cas par exemple lors de la validation d’un formulaire de modification d’objet.

La méthode setValuesToObject effectue cette opération :

Considérons une classe User :

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;
     }
}

Considérons un formulaire pour modifier un utilisateur :

<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>

L’action update du contrôleur Users doit mettre à jour l’instance utilisateur à partir des valeurs POST.
L’utilisation de la méthode setPostValuesToObject permet d’éviter l’affectation des variables postées une à une aux membres de l’objet.
Il est également possible d’utiliser la méthode setGetValuesToObject pour la méthode get, ou setValuesToObject pour affecter les valeurs de tout tableau associatif à un objet.

app/controllers/Users.php
 1 namespace controllers;
 2
 3 use Ubiquity\orm\DAO;
 4 use Uniquity\utils\http\URequest;
 5
 6 class Users extends BaseController{
 7     ...
 8     public function update(){
 9             $user=DAO::getOne("models\User",URequest::post("id"));
10             URequest::setPostValuesToObject($user);
11             DAO::update($user);
12     }
13 }

Note

Les méthodes SetValuesToObject utilisent des setters pour modifier les membres d’un objet. La classe concernée doit donc implémenter des setters pour tous les membres modifiables.

Test de la requête

isPost

La méthode isPost renvoie true si la requête a été soumise via la méthode POST :
Dans le cas ci-dessous, la méthode initialize ne charge la vue vHeader.html que si la requête n’est pas une requête Ajax.

app/controllers/Users.php
 1 namespace controllers;
 2
 3 use Ubiquity\orm\DAO;
 4 use Ubiquity\utils\http\URequest;
 5
 6 class Users extends BaseController{
 7     ...
 8     public function update(){
 9             if(URequest::isPost()){
10                     $user=DAO::getOne("models\User",URequest::post("id"));
11                     URequest::setPostValuesToObject($user);
12                     DAO::update($user);
13             }
14     }
15 }

isAjax

La méthode isAjax renvoie true si la requête est une requête Ajax :

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

isCrossSite

La méthode isCrossSite vérifie que la requête n’est pas cross-site.

Réponse

Note

Pour toutes les fonctionnalités Http, Ubiquity utilise des classes techniques contenant des méthodes statiques. C’est un choix de conception pour éviter l’injection de dépendances qui dégraderait les performances.

La classe UResponse ne gère que les en-têtes, pas le corps de la réponse, qui est conventionnellement fourni par le contenu affiché par les appels utilisés pour écrire des données (echo, print…).

La classe UResponse offre des fonctionnalités supplémentaires permettant de manipuler plus facilement les en-têtes de réponse.

Ajout ou modification d’en-têtes

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

Forcer plusieurs en-têtes du même type :

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

Force le code de réponse HTTP à la valeur spécifiée :

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

Définition d’en-têtes spécifiques

content-type

Définir le type de contenu de la réponse à application/json :

UResponse::asJSON();

Définir le type de contenu de la réponse à text/html :

UResponse::asHtml();

Définir le type de contenu de la réponse à plain/text :

UResponse::asText();

Définir le type de contenu de la réponse à application/xml :

UResponse::asXml();

Définition d’un encodage spécifique (la valeur par défaut est toujours utf-8) :

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

Cache

Forcer la désactivation du cache du navigateur :

UResponse::noCache();

Accept

Définit les types de contenu, exprimés sous forme de types MIME, que le client est en mesure de comprendre.
Voir Acceptation des valeurs par défaut.

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

En-têtes de réponse CORS

Cross-Origin Resource Sharing (CORS) est un mécanisme qui utilise des en-têtes HTTP supplémentaires pour indiquer au navigateur que votre application Web exécutée à une origine (domaine) a la permission d’accéder à des ressources sélectionnées sur un serveur à une origine différente.

Access-Control-Allow-Origin

Réglage de l’origine autorisée :

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

Access-Control-Allow-methods

Définir les méthodes autorisées :

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

Access-Control-Allow-headers

Définir les en-têtes autorisés :

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

Activation globale de CORS

activer CORS pour un domaine avec des valeurs par défaut :

  • Méthodes autorisées : « GET, POST, PUT, DELETE, PATCH, OPTIONS ».

  • En-têtes autorisés : « X-Requested-With, Content-Type, Accept, Origin, Authorization ».

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

Test des en-têtes de réponse

Vérifier si les en-têtes ont été envoyés :

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

Test si le type de contenu de la réponse est application/json :

Important

Cette méthode ne fonctionne que si vous avez utilisé la classe UResponse pour définir les en-têtes.

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

Session

Note

Pour toutes les fonctionnalités Http, Ubiquity utilise des classes techniques contenant des méthodes statiques. C’est un choix de conception pour éviter l’injection de dépendances qui dégraderait les performances.

La classe USession fournit des fonctionnalités supplémentaires pour manipuler plus facilement le tableau natif $_SESSION de php.

Démarrer la session

La session Http est lancée automatiquement si la clé sessionName est renseignée dans le fichier de configuration app/config.php :

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

Si la clé sessionName n’est pas renseignée, il est nécessaire de démarrer la session explicitement pour l’utiliser :

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

Note

Le paramètre name est facultatif mais recommandé pour éviter les conflits de variables.

Création ou modification d’une variable de session

use Ubiquity\utils\http\USession;

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

Récupération de données

La méthode get renvoie la valeur null si la clé name n’existe pas dans les variables de session.

use Ubiquity\utils\http\USession;

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

La méthode get peut être appelée avec le second paramètre facultatif qui renvoie une valeur si la clé n’existe pas dans les variables de session.

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

Note

La méthode session est un alias de la méthode get.

La méthode getAll renvoie toutes les variables de la session :

$sessionVars=USession::getAll();

Test

La méthode exists teste l’existence d’une variable en session.

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

La méthode isStarted vérifie que la session est démarrée.

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

Suppression de variables

La méthode delete permet de supprimer une variable de session :

USession::delete("name");

Clôture explicite de la session

La méthode terminate ferme correctement la session et supprime toutes les variables de session créées :

USession::terminate();

Vues

Ubiquity utilise Twig comme moteur de template par défaut (voir Documentation Twig).
Les vues sont situées dans le dossier app/views. Elles doivent avoir l’extension .html pour être interprétées par Twig.

Ubiquity peut également être utilisé avec un système de vues PHP, pour obtenir de meilleures performances, ou simplement pour permettre l’utilisation de php dans les vues.

Chargement

Les vues sont chargées depuis les contrôleurs :

app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4     ...
5     public function index(){
6                     $this->loadView("index.html");
7             }
8     }
9 }

Chargement de la vue par défaut

Si vous utilisez la méthode de dénomination des vues par défaut :
La vue par défaut associée à une action dans un contrôleur est située dans le dossier views/controller-name/action-name :

views
           Users
          info.html
app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4     ...
5     public function info(){
6                     $this->loadDefaultView();
7             }
8     }
9 }

Chargement et passage de variables

Les variables sont transmises à la vue dans un tableau associatif. Chaque clé crée une variable du même nom dans la vue.

app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4     ...
5     public function display($message,$type){
6                     $this->loadView("users/display.html",["message"=>$message,"type"=>$type]);
7             }
8     }
9 }

Dans ce cas, il est utile d’appeler compact pour créer un tableau contenant des variables et leurs valeurs :

app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4     ...
5     public function display($message,$type){
6                     $this->loadView("users/display.html",compact("message","type"));
7             }
8     }
9 }

Affichage dans une vue

La vue peut alors afficher les variables :

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

Les variables peuvent également avoir des attributs ou des éléments auxquels vous pouvez accéder.

Vous pouvez utiliser un point (.) pour accéder aux attributs d’une variable (méthodes ou propriétés d’un objet PHP, ou éléments d’un tableau PHP), ou la syntaxe dite « d’indice » ([]) :

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

Fonctions supplémentaires Ubiquity

La variable globale app donne accès à des fonctionnalités prédéfinies d’Ubiquity Twig :

  • app est une instance de la classe Framework et donne accès aux méthodes publiques de cette classe.

Obtenir la version installée du framework :

{{ app.version() }}

Renvoie les noms du contrôleur actif et de l’action :

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

Retourne les wrapper classes globales :

Pour la requête :

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

Pour la session :

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

voir Classeframework dans API pour plus d’informations.

Chargement des vue PHP

Désactivez si nécessaire Twig dans le fichier de configuration en supprimant la clé templateEngine.

Créez ensuite un contrôleur qui hérite de « SimpleViewController », ou de « SimpleViewAsyncController » si vous utilisez Swoole ou Workerman :

app/controllers/Users.php
 1 namespace controllers;
 2
 3 use Ubiquity\controllers\SimpleViewController;
 4
 5 class Users extends SimpleViewController{
 6     ...
 7     public function display($message,$type){
 8                     $this->loadView("users/display.php",compact("message","type"));
 9             }
10     }
11 }

Note

Dans ce cas, les fonctions de chargement des assets et des thèmes ne sont pas prises en charge.

Assets

Les assets correspondent aux fichiers javascript, feuilles de style, polices de caractères, images à inclure dans votre application.
Ils se trouvent dans le dossier public/assets.
Il est préférable de séparer les ressources en sous-dossiers par type.

public/assets
      css
         style.css
         semantic.min.css
      js
          jquery.min.js

Intégration de fichiers css ou js :

{{ 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 avec des paramètres supplémentaires :

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

Thèmes

Note

Les thèmes sont totalement inutiles si vous n’avez qu’une seule présentation à appliquer.

Ubiquity supporte des thèmes qui peuvent avoir leurs propres assets et vues selon le modèle du thème à afficher par le contrôleur. Chaque action du contrôleur peut utiliser un thème spécifique, ou peut utiliser le thème par défaut configuré dans le fichier config.php dans templateEngineOptions => array("activeTheme" => "semantic").

Ubiquity est livré avec 3 thèmes par défaut : Bootstrap, Foundation et Semantic-UI.

Installation d’un thème

Avec les devtools, exécuter :

Ubiquity install-theme bootstrap

Le thème installé est l’un des suivants : bootstrap, foundation ou semantic.

Avec webtools, vous pouvez faire de même, à condition que les devtools soient installés et accessibles (dossier Ubiquity ajouté dans le chemin du système) :

_images/themesManager-install-theme.png

Création d’un nouveau thème

Avec les devtools, exécuter :

Ubiquity create-theme myTheme

Création d’un nouveau thème à partir de Bootstrap, Semantic…

Avec les devtools, exécuter :

Ubiquity create-theme myBootstrap -x=bootstrap

Avec les webtools :

_images/themesManager-create-theme.png

Structure et fonctionnement des thèmes

Structure

Dossier vues d’un thème

Les vues d’un thème sont situées dans le dossier app/views/themes/theme-name.

app/views
         themes
                bootstrap
                         main
                              vHeader.html
                              vFooter.html
                semantic
                         main
                              vHeader.html
                              vFooter.html

La classe de base contrôleur est responsable du chargement des vues pour définir l’en-tête et le pied de page de chaque page :

app/controllers/ControllerBase.php
 1     <?php
 2     namespace controllers;
 3
 4     use Ubiquity\controllers\Controller;
 5     use Ubiquity\utils\http\URequest;
 6
 7     /**
 8      * ControllerBase.
 9      **/
10     abstract class ControllerBase extends Controller{
11             protected $headerView = "@activeTheme/main/vHeader.html";
12             protected $footerView = "@activeTheme/main/vFooter.html";
13
14             public function initialize() {
15                     if (! URequest::isAjax ()) {
16                             $this->loadView ( $this->headerView );
17                     }
18             }
19             public function finalize() {
20                     if (! URequest::isAjax ()) {
21                             $this->loadView ( $this->footerView );
22                     }
23             }
24     }

Dossier assets d’un thème

Les assets d’un thème sont créés dans le dossier public/assets/theme-name.

La structure du dossier des assets est souvent la suivante :

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

Changement du thème actif

Changement persistant

activeTheme est défini dans app/config/config.php avec templateEngineOptions => array("activeTheme" => "semantic")

Le thème actif peut être modifié avec les devtools :

Ubiquity config:set --templateEngineOptions.activeTheme=bootstrap

Il est également possible de le faire à partir de la page d’accueil, ou avec les webtools :

Depuis la page d’accueil :

_images/change-theme-home.png

Depuis les webtools :

_images/change-theme-webtools.png

Cette modification peut également être effectuée au moment de l’exécution :

Depuis un contrôleur :

ThemeManager::saveActiveTheme('bootstrap');

Changement local non persistant

Pour définir un thème spécifique pour toutes les actions d’un contrôleur, la méthode la plus simple consiste à surdéfinir la méthode initialize du contrôleur :

app/controllers/Users.php
 1 namespace controllers;
 2
 3 use \Ubiquity\themes\ThemesManager;
 4
 5 class Users extends BaseController{
 6
 7         public function initialize(){
 8             parent::intialize();
 9             ThemesManager::setActiveTheme('bootstrap');
10         }
11     }

Ou si le changement ne doit concerner qu’une seule action :

app/controllers/Users.php
 1 namespace controllers;
 2
 3 use \Ubiquity\themes\ThemesManager;
 4
 5 class Users extends BaseController{
 6
 7         public function doStuff(){
 8             ThemesManager::setActiveTheme('bootstrap');
 9             ...
10         }
11     }

Changement de thème conditionnel, indépendamment du contrôleur :

Exemple avec une modification du thème en fonction d’une variable passée dans l’URL

app/config/services.php
 1use Ubiquity\themes\ThemesManager;
 2use Ubiquity\utils\http\URequest;
 3
 4...
 5
 6ThemesManager::onBeforeRender(function(){
 7             if(URequest::get("th")=='bootstrap'){
 8                     ThemesManager::setActiveTheme("bootstrap");
 9             }
10     });

Support pour appareils mobiles

Ajouter un outil de détection des appareils mobiles.
Installer MobileDetect :

composer require mobiledetect/mobiledetectlib

Il est généralement plus facile de créer des vues différentes par appareil.

Créez un thème spécifique pour la partie mobile (en créant un dossier views/themes/mobile et en y plaçant les vues spécifiques aux appareils mobiles).
Il est important dans ce cas d’utiliser les mêmes noms de fichiers pour la partie mobile et la partie non-mobile.

Il est également conseillé dans ce cas que tous les chargements de vues utilisent l’espace de noms @activeTheme :

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

index.html doit être disponible dans ce cas dans les dossiers views et views/themes/mobile.

Détection globale des mobiles (à partir de services.php)
app/config/services.php
 1use Ubiquity\themes\ThemesManager;
 2
 3...
 4
 5ThemesManager::onBeforeRender(function () {
 6     $mb = new \Mobile_Detect();
 7     if ($mb->isMobile()) {
 8             ThemesManager::setActiveTheme('mobile');
 9     }
10});
Détection locale (depuis un contrôleur)
app/controllers/FooController.php
 1use Ubiquity\themes\ThemesManager;
 2
 3...
 4
 5     public function initialize() {
 6             $mb = new \Mobile_Detect();
 7             if ($mb->isMobile()) {
 8                     ThemesManager::setActiveTheme('mobile');
 9             }
10             parent::initialize();
11     }

Chargement de vues et d’assets

Vues

Pour charger une vue à partir du dossier activeTheme, vous pouvez utiliser l’espace de nom @activeTheme :

app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4
5         public function action(){
6             $this->loadView('@activeTheme/action.html');
7             ...
8         }
9     }

Si le activeTheme est bootstrap, la vue chargée est app/views/themes/bootstrap/action.html.

Vue par défaut

Si vous suivez les conventions de nommage des vues Ubiquity, la vue par défaut chargée pour une action dans un contrôleur lorsqu’un thème est actif est : app/views/themes/theme-name/controller-name/action-name.html.

Par exemple, si le thème actif est bootstrap, la vue par défaut pour l’action display dans le contrôleur Users doit se trouver dans le fichier app/views/themes/bootstrap/Users/display.html.

app/controllers/Users.php
1 namespace controllers;
2
3 class Users extends BaseController{
4
5         public function display(){
6             $this->loadDefaultView();
7             ...
8         }
9     }

Note

Les commandes devtools pour créer un contrôleur ou une action et leur vue associée utilisent le dossier @activeTheme si un thème est actif.

Ubiquity controller Users -v

Ubiquity action Users.display -v

Chargement des assets

Le mécanisme est le même que pour les vues : l’espace de nom @activeTheme fait référence au dossier public/assets/theme-name/.

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

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

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

Si le thème bootstrap est actif, le dossier des assets est public/assets/bootstrap/.

Compilation Css

Pour Bootstrap ou foundation, installez sass :

npm install -g sass

Ensuite, exécutez depuis le dossier racine du projet :

Pour bootstrap:

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

Pour foundation:

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

jQuery Semantic-UI

Par défaut, Ubiquity utilise la bibliothèque phpMv-UI pour la partie UI.
PhpMv-UI permet de créer des composants basés sur Semantic-UI ou Bootstrap et de générer des scripts jQuery en PHP.

Cette bibliothèque est utilisée pour l’interface d’administration webtools.

Intégration

Par défaut, une variable $jquery est injectée dans les contrôleurs au moment de l’exécution.

Cette opération se fait par injection de dépendance, dans app/config.php :

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

Il n’y a donc rien à faire,
mais pour faciliter son utilisation et permettre la complétion de code dans un contrôleur, il est recommandé d’ajouter la documentation de code suivante :

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

     public function index(){}
}

jQuery

Href en requêtes ajax

Créez un nouveau contrôleur et sa vue associée, puis définissez les routes suivantes :

app/controllers/FooController.php
 1namespace controllers;
 2
 3class FooController extends ControllerBase {
 4
 5     public function index() {
 6             $this->loadview("FooController/index.html");
 7     }
 8
 9     /**
10      *
11      *@get("a","name"=>"action.a")
12      */
13     public function aAction() {
14             echo "a";
15     }
16
17     /**
18      *
19      *@get("b","name"=>"action.b")
20      */
21     public function bAction() {
22             echo "b";
23     }
24}

La vue associée :

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

Initialiser le cache du routeur :

Ubiquity init:cache -t=controllers

Testez cette page dans votre navigateur à l’adresse http://127.0.0.1:8090/FooController.

Transformation des requêtes en requêtes Ajax

Le résultat de chaque requête ajax doit être affiché dans une zone de la page définie par son sélecteur jQuery (.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

La variable script_foot contient le script jquery généré par la méthode renderView. Le filtre raw marque la valeur comme étant « sûre », ce qui signifie que dans un environnement où l’échappement automatique est activé, cette variable ne sera pas échappée.

Ajoutons un peu de css pour le rendre plus professionnel :

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 }}

Si nous voulons ajouter un nouveau lien dont le résultat doit être affiché dans une autre zone, il est possible de le spécifier via l’attribut data-target.

La nouvelle action

app/controllers/FooController.php
namespace controllers;

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

La vue associée :

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
Définition des attributs de la requête ajax :

Dans l’exemple suivant, les paramètres passés à la variable attributs de la méthode getHref :

  • supprimer l’historique de la navigation,

  • rendre le loader ajax interne au bouton cliqué.

app/controllers/FooController.php
 1namespace controllers;
 2
 3/**
 4 * @property \Ajax\php\ubiquity\JsUtils $jquery
 5 */
 6class FooController extends ControllerBase {
 7
 8     public function index() {
 9             $this->jquery->getHref('a','.result span', [
10                     'hasLoader' => 'internal',
11                     'historize' => false
12             ]);
13             $this->jquery->renderView("FooController/index.html");
14     }
15     ...
16}

Note

Il est possible d’utiliser la méthode postHref pour utiliser le POST http.

Requêtes ajax classiques

Pour cet exemple, créez la base de données suivante :

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;

Connectez l’application à la base de données, et générez la classe User :

Avec les devtools :

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

Créer un nouveau contrôleur UsersJqueryController.

Ubiquity controller UsersJqueryController -v

Créez les actions suivantes dans UsersJqueryController :

_images/UsersJqueryControllerStructure.png
Action index

L’action index doit afficher un bouton pour obtenir la liste des utilisateurs, chargée via une requête ajax :

app/controllers/UsersJqueryController.php
 1namespace controllers;
 2
 3/**
 4 * Controller UsersJqueryController
 5 *
 6 * @property \Ajax\php\ubiquity\JsUtils $jquery
 7 * @route("users")
 8 */
 9class UsersJqueryController extends ControllerBase {
10
11     /**
12      *
13      * {@inheritdoc}
14      * @see \Ubiquity\controllers\Controller::index()
15      * @get
16      */
17     public function index() {
18             $this->jquery->getOnClick('#users-bt', Router::path('display.users'), '#users', [
19                     'hasLoader' => 'internal'
20             ]);
21             $this->jquery->renderDefaultView();
22     }
23}

La vue par défaut associée à l’action index :

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 }}
Action displayUsers

Tous les utilisateurs sont affichés, et un clic sur un utilisateur doit afficher les détails de l’utilisateur via une requête ajax postée :

app/controllers/UsersJqueryController.php
 1namespace controllers;
 2
 3/**
 4 * Controller UsersJqueryController
 5 *
 6 * @property \Ajax\php\ubiquity\JsUtils $jquery
 7 * @route("users")
 8 */
 9class UsersJqueryController extends ControllerBase {
10...
11     /**
12      *
13      * @get("all","name"=>"display.users","cache"=>true)
14      */
15     public function displayUsers() {
16             $users = DAO::getAll(User::class);
17             $this->jquery->click('#close-bt', '$("#users").html("");');
18             $this->jquery->postOnClick('li[data-ajax]', Router::path('display.one.user', [
19                     ""
20             ]), '{}', '#user-detail', [
21                     'attr' => 'data-ajax',
22                     'hasLoader' => false
23             ]);
24             $this->jquery->renderDefaultView([
25                     'users' => $users
26             ]);
27     }

La vue associée à l’action displayUsers :

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 }}
Action displayOneUser
app/controllers/UsersJqueryController.php
 1namespace controllers;
 2
 3/**
 4 * Controller UsersJqueryController
 5 *
 6 * @property \Ajax\php\ubiquity\JsUtils $jquery
 7 * @route("users")
 8 */
 9class UsersJqueryController extends ControllerBase {
10...
11     /**
12      *
13      * @post("{userId}","name"=>"display.one.user","cache"=>true,"duration"=>3600)
14      */
15     public function displayOneUser($userId) {
16             $user = DAO::getById(User::class, $userId);
17             $this->jquery->hide('#users-content', '', '', true);
18             $this->jquery->click('#close-user-bt', '$("#user-detail").html("");$("#users-content").show();');
19             $this->jquery->renderDefaultView([
20                     'user' => $user
21             ]);
22     }

La vue associée à l’action displayOneUser :

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 }}

Composants Semantic

Nous allons ensuite réaliser un contrôleur implémentant les mêmes fonctionnalités que précédemment, mais en utilisant des composants PhpMv-UI (partie sémantique).

Exemple HtmlButton

Créer un nouveau contrôleur UsersJqueryController.

Ubiquity controller UsersCompoController -v
app/controllers/UsersJqueryController.php
 1namespace controllers;
 2
 3use Ubiquity\controllers\Router;
 4
 5/**
 6 * Controller UsersCompoController
 7 *
 8 * @property \Ajax\php\ubiquity\JsUtils $jquery
 9 * @route("users-compo")
10 */
11class UsersCompoController extends ControllerBase {
12
13     private function semantic() {
14             return $this->jquery->semantic();
15     }
16
17     /**
18      *
19      * @get
20      */
21     public function index() {
22             $bt = $this->semantic()->htmlButton('users-bt', 'Display users');
23             $bt->addIcon('users');
24             $bt->getOnClick(Router::path('display.compo.users'), '#users', [
25                     'hasLoader' => 'internal'
26             ]);
27             $this->jquery->renderDefaultView();
28     }

Note

L’appel à renderView ou renderDefaultView sur l’objet JQuery effectue la compilation du composant et génère le HTML et le JS correspondants.

La vue associée intègre le composant bouton avec le tableau q disponible dans la vue :

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

Le module Normalizer utilise la classe statique NormalizersManager pour gérer la normalisation.

Validateurs

Note

Le module Validators utilise la classe statique ValidatorsManager pour gérer la validation.

La validation permet de vérifier que les données membres d’un objet respectent certaines contraintes.

Ajout de validators

Soit la classe Author que nous souhaitons utiliser dans notre application :

app/models/Author.php
 1     namespace models;
 2
 3     class Author {
 4             /**
 5              * @var string
 6              * @validator("notEmpty")
 7              */
 8             private $name;
 9
10             public function getName(){
11                     return $this->name;
12             }
13
14             public function setName($name){
15                     $this->name=$name;
16             }
17     }

Une contrainte de validation a été ajoutée sur le membre name avec l’annotation @validator, de façon à rendre sa saisie obligatoire.

Génération du cache

Exécuter cette commande dans la console pour générer le cache de la classe Author :

Ubiquity init-cache -t=models

Le cache du validateur est généré dans le fichier app/cache/contents/validators/models/Author.cache.php.

Validation d’instances

Une 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!';
        }
}

Si le name de l’instance de author est vide, l’action devrait afficher :

name : This value should not be empty

La méthode validate retroune un tableau d’instances de ConstraintViolation .

Plusieurs instances

public function testValidateAuthors(){
        $authors=DAO::getAll(Author::class);
        $violations=ValidatorsManager::validateInstances($author);
        foreach($violations as $violation){
                echo $violation.'<br>';
        }
}

Génération des models avec validateurs par défaut

Lorsque les classes sont générées automatiquement à partir de la base de données, des validateurs par défaut sont associés aux membres, basés su les méta-données des champs.

Ubiquity create-model User
app/models/Author.php
 1     namespace models;
 2     class User{
 3             /**
 4              * @id
 5              * @column("name"=>"id","nullable"=>false,"dbType"=>"int(11)")
 6              * @validator("id","constraints"=>array("autoinc"=>true))
 7             **/
 8             private $id;
 9
10             /**
11              * @column("name"=>"firstname","nullable"=>false,"dbType"=>"varchar(65)")
12              * @validator("length","constraints"=>array("max"=>65,"notNull"=>true))
13             **/
14             private $firstname;
15
16             /**
17              * @column("name"=>"lastname","nullable"=>false,"dbType"=>"varchar(65)")
18              * @validator("length","constraints"=>array("max"=>65,"notNull"=>true))
19             **/
20             private $lastname;
21
22             /**
23              * @column("name"=>"email","nullable"=>false,"dbType"=>"varchar(255)")
24              * @validator("email","constraints"=>array("notNull"=>true))
25              * @validator("length","constraints"=>array("max"=>255))
26             **/
27             private $email;
28
29             /**
30              * @column("name"=>"password","nullable"=>true,"dbType"=>"varchar(255)")
31              * @validator("length","constraints"=>array("max"=>255))
32             **/
33             private $password;
34
35             /**
36              * @column("name"=>"suspended","nullable"=>true,"dbType"=>"tinyint(1)")
37              * @validator("isBool")
38             **/
39             private $suspended;
40     }

Ces validateurs peuvent être modifiés.
Les modifications doivent toujours être suivies d’une ré-initialisation du cache des models.

Ubiquity init-cache -t=models

Les informations relatives aux validateurs existants sur les models peuvent être affichées avec les devtools :

Ubiquity info:validation -m=User
_images/info-validation-devtools.png

Retourne les validateurs associés au champ email :

Ubiquity info:validation email -m=User
_images/info-validation-email-devtools.png

Les informations de validation sont également accessibles depuis la partie models des webtools :

_images/info-validation-webtools.png

Types de validators

Basic

Validator

Rôles

Contraintes

Valeurs acceptées

isBool

Vérifie si la valeur est un booléen

true,false,0,1

isEmpty

Vérifie si la valeur n’est pas vide

“”,null

isFalse

Vérifie si la valeur est false

false,”false”,0,”0”

isNull

Vérifie si la valeur est nulle

null

isTrue

Vérifie si la valeur est true

true,”true”,1,”1”

notEmpty

Vérifie si la valeur n’est pas vide

!null && !””

notNull

Vérifie si la valeur n’est pas nulle

!null

type

Vérifie que la valeur est du type {type}

{type}

Comparison

Dates

Multiples

Strings

Transformers

Note

Le module Transformers utilise la classe statique TransformersManager pour gérer les transformations.

Les Transformers sont utilisés pour transformer les données après leur chargement depuis une base de données, ou avant leur affichage dans une vue.

Ajouter des transformers

Soit la classe Author que nous souhaitons utiliser dans notre application :

app/models/Author.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\Transformer;
 4
 5class Author {
 6
 7   #[Transformer('upper')]
 8   private $name;
 9
10   public function getName(){
11      return $this->name;
12   }
13
14   public function setName($name){
15      $this->name=$name;
16   }
17}

Un transformer a été ajouté sur le membre name avec l’annotation @transformer , de façon à mettre en majuscules le nom dans les vues.

Génération du cache

Exécuter la commande suivante dans la console pour générer le cache de la classe Author :

Ubiquity init-cache -t=models

Le cache des transformers est généré avec les méta-données des models dans le dossier app/cache/models/Author.cache.php.

Les informations relatives aux transormers peuvent être affichées à partir des devtools :

Ubiquity info:model -m=Author -f=#transformers
_images/trans-info.png

Utilisation des transformers

Démarrer le manager TransformersManager depuis le fichier app/config/services.php :

app/config/services.php
\Ubiquity\contents\transformation\TransformersManager::startProd();

Le résultat peut être testé depuis l’interface d’administration :

_images/trans-upper.png

ou en créant un contrôleur

app/controllers/Authors.php
 1namespace controllers;
 2
 3class Authors {
 4
 5   public function index(){
 6      DAO::transformersOp='toView';
 7      $authors=DAO::getAll(Author::class);
 8      $this->loadDefaultView(['authors'=>$authors]);
 9   }
10
11}
app/views/Authors/index.html
<ul>
   {% for author in authors %}
      <li>{{ author.name }}</li>
   {% endfor %}
</ul>

Types de transformers

transform

Le type transform est basé sur l’interface TransformerInterface. Il est utilisé lorsque les données doivent être transformées en objet.
Le transormer DateTime est un bon exemple d’un tel transformer :

  • Au chargement des données, le transformer convertit les données de la base en une instance de php DateTime.

  • La méthode reverse effectue l’opération inverse ( php date vers date compatible avec la base de données).

toView

Le type toView est basé sur l’interface TransformerViewInterface. Il est utilisé dans le cas où les données transformées doivent être affichées dans une vue.

toForm

Le type toForm est basé sur l’interface TransformerFormInterface. Il est utilisé lorsque les données doivent être affichées dans un formulaire.

Utilisation des transformers

Transformation sur chargement des données

En cas d’omission, l’opération par défaut transformerOp est transform

$authors=DAO::getAll(Author::class);

Définit transformerOp à toView

DAO::transformersOp='toView';
$authors=DAO::getAll(Author::class);

Transformation après chargement

Retourne la valeur du membre transformée

TransformersManager::transform($author, 'name','toView');

Retourne une valeur transformée :

TransformersManager::applyTransformer($author, 'name','john doe','toView');

Transforme une instance en lui appliquant tous les transformers définis :

TransformersManager::transformInstance($author,'toView');

Transformers existants

Transformer

Type(s)

Description

datetime

transform, toView, toForm

Transforme une date issue de la base de données en une instance de DateTime php

upper

toView

Met la valeur du membre en majuscule

lower

toView

Met la valeur du membre en minuscule

firstUpper

toView

Met une majuscule sur la première lettre de la valeur du membre

password

toView

Masque les caractères de la valeur du membre

md5

toView

Hash la valeur en md5

Créer votre propre transformer

Création

Crée un transformer pour afficher un nom d’utilisateur comme une adresse email locale :

app/transformers/toLocalEmail.php
 1namespace transformers;
 2use Ubiquity\contents\transformation\TransformerViewInterface;
 3
 4class ToLocalEmail implements TransformerViewInterface{
 5
 6   public static function toView($value) {
 7      if($value!=null) {
 8         return sprintf('%s@mydomain.local',strtolower($value));
 9      }
10   }
11
12}

Enregistrement

Enregistrer le transformer en exécutant le script suivant :

TransformersManager::registerClassAndSave('localEmail',\transformers\ToLocalEmail::class);

Utilisation

app/models/User.php
 1namespace models;
 2
 3use Ubiquity\attributes\items\Transformer;
 4
 5class User {
 6
 7   #[Transformer('localEmail')]
 8   private $name;
 9
10   public function getName(){
11      return $this->name;
12   }
13
14   public function setName($name){
15      $this->name=$name;
16   }
17}
DAO::transformersOp='toView';
$user=DAO::getOne(User::class,"name='Smith'");
echo $user->getName();

Smith user name will be displayed as smith@mydomain.local.

Module de traduction

Note

Le module de traduction utilise la classe statique TranslatorManager pour gérer les traductions.

Structure du module

Les traductions sont regroupées par domaine, dans une locale :

Dans le répertoire racine des traductions (par défaut app/translations) :

  • Chaque locale correspond à un sous-dossier.

  • Pour chaque locale, dans un sous-dossier, un domaine correspond à un fichier php.

translations
      en_EN
           messages.php
           blog.php
      fr_FR
            messages.php
            blog.php
  • chaque fichier de domaine contient un tableau associatif de traductions key-> valeur de la traduction.

  • Chaque clé peut être associée à
    • une traduction

    • une traduction contenant des variables (entre % et %)

    • un tableau de traductions pour la pluralisation

app/translations/en_EN/messages.php
return [
     'okayBtn'=>'Okay',
     'cancelBtn'=>'Cancel',
     'deleteMessage'=>['No message to delete!','1 message to delete.','%count% messages to delete.']
];

Démarrage du module

Le démarrage du module se fait logiquement dans le fichier services.php.

app/config/services.php
1Ubiquity\cache\CacheManager::startProd($config);
2Ubiquity\translation\TranslatorManager::start();

En l’absence de paramètres, l’appel de la méthode start utilise la locale en_EN, sans fallbacklocale.

Important

Le module de traduction doit être lancé après le démarrage du cache.

Définition de la locale

Changement de la locale au démarrage du gestionnaire :

app/config/services.php
1Ubiquity\cache\CacheManager::startProd($config);
2Ubiquity\translation\TranslatorManager::start('fr_FR');

Changement de la locale après le chargement du gestionnaire :

TranslatorManager::setLocale('fr_FR');

Définition de la fallbackLocale

La locale fr_EN sera utilisée si fr_FR n’est pas trouvée :

app/config/services.php
1Ubiquity\cache\CacheManager::startProd($config);
2Ubiquity\translation\TranslatorManager::start('fr_FR','en_EN');

Définition du répertoire racine des traductions

Si le paramètre rootDir est absent, le répertoire utilisé par défaut est « app/translations ».

app/config/services.php
1Ubiquity\cache\CacheManager::startProd($config);
2Ubiquity\translation\TranslatorManager::start('fr_FR','en_EN','myTranslations');

Faire une traduction

Avec php

Traduction de la clé okayBtn dans la locale par défaut (spécifiée lors du démarrage du gestionnaire) :

$okBtnCaption=TranslatorManager::trans('okayBtn');

Sans paramètres, l’appel de la méthode trans utilise la locale par défaut, le domaine messages.

Traduction de la clé message utilisant une variable :

$okBtnCaption=TranslatorManager::trans('message',['user'=>$user]);

Dans ce cas, le fichier de traduction doit contenir une référence à la variable user pour la clé message :

app/translations/en_EN/messages.php
['message'=>'Hello %user%!',...];

Dans les vues twig :

Traduction de la clé okayBtn dans la locale par défaut (spécifiée lors du démarrage du gestionnaire) :

{{ t('okayBtn') }}

Traduction de la clé message utilisant une variable :

{{ t('message',parameters) }}

Sécurité

Principes directeurs

Validation des formulaires

Validation côté client

Il est préférable d’effectuer une validation initiale côté client pour éviter de soumettre des données invalides au serveur.

Exemple de la création d’un formulaire dans l’action d’un contrôleur (cette partie pourrait être située dans un service dédié pour une meilleure séparation des couches) :

app/controllers/UsersManagement.php
 1 public function index(){
 2     $frm=$this->jquery->semantic()->dataForm('frm-user',new User());
 3     $frm->setFields(['login','password','connection']);
 4     $frm->fieldAsInput('login',
 5         ['rules'=>'empty']
 6     );
 7     $frm->fieldAsInput('password',
 8         [
 9             'inputType'=>'password',
10             'rules'=>['empty','minLength[6]']
11         ]
12     );
13     $frm->setValidationParams(['on'=>'blur','inline'=>true]);
14     $frm->fieldAsSubmit('connection','fluid green','/submit','#response');
15     $this->jquery->renderDefaultView();
16 }

Ma vue associée :

app/views/UsersManagement/index.html
 {{ q['frm-user'] | raw }}
 {{ script_foot | raw }}
 <div id="response"></div>
_images/frm-user.png

Note

Les contrôleurs CRUD intègrent automatiquement cette validation côté client en utilisant les validateurs attachés aux membres des modèles.

#[Column(name: "password",nullable: true,dbType: "varchar(255)")]
#[Validator(type: "length",constraints: ["max"=>20,"min"=>6])]
#[Transformer(name: "password")]
private $password;
Validation côté serveur

Il est préférable de restreindre les URLs autorisées à modifier les données.
Au préalable, en spécifiant la méthode Http dans les routes, et en testant la requête :

#[Post(path: "/submit")]
public function submitUser(){
   if(!URequest::isCrossSite() && URequest::isAjax()){
      $datas=URequest::getPost();//post with htmlEntities
      //Do something with $datas
   }
}

Note

Le module Ubiquity-security offre un contrôle supplémentaire pour éviter les requêtes intersites.

Après avoir modifié un objet, il est possible de vérifier sa validité, grâce aux validateurs attachés aux membres du Modèle associé :

#[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...
      }
   }
}

Opérations DAO

Il est toujours recommandé d’utiliser des requêtes paramétrées, quelles que soient les opérations effectuées sur les données :
  • Pour éviter les injections SQL.

  • Pour permettre l’utilisation de requêtes préparées, accélérant le traitement.

$googleUsers=DAO::getAll(User::class,'email like ?',false,['%@gmail.com']);
$countActiveUsers=DAO::count(User::class,'active= ?',[true]);

Note

Les opérations DAO qui prennent des objets comme paramètres utilisent ce mécanisme par défaut.

DAO::save($user);

Gestion des mots de passe

Le transformateur Password permet à un champ d’être du type mot de passe lorsqu’il est affiché dans un formulaire CRUD généré automatiquement.

#[Transformer(name: "password")]
private $password;

Après soumission d’un formulaire, il est possible de crypter un mot de passe à partir de la classe URequest :

$encryptedPassword=URequest::password_hash('password');
$user->setPassword($encryptedPassword);
DAO::save($user);

L’algorithme utilisé dans ce cas est défini par le paramètre php PASSWORD_DEFAULT.

Il est également possible de vérifier de la même manière un mot de passe saisi par un utilisateur, en le comparant à un hachage :

if(URequest::password_verify('password', $existingPasswordHash)){
   //password is ok
}

Important

Configurez Https pour éviter d’envoyer les mots de passe en clair.

Module de sécurité/ Gestion des ACL

En plus de ces quelques règles, vous pouvez procéder à des installations si nécessaire :

Module sécurité

Installation

Installer le module Ubiquity-security à partir de l’invite de commande ou des Webtools (partie Composer).

composer require phpmv/ubiquity-security

Activez ensuite l’affichage de la partie Sécurité dans les Webtools :

_images/display-security.png

Session CSRF

La session est par défaut protégée contre les attaques CSRF via la classe VerifyCsrfToken (même sans le module Ubiquity-security). |Une instance de jeton (CSRFToken) est générée au démarrage de la session. La validité du jeton est ensuite vérifiée via un cookie à chaque requête.

_images/security-part.png

Cette protection peut être personnalisée en créant une classe implémentant 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
   }
}

Démarrer la protection personnalisée dans les 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()));

Désactiver la protection

Si vous n’avez pas besoin de protéger votre session contre les attaques Csrf, démarrez la session avec la classe « NoCsrfProtection ».

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()));

Gestion CSRF

Le service CsrfManager peut être démarré directement depuis l’interface webtools. |Son rôle est de fournir des outils pour protéger les routes sensibles des attaques Csrf (celles qui permettent la validation des formulaires par exemple).

_images/csrf-manager-started.png
  • Le service est démarré dans le fichier services.php.

app/config/services.php
 \Ubiquity\security\csrf\CsrfManager::start();

Exemple de protection de formulaire :

La vue du formulaire :

<form id="frm-bar" action='/submit' method='post'>
   {{ csrf('frm-bar') }}
   <input type='text' id='sensitiveData' name='sensitiveData'>
</form>

La méthode csrf génère un jeton pour le formulaire (En ajoutant un champ caché dans le formulaire correspondant au jeton.).

La soumission du formulaire dans un contrôleur :

use Ubiquity\security\csrf\UCsrfHttp;

#[Post('/submit')]
public function submit(){
   if(UCsrfHttp::isValidPost('frm-bar')){
      //Token is valid! => do something with post datas
   }
}

Note

Il est également possible de gérer cette protection via un cookie.

Exemple de protection avec ajax :

Le champ méta csrf-token est généré sur toutes les 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] );
      }
   }
}

Ce champ est ajouté dans la 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 %}

Exemple avec un bouton qui envoie des données via ajax. Le paramètre csrf est mis à true. Donc quand la requête est postée, le csrf-token est envoyé dans les headers de la requête.

#[Get(path: "/ajax")]
public function ajax(){
   $this->jquery->postOnClick('#bt','/postAjax','{id:55}','#myResponse',['csrf'=>true]);
   $this->jquery->renderDefaultView();
}

La route de soumission peut vérifier la présence et la validité du jeton :

#[Post(path: "postAjax")]
public function postAjax(){
   if(UCsrfHttp::isValidMeta('postAjax')){
      var_dump($_POST);
   }else{
      echo 'invalid or absent meta csrf-token';
   }
}

Gestionnaire de cryptage

Le service EncryptionManager peut être lancé directement à partir de l’interface webtools.

  • Dans ce cas, une clé est générée dans le fichier de configuration « app/config/config.php ».

  • Le service est démarré dans le fichier services.php.

app/config/services.php
 \Ubiquity\security\data\EncryptionManager::start($config);

Note

Par défaut, le cryptage est effectué en AES-128.

_images/encryption-manager-started.png

Changement du cypher code :

Mise à niveau vers AES-256 :

app/config/services.php
\Ubiquity\security\data\EncryptionManager::startProd($config, Encryption::AES256);

Générer une nouvelle clé :

Ubiquity new:key 256

La nouvelle clé est générée dans le fichier app/config/config.php.

Cryptage des données des modèles

Le transformateur Crypt peut aussi être utilisé sur les membres d’un modèle :

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
Cryptage des données génériques

Cryptage des chaînes :

$encryptedBar=EncryptionManager::encryptString('bar');

Pour ensuite le décrypter :

echo EncryptionManager::decryptString($encryptedBar);

Il est possible de crypter tout type de données :

$encryptedUser=EncryptionManager::encrypt($user);

Pour ensuite le décrypter, avec éventuellement sérialisation/désérialisation s’il s’agit d’un objet :

$user=EncryptionManager::decrypt($encryptedUser);

Gestionnaire des politiques de sécurité du contenu

Le service ContentSecurityManager peut être lancé directement à partir de l’interface webtools.

  • Le service est démarré dans le fichier services.php.

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

Avec cette configuration par défaut, un nonce est ajouté aux scripts jquery générés avec phpmv-ui. Le contrôle du CSP se fait en mode Report-only .

_images/csp-manager-started.png

Ajout d’un nonce

Exemple d’ajout de nonce sur les pages d’en-tête et de pied de page :

Mise à jour du contrôleur de base
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]);
             }
     }
}

Gestion des mots de passe

Jeton d’utilisateurs

Gestion des ACL

Installation

Installez le module Ubiquity-acl à partir de l’invite de commande ou des Webtools (partie Composer).

composer require phpmv/ubiquity-acl

Activez ensuite l’affichage de la partie Acl dans les Webtools :

_images/display-acl.png

Interface ACL dans les webtools :

_images/acl-part.png

Règles Acl

Les ACLs sont utilisées pour définir l’accès à une application Ubiquity. Elles sont définies selon les principes suivants :

Une application Ubiquity est composée de :
  • Ressources (éventuellement des contrôleurs, ou des actions de ces contrôleurs)

  • Rôles, éventuellement attribués à des utilisateurs. Chaque Rôle peut hériter de rôles parents.

  • Permissions, qui correspondent à un droit de faire. Chaque permission a un niveau (représenté par une valeur entière).

Règles supplémentaires :
  • Un AclElement (Allow) accorde une permission à un rôle sur une ressource.

  • Chaque rôle hérite des autorisations de ses parents, en plus des siennes.

  • Si un rôle a un certain niveau d’autorisation d’accès à une ressource, il aura également toutes les autorisations d’un niveau inférieur sur cette ressource.

  • L’association d’une ressource et d’une permission à un contrôleur ou à une action de contrôleur définit un élément map.

_images/acl-diagram.png
Règles de nommage :
  • Rôle, en lettres capitales, commençant par une arobase (@USER, @ADMIN, @ALL…).

  • Permissions, en majuscules, nommées à l’aide d’un verbe (READ, WRITE, OPEN…).

  • Ressource, majuscule à la première lettre (Produits, Clients…)

Démarrage des ACLs

Le service AclManager peut être démarré directement depuis l’interface webtools, dans la partie Security.

  • Le service est démarré dans le fichier services.php.

app/config/services.php
 \Ubiquity\security\acl\AclManager::startWithCacheProvider();

ACLCacheProvider

Ce fournisseur par défaut vous permet de gérer les ACL définies par des attributs ou des annotations.

AclController

Un AclController permet de gérer automatiquement les accès à ses propres ressources en se basant sur des ACL.
Il est possible de les créer automatiquement à partir des webtools.

_images/new-acl-controller.png

Mais il ne s’agit que d’un contrôleur de base, utilisant le trait AclControllerTrait.

Ce contrôleur va juste redéfinir la méthode _getRole, pour qu’elle renvoie le rôle de l’utilisateur actif, par exemple.

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!';
   }
}
L’autorisation a été accordée pour la ressource :
  • Sans spécifier la ressource, chaque action du contrôleur est définie comme une ressource.

  • Sans spécifier la permission, la permission « ALL » est utilisée.

_images/me-allow.png

Et cette association est présente dans la map des Acls :

_images/me-map.png
AclController avec authentication

Note

L’utilisation à la fois de WithAuthTrait et de ``AclControllerTrait`” nécessite de lever l’ambiguïté sur la méthode ``isValid`”.

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 avec Rôle, ressource et permission

Allow sans création préalable :

@USER est autorisé à accéder à la ressource Foo avec la permission READ.

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

Le rôle, la ressource et la permission sont automatiquement créés dès qu’ils sont invoqués avec Allow.

Allow avec création explicite :

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';
   }
}
Ajout d’ACL à l’exécution

Que ce soit dans un contrôleur ou dans un service, il est possible d’ajouter des rôles, des ressources, des permissions et des autorisations au moment de l’exécution :

Par exemple :\N- Ajouter un rôle @USER héritant de @GUEST.

use Ubiquity\security\acl\AclManager;

AclManager::addRole('@GUEST');
AclManager::addRole('@USER',['@GUEST']);
Définir les ACLs avec une base de données

Les ACL définies dans la base de données s’ajoutent aux ACL définies via les annotations ou les attributs.

Initialisation

L’initialisation permet de créer les tables associées aux ACLs (Role, Resource, Permission, AclElement). Elle ne doit être faite qu’une seule fois, et en mode dev uniquement.

A placer par exemple dans le fichier app/config/bootstrap.php :

use Ubiquity\controllers\Startup;
use Ubiquity\security\acl\AclManager;

$config=Startup::$config;
AclManager::initializeDAOProvider($config, 'default');

Démarrage

Dans le fichier app/config/services.php :

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)
]);

Stratégies de définition des ACL

Avec peu de ressources :

Définir les autorisations pour chaque action du contrôleur ou chaque groupe d’actions :

Les ressources correspondent logiquement aux contrôleurs, et les permissions aux actions. Mais cette règle peut ne pas être respectée, et une action peut être définie comme une ressource, selon les besoins.

La seule règle obligatoire est qu’une paire contrôleur/action ne peut correspondre qu’à une seule paire ressource/permission (pas nécessairement 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!';
   }

}

Avec plus de ressources :

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

Le module REST implémente un CRUD de base,
avec un système d’authentification, directement testable dans la partie administration.

REST et routage

Le routeur est essentiel au module REST, puisque REST (Representation State Transfer) est basé sur les URL et les méthodes HTTP.

Note

Pour des raisons de performance, les routes REST sont mises en cache indépendamment des autres routes.
Il est donc nécessaire de démarrer le routeur d’une manière particulière pour activer les routes REST et ne pas obtenir une erreur 404 récurrente.

Le routeur est démarré dans le fichier services.php.

Sans l’activation des routes REST :

app/config/services.php
...
Router::start();

Pour activer les routes REST dans une application qui a également une partie non-REST :

app/config/services.php
...
Router::startAll();

Pour activer uniquement les routes REST :

Router::startRest();

Il est possible de lancer le routage de manière conditionnelle (cette méthode ne sera efficace que si le nombre de routes est important dans l’une ou l’autre partie) :

app/config/services.php
...
     if($config['isRest']()){
             Router::startRest();
     }else{
             Router::start();
     }

Ressource REST

Un contrôleur REST peut être directement associé à un modèle.

Note

Si vous n’avez pas de base de données Mysql sous la main, vous pouvez télécharger celle-ci : messagerie.sql

Création

Avec les devtools :

Ubiquity rest RestUsersController -r=User -p=/rest/users

Ou avec les webtools :

Allez dans la partie REST puis choisir Add a new resource :

_images/addNewResource.png

Le contrôleur créé :

app/controllers/RestUsersController.php
 1     namespace controllers;
 2
 3     /**
 4      * Rest Controller RestUsersController
 5      * @route("/rest/users","inherited"=>true,"automated"=>true)
 6      * @rest("resource"=>"models\\User")
 7      */
 8     class RestUsersController extends \Ubiquity\controllers\rest\RestController {
 9
10     }

Comme les attributs automated et inherited de la route sont définis sur true, le contrôleur a les routes par défaut de la classe parente.

The base controller RestController is not standardized, it should be considered as an example for data interrogation.

Interface de test

Les webtools fournissent une interface pour l’interrogation des données :

_images/createdResource.png
Obtenir une instance

Une instance d’utilisateur est accessible par sa clé primaire (id) :

_images/getOneResource.png

Inclusion des membres associés : l’organisation de l’utilisateur

_images/getOneResourceInclude.png

Inclusion des membres associés : organisation, connexions et groupes de l’utilisateur.

_images/getOneResourceIncludeAll.png
Obtenir plusieurs instances

Récupérer toutes les instances :

_images/getAllOrgas.png

Définir une condition :

_images/condition-orgas.png

Inclure les membres associés :

_images/include-orgas.png
Ajout d’une instance

Les données sont envoyées par la méthode POST, avec un content-type défini à application/x-www-form-urlencoded :

Ajoutez les paramètres du nom et du domaine en cliquant sur le bouton parameters :

_images/post-parameters.png

L’ajout nécessite une authentification, donc une erreur est générée, avec le statut 401 :

_images/unauthorized-post.png

L’interface d’administration vous permet de simuler l’authentification par défaut et d’obtenir un jeton, en sollicitant la méthode connect :

_images/connect.png

Le jeton est alors automatiquement envoyé dans les requêtes suivantes.
L’enregistrement peut alors être inséré.

_images/added.png
Mise à jour d’une instance

La mise à jour suit le même schéma que l’insertion.

Suppression d’une instance
_images/delete-instance.png

Personnalisation

Routes

Il est bien sûr possible de personnaliser et de simplifier les routes.
Dans ce cas, il est préférable d’utiliser l’héritage depuis la classe RestBaseController, et de ne pas activer les routes automatiques.

app/controllers/RestOrgas.php
 1namespace controllers;
 2
 3use models\Organization;
 4
 5/**
 6 * Rest Controller for organizations
 7 *
 8 * @route("/orgas")
 9 * @rest
10 */
11class RestOrgas extends \Ubiquity\controllers\rest\RestBaseController {
12
13     public function initialize() {
14             $this->model = Organization::class;
15             parent::initialize();
16     }
17
18     /**
19      *
20      * @get
21      */
22     public function index() {
23             $this->_get();
24     }
25
26     /**
27      *
28      * @get("{keyValues}")
29      */
30     public function get($keyValues) {
31             $this->_getOne($keyValues);
32     }
33
34     /**
35      *
36      * @post("/")
37      */
38     public function add() {
39             $this->_add();
40     }
41
42     /**
43      *
44      * @patch("{keyValues}")
45      */
46     public function update(...$keyValues) {
47             $this->_update(...$keyValues);
48     }
49
50     /**
51      *
52      * @delete("{keyValues}")
53      */
54     public function delete(...$keyValues) {
55             $this->_delete(...$keyValues);
56     }
57}

Après avoir réinitialisé le cache, l’interface de test montre les routes accessibles :

_images/custom-orgas.png
Modification des données envoyées

Par sur-définition

Il est possible de modifier les données envoyées aux méthodes update et add, afin d’ajouter, modifier ou supprimer la valeur des champs avant l’envoi.
Soit en surdéfinissant la méthode getDatas :

app/controllers/RestOrgas.php
...

     protected function getDatas() {
             $datas = parent::getDatas();
             unset($datas['aliases']);// Remove aliases field
             return $datas;
     }

Avec les events

Soit de manière plus globale en agissant sur les événements de repos :

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 implémente une authentification Oauth2 avec des jetons Bearer.
Seules les méthodes avec l’annotation @authorization nécessitent l’authentification, ce sont les méthodes de modification (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 );
             }

La méthode connect d’un contrôleur REST établit la connexion et renvoie un nouveau jeton.
Il appartient au développeur de surcharger cette méthode pour gérer une éventuelle authentification avec login et mot de passe.

_images/token.png

Simulation d’une connexion avec login

Dans cet exemple, la connexion consiste simplement à envoyer une variable utilisateur par la méthode post.
Si l’utilisateur est fourni, la méthode connect de l’instance $server retourne un jeton valide qui est stocké dans la session (la session agit ici comme une base de données).

app/controllers/RestOrgas.php
 1     namespace controllers;
 2
 3     use Ubiquity\utils\http\URequest;
 4     use Ubiquity\utils\http\USession;
 5
 6     /**
 7      * Rest Controller RestOrgas
 8      * @route("/rest/orgas","inherited"=>true,"automated"=>true)
 9      * @rest("resource"=>"models\\Organization")
10      */
11     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
12
13             /**
14              * This method simulate a connection.
15              * Send a <b>user</b> variable with <b>POST</b> method to retreive a valid access token
16              * @route("methods"=>["post"])
17              */
18             public function connect(){
19                     if(!URequest::isCrossSite()){
20                             if(URequest::isPost()){
21                                     $user=URequest::post("user");
22                                     if(isset($user)){
23                                             $tokenInfos=$this->server->connect ();
24                                             USession::set($tokenInfos['access_token'], $user);
25                                             $tokenInfos['user']=$user;
26                                             echo $this->_format($tokenInfos);
27                                             return;
28                                     }
29                             }
30                     }
31                     throw new \Exception('Unauthorized',401);
32             }
33     }

Pour chaque requête avec authentification, il est possible de récupérer l’utilisateur connecté (il est ajouté ici dans les en-têtes de réponse) :

app/controllers/RestOrgas.php
 1     namespace controllers;
 2
 3     use Ubiquity\utils\http\URequest;
 4     use Ubiquity\utils\http\USession;
 5
 6     /**
 7      * Rest Controller RestOrgas
 8      * @route("/rest/orgas","inherited"=>true,"automated"=>true)
 9      * @rest("resource"=>"models\\Organization")
10      */
11     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
12
13             ...
14
15             public function isValid($action){
16                     $result=parent::isValid($action);
17                     if($this->requireAuth($action)){
18                             $key=$this->server->_getHeaderToken();
19                             $user=USession::get($key);
20                             $this->server->_header('active-user',$user,true);
21                     }
22                     return $result;
23             }
24     }

Utilisez l’interface webtools pour tester la connexion :

_images/connected-user.png

Personnalisation

Jetons d’api

Il est possible de personnaliser la génération de jetons, en surdéfinissant la méthode getRestServer :

app/controllers/RestOrgas.php
 1     namespace controllers;
 2
 3     use Ubiquity\controllers\rest\RestServer;
 4     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
 5
 6             ...
 7
 8             protected function getRestServer(): RestServer {
 9                     $srv= new RestServer($this->config);
10                     $srv->setTokenLength(32);
11                     $srv->setTokenDuration(4800);
12                     return $srv;
13             }
14     }

Origines autorisées et CORS

Cross-Origin Resource Sharing (CORS)

Si vous accédez à votre api depuis un autre site, il est nécessaire de configurer CORS.

Dans ce cas, pour les requêtes de type PATCH, PUT, DELETE, votre api doit définir une route permettant à CORS d’effectuer son contrôle pré-requête en utilisant la méthode OPTIONS.

app/controllers/RestOrgas.php
1     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
2
3             ...
4
5             /**
6              * @options('{url}')
7              */
8             public function options($url='') {}
9     }
Origines autorisées

Les origines autorisées permettent de définir les clients qui peuvent accéder à la ressource dans le cas d’une requête inter-domaine en définissant l’en-tête de réponse Access-Control-Allow-Origin.
Ce champ d’en-tête est renvoyé par la méthode OPTIONS.

app/controllers/RestOrgas.php
 1     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
 2
 3             ...
 4
 5             protected function getRestServer(): RestServer {
 6                     $srv= new RestServer($this->config);
 7                     $srv->setAllowedOrigin('http://mydomain/');
 8                     return $srv;
 9             }
10     }

Il est possible d’autoriser plusieurs origines :

app/controllers/RestOrgas.php
 1     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
 2
 3             ...
 4
 5             protected function getRestServer(): RestServer {
 6                     $srv= new RestServer($this->config);
 7                     $srv->setAllowedOrigins(['http://mydomain1/','http://mydomain2/']);
 8                     return $srv;
 9             }
10     }

Réponse

Pour changer le format des réponses, il est nécessaire de créer une classe héritant de ResponseFormatter.
Nous allons nous inspirer de HAL, et changer le format des réponses par :

  • ajout d’un lien vers self pour chaque ressource

  • ajout d’un attribut « _embedded » pour les collections

  • suppression de l’attribut « data » pour les ressources uniques

app/controllers/RestOrgas.php
 1     namespace controllers\rest;
 2
 3     use Ubiquity\controllers\rest\ResponseFormatter;
 4     use Ubiquity\orm\OrmUtils;
 5
 6     class MyResponseFormatter extends ResponseFormatter {
 7
 8             public function cleanRestObject($o, &$classname = null) {
 9                     $pk = OrmUtils::getFirstKeyValue ( $o );
10                     $r=parent::cleanRestObject($o);
11                     $r["links"]=["self"=>"/rest/orgas/get/".$pk];
12                     return $r;
13             }
14
15             public function getOne($datas) {
16                     return $this->format ( $this->cleanRestObject ( $datas ) );
17             }
18
19             public function get($datas, $pages = null) {
20                     $datas = $this->getDatas ( $datas );
21                     return $this->format ( [ "_embedded" => $datas,"count" => \sizeof ( $datas ) ] );
22             }
23     }

Assignation ensuite de « MyResponseFormatter » au contrôleur REST en surchargeant la méthode « getResponseFormatter » :

app/controllers/RestOrgas.php
1     class RestOrgas extends \Ubiquity\controllers\rest\RestController {
2
3             ...
4
5             protected function getResponseFormatter(): ResponseFormatter {
6                     return new MyResponseFormatter();
7             }
8     }

Testez les résultats avec les méthodes getOne et get :

_images/getOneFormatted.png _images/getFormatted.png

APIs

Contrairement aux ressources REST, les contrôleurs de type APIs sont multi-ressources.

SimpleRestAPI

JsonApi

Ubiquity implémente la spécification jsonApi avec la classe JsonApiRestController.
JsonApi est utilisé par EmberJS et d’autres.
voir https://jsonapi.org/ pour en savoir plus.

Création

Avec les devtools :

Ubiquity restapi JsonApiTest -p=/jsonapi

Ou avec les webtools :

Allez dans la partie REST puis choisir Add a new resource :

_images/jsonapi-creation.png

Testez l’api avec les webtools :

_images/jsonapi-admin.png
Récupérer un tableau d’objets

Par défaut, tous les membres associés sont inclus :

_images/getAll.png
Inclusion des membres associés

vous devez utiliser le paramètre include de la requête :

URL

Description

/jsonapi/user?include=false

Aucun membre associé n’est inclus

/jsonapi/user?include=organization

Inclusion de l’organisation

/jsonapi/user?include=organization,connections

Inclusion de l’organisation et des connexions

/jsonapi/user?include=groupes.organization

Inclusion des groupes, et de leur organisation

Filtrage d’instances

il est nécessaire d’utiliser le paramètre filter de la requête,
Le paramètre filter correspond à la partie where d’une instruction SQL :

URL

Description

/jsonapi/user?1=1

Sans filtrage

/jsonapi/user?firstname='Benjamin'

Retourne tous les utilisateurs prénommés Benjamin

/jsonapi/user?filter=firstname like 'B*'

Retourne tous les utilisateurs dont le nom commence par un B

/jsonapi/user?filter=suspended=0 and lastname like 'ca*'

Retourne tous les utilisateurs suspendus dont le nom commence par « ca »

Pagination

vous devez utiliser les paramètres page[number] et page[size] de la requête :

URL

Description

/jsonapi/user

Sans pagination

/jsonapi/user?page[number]=1

Affiche la première page (page size vaut 1)

/jsonapi/user?page[number]=1&&page[size]=10

Affiche la première page (page size vaut 10)

Ajout d’une instance

Les données, contenues dans data[attributes], sont envoyées par la méthode POST, avec un type de contenu défini à application/json ; charset=utf-8.

Ajoutez vos paramètres en cliquant sur le bouton paramètres :

_images/add-parameters.png

L’ajout nécessite une authentification, une erreur est donc générée, avec le statut 401 si le jeton est absent ou expiré.

_images/add-response.png
Suppression d’une instance

La suppression requiert la méthode DELETE, et l’utilisation de l”id de l’objet à supprimer :

_images/delete-response.png

Webtools

Note

Les webtools vous permettent de gérer une application Ubiquity via une interface web. Depuis Ubiquity 2.2.0, les webtools sont dans un repository séparé.

Installation

Mettez à jour les devtools si nécessaire pour commencer :

composer global update

Lors de la création d’un projet

Créer un projet avec les webtools (option -a)

Ubiquity new quick-start -a

Dans un projet existant

Dans une console, aller à la racine du projet et exécuter :

Ubiquity admin

Démarrage

Démarrez le serveur web embarqué, à partir du dossier du projet :

Ubiquity serve

allez à l’adresse : http://127.0.0.1:8090/Admin

_images/interface.png

Personnalisation

Cliquez sur customize pour afficher uniquement les outils que vous utilisez :

_images/customizing.png _images/customized.png

Modules webtools

Routes

_images/routes.png

Affiche les routes par défaut (non REST).

Operations:

  • Filtrer les routes

  • Tester les routes (GET, POST…)

  • Initialiser le cache du routeur

Contrôleurs

_images/controllers.png

Affiche les contrôleurs non REST.

Operations:

  • Créer un contrôleur (et éventuellement la vue associée à l’action index par défaut)

  • Créer une action dans un contrôleur (éventuellement la vue associée, la route associée)

  • Créer un contrôleur spécial (CRUD ou Auth)

  • Tester une action (GET, POST…)

Modèles

_images/models.png

Affiche les métadonnées des modèles, permet de parcourir les entités.

Operations:

  • Créer des modèles à partir d’une base de données

  • Générer le cache des modèles

  • Générer un script de base de données à partir de modèles existants

  • Effectuer des opérations CRUD sur les modèles

Rest

_images/rest.png

Affiche et gère les services REST.

Operations:

  • Ré-initiliser le cache Rest et les routes

  • Créer un nouveau service (en utilisant une api)

  • Créer une nouvelle ressource (associée à un modèle)

  • Tester et interroger un service web à l’aide de méthodes http

  • Effectuer des opérations CRUD sur les modèles

Cache

_images/cache.png

Affiche les fichiers de cache.

Operations:

  • Supprimer ou réinitialiser le cache des modèles

  • Supprimer ou réinitialiser le cache des contrôleurs

  • Supprimer les autres fichiers de cache

Maintenance

_images/maintenance.png

Permet de gérer les modes de maintenance.

Operations:

  • Créer ou mettre à jour un mode de maintenance

  • Désactiver/activer un mode de maintenance

  • Supprimer un mode de maintenance

Config

_images/config.png

Permet d’afficher et de modifier la configuration de l’application.

Git

_images/git.png

Synchronise le projet en utilisant git.

Operations:

  • Configuration avec des repositories externes

  • Commit

  • Push

  • Pull

_images/themes.png

Gère les thèmes Css.

Operations:

  • Installer un thème existant

  • Activer un thème

  • Créer un nouveau thème (éventuellement basé sur un thème existant)

Contribuer

Exigences système

Avant de travailler sur Ubiquity, ajoutez à votre environnement la configuration suivante :

  • Git

  • PHP 7.1 ou plus.

Télécharger le code source Ubiquity

Sur le repository github Ubiquity :

  • Forkez le projet Ubiquity

  • Clonez votre fork localement

git clone git@github.com:USERNAME/ubiquity.git

Travaillez sur votre partie

Note

Avant de commencer, vous devez savoir que tous les correctifs que vous allez soumettre doivent être publiés sous la licence Apache 2.0, sauf si cela est explicitement spécifié dans vos commits.

Créez une branche dédiée

Note

Utilisez un nom descriptif pour la nommer :

  • issue_xxx où xxx est le numéro de l’issue est une bonne convention pour les corrections de bugs.

  • feature_name est une bonne convention pour les nouvelles fonctionnalités

git checkout -b NEW_BRANCH_NAME master

Travaillez sur votre partie

Travaillez sur votre code et commitez aussi souvent que nécessaire, en gardant à l’esprit les éléments suivants :

  • Lire la partie Ubiquity coding standards;

  • Ajoutez des tests unitaires, fonctionnels ou d’acceptation pour prouver que le bug est corrigé ou que la nouvelle fonctionnalité fonctionne réellement ;

  • Faites des commits atomiques et logiquement séparés (utilisez git rebase pour avoir un historique propre et logique) ;

  • Rédigez de bons messages de commit (voir le conseil ci-dessous).

  • Incrémentez les numéros de version dans tous les fichiers modifiés, en respectant les règles semver :

    Étant donné un numéro de version MAJOR.MINOR.PATCH, incrémenter le :

    • La partie MAJOR si des changements non rétro compatibles sont effectués sur l’API,

    • la partie MINOR lorsque vous ajoutez des fonctionnalités de manière rétrocompatible, et

    • la partie PATCH lorsque vous effectuez des corrections de bugs ou des améliorations rétro compatibles.

Soumettez vos modifications

Mettez à jour la partie [Unrelease] du fichier CHANGELOG.md en intégrant vos modifications dans les parties appropriées :

  • Added

  • Changed

  • Removed

  • Fixed

Avant de soumettre votre patch, mettez à jour votre branche (nécessaire si vous mettez du temps à terminer vos modifications) :

git checkout master
git fetch upstream
git merge upstream/master
git checkout NEW_BRANCH_NAME
git rebase master

Faire une Pull Request

Vous pouvez maintenant faire une Pull Request sur le repository github Ubiquity .

Guide de développement

Note

Bien que le framework soit relativement récent, veuillez noter que certaines des premières classes d’Ubiquity ne suivent pas entièrement ce guide et n’ont pas été modifiées pour des raisons de rétrocompatibilité.
Néanmoins, tous les nouveaux codes doivent suivre ce guide.

Choix de conception

Sollicitation et utilisation de services

Injection de dépendances

Évitez d’utiliser l’injection de dépendances pour toutes les parties du framework, en interne.
L’injection de dépendances est un mécanisme gourmand en ressources :

  • Elle nécessite d’identifier l’élément à instancier ;

  • Puis de procéder à sont instanciation :

  • Pour enfin assigner le résultat à une variable.

Obtenir des services d’un conteneur

Evitez également les accès publics aux services enregistrés dans un conteneur de services.
Ce type d’accès implique la manipulation d’objets dont le type de retour est inconnu, et peu facile à manipuler pour le développeur.

Par exemple, il est difficile de manipuler le retour non typé de $this->serviceContainer->get('translator'), comme le permettent certains frameworks, et de savoir quelles méthodes appeler dessus.

Lorsque cela est possible, et lorsque cela ne réduit pas trop la flexibilité, l’utilisation de classes statiques est conseillée :

Pour un développeur, la classe TranslatorManager est accessible depuis un l’ensemble du projet sans aucune instanciation d’objet.
Elle expose son interface publique et permet la complétion de code :

  • Le translator n’a pas besoin d’être injecté pour être utilisé ;

  • Il n’a pas besoin d’être récupéré depuis un conteneur de services.

L’utilisation de classes statiques crée inévitablement une forte dépendance et affecte la flexibilité.
Mais pour revenir à l’exemple du Traducteur, il n’y a aucune raison de le modifier s’il est satisfaisant. |brl Il n’est pas souhaitable de vouloir à tout prix apporter de la flexibilité quand elle n’est pas nécessaire, pour qu’en conséquence, l’utilisateur constate ensuite que son application est un peu lente.

Optimisation

L’exécution de chaque ligne de code peut avoir des répercussions importantes sur les performances.
Comparez et évaluez les solutions de mise en œuvre, en particulier si le code est sollicité à plusieurs reprises :

Qualité du code

Ubiquity utilise Scrutinizer-CI pour l’évaluation de la qualité du code.

  • Pour les classes et les méthodes :

    • Les évaluations A ou B sont satisfaisantes

    • C est acceptable, mais à éviter autant que possible

    • Les notes plus basses sont à prohiber

Complexité du code

  • Les méthodes complexes doivent être divisées en plusieurs, pour faciliter la maintenance et permettre la réutilisation ;

  • Pour les classes complexes, effectuer une refactorisation de type extract-class ou extract-subclass en utilisant les traits.

Duplications de code

Évitez absolument la duplication du code, sauf si la duplication est minime et est justifiée par les performances.

Bugs

Essayez de résoudre tous les bugs signalés au fur et à mesure, sans les laisser s’accumuler.

Tests

Toute correction de bug qui n’inclut pas un test prouvant l’existence du bug corrigé, peut être suspecte.
De même pour les nouvelles fonctionnalités dont on ne peut prouver qu’elles fonctionnent réellement.

Il est également important de maintenir un taux de couverture du code par les tests acceptable, qui peut baisser si une nouvelle fonctionnalité n’est pas testée.

Documentation du code

Le code actuel n’est pas encore entièrement documenté, n’hésitez pas à contribuer afin de combler cette lacune.

Standards de code

Les standards de code Ubiquity sont basés sur les standards PSR-1 , PSR-2 et PSR-4 il est donc préférable de les connaître.
Les quelques exceptions aux standards sont normalement reportées dans ce guide.

Conventions de nommage

  • Utiliser camelCase pour les variables php, les membres, les fonctions et noms de méthodes, les arguments (e.g. $modelsCacheDirectory, isStarted()) ;

  • Chaque classe php doit avoir un namespace (voir PSR-4) et utiliser la convention UpperCamelCase pour son nommage (e.g. CacheManager) ;

  • Préfixer toutes les classes abstraites avec Abstract, à l’exception des classes PHPUnit BaseTests :

  • Suffixer les interfaces avec Interface ;

  • Suffixer les traits avec Trait ;

  • Suffixer les exceptions avec Exception ;

  • Suffixer les classes principales de type manager avec Manager (e.g. CacheManager, TranslatorManager) ;

  • Préfixer les classes utilitaires avec un U (e.g. UString, URequest) ;

  • Utiliser UpperCamelCase pour le nommage des fichiers PHP (e.g. CacheManager.php) ;

  • Utiliser les majuscules pour les constantes (e.g. const SESSION_NAME=”Ubiquity”).

Indentation, tabulations, accolades

  • Utiliser des tabulations, et non des espaces ; (! PSR-2)

  • Utiliser les accolades sans passage à la ligne suivante ; (! PSR-2)

  • Utilisez des accolades pour indiquer le corps de la structure de contrôle, quel que soit le nombre d’instructions qu’elle contient ;

Classes

  • Définir une classe par fichier ;

  • Déclarer l’héritage de la classe et toutes les interfaces implémentées sur la même ligne que le nom de la classe ;

  • Déclarer les membres de données avant les méthodes ;

  • Déclarer les méthodes privées en premier, puis celles qui sont protégées pour finir par les méthodes publiques :

  • Déclarer tous les arguments sur la même ligne que le nom de la méthode/fonction, quel que soit le nombre d’arguments ;

  • Utiliser des parenthèses lors de l’instanciation d’une classe, même si le constructeur ne prend aucun argument ;

  • Ajouter le statement use pour chaque classe qui ne fait pas partie de l’espace de noms global ;

Opérateurs

  • Utiliser la comparaison identique ou égale lorsque vous le jugez nécessaire ;

Exemple

<?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

Vous pouvez importer ces fichiers de standardisation intégrant ces règles dans votre IDE :

Si votre IDE favori ne figure pas dans la liste, vous pouvez soumettre le fichier de normalisation associé en créant une nouvelle PR.

Guide de documentation

Ubiquity fourni deux espaces de documentation :

  • Le guide, qui aide à apprendre les manipulations et les concepts ;

  • La documentation API, qui sert de référence lors du codage.

Vous pouvez aider à améliorer les guides Ubiquity en les rendant plus cohérents, consistants ou lisibles, en ajoutant des informations manquantes, en corrigeant des erreurs factuelles, en corrigeant des fautes de frappe ou en les mettant à jour pour la dernière version d’Ubiquity.

Pour ce faire, apportez des modifications aux fichiers sources des guides Ubiquity (situés ici sur GitHub). Ensuite, ouvrez une Pull Request pour appliquer vos changements à la branche master.

Lorsque vous travaillez sur la documentation, pensez à prendre en compte les consignes.

Serveurs configuration

Important

Depuis la version 2.4.5, pour des raisons de sécurité et de simplification, la racine d’une application Ubiquity est localisée dans le dossier public.

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

Avec Apache 2.4, Order Allow,Deny a été remplacé par 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>
déplacement de index.php dans le dossier public

Si vous avez créé votre projet avec une version antérieure à la 2.4.5, vous devez modifier index.php et déplacer les fichiers index.php et .htaccess dans le dossier public.

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

Assurez-vous que les packages libapache2-mod-fastcgi et php7.x-fpm sont installés (remplacer x par votre version de php).

Configuration php-pm :

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

Configuration Apache 2.4 :

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

Configuration nginX :

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

Configuration Swoole :

.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

Configuration Workerman :

.ubiquity/workerman-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "socket"=>[
        "count" => 4,
        "reuseport" =>true
    ]
);

RoadRunner

Configuration RoadRunner :

.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"]

Optimisation Ubiquity

Ubiquity est rapide, mais peut l’être encore plus en optimisant quelques éléments.

Note

Le serveur de test intégré (accessible par Ubiquity serve) utilise ses propres fichiers de configuration et de lancement (dans le dossier .ubiquity de votre projet).
Il ne doit donc pas être utilisé pour évaluer les résultats des modifications apportées.

Testez vos pages en utilisant une configuration logicielle et matérielle similaire à celle utilisée en production.
Utilisez un outil de benchmark pour évaluer vos changements au fur et à mesure (Apache Bench par exemple).

Cache

Système

Choisissez et testez parmi les différents systèmes de cache (ArrayCache, PhpFastCache, MemCached).
Le système de cache est défini dans le fichier de configuration :

app/config/config.php
     "cache" => [
             "directory" => "cache/",
             "system" => "Ubiquity\\cache\\system\\ArrayCache",
             "params" => []
     ]

Le cache par défaut ArrayCache est souvent la solution la plus optimisée.

Génération

Générer le routeur et le cache ORM (pensez que les annotations ne sont jamais utilisées à l’exécution) :

Ubiquity init-cache

Contenus statiques

Si votre application comporte des pages qui sont générées par PHP mais qui changent rarement, vous pouvez les mettre en cache :

  • Le résultat de la requête (utilisant les méthodes DAO)

  • La réponse de la route (avec l’annotation @route)

fichier index

Supprimez la ligne définissant le signalement des erreurs au moment de l’exécution, et assurez-vous que l’affichage des erreurs est désactivé dans php.ini.

index.php
error_reporting(\E_ALL);//To be removed

Optimisation de la configuration

La configuration est accessible depuis le fichier app/config/config.php.

Ne gardez que les éléments essentiels à votre application.

key

rôle

Optimisation

siteUrl

Utilisé par les méthodes Ajax, et par les fonctions url et path de Twig.

A supprimer si ces fonctions ne sont pas utilisées

database

Utilisé par l’ORM Ubiquity

A supprimer si la partie ORM n’est pas utilisée

sessionName

Si elle est assignée, démarre ou récupère la session php pour chaque requête.

A supprimer si la session est inutile

templateEngine

S’il est assigné, instancie un nouvel objet Moteur pour chaque requête.

A supprimer si les vues ne sont pas utilisées

templateEngineOptions

Options attribuées à l’instance du moteur de templates

mettre l’option cache à true si Twig est utilisé

test

A enlever (déprécié)

debug

Active ou désactive les logs

Mettre à false en production

logger

Définit l’instance de logger

A enlever en production

di

Définit les services à injecter

La seule clé lue à l’exécution est @exec

cache

Définit le chemin du cache et la classe de base du cache, utilisés par les modèles, le routeur, l’injection de dépendance.

mvcNS

Définit les chemins ou les espaces de noms utilisés par les contrôleurs, les modèles et les contrôleurs Rest

isRest

Définit la condition pour détecter si un chemin correspond à un contrôleur Rest

A supprimer si vous n’utilisez pas explicitement cette condition dans votre code

Exemple de configuration sans session, et sans injection de dépendances:

app/config/config.php
1<?php
2return array(
3             "templateEngine"=>'Ubiquity\\views\\engine\\Twig',
4             "templateEngineOptions"=>array("cache"=>true),
5             "debug"=>false,
6             "cache"=>["directory"=>"cache/","system"=>"Ubiquity\\cache\\system\\ArrayCache","params"=>[]],
7             "mvcNS"=>["models"=>"models","controllers"=>"controllers","rest"=>""]
8);

Optimisation des services

Les services chargés sont accessibles depuis le fichier app/config/services.php.

Comme pour le fichier de configuration, ne conservez que les éléments essentiels à votre application.

Lignes

Rôle

\Ubiquity\cache\CacheManager::startProd($config)

Démarre le cache pour l’ORM, la base de données, le routeur, l’injection de dépendances.

UbiquityormDAO::start()

A utiliser uniquement en la présence de plusieurs bases de données

Router::start()

A utiliser uniquement si les routes sont définies avec des annotations.

Router::addRoute(« _default », « controllers\IndexController »)

Définit la route par défaut (à supprimer en production)

\Ubiquity\assets\AssetsManager::start($config)

Affecte la variable siteUrl à partir du ThemeManager, à utiliser uniquement si les fonctions css et js de twig sont utilisées

Exemple d’un fichier Services avec une base de données et le démarrage du routeur :

app/config/services.php
1<?php
2\Ubiquity\cache\CacheManager::startProd($config);
3\Ubiquity\controllers\Router::start();

Optimisation de l’autoloader

En production, supprimez les dépendances utilisées uniquement en développement, et générez le fichier map des classes optimisé :

composer install --no-dev --classmap-authoritative

Si les dépendances utilisées ont déjà été supprimées et que vous souhaitez uniquement mettre à jour le fichier map (après avoir ajouté ou supprimé une classe) :

composer dump-autoload -o  --classmap-authoritative

Note

Le paramètre –no-dev` supprime la dépendance ubiquity-dev requise par webtools. Si vous utilisez webtools en production, ajoutez la dépendance phpmv/ubiquity-dev :

composer require phpmv/ubiquity-dev

Optimisation PHP

Veuillez noter que d’autres applications peuvent utiliser les valeurs modifiées sur le même serveur.

OP-Cache

OPcache améliore les performances de PHP en stockant le bytecode des scripts précompilés dans la mémoire partagée, ce qui évite à PHP de devoir charger et analyser les scripts à chaque requête.

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

Si vous utilisez le serveur web ubiquity-swoole :

php.ini
; Determines if Zend OPCache is enabled for the CLI version of PHP
opcache.enable_cli=1

Pour compléter

N’oubliez pas que le framework utilisé ne fait pas tout. Vous devez également optimiser votre propre code.

Ubiquity commandes

Note

Cette partie est accessible à partir des webtools, donc si vous avez créé votre projet avec l’option -a ou avec la commande create-project.

Commandes

A partir des webtools, aller dans la partie commands,

_images/commands-elm.png

ou directement à l’adresse http://127.0.0.1:8090/Admin/commands.

Liste des commandes

Activer l’onglet Commands pour obtenir la liste des commandes devtools.

_images/commands-list.png

Informations sur une commande

Il est possible d’obtenir de l’aide sur une commande (ce qui produit un résultat équivalent à Ubiquity help cmdName).

_images/command-help.png

Exécution de commandes

Un clic sur le bouton run d’une commande affiche un formulaire pour saisir ses paramètres (ou l’exécute directement si elle n’en prend aucun).

_images/command-run.png

Après avoir saisi les paramètres, l’exécution produit un résultat.

_images/command-exec.png

Suite de commandes

Retourner à l’onglet My commands : Il est possible de sauvegarder une séquence de commandes (avec des paramètres enregistrés), puis d’exécuter cette même séquence :

Création de suite

Cliquer sur add command suite

_images/new-suite-btn.png

Ajouter les commandes souhaitées et modifier les paramètres :

_images/new-commands-suite.png

La validation génère la suite :

_images/commands-suite-created.png

Exécution d’une suite de commandes

En cliquant sur le bouton « run » de la suite, on exécute la liste des commandes qu’elle contient :

_images/commands-suite-exec.png

Création d’une commande personnalisée

Cliquer sur le bouton Create devtools command.

_images/create-devtools-command-btn.png

Saisir les caractéristiques de la nouvelle commande :

  • Le nom de la commande

  • Sa valeur : nom de l’argument principal

  • Les paramètres de la commande : avec plusieurs paramètres, les séparer avec une virgule

  • La description de la commande

  • Les alias de la commande : En cas d’alias multiples, utilisez la virgule comme séparateur.

_images/create-command.png

Note

Les commandes personnalisées sont créées dans le dossier app/commands du projet.

_images/custom-command-exec.png

La classe générée :

app/commands/CreateArray.php
 1namespace commands;
 2
 3use Ubiquity\devtools\cmd\commands\AbstractCustomCommand;
 4use Ubiquity\devtools\cmd\ConsoleFormatter;
 5use Ubiquity\devtools\cmd\Parameter;
 6
 7class CreateArray extends AbstractCustomCommand {
 8
 9     protected function getValue(): string {
10             return 'jsonValue';
11     }
12
13     protected function getAliases(): array {
14             return array("createarray","arrayFromJson");
15     }
16
17     protected function getName(): string {
18             return 'createArray';
19     }
20
21     protected function getParameters(): array {
22             return ['f' => Parameter::create('fLongName', 'The f description.', [])];
23     }
24
25     protected function getExamples(): array {
26             return ['Sample use of createArray'=>'Ubiquity createArray jsonValue'];
27     }
28
29     protected function getDescription(): string {
30             return 'Creates an array from JSON and save to file';
31     }
32
33     public function run($config, $options, $what, ...$otherArgs) {
34             //TODO implement command behavior
35             echo ConsoleFormatter::showInfo('Run createArray command');
36     }
37}

La commande CreateArray implémentée :

app/commands/CreateArray.php
 1namespace commands;
 2
 3use Ubiquity\devtools\cmd\commands\AbstractCustomCommand;
 4use Ubiquity\devtools\cmd\ConsoleFormatter;
 5use Ubiquity\devtools\cmd\Parameter;
 6use Ubiquity\utils\base\UFileSystem;
 7
 8class CreateArray extends AbstractCustomCommand {
 9
10     protected function getValue(): string {
11             return 'jsonValue';
12     }
13
14     protected function getAliases(): array {
15             return array(
16                     "createarray",
17                     "arrayFromJson"
18             );
19     }
20
21     protected function getName(): string {
22             return 'createArray';
23     }
24
25     protected function getParameters(): array {
26             return [
27                     'f' => Parameter::create('filename', 'The filename to create.', [])
28             ];
29     }
30
31     protected function getExamples(): array {
32             return [
33                     'Save an array in test.php' => "Ubiquity createArray \"{\\\"created\\\":true}\" -f=test.php"
34             ];
35     }
36
37     protected function getDescription(): string {
38             return 'Creates an array from JSON and save to file';
39     }
40
41     public function run($config, $options, $what, ...$otherArgs) {
42             echo ConsoleFormatter::showInfo('Run createArray command');
43             $array = \json_decode($what, true);
44             $error = \json_last_error();
45             if ($error != 0) {
46                     echo ConsoleFormatter::showMessage(\json_last_error_msg(), 'error');
47             } else {
48                     $filename = self::getOption($options, 'f', 'filename');
49                     if ($filename != null) {
50                             UFileSystem::save($filename, "<?php\nreturn " . var_export($array, true) . ";\n");
51                             echo ConsoleFormatter::showMessage("$filename succefully created!", 'success', 'CreateArray');
52                     } else {
53                             echo ConsoleFormatter::showMessage("Filename must have a value!", 'error');
54                     }
55             }
56     }
57}

Exécution d’une commande personnalisée

La nouvelle commande est accessible depuis les devtools, à condition qu’elle soit bien présente dans le projet :

Ubiquity help createArray
_images/custom-command-help.png
Ubiquity createArray "{\"b\":true,\"i\":5,\"s\":\"string\"}" -f=test.php
_images/custom-command-devtools.png

Gestion Composer

Note

Cette partie est accessible depuis les webtools, donc si vous avez créé votre projet avec l’option -a ou avec la commande create-project.

Accès

Depuis les webtools, activer la patir composer.

_images/composer-elm.png

ou aller directement à l’adresse http://127.0.0.1:8090/Admin/composer.

Liste des dépendances

L’interface affiche la liste des dépendances déjà installées, et celles qui sont directement installables.

_images/composer-dependencies.png

Installation de dépendance

Parmi les dépendances listées :

Cliquez sur le bouton add des dépendances que vous voulez ajouter.

_images/composer-add-1.png

Puis cliquer sur le bouton Generate composer update :

_images/composer-add-2.png

La validation génère la mise à jour.

Pour les dépendances non listées :

Cliquer sur le bouton Add dependency :

_images/composer-add-dependency.png
  • Saisir le nom du vendor (provider) ;

  • Sélectionner le package dans la liste :

  • Sélectionnez éventuellement une version (si aucune ne l’est, la dernière version stable sera installée).

Suppression de dépendance

Cliquez sur le bouton remove des dépendances que vous voulez supprimer.

_images/composer-remove-1.png

Cliquer ensuite sur le bouton Generate composer update puis valider la mise à jour.

Note

Il est possible d’effectuer plusieurs opérations d’addition ou de suppression et de les valider ensemble.

Optimisation Composer

Cliquer sur le bouton Optimize autoloader.

L’optimisation de l’autoload se fait avec l’option authoritative classmap.

Mise en cache Ubiquity

Dépendances Ubiquity

  • php >=7.4

  • phpmv/ubiquity => Ubiquity core

En production

Moteur de template

Twig est nécessaire s’il est utilisé en tant que moteur de template, ce qui n’est pas une obligation.

  • twig/twig => Template engine

Sécurité

  • phpmv/ubiquity-security => Csrf, Csp…

  • phpmv/ubiquity-acl => Management des contrôles d’accès

En développement

Webtools

  • phpmv/ubiquity-dev => dev classes pour les webtools et les devtools depuis la v2.3.0

  • phpmv/php-mv-ui => Librairie front

  • mindplay/annotations => Librairie pour annotations , requis pour générer le cache, les modèles…

  • monolog/monolog => Librairie pour les logs

  • czproject/git-php => Opérations git (+ git console requise)

Devtools

  • phpmv/ubiquity-devtools => Cli console

  • phpmv/ubiquity-dev => dev classes pour les webtools et les devtools depuis la v2.3.0

  • mindplay/annotations => Librairie pour annotations , requis pour générer le cache, les modèles…

Tests

  • codeception/codeception => Tests

  • codeception/c3 => C3 integration

  • phpmv/ubiquity-codeception => Codeception pour Ubiquity

Module client OAuht2

Note

Cette partie est accessible depuis les webtools, donc si vous avez créé votre projet avec l’option -a ou avec la commande create-project. Le module OAuth n’est pas installé par défaut. Il utilise la bibliothèque HybridAuth.

Installation

Depuis la racine du projet :

composer require phpmv/ubiquity-oauth

Note

Il est également possible d’ajouter la dépendance ubiquity-oauth en utilisant la partie Composer du module d’administration.

_images/composer-add-1.png

Configuration OAuth

Configuration globale

_images/oauth-part-0.png

Cliquez sur le bouton Global configuration, et modifiez l’URL de rappel, qui correspond à l’url de rappel locale après une connexion réussie.

_images/oauth-part-callback.png

Contrôleur OAuth

Cliquez sur le bouton Create Oauth controller et attribuez à la route la valeur précédemment donnée au callback :

_images/create-oauth-controller.png

Valider et réinitialiser le cache du routeur :

_images/create-oauth-controller-created.png

Fournisseurs

Note

Pour une authentification OAuth, il est nécessaire de créer au préalable une application chez le fournisseur, et de prendre connaissance des clés de l’application (id et secret).

Cliquer sur le bouton Add provider et sélectionner Google:

_images/provider-config.png

Vérifiez la connexion en cliquant sur le bouton Check :

_images/google-check.png

Information post connexion :

_images/google-check-infos.png

Personnalisation de OAuthController

Le contrôleur créé est le suivant :

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
   }
}
  • La méthode _oauth correspond à l’url de callback

  • La méthode onConnect est déclenchée à la connexion et peut être surchargée.

Exemple :

  • Récupération possible d’un utilisateur associé dans la base de données

  • ou création d’un nouvel utilisateur

  • Ajout de l’utilisateur connecté et 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:/');
     }

Plateformes asynchrones

Note

Ubiquity supporte plusieurs plateformes : Swoole, Workerman, RoadRunner, PHM-PM,ngx_php.

Swoole

Installer l’extension Swoole sur votre système (linux) ou dans votre image Docker :

#!/bin/bash
pecl install swoole

Lancer Ubiquity Swoole (la première fois, le paquet ubiquity-swoole sera installé) :

Ubiquity serve -t=swoole

Configuration du serveur

.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
        ]
);

Le port peut aussi être changé au démarrage du serveur :

Ubiquity serve -t=swoole -p=8999

Optimisation des services

Le démarrage des services ne sera effectué qu’une seule fois, au démarrage du serveur.

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();
});
La méthode warmUpControllers :
  • Instancie les contrôleurs

  • Effectue l’injection de dépendances

  • prépare l’appel des méthodes initialize et finalize (initialisation des constantes d’appel)

Au démarrage de chaque Worker, la méthode warmup des contrôleurs peut par exemple initialiser les requêtes DAO préparées :

app/controllers/FooController.php
     public static function warmup() {
             self::$oneFooDao = new DAOPreparedQueryById('models\\Foo');
             self::$allFooDao = new DAOPreparedQueryAll('models\\Foo');
     }

Workerman

Workerman ne nécessite aucune installation particulière (sauf pour libevent qu’il est recommandé d’utiliser en production pour des raisons de performance).

Lancer Ubiquity Workerman (la première fois, le paquet ubiquity-workerman sera installé) :

Ubiquity serve -t=workerman

Configuration du serveur

.ubiquity/workerman-config.php
<?php
return array(
    "host" => "0.0.0.0",
    "port" => 8080,
    "socket"=>[
        "count" => 4,
        "reuseport" =>true
    ]
);

Le port peut aussi être changé au démarrage du serveur :

Ubiquity serve -t=workerman -p=8999

Optimisation des services

Le démarrage des services ne sera effectué qu’une seule fois, au démarrage du serveur.

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