Génération de PDF avec CodeIgniter

Introduction

Dans votre application, vous désirez générer un fichier PDF avec vos données, votre charte graphique, etc. Voici un exemple d’utilisation d’une bibliothèque, qui permet de faire :

  • une entête
  • un pied de page
  • ajouter une pagination (numéro de la page en cours ; nombre de page du document)
  • insérer des images
  • insérer du texte HTML

 http://hulan.info/item/html2pdf-convert-your-xhtml-to-pdf-easily

 

Démarrage

Voici comment procéder pour installer cette bibliothèque dans votre projet :

  • Copier dans application/libraries les fichiers :
    • Fpdf_rotate.php
    • Fpdf.php
    • Pdf_html.php
    • Pdf.php
  • Copier dans application/third_party les fichiers :
    • tout le répertoire « fpdf/fonts »
  • Modifier le fichier application/config/config.php avec ces lignes :
//FPDF FONT DIRECTORY
$config['fonts_path'] = APPPATH.'third_party/fpdf/fonts/';

 

 

Générer le PDF depuis votre controller

Définir le point d’entrée du controller (classique CodeIgniter), par exemple « getAttestation() » :

public function getAttestation(){

 

Définir le répertoire des polices :

define('FPDF_FONTPATH',$this->config->item('fonts_path'));

 

Charger les bibliothèques :

$this->load->library(array('fpdf','fpdf_rotate','pdf', 'pdf_html'));

 

Mettre du contenu dans le PDF :

$pdf = new PDF_HTML();

$pdf->Open();
$pdf->SetTopMargin(10);
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetFont('Arial', '', 12);
$pdf->SetDrawColor(0);

$pdf->SetTitle('Attestation');
$pdf->Ln(5);
$pdf->SetFontSize(16);
$pdf->x = 70;
$pdf->Write(0, utf8_decode( "OK") );
$pdf->Output('', 'I'); // I : inline file

 

et terminer la fonction « getAttestation() »:

}

Lorsqu’on lance l’URL de ce controller dans le navigateur, un fichier PDF est généré :

 

Le fichier a pour titre « Attestation », comme prévu par le code.

CodeIgniter + React JS

Introduction

ReactJS est une bibliothèque prometteuse pour changer la façon dont les applications web sont conçues. J’ai retenu une chose importante, ce qui fait la différence par rapport à un appel AJAX simple et classique, c’est que React compare le DOM de la page avant l’appel et après l’appel pour appliquer les changements uniquement sur les modifications.

Il est donc inutile de faire recharger une page complète en cas d’ajout d’item dans une liste, ou même de l’ajouter « à la main » (avec un « append » en jQuery). React s’occupe de faire ces changements.

Soudain, on a envie de passer par cette bibliothèque pour tous les petites modifications à faire sur un écran (notification, compteur) et en exploitant cette technologie à son paroxysme, toute l’application y passe.

 

Quels impacts pour une application MVC ?

Si on prend React avec le framework « Flux », le MVC est mort. Dommage ! Faut-il recoder mes applications avec un autre paradigme ? Pas sûr.

Par contre, une application CodeIgniter qui désire utiliser React doit fondamentalement revoir la signification du V, pour la « Vue ».

En théorie, la vue, c’est ce que que les utilisateurs « voient », donc le HTML qui résulte du serveur hébergeant l’application.

Avec React, la Vue au sens HTML n’est plus à gérer par l’application. React s’en chargera.

Version classique du MVC de CodeIgniter (ou d’autres frameworks)

 

Casser la vue !

Oui, il faut casser la vue. L’application CodeIgniter ne fera que des interactions en manipulant des données JSON, et n’ayant que pour interface technique des WebServices REST.

Une autre application pourra se charger de la vue, des interactions utilisateur et des actions vers le serveur (liste des items, ajout, modification, suppression d’un item). Elle appellera l’application CodeIgniter pour le stockage en base de données, les règles métier, etc.

La partie « vue » de l’application est supportée par une autre application dédiée à l’interaction utilisateur. L’avantage est que si vous avez une application mobile à réaliser, elle pourra interagir avec cette application CodeIgniter en utilisant les mêmes WebServices.

Avec React, la vue peut supportée par une autre application.

Conclusion

La question qu’on peut se poser est la suivante : est-ce utile de s’appuyer sur un Framework MVC pour mon application s’il n’y a plus de Vue ?

S’il y a du boulot côté Modèle ou Controller (ou service) = des objets complexes, des règles métiers, la réponse est oui.

Si c’est une représentation HTML des objets simples en base de données, avec peu de règles métier, la réponse est non.

Raspberry Pi + Camera : pilotage par un smartphone

Intérêt

Le raspberry est une petite boite contenant l’équivalent d’un PC assez puissant pour faire quelques petites choses intéressantes. L’objectif est de brancher le raspberry sur une source d’alimentation (dans la voiture, près de la télé, sous un télescope) et de piloter quelques actions depuis un smartphone.

Exemples d’application :

  • Près d’un point de surveillance de la maison : Caméra de surveillance, avec enregistrement des vidéos sur une clé USB
  • Dans la voiture : Enregistrement de la conduite de manière automatique. en cas de litiges, vous aurez un enregistrement vidéo
  • Sous un télescope : avec une webcam derrière l’objectif pour faire la mise au point et une série de prises de vue enregistrées sur la clé USB
  • Près d’un moniteur ou d’une télé : pour lancer une présentation PowerPoint / PDF, pour jouer des vidéos, avec la webcam pour prendre des photos

Tout ça, avec un smartphone en guise de télécommande Wifi.

 

Configuration matérielle pour le démarrage

Voici le matériel nécessaire pour configurer le Raspberry complètement :

  • Un clavier USB
  • Une souris USB
  • Un écran
  • Une clé USB
  • Un accès à Internet

 

Configuration logicielle pour le démarrage

  1. Installation de l’image Ubuntu Mate pour Raspberry
  2. Installation des services :
    1. LAMP (Apache + PHP + MySQL), avec changement du user apache
    2. openssh-server (pour accès shell par SSH)
    3. usbmount (auto-mount les clés USB)
  3. Configuration des points de montage : pour la clé USB :  « uid=1000,noauto,user »
  4. Désactivation du firewall Ubuntu
  5. Installation d’un serveur FTP et SSH pour déposer les fichiers depuis un autre PC et lancer des lignes de commandes

 

Configuration logicielle étape 2

Cette étape se fait depuis un PC pour plus de confort. On a donc mis le Raspberry sur un réseau interne :

  • Un PC connecté via RJ45 à un routeur
  • Le Raspberry connecté en RJ45 au routeur

Par la suite, le câble RJ45 sera supprimé.

 

Le clavier, la souris et l’écran peuvent être supprimés après avoir installé les serveurs SSH et FTP.

L’installation d »es logiciels continue :

  1. Installation d’une application web pour accéder au Raspberry depuis un smartphone (PHP avec CodeIgniter) – Réalisée par mes soins ; cf chapitre « Application web pour contrôler le Raspberry » ci-dessous
  2. Changement de la politique de sécurité pour autoriser l’arret et reboot par un utilisateur quelconque (/usr/share/polkit-1/actions/org.freedesktop.login1.policy)
  3. Connexion automatique d’un user (pour ne pas avoir de mire de connexion)
  4. Ajout d’un script à la connexion pour lancer « xhost + » (pour que d’autres machines puissent ouvrir des fenêtres dans la session ouverte)
  5. Activation du dongle Wifi en mode AccessPoint (point d’accès Wifi) et paramétrage du DHCP

 

Application web pour contrôler le Raspberry

Cette application web permettra de piloter le Rasbberry depuis un smartphone. Elle est développée en PHP avec le Framework CodeIgniter. Un template HTML permet d’avoir une charte graphique en Responsive Design : http://binarycart.com/bclivedemos/01-05-2014/v1/bs-binary-admin/index.html

 

Fonctionnalités basiques de l’application PHP :

  • Etat du raspberry : charge machine, type de processeur, version du noyau, version et nom du système (Ubuntu)
  • Lancement de scripts SH (exemple : lancer une présentation avec un fichier PDF précis)
  • Parcours dans les répertoires
  • Caméra : prise de photo, enregistrement d’une vidéo
  • Arrêt et relance du raspberry

 

Utilisation avec un smartphone

Après avoir mis sous tension le raspberry, le smartphone peut s’y connecter par wifi. Un simple navigateur permet d’accéder aux fonctionnalités. Le smartphone devient une télécommande.

 

Copies d’écran sur smartphone

Page de connexion

Page d’accueil : description de la machine

Menu

Page des scripts à lancer

Page des médias USB connectés et détectés

Parcours des fichiers du Raspberry

Page de la gestion de la caméra : prendre une photo, prendre des vidéos

Page d’arrêt / relance du Raspberry

Plugin MultiSelect dans le générateur de code

Introduction

Dans le générateur de code (voir l’article qui le décrit), les fichiers classiques de l’entité sont générés, mais il en manque une partie. Lorsqu’on défini une liaison n-n d’une entité vers une autre, on utilise un « objet de liaison ». Cet objet se décrit par :

  • un identifiant qui est clé unique de l’objet de liaison
  • l’identifiant de l’objet A
  • l’identifiant de l’objet B

Grâce à cet objet, un objet A est relié à n objets B et inversement, un objet B est relié à n objets A.

Exemple : entité « Album de bande dessinée » et entité « Auteur » :

  • un album de BD peut avoir plusieurs auteurs
  • un auteur peut avoir fait plusieurs albums de BD

 

L’objectif est de générer automatiquement du code pour avoir un multiselect dans la création et d’édition d’une entité A ou B.

 

Application avec les BD et les auteurs

L’objet « LienAuteurAlbum » est décrit comme ceci :

  • identifiant du lien
  • identifiant de l’album
  • identifiant de l’auteur

Remarque : l’identifiant du lien permet de supprimer facilement et sereinement un enregistrement avec une seule information, en plus des fonctionnalités offertes avec les autres champs.

Après moult réflexion, j’ai ajouté des features sur chaque entité de liaison :

Ces modifications sont faites dans la génération du helper et du modèle. Deux nouvelles vues ont étés faire pour l’appel du plugin multiselect, sous forme d’un fragment (une vue qui s’appelle depuis une vue principale). Une vue pour la création et une vue pour la modification.

Par contre, il faut modifier manuellement les controller et les vues des entités (exemple : Album) pour utiliser cet objet de liaison.

Appel du code généré pour la vue en édition ou en création d’album

Lorsque je crée un album, je veux avoir le multiselect des auteurs. Il faut alors ajouter dans la vue de création d’un album le code suivant :

<?php 

$data["multiselectOf"] = 'Auteur';
$this->load->view('lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php', $data);

?>

Le fichier « lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php » est généré et ne doit pas être modifié. Dans le paramètre « multiselectOf », il faut spéficier que je veux un multiselect de … « Auteur », afin d’avoir une liste d’auteurs à choisir.

Idem dans la vue d’édition d’un Album.

 

Modification du controller d’édition d’album

Dans le controller d’édition, lorsqu’on sauve les données saisies, il faut [1] supprimer les anciennes relations dans « LienAuteurAlbum » dans lesuqelles il y a l’album édité et [2] créer des nouveaux objets « LienAuteurAlbum » avec cet album et les auteurs cochés.

Première partie :

$lienAuteurAlbum_autidaut = $this->input->post('LienAuteurAlbum_autidaut');
LienAuteurAlbum_model::deleteAllLienAuteurAlbumsBy_xaaidalb($this->db, $model->albidalb);

ligne 1 : récupération des liens avec des identifiants d’auteurs que l’utilisateur a coché dans le multiselect

ligne 2 : suppression des anciens liens à partir de l’identifiant de l’album

 

Deuxième partie :

foreach($lienAuteurAlbum_autidaut as $autidaut){
            $auteurModel = new LienAuteurAlbum_model();
            $auteurModel->xaaidalb = $model->albidalb;
            $auteurModel->xaaidaut = $autidaut;
            $auteurModel->save($this->db);
}

Pour chaque auteur coché, on créer un nouveau lien avec :

  • album : celui que je suis en train d’éditer
  • auteur : un auteur coché

Remarque : Dans le controller de création d’un album, la première partie destinée à supprimer les anciens liens avec les auteurs n’est pas à faire.

 

 Sans oublier le javascript

Le plugin JS est à charger après les différents appels de jQuery et du JS du plugin multiselect. J’ai choisi de le mettre manuellement dans le JS de la vue de l’objet édité ou créé.

$(function(){
    $("#LienAuteurAlbum_autidaut").multiselect();
});

 

Résultat

Dans cet exemple, le champ « Genre » est un select simple (déjà disponible dans le générateur). Le champ « Liste des auteurs » fait appel au plugin multiselect.

 

Conclusion

Finalement, la génération de code a ses limites, mais beaucoup de choses sont pré-mâchées. Pour aider les futurs développeurs à intégrer le plugin, j’ai inséré le code à copier-coller dans la vue et dans le JS.

Rien n’est généré pour le controller, il faudra lire cet article si vous ne savez pas comment faire (mais je trouve que ce n’est pas trop difficile).

 

Application Back-office & Mobile : CodeIgniter & Kivy

Introduction

Après quelques essais avec Kivy pour faire une application mobile qui indique des données d’infusion sur les thés, je me lance pour faire une application en back-office pour gérer les données. L’application mobile récupère les données en JSON et stocke dans une base SQLite.

Voici les outils utilisés de chaque coté :

Back-office Appli Mobile
Générateur de code Écriture du code « à la main »
PHP, CodeIgniter Python, Kivy
MySQL SQLite3

 

Principe général :

 

Application Back-office

Avec le générateur de code (plus d’infos sur ce site), l’application est créée, déployée et les données ajoutées, le tout en 1 heure sur « free.fr ». Le plus long a été de copier / coller les données entre ma source (le site du palais des thés) et ma nouvelle application.

Le controller fournissant le flux JSON est lui aussi généré :

Les écrans n’ont pas été retravaillés suite à la génération de code, mais c’est suffisant pour tout faire fonctionner sur l’environnement de « free.fr » : http://jc.specs.free.fr/TeaTime/ (un identifiant et mot de passe sont nécessaires…). Le nouveau thème Ubuntu (en gris et orange a été choisi).

Écrans du back-office

Catégorie de thés :

Thés :

Application mobile

Avec Kivy, je disposais déjà d’une application qui lit un JSON sous forme de fichier. Il m’a suffit de lui permettre de lire des fichiers JSON par Internet, de remplir une base de données SQLite et de la remplir.

Voici les étapes de réalisation :

  1. Lire dans un processus parallèle le JSON. Au lieu d’attendre que l’URL réponde, les widgets de l’écran de l’appli mobile sont disponibles.
  2. Stocker le JSON dans une base SQLite embarquée avec l’application. Lorsque c’est fait, un widget de l’application est modifié : un label devient « Fichier chargé ».
  3. Lorsque l’utilisateur entre dans les écrans de l’appli mobile, il faut lire la base de données SQLite et valoriser les objets qui seront utilisés par Kivy.

Kyvi n’est d’aucune utilité pour réaliser ces opérations de synchronisation ; Python prend le dessus : gestion des processus asynchrones, lecture d’une URL, transformation de JSON en objet, accès à une base de données SQLite.

 

Écrans de l’appli mobile

   

Le fichier APK de l’application mobile est téléchargeable sur http://jc.specs.free.fr/TeaTime/

 

Test unitaires dans le générateur de code

Après avoir intégré avec succès les tests unitaires dans CodeIgniter avec Toast, j’ai ajouté un template de test unitaire dont l’objectif est de s’assurer que la couche modèle fonctionne. Ces tests sont indépendants de la BDD métier car ils utilisent une base SQLite3.

Pour ce faire, Toast permet de créer un controller dédié à cette tâche.

J’ai choisi une entité qui s’appelle « Groupe » et qui a 3 propriétés « grpidgrp = Identifiant », « grplblib = Libellé », « grpidsoc = Clé étrangère vers l’entité Société ». Voici en détail les tests réalisés (automatiquement générés avec le code de l’application, donc).
(Tout ce code est généré, rien n’a été retouché)

/*
 * Created by generator
 *
 */
require_once(APPPATH . '/controllers/test/Toast.php');

class GroupeTest extends Toast {

	function __construct(){
		parent::__construct();
		$this->load->database('test');

		$this->load->model('Groupe_model');

	}

	/**
	 * OPTIONAL; Anything in this function will be run before each test
	 * Good for doing cleanup: resetting sessions, renewing objects, etc.
	 */
	function _pre() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	/**
	 * OPTIONAL; Anything in this function will be run after each test
	 * I use it for setting $this->message = $this->My_model->getError();
	 */
	function _post() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	public function test_insert(){
		$this->message = "Tested methods: save, getGroupe, delete";
		// création d'un enregistrement
		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);
		// $groupe_insert->grpidgrp est maintenant affecté

		$groupe_select = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		$this->_assert_equals($groupe_select->grpidgrp, $groupe_insert->grpidgrp);
		Groupe_model::delete($this->db, $groupe_select->grpidgrp);
	}

	public function test_update(){
		$this->message = "Tested methods: save, update, getGroupe, delete";
		$groupe_insert = new Groupe_model();

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test1_0';
		$groupe_insert->grpidsoc = 90;
		$groupe_insert->update($this->db);

		$groupe_update = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		if(!$this->_assert_equals($groupe_insert->grpidgrp, $groupe_update->grpidgrp)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grplblib, $groupe_update->grplblib)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grpidsoc, $groupe_update->grpidsoc)) {
			return false;
		}

		Groupe_model::delete($this->db, $groupe_insert->grpidgrp);
	}

	public function test_count(){
		$this->message = "Tested methods: getCountGroupes, save, getGroupe, delete";

		// comptage pour vérification : avant
		$countGroupesAvant = Groupe_model::getCountGroupes($this->db);

		// création d'un enregistrement
		$groupe = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe->grplblib = 'test_0';
		$groupe->grpidsoc = 0;
		$groupe->save($this->db);

		// comptage pour vérification : après insertion
		$countGroupesApres = Groupe_model::getCountGroupes($this->db);

		// verification d'ajout d'un enregistrement
		$this->_assert_equals($countGroupesAvant +1, $countGroupesApres);

		// recupération de l'objet par son  grpidgrp
		$groupe = Groupe_model::getGroupe($this->db, $groupe->grpidgrp);

		// suppression de l'enregistrement
		Groupe_model::delete($this->db, $groupe->grpidgrp);

		// comptage pour vérification : après suppression
		$countGroupesFinal = Groupe_model::getCountGroupes($this->db);
		$this->_assert_equals($countGroupesAvant, $countGroupesFinal);

	}

	function test_list(){
		$this->message = "Tested methods: save, getAllGroupes, delete";

		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		$groupes = Groupe_model::getAllGroupes($this->db);
		if( ! $this->_assert_not_empty($groupes) ) {
			return FALSE;
		}
		$found = 0;
		foreach ($groupes as $groupe) {
			if($groupe->grpidgrp == $groupe_insert->grpidgrp &&
					$this->_assert_equals($groupe->grplblib, $groupe_insert->grplblib ) &&
					$this->_assert_equals($groupe->grpidsoc, $groupe_insert->grpidsoc )
				){
				$found++;
			}
		}
		if( $found == 1 ){
			Groupe_model::delete($this->db, $groupe->grpidgrp);
			return TRUE;
		}else{
			return FALSE;
		}
	}

}

 

Il faut récupérer le script SQLite3 généré et l’intégrer dans la BDD de TU :

/*
 * Lancer la commande suivante pour insérer les données
 * cat cretab_groupe.sqlite | sqlite3 test_database.sdb
 */

CREATE TABLE expgrp (
	grpidgrp integer NOT NULL PRIMARY KEY AUTOINCREMENT ,
	grplblib varchar(255) NOT NULL ,
	grpidsoc integer NOT NULL
);

 

Dans un navigateur, voici le résultat sur l’URL du controller de TU :

A ce stade, aucune ligne de code n’a dû être écrite par un développeur. Il pourra commencer son application avec des bases saines. J’ose le terme de « Framework Métier », permettant de poser les briques du système à réaliser.

 

SQLite avec CodeIgniter

Introduction

Voici un descriptif des étapes à faire pour connecter son application CodeIgniter v2 à une base de données SQLite3. Cet article est à lier à celui sur les tests unitaires dans CodeIgniter avec Toast. L’intérêt est qu’une base de données de test est à disposition pour ne faire que des tests et laisser la base cible (de dev par exemple) en place, sans rien y changer.

Attention : CodeIgniter version 2.1.0 ne fonctionne pas avec SQLite3. Il faut passer à la version 2.1.3

 

Étapes

Installation des packages :

sudo apt-get install sqlite3 php5-sqlite php5-curl

Vérification des extensions PHP:

ls /etc/php5/apache2/conf.d

Il doit y a voir « pdo.ini » et « sqlite3.ini »

 

Définir des tables dans la base de données :

cat script.sql | sqlite3 test_database.sqlite

Le fichier « script.sql » contient les lignes suivantes :

CREATE TABLE expsoc (
    socidsoc integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    soclblib varchar(255) NOT NULL
);

 

Autoriser l’accès au fichier « test_database.sqlite » :

chmod o+rw test_database.sqlite

 

Ajouter un fichier de langue (vide) dans /application/language/french/db_lang.php

 

Configurer la base de données dans CodeIgniter :

require_once(APPPATH . '/controllers/test/Toast.php');

class SocieteTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
		$this->load->model('Societe_model');

		$config['hostname'] = "sqlite:".APPPATH."/database/test_database.sdb";
		$config['username'] = "";
		$config['password'] = "";
		$config['database'] = "";
		$config['dbdriver'] = "pdo";
		$config['dbprefix'] = "";
		$config['pconnect'] = TRUE;
		$config['db_debug'] = TRUE;
		$config['cache_on'] = FALSE;
		$config['cachedir'] = "";
		$config['char_set'] = "utf8";
		$config['dbcollat'] = "utf8_general_ci";
		$config['swap_pre'] = '';
		$config['autoinit'] = TRUE;
		$config['stricton'] = FALSE;

		$this->load->database($config);

	}
	...
}

 

Un test basique

Voici une méthode du controller de test (dont le début est ci-dessus) pour valider que les accès à la base de données sont toujours bons :

    public function test_count(){

        // comptage pour vérification : avant
        $countSocietesAvant = Societe_model::getCountSocietes($this->db);

        // création d'un enregistrement
        $societe = new Societe_model();
        $societe->socidsoc = 1;
        $societe->soclblib = "Ma société";
        $societe->save($this->db);

        // comptage pour vérification : après insertion
        $countSocietesApres = Societe_model::getCountSocietes($this->db);

        // verification d'ajout d'un enregistrement
        $this->_assert_equals($countSocietesAvant +1, $countSocietesApres);

        // recupération de la societe socidsoc=1
        $societe = Societe_model::getSociete($this->db, 1);

        // suppression de l'enregistrement
        Societe_model::delete($this->db, $societe->socidsoc);

        // comptage pour vérification : après suppression
        $countSocietesFinal = Societe_model::getCountSocietes($this->db);

        $this->_assert_equals($countSocietesAvant, $countSocietesFinal);

    }

On y fait :

  1. Décompte des enregistrements dans la base
  2. Ajout d’un enregistrement
  3. Décompte des enregistrements dans la base
  4. Vérification de l’ajout : [nb avant] + 1 == [nb après]
  5. Récupération de l’enregistrement ajouté
  6. Suppression de l’enregistrement
  7. Décompte des enregistrements dans la base
  8. Vérification de la suppression : [nb avant] == [nb à la fin]

 

Lancement du test

L’URL suivante permet de tester unitairement certains accès à la base :

http://localhost/monprojet/index.php/test/societetest/count

 

Voici le résultat :

 

Reste à faire

Il reste à intégrer ce genre de tests unitaires dans le générateur de code afin de disposer de ces tests dès que l’application est prête :

  • Un nouveau template pour le controller de test (1 controller de test par entité)
  • Nouveau template pour le SQL à passer dans SQLite3 (1 fichier sqlite par entité)

 

Test unitaires avec CodeIgniter

Introduction

Les tests unitaires, tout le monde connait. En Java, junit fait l’unanimité (en plus, il est intégré à Eclipse ce qui en fait une arme redoutable). En PHP, avec le framework CodeIgniter, beaucoup de choses sont proposées, mais sans tutoriel pour montrer comment ça fonctionne concrètement.

Voici ma méthode, avec PHPUnit, Toast et CodeIgniter v2. Bien sûr, ça n’a pas fonctionné du premier coup (ce serait trop facile). Les prérequis sont :

 

Il faut commencer par revoir l’installation de Toast

Juste un petit mot sur Toast : il est basé sur les classes de test unitaires du framework de Codeigniter (qui s’appuie sur PHPUnit pour les tests unitaires) ; il est composé de quelques fichiers seulement (facile à maintenir et à customiser) ; il permet de se connecter à une base données indépendantes, juste pour les tests. C’est donc un bon candidat pour faire ces tests unitaires.

  • Les fichiers Toast sont dans « monProjet/application/controllers/test/ » pour les contrôleurs (qui feront les tests) et dans « monProjet/application/views/test/ » pour le rendu graphique.
  • Fichier « Toast.php » : la classe doit être déclarée pour utiliser CodeIgniter v2 et il faut recâbler les accès au nom de la classe de test :
abstract class Toast extends CI_Controller {
  ...
  function __construct(){
		parent::__construct();
		$this->load->library('unit_test');
		$this->modelname = get_class($this); //$name;
		$this->modelname_short = get_class($this); //basename($name, '.php');
		$this->messages = array();
	}
  ...
}
  • Idem avec le fichier « Toast_all.php » :
class Toast_all extends CI_Controller {
  ...
  function __construct(){ 
      parent::__construct(); 
  }
  ...
}

 

Le premier test unitaire basique peut commencer

  • Créez un nouveau fichier « SimpleTest.php » :

Il faut préfixer les méthodes de test par « test_ » et seul la fin du nom de la méthode sera présentée dans le rendu graphique.

require_once(APPPATH . '/controllers/test/Toast.php');

class SimpleTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
	}

	public function test_basic(){
		$my_var = 2 + 2;
		$this->_assert_equals($my_var, 4);
	}

	function test_false(){
		// Test code goes here
		$my_var = true;
		$this->_assert_false($my_var);
	}
}

Ce code est celui de Toast. Je pense faire une version plus correcte, mais ce sera pour plus tard…

 

Voir le résultat du test

Le test unitaire est vu comme un contrôleur du projet. Il suffit d’y accéder par son URL, dans son navigateur favori :

http://localhost/monProjet/index.php/test/simpletest

Le résultat est une page web qui présente les tests réalisés :

On peut cliquer sur le nom de la classe ou sur le nom de la méthode pour limiter l’affichage. Toast propose aussi un lancement de tous les tests disponibles.

Par la suite, on peut imaginer automatiser le lancement de ces tests avec un « wget » pour lancer le test suivi d’un « grep FAILED » pour filtrer le résultat. Je me demande si un rendu XML ou JSON peut être réalisé…

 

La suite

Pour clôturer ce sujet, je ferai un VRAI test unitaire dans mon projet, maintenant qu’on sait comment ça fonctionne. Si vous avez des idées de customisation du rendu graphique, postez-les !

J’intègrerai aussi certainement des tests unitaires dans mon générateur de code afin de pondre aussi des tests tout prêts.

 

i18n dans le générateur de CRUD

Après l’article sur une amélioration de l’internationalisation dans CodeIgniter, j’ai intégré la gestion des traductions dans le générateur de CRUD pour CodeIgniter. Un fichier de plus est généré (un fichier de langue) par entité et tous les libellés sont maintenant appelés par cette interface. Les vues n’ont plus aucun libellé.

En conclusion, mon dernier template « Bootstrap » contient cette évolution, mais je ne pense pas le faire pour les autres templates, sauf si un nouveau projet sur un template voit le jour.

Voici le fichier de langue qui sera parsé par le générateur :

%[kind : lang]
%[file : messages_%%(self.obName.lower())%%_lang.php]
%[path : language/french]
<?php
/**
 * Message file for entity %%(self.obName)%%
 *
 * Please don't forget to load this file:
 *  Solution A : Use "/application/config/autoload.php"
 *               Add this line:
 *               $autoload['language'] = array(..., 'messages_%%(self.obName.lower())%%', ...);
 *
 *  Solution B : Load this message file anywhere you want.
 *  
 */

$lang['%%(self.obName.lower())%%.message.confirm.deleted'] = "%%(self.displayName)%% supprimé";
$lang['%%(self.obName.lower())%%.message.confirm.added'] = "%%(self.displayName)%% créé avec succès";
$lang['%%(self.obName.lower())%%.message.confirm.modified'] = "%%(self.displayName)%% mis à jour avec succès";

$lang['%%(self.obName.lower())%%.form.create.title'] = "Ajouter un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.edit.title'] = "Editer un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.list.title'] = "Liste des %%(self.displayName.lower())%%s";

$lang['%%(self.obName.lower())%%.menu.item'] = "%%(self.displayName)%%";

%%allAttributesCode = ""
for field in self.fields:
    attributeCode = """$lang['%(objectObName)s.form.%(dbName)s.label'] = "%(obName)s";
$lang['%(objectObName)s.form.%(dbName)s.description'] = "%(desc)s";  
""" % {    'dbName': field.dbName,
        'obName': field.obName,
        'objectObName':self.obName.lower(),
        'desc' : field.description
    }
    allAttributesCode += attributeCode
    
RETURN = allAttributesCode
%%

?>

 

i18n avec arguments : extension pour CodeIgniter

Dans CodeIgniter, il est possible de réaliser un fichier de traduction et d’y faire appel dans les vues.

Des articles sur le web donnent des pistes : [Google]

Par contre, pour ajouter des arguments à la chaine de texte à traduire, ça se corse. Comme en java, j’aimerai avoir le texte « Vous avez 5 messages dont 2 non lus ». La chaine de base serait « Vous avez {arg1} messages dont {arg2} non lus » et les 2 arguments seraient « 5″ et « 2″.

Voici comment procéder avec CodeIgniter v2.1.0 :

  1. Créez un fichier « /application/core/MY_Lang.php » :
    <?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
    
    class MY_Lang extends CI_Lang{
    
        public function __construct() {
            parent::__construct();
        }
    
        /**
         * Fetch a single line of text from the language array. Takes variable number
         * of arguments and supports wildcards in the form of '%1', '%2', etc.
         * Overloaded function.
         *
         * @access public
         * @return mixed false if not found or the language string
         */
        public function line(){
            //get the arguments passed to the function
            $args = func_get_args();
    
            //count the number of arguments
            $c = count($args);
    
            //if one or more arguments, perform the necessary processing
            if ($c) {
                //first argument should be the actual language line key
                //so remove it from the array (pop from front)
                $line = array_shift($args);
    
                //check to make sure the key is valid and load the line
                $line = ($line == '' OR ! isset($this->language[$line])) ? $line : $this->language[$line];
    
                //if the line exists and more function arguments remain
                //perform wildcard replacements
                if ($line && $args) {
                    $i = 1;
                    foreach ($args as $arg)
                    {
                        $line = preg_replace('/\%'.$i.'/', $arg, $line);
                        $i++;
                    }
                }
            } else {
                //if no arguments given, no language line available
                $line = false;
            }
    
            return $line;
        }
    
    }
    
    ?>
  2. Utilisez vos arguments dans le fichier de traduction de cette façon :
    <?php
    $lang['list.evenements.title'] = "Liste des evènements pour %1";
    ?>
  3. Passez par la nouvelle classe pour traduire vos libellés avec les arguments :
    <?= $this->lang->line('list.evenements.title', $month.'/'.$year) ?>

Dernière modification : si la traduction n’est pas retrouvée, afficher le premier paramètre.