Tag Archives

3 Articles
Introduction au ZF – Partie 5 : Doctrine

Introduction au ZF – Partie 5 : Doctrine

Pour les Models, je pensais vous montrer l’exemple concret de l’usine à gaz présentée par Zend. Mais vu que le Zend Framework 2.0 va le changer pour doctrine, j’ai décidé de passer dessus. Doctrine a plusieurs points positifs :

  • Il utilise PDO donc il y a toujours une couche d’abstraction
  • Il génère automatiquement les models (si on utilise l’utilitaire)
  • Il est plus performant que Zend_Db
  • Tout est objet
  • Conforme au pattern MVC

Pour ces raisons, Doctrine semble être le choix le plus judicieux. Maintenant il ne reste plus qu’à l’intégrer à notre projet. Pour informations, j’utilise la version 1.2.2 de Doctrine qui est la dernière version stable en date. Vous pouvez la récupérer à cette adresse.

Je vous conseil aussi d’avoir la documentation officielle sous le bras pour bien comprendre tout ce qu’il va se dire ici. Avant de continuer, voici la base de donnée finale que nous allons utiliser tout au long de ce tutoriel. Je mets le contenu final ici pour que l’on génère les Models complets dès le début.

C’est avec une application bien pensée qu’on arrive à un bon résultat.

Et voici maintenant le script avec un utilisateur par défaut. Le mot de passe décrypté est azerty.

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Base de données: `tutosite`
--
CREATE DATABASE `tutosite` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `tutosite`;

-- --------------------------------------------------------
--
-- Structure de la table `article`
--

CREATE TABLE IF NOT EXISTS `article` (
  `idArticle` int(11) NOT NULL AUTO_INCREMENT,
  `idUtilisateur` int(11) NOT NULL,
  `idRubrique` int(11) NOT NULL,
  `titre` varchar(45) NOT NULL,
  `corps` text NOT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`idArticle`),
  KEY `fk_Article_Utilisateurs` (`idUtilisateur`),
  KEY `fk_Article_Rubrique` (`idRubrique`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

--
-- Contenu de la table `article`
--
-- --------------------------------------------------------
--
-- Structure de la table `commentaire`
--

CREATE TABLE IF NOT EXISTS `commentaire` (
  `idCommentaire` int(11) NOT NULL AUTO_INCREMENT,
  `idArticle` int(11) NOT NULL,
  `pseudo` varchar(45) NOT NULL,
  `email` varchar(45) DEFAULT NULL,
  `message` text NOT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`idCommentaire`),
  KEY `fk_Commentaire_Article` (`idArticle`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

--
-- Contenu de la table `commentaire`
--
-- --------------------------------------------------------
--
-- Structure de la table `rubrique`
--

CREATE TABLE IF NOT EXISTS `rubrique` (
  `idRubrique` int(11) NOT NULL,
  `Nom` varchar(45) NOT NULL,
  PRIMARY KEY (`idRubrique`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Contenu de la table `rubrique`
--
-- --------------------------------------------------------
--
-- Structure de la table `tag`
--

CREATE TABLE IF NOT EXISTS `tag` (
  `idTag` int(11) NOT NULL,
  `Nom` varchar(45) NOT NULL,
  `Couleur` varchar(7) NOT NULL DEFAULT '#000000',
  `Hicolor` varchar(7) NOT NULL DEFAULT '#000000',
  PRIMARY KEY (`idTag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Contenu de la table `tag`
--
-- --------------------------------------------------------
--
-- Structure de la table `tagarticle`
--

CREATE TABLE IF NOT EXISTS `tagarticle` (
  `idTag` int(11) NOT NULL,
  `idArticle` int(11) NOT NULL,
  PRIMARY KEY (`idTag`,`idArticle`),
  KEY `fk_Tag_has_Article_Tag` (`idTag`),
  KEY `fk_Tag_has_Article_Article` (`idArticle`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

--
-- Contenu de la table `tagarticle`
--
-- --------------------------------------------------------
--
-- Structure de la table `utilisateurs`
--

CREATE TABLE IF NOT EXISTS `utilisateurs` (
  `idutilisateur` int(11) NOT NULL AUTO_INCREMENT,
  `login` varchar(50) NOT NULL,
  `passwd` varchar(100) NOT NULL,
  PRIMARY KEY (`idutilisateur`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

--
-- Contenu de la table `utilisateurs`
--

INSERT INTO `utilisateurs` (`idutilisateur`, `login`, `passwd`) VALUES
(1, 'admin', 'ab4f63f9ac65152575886860dde480a1 ');

--
-- Contraintes pour les tables exportées
--
--
-- Contraintes pour la table `article`
--
ALTER TABLE `article`
  ADD CONSTRAINT `fk_Article_Rubrique` FOREIGN KEY (`idRubrique`) REFERENCES `rubrique` (`idRubrique`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  ADD CONSTRAINT `fk_Article_Utilisateurs` FOREIGN KEY (`idUtilisateur`) REFERENCES `utilisateurs` (`idutilisateur`) ON DELETE NO ACTION ON UPDATE NO ACTION;
--
-- Contraintes pour la table `commentaire`
--
ALTER TABLE `commentaire`
  ADD CONSTRAINT `fk_Commentaire_Article` FOREIGN KEY (`idArticle`) REFERENCES `article` (`idArticle`) ON DELETE NO ACTION ON UPDATE NO ACTION;

Voilà pour la partie base de données SQL. Maintenant nous allons nous attaquer à la configuration. Nous allons tout d’abord créer les répertoires suivants :

  • Dossier contenant les données des tables en script YAML générés par Doctrine /application/configurations/data/fixtures
  • Dossier contenant les scripts SQL générés par Doctrine /application/configurations/data/sql
  • Dossier de migration de la base de données /application/configurations/migration
  • Dossier contenant le script de génération des cmodels /application/scripts
  • Dossier contenant nos models générés /library/App/Models

Quand la création des dossiers est faite, vous allez télécharger Doctrine 1.2.2. La version sandbox est inutile ici vu que les scripts donnés pour la génération doivent être complètement refait pour Zend.

Lorsque vous avez téléchargé Doctrine, vous allez extraire son contenu dans /library. Vous devriez alors avoir le dossier /library comme la capture sur votre droite. Le dossier vendor permet d’utiliser les scripts YAML quand au dossier Doctrine, il contient tout le cœur de Doctrine.

Maintenant que Doctrine a été correctement ajouté au dossier Library, Que la base de donnée a bien été ajoutée au SGBD, nous pouvons passer à la suite qui va consister à configurer proprement Zend.

Tout d’abord, nous allons rajouter les lignes suivantes au fichier /application/configurations/app.ini

; ---
; Doctrine
; ---

; configuration de base
; Le dsn correspond aux infos de connexion. Ici nous appelons une base mysql
; SGBD : MySqL
; Login : Root
; Password : azerty //si aucun password mettre juste root@localhost/bdd
; Serveur : localhost
; BDD : tutosite
doctrine.dsn                = "mysql://root:azerty@localhost/tutosite"
doctrine.data_fixtures_path = APPLICATION_PATH "/configurations/data/fixtures"
doctrine.sql_path           = APPLICATION_PATH "/configurations/data/sql"
doctrine.migrations_path    = APPLICATION_PATH "/configurations/migrations"
doctrine.yaml_schema_path   = APPLICATION_PATH "/configurations/schema.yml"
doctrine.models_path = APPLICATION_PATH "/../library/App/Models"

; règles lors de la génération des classes
; on utilise la convention de nommage PEAR
doctrine.generate_models_options.pearStyle  = true
; on génère les classes de table
doctrine.generate_models_options.generateTableClasses  = true
; on génère les classes de base. Ces classes contiennent les définitions de nos tables
doctrine.generate_models_options.generateBaseClasses  = true
; on définit un préfixe pour les classes de base
doctrine.generate_models_options.baseClassPrefix  = "Base_"
; on ne pet rien ici, avec la convention pear, ils seront ajoutés dans le sous dossier Base
doctrine.generate_models_options.baseClassesDirectory  =
; on ne met pas le préfixe au nom de fichier
doctrine.generate_models_options.classPrefixFiles  = false
; toutes les classes commenceront par ce préfixe
doctrine.generate_models_options.classPrefix = "App_Models_"

Ensuite, nous allons nous intéresser au /application/Bootstrap.php et nous allons ajouter la fonction d’initialisation de Doctrine :

protected function _initDoctrine() 
{
    //on met Doctrine en autoload
    $this->getApplication()
         ->getAutoloader()
         ->pushAutoloader ( array ('Doctrine', 'autoload' ) );
    spl_autoload_register(array('Doctrine', 'modelsAutoload'));

    //on récupère une instance de Doctrine
    $manager = Doctrine_Manager::getInstance ();

    //permet de valider automatiquement l'intégrité des données
    //ce qui veut dire que l'on ne peut pas mettre une variable de type string
    //dans une variable de type int.
    $manager->setAttribute (Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
    //l’AUTO_ACCESSOR_OVERRIDE va nous permettre de personnaliser l’assignation de données.
    $manager->setAttribute ( Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true );
    //Doctrine permet de personnaliser également les classes de table en permettant 
    //de créer des méthodes propres à une table. 
    //Ce paramètre permet de charger le fichier contenant nos méthodes personnalisées.
    $manager->setAttribute (
     Doctrine::ATTR_MODEL_LOADING, 
     Doctrine::MODEL_LOADING_CONSERVATIVE
    );
    //on permet le chargement des classes table
    $manager->setAttribute ( Doctrine::ATTR_AUTOLOAD_TABLE_CLASSES, true );

    //on récupère toutes les options doctrine du fichier app.ini
    $doctrineConfig = $this->getOption('doctrine');

    //on récupère la variable doctrine.models_path du fichier app.ini
    //afin d'avoir le répertoire des models
    //pour que Doctrine les charge
    Doctrine::loadModels($doctrineConfig['models_path']);

    //on récupère la connexion à mysql et on la nomme doctrine
    $conn = Doctrine_Manager::connection($doctrineConfig['dsn'],'doctrine');
    //je sais plus ce que sa veut dire, mais il le faut
    $conn->setAttribute(Doctrine::ATTR_USE_NATIVE_ENUM,true);

    //on définit la sortie encodée en UTF-8
    $conn->setCharset('utf8');
    $conn->setCollate('utf8_general_ci');

    //on retourne la connexion
    return $conn;
}

Et pour que tout soit chargé directement, il suffit d’ajouter ces lignes dans la fonction _initNamSpaces() :

$autoloader->registerNamespace('Doctrine_');
$autoloader->registerNamespace('Doctrine');

Bien, maintenant Doctrine est correctement intégré à notre framework. Il ne reste plus qu’à générer les classes. Pour ce faire, il nous fait le fichier script suivant /application/scripts/Doctrine.php :

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH',
              realpath(dirname(__FILE__) . '/..'));

// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV',
              (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV')
                                         : 'production'));

// Typically, you will also want to add your library/ directory
// to the include_path, particularly if it contains your ZF install
set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    get_include_path(),
)));

require_once 'Doctrine.php';

/** Zend_Application */
require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configurations/app.ini'
);

$application->getBootstrap()->bootstrap('doctrine');
$config = $application->getOption('doctrine');

$cli = new Doctrine_Cli($config);
$cli->run($_SERVER['argv']);

Vous l’aurez sans doute deviné, il s’agit du fichier /public/index.php avec à la fin l’instanciation de Doctrine. Cette instanciation nous permet d’utiliser, par le biais de la ligne de commande, Doctrine et, ainsi, générer nos classes.

Pour ce faire, allez dans votre invite de commande et rendez vous dans le dossier qui contient le script. Tapez juste la commande suivante :

php Doctrine.php

Elle devrait vous retourner le résultat suivant :

Il ne nous reste plus qu’à taper la commande :

php doctrine.php generate-models-db
php doctrine.php generate-yaml-db

Dans le dossier /application/configurations/ nous trouverons le dossier schema.yml contenant le script YAML de notre base de données. Et maintenant, si nous regardons dans le dossier /library/App/Models, nous avons pleins de classes de créées.

Prenons par exemple la table Utilisateurs. Elle aura 3 classes dédiées :

  • App_Models_Utilisateurs : permet de définir des fonction sur un objet utilisateur
  • App_Models_UtilisateursTable : permet de définir les différentes requêtes à faire sur notre table
  • App_Models_Base_Utilisateurs : contient la définition de la table Utilisateurs.

Justement, afin de tester et de bien comprendre ces classes, nous allons ajouter une fonction à la classe App_Models_UtilisateursTable. La fonction en question n’est rien d’autre qu’un fetchAll permettant de faire un Select de base.

/**
 * Select de base
 * 
 * @param string $where
 * @param string $order
 * @param int $limit
 * @param int $offset
 * @return Collection
 */
 public function fetchAll($where=null, $order=null, $limit=null, $offset=null)
 {
     $select = Doctrine_Query::create()->select('*')
         //dans le from on met le nom de la classe
         //Doctrine s'occupe du reste
         ->from('App_Models_Utilisateurs');

     if(isset($where)){ $select->where($where); }
     if(isset($order)){ $select->orderBy($order); }
     if(isset($limit)){ $select->limit($limit); }
     if(isset($offset)){ $select->offset($offset); }

     return $select->execute();
 }

Ensuite pour utiliser cette requête, nous allons éditier le controller Admin_AccueilController. Dans d’action d’index ajoutez les lignes suivantes :

//on récupère la classe table de App_Models_Utilisateurs
$table = Doctrine_Core::getTable('App_Models_Utilisateurs');
//on liste tout le monde
$liste = $table->fetchAll();
//on traite le résultat
foreach($liste as $unuser)
{
    print_r($unuser->login);
    print_r($unuser->passwd);
}

Normalement, si vous vous rendez sur la page concernée, vous devriez voir afficher le login et le mot de passe des utilisateurs. Voilà, maintenant vous pouvez utiliser Doctrine au sein du Framework Zend. La prochaine fois, nous verrons la création des premiers articles de notre Blog.

Phing : PHing Is Not GNU make

by Throrïn 0 Comments

Au travail, j’ai découvert une extension PEAR pour PHP vraiment utile. Il s’agit de Phing (PHing Is Not GNU make). Phing est un projet servant à construire des projets ou à effectuer différentes étapes de façon automatique.

Pour résumer, C/C++ a son make, Linux a ses scripts bash, Windows a ses batch et PHP a Phing. Phing utilise le langage XML pour formater les scripts.

Je l’ai trouvé simple à prendre en main (en regardant la doc). Il intègre des solutions FTP, SQL (avec PDO), commandes systèmes et les règles de programmation de base (if, …). Pour vous donner une petite idée des performances de Phing, j’ai dû créer aujourd’hui un script permettant la création d’un SVN repository avec, la création sur le serveur ftp du dossier du projet (pour le site web) et pour finir à créer l’utilisateur et la base de données rattachée sur un autre serveur MySQL.

Je pense que j’aurais pu faire l’équivalent en scripts bash sur mon serveur Ubuntu mais on m’a demandé de l’effectuer avec ce langage. Des améliorations de ce script m’ont été demandées donc je vais bien voir ce qu’il est vraiment possible.

Voici justement un petit aperçu de script Phing (fournit dans le quickstart de la documentation).

<?xml version="1.0" encoding="UTF-8"?>
<project name="FooBar" default="dist">
    <!-- ============================================  -->   
    <!-- Target: prepare                               -->
    <!-- ============================================  -->
    <target name="prepare">
        <echo msg="Making directory ./build" />
        <mkdir dir="./build" />
    </target>



    <!-- ============================================  -->
    <!-- Target: build                                 -->
    <!-- ============================================  -->
    <target name="build" depends="prepare">
        <echo msg="Copying files to build directory..." />
        <echo msg="Copying ./about.php to ./build directory..." />
        <copy file="./about.php" tofile="./build/about.php" />
        <echo msg="Copying ./browsers.php to ./build directory..." />
        <copy file="./browsers.php" tofile="./build/browsers.php" />
        <echo msg="Copying ./contact.php to ./build directory..." />
        <copy file="./contact.php" tofile="./build/contact.php" />
    </target>



    <!-- ============================================  -->
    <!-- (DEFAULT)  Target: dist                       --> 
    <!-- ============================================  -->
    <target name="dist" depends="build">
        <echo msg="Creating archive..." />
        <tar destfile="./build/build.tar.gz" compression="gzip">
            <fileset dir="./build">
                <include name="*" />
            </fileset>
        </tar>
        <echo msg="Files copied and compressed in build directory OK!" />
    </target>
</project>

Et pour plus d’infos, je vous invite à aller sur leur site.

Singleton pour PDO

by Throrïn 1 Comment

En me baladant sur le net, j’ai voulu voir si quelqu’un avait un modèle singleton pour instancier PDO d’une meilleure façon que moi. Et, j’ai rien trouvé.

Comme vous le savez sûrement, le patron Singleton sert à instancier une seule fois un objet et quand quelqu’un en a besoin, au lieu de ré instancier cet objet, il récupère la seule instance existante.

Et voici le squelette du Singleton :

class Example
{
    // instance de la classe
    private static $instance;

    // Un constructeur prive ; empêche la création directe d'objet
    private function __construct() 
    {
        echo 'Je suis construit';
    }

    // La méthode singleton
    public static function singleton() 
    {
        if (!isset(self::$instance)) {
            $c = __CLASS__;
            self::$instance = new $c;
        }

        return self::$instance;
    }

    // Exemple d'une méthode
    public function bark()
    {
        echo 'Woof!';
    }

    // Prévient les utilisateurs sur le clônage de l'instance
    public function __clone()
    {
        trigger_error('Le clônage n\'est pas autorisé.', E_USER_ERROR);
    }
}

En suivant ce modèle, j’ai donc créé mon singleton pour la classe PDO. Si vous ne le savez pas, PDO est une classe d’abstraction pour gérer la connexion avec une base de données. Voici donc le résultat obtenu :

/**
 * Classe Connexion
 *
 * Singleton permettant de se connecter à la base de donnée, utilisée par toutes 
 * les classes qui ont besoin d'un accès à la base de donnée.  
 * Il y a de renseigner à l'interieur:
 * <ul>
 *     <li>le type de base de donnée (mysql, sqllite, ...)</li>
 *     <li>le nom de la base de donnée</li>
 *     <li>le login et le mot de passe utilisateur du serveur SQL</li>
 * </ul>
 *
 * Cette classe ne permet que l'instanciation d'un objet PDO pour se connecter à la base de donnée.
 * Vous pouvez utiliser cette classe comme ceci:
 * <pre>
 *  $db = connexion::getInstance();
 *  $con = $db->getDbh();
 * </pre>
 *
 * @link http://fr3.php.net/manual/fr/book.pdo.php classe PDO
 *
 * @author Benjamin Besse
 * @version 0.4
 * @package Connexion
 * @copyright Benjamin Besse
 * @todo
 * <ul>
 *     <li>0.1: création de la classe Connexion</li>
 *     <li>0.2: passage de la classe en design patern singleton</li>
 *     <li>0.3: ajout de l'encodage par défaut en UTF-8 à la lecture</li>
 *     <li>0.4: ajout d'un try cath pour la gestion d'erreur au niveau de la connexion à la base de donnée</li>
 * </ul>
 */
class connexion
{
    /**
     * Instance de la classe connexion
     * @access private
     * @var connexion
     * @see getInstance
     */
    private static $instance;

    /**
     * Type de la base de donnée.
     * @access private
     * @var string
     * @see __construct
     */
    private $type = "mysql";

    /**
     * Adresse du serveur hôte.
     * @access private
     * @var string
     * @see __construct
     */
    private $host = "localhost";

    /**
     * Nom de la base de donnée.
     * @access private
     * @var string
     * @see __construct
     */
    private $dbname = "legendesfeeriques";

    /**
     * Nom d'utilisateur pour la connexion à la base de données
     * @access private
     * @var string
     * @see __construct
     */
    private $username = "root";

    /**
     * Mot de passe pour la connexion à la base de donnée
     * @access private
     * @var string
     * @see __construct
     */
    private $password = '';

    private $dbh;

    /**
     * Lance la connexion à la base de donnée en le mettant
     * dans un objet PDO qui est stocké dans la variable $dbh
     * @access private
     */
    private function __construct()
    {
        try{
            $this->dbh = new PDO(
                $this->type.':host='.$this->host.'; dbname='.$this->dbname, 
                $this->username, 
                $this->password,
                array(PDO::ATTR_PERSISTENT => true)
            );

            $req = "SET NAMES UTF8";
            $result = $this->dbh->prepare($req);
            $result->execute();
        }
        catch(PDOException $e){
            echo "<div class="\"error\"">Erreur !: ".$e->getMessage()."</div>";
            die();
        }
    }

    /**
     * Regarde si un objet connexion a déjà été instancier,
     * si c'est le cas alors il retourne l'objet déjà existant
     * sinon il en créer un autre.
     * @return $instance
     */
    public static function getInstance()
    {
        if (!self::$instance instanceof self)
        {
            self::$instance = new self;
        }
        return self::$instance;
    }

    /**
     * Permet de récuprer l'objet PDO permettant de manipuler la base de donnée
     * @return $dbh
     */
    public function getDbh()
    {
        return $this->dbh;
    }
}

Et je l’appelle comme ceci dans mes classes :

class Circuit
{ 
    private $con; //variable de connexion

    public function __construct()
    {
        $db = connexion::getInstance();
        $this->con = $db->getDbh();
    }
}

A partir de là, toutes les fonctions de PDO sont réutilisables sans avoir besoin de les redéfinir. J’espère avoir pu vous aider avec ceci.