Caution: You are browsing the legacy symfony 1.x part of this website.
SymfonyWorld Online 2020
100% online
30+ talks + workshops
Live + Replay watch talks later

Jour 3 : Le modèle de données

Ceux d'entre vous qui brûlent d'ouvrir leur éditeur de texte et de définir un peu de PHP seront heureux d'apprendre que le tutoriel d'aujourd'hui va nous entraîner dans un certain développement. Nous allons définir le modèle de données de Jobeet, utiliser un ORM pour interagir avec la base de données, et construire le premier module de l'application. Mais, comme symfony fait beaucoup de travail pour nous, nous aurons un module web pleinement opérationnel sans trop écrire de code PHP.

Activer le plug-in sfDoctrinePlugin

Si vous lisez ce passage, c'est que vous avez décider de poursuivre le tutoriel Jobeet avec l'ORM Doctrine au lieu de Propel. Activer Doctrine est particulièrement simple dans la mesure où il suffit d'activer sfDoctrinePlugin, puis désactiver sfPropelPlugin. Cette manipulation de la manière suivante dans le fichier config/ProjectConfiguration.class.php du projet symfony.

public function setup()
{
  $this->enablePlugins(array('sfDoctrinePlugin'));
  $this->disablePlugins(array('sfPropelPlugin'));
}

Si vous préférez avoir tous les plugins activés par défaut, alors vous pouvez suivre la manipulation suivante :

public function setup()
{
  $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
}

note

Après ce changement, vous obtiendrez un message d'erreur tant que vous n'aurez pas reconfigurer le fichier config/databases.yml. Cette étape est expliquée plus loin pour utiliser sfDoctrineDatabase.

Après avoir réaliser ces changements, n'oubliez pas de vider votre cache.

$ php symfony cc

Comme c'est expliqué plus loin dans le tutoriel, chaque plug-in peut embarquer des ressources (images, feuilles de styles, et JavaScripts). Lorsqu'un nouveau plug-in est installé ou activé, nous devront les installées à partir de la tâche plugin:publish-assets :

$ php symfony plugin:publish-assets

Nous avons besoin de supprimer le répertoire web/sfPropelPlugin :

$ rm web/sfPropelPlugin

tip

Lorsque l'on utilise Doctrine au lieu de Propel, il est recommandé de supprimer les fichiers config/propel.ini et config/schema.yml afin d'avoir une installation saine qui ne contient aucune référence à Propel.

$ rm config/propel.ini
$ rm config/schema.yml

Le modèle relationnel

Les histoires d'utilisateurs que nous avons écrites hier, décrivent les objets principaux de notre projet : les emplois, les sociétés affiliées et les catégories. Voici le schéma correspondant aux relations entre entités :

Diagramme des classes

En plus des colonnes décrites dans les histoires, nous avons également ajouté un champ created_at à certaines tables. Symfony reconnaît de tels champs et définit la valeur avec celle de l'heure actuelle du système quand un enregistrement est créé. C'est la même chose pour les champs updated_at : leur valeur est définie avec celle de l'heure du système quand l'enregistrement est mis à jour.

Le schéma

Pour stocker les emplois, les sociétés affiliées et les catégories, il nous faut évidemment une base de données relationnelle.

Mais comme Symfony est un framework orienté-objet, on aime manipuler des objets quand nous le pouvons. Par exemple, au lieu d'écrire des instructions SQL pour récupérer des enregistrements de la base de données, nous préférons utiliser des objets.

Les informations de base de données relationnelle doivent être mappées sur un modèle objet. Cela peut être fait avec un outil ORM et heureusement, symfony est livré avec deux d'entre eux : Propel et Doctrine. Dans ce tutoriel, nous allons utiliser Doctrine.

L'ORM a besoin d'une description des tables et leurs relations pour créer les classes liées. Il y a deux façons de créer ce schéma de description : par introspection d'une base de données existante ou en la créant à la main.

Comme la base de données n'existe pas encore et que nous voulons garder agnostique la base de données Jobeet, nous allons créer le fichier du schéma à la main en éditant le fichier vide config/doctrine/schema.yml :

tip

Vous aurez besoin de créer manuellement le répertoire config/doctrine/ dans votre projet car il n'existe pas encore :

$ mkdir config/doctrine
# config/doctrine/schema.yml
JobeetCategory:
  actAs: { Timestampable: ~ }
  columns:
    name: { type: string(255), notnull: true, unique: true }
 
JobeetJob:
  actAs: { Timestampable: ~ }
  columns:
    category_id:  { type: integer, notnull: true }
    type:         { type: string(255) }
    company:      { type: string(255), notnull: true }
    logo:         { type: string(255) }
    url:          { type: string(255) }
    position:     { type: string(255), notnull: true }
    location:     { type: string(255), notnull: true }
    description:  { type: string(4000), notnull: true }
    how_to_apply: { type: string(4000), notnull: true }
    token:        { type: string(255), notnull: true, unique: true }
    is_public:    { type: boolean, notnull: true, default: 1 }
    is_activated: { type: boolean, notnull: true, default: 0 }
    email:        { type: string(255), notnull: true }
    expires_at:   { type: timestamp, notnull: true }
  relations:
    JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id, foreignAlias: JobeetJobs } 
 
JobeetAffiliate:
  actAs: { Timestampable: ~ }
  columns:
    url:       { type: string(255), notnull: true }
    email:     { type: string(255), notnull: true, unique: true }
    token:     { type: string(255), notnull: true }
    is_active: { type: boolean, notnull: true, default: 0 }
  relations:
    JobeetCategories:
      class: JobeetCategory
      refClass: JobeetCategoryAffiliate
      local: affiliate_id
      foreign: category_id
      foreignAlias: JobeetAffiliates
 
JobeetCategoryAffiliate:
  columns:
    category_id:  { type: integer, primary: true }
    affiliate_id: { type: integer, primary: true }
  relations:
    JobeetCategory:  { onDelete: CASCADE, local: category_id, foreign: id }
    JobeetAffiliate: { onDelete: CASCADE, local: affiliate_id, foreign: id }

tip

Si vous avez décidé de créer les tables en écrivant des instructions SQL, vous pouvez générer le fichier de configuration correspondant schema.yml en exécutant la tâche doctrine:build-schema :

$ php symfony doctrine:build-schema

La tâche ci-dessus suppose que vous ayez une base de données configurées dans databases.yml. Nous vous montrerons comment configurer la base de données dans une étape suivante. Si vous essayez et exécutez cette tâche maintenant, elle ne fonctionnera pas car elle ne connait pas la base de données pour construire le schéma.

Le schéma est la traduction directe du diagramme de relations des entités dans le format YAML.

sidebar

Le format YAML

Selon le site officiel YAML, YAML est "un standard de sérialisation de données compréhensibles par un humain, quel que soit le langage de programmation".

Autrement dit, YAML est un langage simple pour décrire les données (des chaînes, des entiers, des dates, des tableaux et des séries).

En YAML, la structure est présentée à travers l'indentation, la séquence des éléments sont indiqués par un tiret, et les paires clé/valeur dans le fichier sont séparés par deux points. YAML possède également une syntaxe abrégée pour décrire la même structure avec moins de lignes, où les tableaux sont explicitement indiqués avec [] et les séries avec {}.

Si vous n'êtes pas encore familiarisé avec YAML, il est temps de commencer car le framework symfony l'utilise intensivement pour ses fichiers de configuration.

Il y a une chose importante que vous devez retenir lors de l'édition d'un fichier YAML : l'indentation doit être faite avec un ou plusieurs espaces, mais jamais avec les tabulations.

Le fichier schema.yml contient la description de toutes les tables et leurs colonnes. Chaque colonne est décrite avec les informations suivantes :

  • type: Le type de colonne (boolean, integer, float, decimal, string, array, object, blob, clob, timestamp, time, date, enum, gzip)
  • notnull: Réglez-le à true si vous souhaitez que la colonne soit requise
  • unique: Réglez-le à true si vous souhaitez créer un index unique pour la colonne.

note

L'attribut onDelete définit le comportement ON DELETE des clés étrangères, et Doctrine supporte CASCADE, SETNULL, et RESTRICT. Par exemple, quand un enregistrement job est supprimé, tous les enregistrements connexes jobeet_category_affiliate seront automatiquement supprimés par la base de données.

La base de données

Le framework symfony supporte toutes les PDO-supporté par les bases de données (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO est la couche d'abstraction de la base de données intégrée à PHP.

Utilisons MySQL pour ce tutoriel:

$ mysqladmin -uroot -p create jobeet
Enter password: mYsEcret ## The password will echo as ********

note

N'hésitez pas à choisir un autre moteur de base de données si vous le voulez. Il ne sera pas difficile d'adapter le code que nous allons écrire car nous allons utiliser l'ORM qui va écrire le code SQL pour nous.

Nous devons dire à symfony d'utiliser cette base de données pour le projet Jobeet :

Le fichier par défaut config/databases.yml contient la connexion qui référence propel. Parce que nous utilisons Doctrine, nous avons besoin de supprimer config/databases.yml que nous pouvons re-générer pour Doctrine.

$ rm config/databases.yml

Maintenant, exécutez simplement la commande suivante pour générer un nouveau fichier de configuration de base de données pour Doctrine :

$ php symfony configure:database --name=doctrine
  ➥ --class=sfDoctrineDatabase
  ➥ "mysql:host=localhost;dbname=jobeet" root mYsEcret

La tâche configure:database prend trois arguments: le PDO DSN, le nom d'utilisateur et le mot de passe pour accéder à la base de données. Si vous n'avez pas besoin d'un mot de passe pour accéder à votre base de données sur le serveur de développement, omettez simplement le troisième argument.

note

La tâche configure:database stocke la configuration de la base de données dans le fichier de configuration de config/databases.yml. Au lieu d'utiliser la tâche, vous pouvez éditer ce fichier à la main.

caution

En passant le mot de passe base de données sur la ligne de commande est pratique mais peu sûr. En fonction de qui a accès à votre environnement, il pourrait être préférable de modifier le fichier config/databases.yml pour changer le mot de passe. Bien sûr, pour garder le mot de passe sûr, le mode d'accès du fichier de configuration devraient également être limités.

L'ORM

Grâce à la description de base de données à partir du fichier schema.yml, nous pouvons utiliser les tâches intégrées Doctrine qui génèrent les instructions SQL nécessaires pour créer les tables de la base de données :

D'abord afin de générer le code SQL, vous devez construire vos modèles à partir de vos fichiers du schéma.

$ php symfony doctrine:build-model

Maintenant que vos modèles sont présents, vous pouvez générer et insérer le code SQL.

$ php symfony doctrine:build-sql

La tâche doctrine:build-sql génère les instructions SQL dans le répertoire data/sql/, optimisées pour le moteur de la base de données que nous avons configuré :

# snippet from data/sql/schema.sql
CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255)
NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug
VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id))
ENGINE = INNODB;

Pour créer les tables dans la base de données, vous devez exécuter la tâche doctrine:insert-sql :

$ php symfony doctrine:insert-sql

tip

Comme pour tout outil en ligne de commande, les tâches de symfony peuvent prendre des arguments et des options. Chaque tâche est livrée avec un message d'aide intégré qui peut être affiché en exécutant la tâche help :

$ php symfony help doctrine:insert-sql

Le message d'aide liste tous les arguments et les options possibles, il donne les valeurs par défaut pour chacune d'elles, et fournit quelques exemples d'utilisation utile.

L'ORM génère également des classes PHP qui met les enregistrements de la table en objets :

$ php symfony doctrine:build-model

La tâche doctrine:build-model crée des fichiers PHP dans le répertoire lib/model/ qui peut être utilisé pour interagir avec la base de données.

En naviguant dans les fichiers générés, vous avez probablement remarqué que Doctrine génère trois classes par table. Pour la table jobeet_job :

  • JobeetJob: Un objet de cette classe représente un seul enregistrement de la table de jobeet_job. La classe est vide par défaut.
  • BaseJobeetJob: La classe parent de JobeetJob. Chaque fois que vous exécutez doctrine:build-model, cette classe est écrasée, ainsi toutes les personnalisations doivent être faites dans la classe JobeetJob.

  • JobeetJobTable: La classe définit des méthodes qui renvoient surtout des collections d'objets de JobeetJob. La classe est vide par défaut.

Les valeurs des colonnes d'un enregistrement peuvent être manipulées avec un objet modèle en utilisant des accesseurs (des méthodes get()) et des mutateurs (des méthodes set()) :

$job = new JobeetJob();
$job->setPosition('Web developer');
$job->save();
 
echo $job->getPosition();
 
$job->delete();

Vous pouvez également définir des clés étrangères en reliant directement les objets entre eux :

$category = new JobeetCategory();
$category->setName('Programming');
 
$job = new JobeetJob();
$job->setCategory($category);

La tâche doctrine:build-all est un raccourci des tâches que nous avons exécuté dans cette section et plus encore. Aussi, exécutez cette tâche maintenant pour génèrer les formulaires et les validateurs pour les classes modèle de Jobeet :

$ php symfony doctrine:build-all --no-confirmation

Vous pourrez voir les validateurs à l'oeuvre à la fin de la journée et les formulaires seront bien expliquées en détail le jour 10.

Comme nous le verrons un peu plus tard, symfony charge automatiquement les classes PHP pour vous, ce qui signifie que vous n'avez jamais besoin d'utiliser require dans votre code. C'est une des nombreuses choses que symfony automatise pour les développeurs mais il y a une contre partie : Chaque fois que vous ajoutez une classe vous devez effacer le cache. Comme la tâche doctrine:build-model a créée de nouvelles classes, effaçons le cache :

 $ php symfony cache:clear

tip

Une tâche symfony est composée d'un espace de nom et d'un nom de tâche. Chaque tâche a un raccourci avec le moins d'ambiguïté avec les autres tâches. La commande suivante est équivalente à cache:clear :

$ php symfony cache:cl
$ php symfony ca:c

Comme la tâche cache:clear est souvent utilisée, il a une abréviation encore plus courte :

$ php symfony cc

Les données initiales

Les tables ont été créées dans la base de données mais elles n'ont pas de données. Pour toute application web, il existe trois types de données :

  • Les données initiales : Les données initiales sont nécessaires à l'application pour travailler. Par exemple, Jobeet a besoin de quelques catégories initiales. Sinon, personne ne sera en mesure de soumettre un emploi. Nous avons également besoin d'un utilisateur administrateur pour être capable de se connecter au backend.

  • Les données de test : Les données de test sont nécessaires pour l'application à tester. En tant que développeur, vous devrez écrire des tests pour s'assurer que Jobeet se comporte comme décrit dans les histoires d'utilisateur, et la meilleure façon est d'écrire des tests automatisés. Donc, chaque fois que vous exécutez vos tests, vous avez besoin d'une base de données propre avec quelques nouvelles données pour tester tout de suite.

  • Les données utilisateurs : Les données utilisateurs sont créés par des utilisateurs pendant la durée de vie normale de l'application.

Chaque fois que symfony crée les tables dans la base de données, toutes les données sont perdues. Pour peupler la base de données avec des données initiales, nous pourrions créer un script PHP, ou exécuter des instructions SQL avec le programme mysql. Mais comme le besoin est assez fréquent, il y a une meilleure façon avec symfony : créer des fichiers YAML dans le répertoire data/fixtures/ et utiliser la tâche doctrine:data-load pour les charger dans la base de données.

Premièrement, créer les fichiers de jeu de test suivants :

# data/fixtures/categories.yml
JobeetCategory:
  design:
    name: Design
  programming:
    name: Programming
  manager:
    name: Manager
  administrator:
    name: Administrator
 
# data/fixtures/jobs.yml
JobeetJob:
  job_sensio_labs:
    JobeetCategory: programming
    type:         full-time
    company:      Sensio Labs
    logo:         sensio-labs.gif
    url:          http://www.sensiolabs.com/
    position:     Web Developer
    location:     Paris, France
    description:  |
      You've already developed websites with symfony and you want to work
      with Open-Source technologies. You have a minimum of 3 years
      experience in web development with PHP or Java and you wish to
      participate to development of Web 2.0 sites using the best
      frameworks available.
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com
    is_public:    true
    is_activated: true
    token:        job_sensio_labs
    email:        [email protected]
    expires_at:   '2010-10-10'
 
  job_extreme_sensio:
    JobeetCategory:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         extreme-sensio.gif
    url:          http://www.extreme-sensio.com/
    position:     Web Designer
    location:     Paris, France
    description:  |
      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
      enim ad minim veniam, quis nostrud exercitation ullamco laboris
      nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
      in reprehenderit in.
 
      Voluptate velit esse cillum dolore eu fugiat nulla pariatur.
      Excepteur sint occaecat cupidatat non proident, sunt in culpa
      qui officia deserunt mollit anim id est laborum.
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com
    is_public:    true
    is_activated: true
    token:        job_extreme_sensio
    email:        [email protected]
    expires_at:   '2010-10-10'

note

Le fichier de jeu de test des emplois fait référence à deux images. Vous pouvez les télécharger (/get/jobeet/sensio-labs.gif, /get/jobeet/extreme-sensio.gif) et les mettre sous le répertoire web/uploads/jobs/.

Un fichier de jeu de test est écrit en YAML, et définit les objets du modèle, étiquetés avec un nom unique (par exemple, nous avons défini deux emplois étiquetés job_sensio_labs et job_extreme_sensio). Cette étiquette est d'une grande utilitée pour relier les objets liés, sans avoir à définir les clés primaires (qui sont souvent auto-incrémenté et ne peuvent pas être attribuées). Par exemple, la catégorie d'emploi de job_sensio_labs est programming, qui est l'étiquette donnée à la catégorie 'Programming'.

tip

Dans un fichier YAML, quand une chaîne contient des sauts de ligne (comme la colonne description dans le fichier de jeu de test d'emploi), vous pouvez utiliser le pipe (|) pour indiquer que la chaîne va continuer sur plusieurs lignes.

Bien qu'un fichier de jeu de test peut contenir des objets d'un ou de plusieurs modèles, nous avons décidé de créer un fichier par modèle pour les jeux de test de Jobeet.

note

Propel exige que les fichiers de jeux de test soient préfixés avec un nombre pour déterminer l'ordre dans lequel les fichiers seront chargés. Avec Doctrine, ce n'est pas nécessaire car tous les jeux de test seront chargés et enregistrés dans le bon ordre afin de s'assurer que les clés étrangères soient définies correctement.

Dans un fichier de jeu de test, vous n'avez pas besoin de définir toutes les valeurs des colonnes. Sinon, symfony va utiliser la valeur par défaut définie dans le schéma de base de données. Et comme symfony utilise Doctrine pour charger les données dans la base de données, tous les comportements intégrés (comme le paramètrage automatique des colonnes created_at ou updated_at) et les comportements personnalisés que vous pourrez ajouter dans les classes du modèle qui sont activés.

Le chargement des données initiales dans la base de données est aussi simple que de lancer la tâche doctrine:data-load :

$ php symfony doctrine:data-load

tip

La tâche doctrine:build-all-reload est un raccourci pour la tâche doctrine:build-all suivie par la tâche doctrine:data-load.

Exécutez la tâche doctrine:build-all-reload pour s'assurer que tout est généré à partir de votre schéma.

$ php symfony doctrine:build-all-reload

Le voir en action dans le navigateur

Nous avons beaucoup utilisé l'interface en ligne de commande, mais ce n'est pas vraiment excitant, surtout pour un projet web. Nous avons maintenant tout ce qu'il faut pour créer des pages web qui interagissent avec la base de données.

Voyons comment afficher la liste des emplois, comment modifier un emploi existant et la façon de supprimer un emploi. Comme expliqué au cours du jour 1, un projet symfony est composé d'applications. Chaque application est ensuite divisé en modules. Un module est un ensemble autonome de code PHP qui représente une caractéristique de l'application (le module API par exemple), ou un ensemble de manipulations, l'utilisateur peut le faire sur un modèle d'objet (un module emploi par exemple).

Symfony est capable de générer automatiquement un module pour un modèle donné qui fournit des fonctions de manipulation de base :

$ php symfony doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob

La doctrine:generate-module génère un module job dans l'application frontend pour le modèle JobeetJob. Comme pour la plupart des tâches de symfony, certains fichiers et certains répertoires ont été créés pour vous sous le répertoire apps/frontend/modules/job/ :

Répertoire Description
actions/ Les actions du module
templates/ Les Templates du module

The actions/actions.class.php file defines all the available action for the job module:

Nom de l'action Description
index Affiche les enregistrements de la table
show Affiche les champs et leurs valeurs pour un enregistrement donné
new Affiche le formulaire pour créer un nouvel enregistrement
create Crée un nouvel enregistrement
edit Affiche le formulaire pour éditer un enregistrement existant
update Met à jour un enregistrement en fonction des valeurs sousmises par l'utilisateur
delete Supprime un enregistrement donné de la table

Vous pouvez tester le module job dans un navigateur :

 http://jobeet.localhost/frontend_dev.php/job

Le module Job

Si vous essayez de modifier un emploi, vous remarquerez que l'id de la catégorie a une liste de tous les noms des catégories. La valeur de chaque option est obtenu à partir de la méthode __toString().

Doctrine va essayer de fournir une méthode de base __toString() en devinant un nom de colonne descriptif comme title, name, subject, etc. Si vous voulez quelque chose de personnalisé, vous aurez besoin d'ajouter vos propres méthodes __toString() comme ci-dessous. Le modèle JobeetCategory est capable de deviner la méthode __toString() en utilisant le nom de la colonne de la table jobeet_category.

// lib/model/doctrine/JobeetJob.class.php
class JobeetJob extends BaseJobeetJob
{
  public function __toString()
  {
    return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation());
  }
}
 
// lib/model/doctrine/JobeetAffiliate.class.php
class JobeetAffiliate extends BaseJobeetAffiliate
{
  public function __toString()
  {
    return $this->getUrl();
  }
}

Vous pouvez désormais créer et éditer des emplois. Essayez de laisser un champ obligatoire vide, ou essayez d'entrer une date invalide. C'est vrai, symfony a créé des règles de validation de base par introspection du schéma de la base de données.

validation

A demain

C'est tout pour aujourd'hui. Je vous l'indiquais dans l'introduction. Aujourd'hui, nous avons à peine écrit du code PHP, mais nous avons un module web de travail pour le modèle job, prêts à être modifié et personnalisé. Rappelez-vous, pas de code PHP signifie pas de bogues non plus !

Si vous avez encore de l'énergie, n'hésitez pas à lire le code généré pour le module et le modèle et essayer de comprendre comment il fonctionne. Sinon, ne vous inquiétez pas et dormez bien, car demain nous allons parler de l'un des paradigmes le plus utilisé dans les frameworks web, le modèle de conception MVC.

Comme pour tout autre jour, le code d'aujourd'hui est disponible sur le dépôt SVN Jobeet. Faites un checkout de la balise release_day_03 :

$ svn co http://svn.jobeet.org/doctrine/tags/release_day_03/ jobeet/