Site RESTful avec Slim3 et ng-admin

SLIM 3 est un microframework php qui permet de créer des API. Associé à un module AngularJS nommé ng-admin[0], il permet de construire un tableau de bord d’administration d’une base de données dans une application de type SPA (Single Page Application). Nous verrons ici comment mettre en œuvre l’ensemble.

Les vidéos

  1. Création du projet dans NetBeans, installation du micro Framework Slim 3 avec composer
  2. Création de l’API. mise en place du routage, interrogation de ma base mySQL
  3. Installation du moteur de vue ng-admin et affichage d’une liste de données.
  4. Ajout, modification et suppression de données en faisant interagir ng-admin et l’API Slim 3

Prérequis

L’ordinateur hôte de la solution doit avoir :

Tous ces logiciels sont multiplateformes et fonctionnent quel que soit l’OS.

Création de la solution (Netbeans)

Dans Netbeans (exemple pour créer un projet portant le nom : « monProjet »)

Fichier->Nouveau Projet cela lance un assistant en 5 points

  1. Sélectionner un projet : Choisir Catégories -> PHP et Projets -> PHP Application, cliquer sur Suivant
  2. Name and Location : Project Name : monProjet, Sources Folder : pointer sur le dossier root du serveur et un sous dossier portant le nom du projet : par exemple c:\laragon\www\monProjet, PHP version (celle qui est installée sur l’ordinateur), Default Encoding : UTF-8, cliquer sur Suivant.
  3. Run Configuration : Run As : Local Web Site, project URL : le chemin du virtual host créé sur le serveur (si ce n’est fait automatiquement, il faut le faire) : http://monProjet.dev:8080
  4. PHP Frameworks : laisser vide, cliquer sur Suivant
  5. Composer : (installation des bibliothèques php nécessaires au projet, ici nous allons ajouter le paquet Slim 3 et le paquet Slim PDO).
    Dans token -> slim puis cliquer sur Search, sélectionner slim/slim, dans la liste déroulante Version choisir 3.x-dev , puis cliquer sur le bouton afin de faire apparaitre slim/slim (3.x-dev) dans la fenêtre de droite.
    refaire la même chose avec le paquet slim/PDO version dev-master

Cliquer sur Terminer, les différents dossiers du projet vont être créés, et composer va télécharger et installer les bibliothèques

A l’issue de l’opération, le dossier du projet ressemble à ceci :

Le dossier nbprojet contient les fichiers de paramétrage du projet necessaires à NetBeans (on n’y touche jamais)

Le dossier vendor, qui contient les bibliothèques téléchargées et un fichier autoload.php, qui comme son nom l’indique, permet de charger automatiquement les classes et leur espace de nom afin de les rendre disponibles facilement.

Composer.json est le fichier qui contient les références des packages installés,

{
    "name": "vendor/mon-projet",
    "description": "Description of project monProjet.",
    "authors": [
        {
            "name": "francois",
            "email": "your@email.here"
        }
    ],
    "require": {
        "slim/slim": "3.x-dev",
        "slim/pdo": "dev-master"
    }
}

Si on ajoute ou supprime une dépendance dans la partie « require » du fichier, puis qu’on effectue une commande composer update, le projet se met à jour automatiquement et chargeant ou déchargeant les bibliothèques ajoutées ou retirées. Un autoload.php est alors recréer. Composer.lock contient les informations de l’autoload actuel. (on ne touche jamais ce fichier).

Enfin un fichier index.php et un squelette de page web

L’API

Dans notre application, tous les accès au modèle de données se font au travers de requêtes web vers une API en respectant le format REST[1].

Le format REST considère l’url, non plus comme l’appel d’une page web mais comme l’appel à une fonction, les arguments étant passés soit dans l’url (pour les requêtes de type GET, DELETE) dans le corps (pour les requêtes POST) ou bien les deux (requêtes PUT).

Dans le format REST, les verbes des requêtes permettent de définir le type d’action que le serveur devra effectuer.

Exemple :

Verbe

url

Action

GET

/products   

Récupère un jeu de
données contenant tous les éléments « products »

GET

/products/:id

Récupère un jeu de
données contenant 1 élément « products » ayant l’identifiant « :id »

POST

/products   

Créer un « product »
dans la table « products » , les paramètres passent dans le corps
de la requête

PUT

/products/:id

Mise à jour du product
 d’identifiant « :id », les
paramètres passent dans le corps de la requête

DELETE

/products/:id

Destruction du product
d’identifiant « :id »

Grâce à ces 4 verbes, on peut créer des services web qu’on nomme CRUD (Create Read Modify Delete).

Le serveur renvoie au client un jeu de données soit au format XML soit au format JSON.

Exemple :

GET /products

Renvoie un tableau JSON de type :

[
    {
      "id": 1,
      "name": "LG P880 4X HD",
      "description": "My first awesome phone!",
      "price": null,
      "category_id": 5,
      "created": "2014-06-01 01:12:26",
      "modified": "2014-05-31 17:12:26"
    },
    {
      "id": 2,
      "name": "Google Nexus 4",
      "description": "The most awesome phone of 2013!",
      "price": "56",
      "category_id": 2,
      "created": "2014-06-01 01:12:26",
      "modified": "2014-05-31 17:12:26"
    },
…
]

GET /products/2

Renvoie un objet JSON ressemblant à ceci :

{
      "id": 2,
      "name": "Google Nexus 4",
      "description": "The most awesome phone of 2013!",
      "price": "56",
      "category_id": 2,
      "created": "2014-06-01 01:12:26",
      "modified": "2014-05-31 17:12:26"
}

 

L’API sera construite à l’aide du microframework Slim Framework 3[2]. Ce dernier a été créé pour développer simplement des API, il possède un routeur pour gérer les diverses requêtes et un injecteur de dépendance qui permet d’utiliser les bibliothèques tierces.

Créer un dossier « api » sur la racine du projet. Ajouter un fichier index.php et un fichier .htaccess

Comme indiqué sur la page https://www.slimframework.com/docs/start/web-servers.html , compléter la fichier .htaccess de la manière suivante :

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Vérifier aussi que dans la configuration de Apache l’option AllowOverride est sur All.

Sur un raspberry il s’agit du fichier /etc/apache2/apache2.conf, section :

<Directory /var/www/>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

Comme indiqué page https://www.slimframework.com/docs/tutorial/first-app.html , le squelette de la page index est le suivant :

<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '../vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response) {
    $name = $request->getAttribute('name');
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

Nous allons nous inspirer de ce squelette afin que l’api puisse répondre  des données statiques dans un premier temps, puis des données issues de la BDD

exemple avec des données issues de la base de données

On suppose que la base de données est créée et qu’elle contient une table « ouvrants » (champs id et nom) avec quelques valeurs.

Dans Slim l’accès à la base de donnée est simplifié grâce à la bibliothèque slim/PDO . D’après la documentation[3]

L’usage typique se fait de la manière suivante :

require_once 'vendor/autoload.php';

$dsn = 'mysql:host=your_db_host;dbname=your_db_name;charset=utf8';
$usr = 'your_db_username';
$pwd = 'your_db_password';

$pdo = new \Slim\PDO\Database($dsn, $usr, $pwd);

// SELECT * FROM users WHERE id = ?
$selectStatement = $pdo->select()
                       ->from('users')
                       ->where('id', '=', 1234);

$stmt = $selectStatement->execute();
$data = $stmt->fetch();

// INSERT INTO users ( id , usr , pwd ) VALUES ( ? , ? , ? )
$insertStatement = $pdo->insert(array('id', 'usr', 'pwd'))
                       ->into('users')
                       ->values(array(1234, 'your_username', 'your_password'));

$insertId = $insertStatement->execute(false);

// UPDATE users SET pwd = ? WHERE id = ?
$updateStatement = $pdo->update(array('pwd' => 'your_new_password'))
                       ->table('users')
                       ->where('id', '=', 1234);

$affectedRows = $updateStatement->execute();

// DELETE FROM users WHERE id = ?
$deleteStatement = $pdo->delete()
                       ->from('users')
                       ->where('id', '=', 1234);

$affectedRows = $deleteStatement->execute();

 

On a ici les prototypes de l’instanciation d’un objet $pdo et des diverses requêtes select, delete, insert et update. L’intégration dans Slim se fait via un « container » permettant d’instancier un objet de la bibliothèque slim/PDO qui se situe dans un autre espace de nom que l’objet $app de Slim…

Le container va permettre de préparer, gérer et injecter des dépendances à l’application. L’avantage est que l’objet lié au container est créé à la demande et non de manière permanente. D’autre part les objets de l’application pourront accéder à des ressources dans d’autres espaces de nom.

Pour créer un container « db », insérer le code suivant :

<?php

use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '../vendor/autoload.php';

$config = [
    'settings' => [
        'displayErrorDetails' => true,

       
    ],
];
$app = new \Slim\App($config);
$container = $app->getContainer();
$container['db'] = function () {
    $dsn = 'mysql:host=localhost;dbname=apidb;charset=utf8';
    $usr = 'root';
    $pwd = '';
    $pdo = new \Slim\PDO\Database($dsn, $usr, $pwd);
    return $pdo;
};

Cela permet de créer un objet « db » qui sera instancié à la demande.

création d’une requête SELECT

Remplacer le code de la fonction $app->get(…) par celui-ci

/*********************************************************
 *    gestion des requêtes GET
 ******************************************************** */
$app->get('/{table}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    // SELECT * FROM {table}
    $selectStatement = $this->db->select()
            ->from($table);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetchAll();
    return $response->withJson($data);
});

$this->db fait référence au container  « db » créé précédemment.

Sur le même principe si on veut que l’api renvoie juste un produit dont on spécifie l’id, le code sera le suivant :

$app->get('/{table}/{id}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    $id = $request->getAttribute('id');
    // SELECT * FROM {table} WHERE id = {id}
    $selectStatement = $this->db->select()->from($table)->where("id","=",$id);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetch();
    return $response->withJson($data);
});

Code complet de la page /api/index.php

<?php

use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '../vendor/autoload.php';

$config = [
    'settings' => [
        'displayErrorDetails' => true,
       
    ],
];
$app = new \Slim\App($config);
$container = $app->getContainer();
$container['db'] = function () {
    $dsn = 'mysql:host=localhost;dbname=apidb;charset=utf8';
    $usr = 'root';
    $pwd = '';
    $pdo = new \Slim\PDO\Database($dsn, $usr, $pwd);
    return $pdo;
};

$app->get('/{table}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    // SELECT * FROM users WHERE id = ?
    $selectStatement = $this->db->select()
            ->from($table);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetchAll();
    return $response->withJson($data);
});

$app->get('/{table}/{id}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    $id = $request->getAttribute('id');
    // SELECT * FROM {table} WHERE id = {id}
    $selectStatement = $this->db->select()->from($table)->where("id","=",$id);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetch();
    return $response->withJson($data);
});

$app->run();

Installation de NG-ADMIN

ouvrir un terminal dans le dossier qui contient la solution puis effectuer la commande

npm install -g npm@2.x (sous linux faire précéder de sudo)
npm install ng-admin –-save

Cela installera le dossier node_modules et le sous dossier ng-admin qui contient tout le matériel pour le front-end.

Le dossier du projet va ressembler à ceci :

Modifier la page web index.php à la racine du site. Le code de la page est le suivant :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Controle d'accès</title>
       <link rel="stylesheet" href="node_modules/ng-admin/build/ng-admin.min.css">
    </head>
    <body ng-app="myApp">
        <div ui-view="ng-admin"></div>
        <script src="node_modules/ng-admin/build/ng-admin.min.js" type="text/javascript"></script>
        <script src="js/app.js"></script>
    </body>
</html>

Le corps de la page est constitué d’une simple balise <div ui-view=”ng-admin”></div> et le <body> fait référence à l’application angularJS sous-jacente grâce à l’attribut ng-app=”myApp”[4]

Rajouter un dossier « js » et un fichier nommé app.js (dont il est fait référence dans la page index.php). Le script va créer un module angularJS nommé « myApp » et qui intégrera la bibliothèque [‘ng-admin’], la suite permet de configurer l’application. La documentation détaillée au format pdf de ng-admin est accessible ici : https://www.gitbook.com/book/marmelab/ng-admin/details

Code de /js/app.js

// declare a new module called 'myApp', and make it require the `ng-admin` module as a dependency
var myApp = angular.module('myApp', ['ng-admin']);
// declare a function to run when the module bootstraps (during the 'config' phase)
myApp.config(['NgAdminConfigurationProvider', function (nga) {
        // create an admin application
        var admin = nga.application('Mes produits')
                .baseApiUrl('/api/'); // main API endpoint;

        /************************************
         *  entité produts
         ************************************/
        var produits = nga.entity('products');
        var categories= nga.entity('categories');
        categories.listView();
        // tableau = listview
        produits.listView()
                .title('les produits')
                .fields([
                    nga.field('id'),
                    nga.field('name').label('nom'),
                    nga.field('description'),
                    nga.field('price').label('prix')
                  ]) ;
        admin.addEntity(produits);
        admin.addEntity(categories);
        nga.configure(admin);
    }]);

L’application fonctionne,

Reste à intégrer dasn l’API les fonctions d’ajout, de mise à jour et d’effacement des produits.

code complet

/api /index.php

<?php

use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '../vendor/autoload.php';

$config = [
    'settings' => [
        'displayErrorDetails' => true,

       
    ],
];
$app = new \Slim\App($config);
$container = $app->getContainer();
$container['db'] = function () {
    $dsn = 'mysql:host=localhost;dbname=apidb;charset=utf8';
    $usr = 'root';
    $pwd = '';
    $pdo = new \Slim\PDO\Database($dsn, $usr, $pwd);
    return $pdo;
};

$app->get('/{table}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    // SELECT * FROM users WHERE id = ?
    $selectStatement = $this->db->select()
            ->from($table);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetchAll();
    return $response->withJson($data);
});

$app->get('/{table}/{id}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    $id = $request->getAttribute('id');
    // SELECT * FROM users WHERE id = ?
    $selectStatement = $this->db->select()->from($table)->where("id","=",$id);
    $stmt = $selectStatement->execute();
    $data = $stmt->fetch();
    return $response->withJson($data);
});

$app->post('/{table}', function (Request $request, Response $response) {
    $data=$request->getParsedBody();
    $table = $request->getAttribute('table');
    $keys= array_keys($data);
    $values=array_values($data);
    $insertStatement= $this->db->insert($keys)
            ->into($table)
            ->values($values);
    $stmt = $insertStatement->execute();
    $response->withJson($stmt);  
    
});

$app->put('/{table}/{id}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    $id = $request->getAttribute('id');
    $data=$request->getParsedBody();
    $updateStatement = $this->db->update($data)
            ->table($table)
            ->where('id',"=",$id);
        $stmt = $updateStatement->execute();
    $response->withJson($stmt);

});
$app->delete('/{table}/{id}', function (Request $request, Response $response) {
    $table = $request->getAttribute('table');
    $id = $request->getAttribute('id');
    $deleteStatement = $this->db->delete()->from($table)->where('id', '=', $id);
    $stmt = $deleteStatement->execute();
    $response->withJson($stmt);
});

$app->run();

 

/js/app.js

// declare a new module called 'myApp', and make it require the `ng-admin` module as a dependency
var myApp = angular.module('myApp', ['ng-admin']);
// declare a function to run when the module bootstraps (during the 'config' phase)
myApp.config(['NgAdminConfigurationProvider', function (nga) {
        // create an admin application
        var admin = nga.application('Mes produits')
                .baseApiUrl('/api/'); // main API endpoint;

        /************************************
         *  entité produts
         ************************************/
        var produits = nga.entity('products');
        var categories= nga.entity('categories');
        categories.listView();
        // tableau = listview
        produits.listView()
                .title('les produits')
                .fields([
                    nga.field('id'),
                    nga.field('name').label('nom'),
                    nga.field('description'),
                    nga.field('price').label('prix'),
                    nga.field('category_id','reference')
                            .targetEntity(categories)
                            .targetField(nga.field('name'))
                            .label('catégorie')
                ])
                .listActions(['edit']);
        produits.creationView()
                .title('Ajout d\'un produit')
                .fields(produits.listView().fields());
        
        produits.editionView()
                .fields(produits.creationView().fields());
        admin.addEntity(produits);
        admin.addEntity(categories);
        nga.configure(admin);
    }]);

 

  1. https://marmelab.com/fr/blog
  2. https://openclassrooms.com/courses/utilisez-des-api-rest-dans-vos-projets-web/pourquoi-rest
  3. https://www.grafikart.fr/tutoriels/php/slim-framework-831
  4. https://github.com/FaaPz/Slim-PDO
  5. Voir formation angularJS : https://www.grafikart.fr/formations/angularjs
    ou le livre « AngularJS » editions ENI ISBN 978-2-7460-9334-8