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.
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 :
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 Propel.
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.
note
Certains outils vous permettent de construire une base de données graphiquement
(par exemple Dbdesigner de Fabforce)
et de générer directement un schema.xml
(avec DB Designer 4 pour convertir le schéma
en Propel).
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
:
# config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true, index: unique } jobeet_job: id: ~ category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true } type: { type: varchar(255) } company: { type: varchar(255), required: true } logo: { type: varchar(255) } url: { type: varchar(255) } position: { type: varchar(255), required: true } location: { type: varchar(255), required: true } description: { type: longvarchar, required: true } how_to_apply: { type: longvarchar, required: true } token: { type: varchar(255), required: true, index: unique } is_public: { type: boolean, required: true, default: 1 } is_activated: { type: boolean, required: true, default: 0 } email: { type: varchar(255), required: true } expires_at: { type: timestamp, required: true } created_at: ~ updated_at: ~ jobeet_affiliate: id: ~ url: { type: varchar(255), required: true } email: { type: varchar(255), required: true, index: unique } token: { type: varchar(255), required: true } is_active: { type: boolean, required: true, default: 0 } created_at: ~ jobeet_category_affiliate: category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
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 propel:build-schema
:
$ php symfony propel: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.
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
,tinyint
,smallint
,integer
,bigint
,double
,float
,real
,decimal
,char
,varchar(size)
,longvarchar
,date
,time
,timestamp
,blob
, etclob
)required
: Réglez-le àtrue
si vous souhaitez que la colonne soit requise~index|Database indexes~
: Réglez-le àtrue
si vous souhaitez créer un index pour la colonne ou àunique
si vous voulez qu'un index unique soit créé pour la colonne.primaryKey
: Définit la colonne comme une clé primaire pour la table.foreignTable
,foreignReference
: Définit la colonne pour être une clé étrangère pour une autre table.
Pour les colonnes avec ~
, cela signifie null
en YAML (id
, created_at
, et
updated_at
), symfony symfony devinera la meilleure configuration (une clé primaire pour id
et l'horodatage pour created_at
et updated_at
).
note
L'attribut onDelete
définit le comportement
ON DELETE
des clés étrangères, et Propel 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 ou par Propel si le
moteur sous-jacent ne prend pas en charge cette fonctionnalité.
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 :
$ php symfony configure:database ➥ "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 Propel qui génèrent les instructions SQL nécessaires pour créer les
tables de la base de données :
$ php symfony propel:build-sql
La tâche propel: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/lib.model.schema.sql CREATE TABLE `jobeet_category` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `jobeet_category_U_1` (`name`) )Type=InnoDB;
Pour créer les tables dans la base de données, vous devez exécuter la
tâche propel:insert-sql
:
$ php symfony propel:insert-sql
Comme la tâche supprime les tables actuelles avant de les re-créer, vous avez l'obligation
de confirmer l'opération. Vous pouvez également ajouter l'option --no-confirmation
afin de contourner la question, qui est utile si vous souhaitez exécuter la tâche
à partir d'un batch non-interactif :
$ php symfony propel:insert-sql --no-confirmation
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 propel: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 propel:build-model
La tâche propel: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 Propel
génère quatre classes par table. Pour la table
jobeet_job
:
JobeetJob
: Un objet de cette classe représente un seul enregistrement de la table dejobeet_job
. La classe est vide par défaut.BaseJobeetJob
: La classe parent deJobeetJob
. Chaque fois que vous exécutezpropel:build-model
, cette classe est écrasée, ainsi toutes les personnalisations doivent être faites dans la classeJobeetJob
.JobeetJobPeer
: La classe définit des méthodes statiques qui pour la plupart retournent des collections d'objets deJobeetJob
. La classe est vide par défaut.BaseJobeetJobPeer
: La classe parent deJobeetJobPeer
. Chaque fois que vous exécutezpropel:build-model
, cette classe est écrasée, ainsi toutes les personnalisations doivent être faites dans la classeJobeetJobPeer
.
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 propel: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 propel: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 propel: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 propel:data-load
pour les charger
dans la base de données.
Premièrement, créer les fichiers de jeu de test suivants :
# data/fixtures/010_categories.yml JobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator } # data/fixtures/020_jobs.yml JobeetJob: job_sensio_labs: category_id: 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: job@example.com expires_at: 2010-10-10 job_extreme_sensio: category_id: 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: job@example.com 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.
tip
Notez les nombres préfixant les noms des fichiers. Il s'agit d'un moyen simple de contrôler l'ordre de chargement des données. Plus tard dans le projet, si nous avons besoin d'insérer des nouveaux fichiers de jeu de test, il sera facile car nous avons des numéros libres entre ceux qui existent.
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 Propel 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 propel:data-load
:
$ php symfony propel:data-load
tip
La tâche propel:build-all-load
est un raccourci pour la tâche propel:build-all
suivie par la tâche propel:data-load
.
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 propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob
La propel: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
Si vous essayez de modifier un emploi, vous aurez une exception parce que symfony a besoin
d'une représentation en texte d'une catégorie. Une représentation d'objet PHP peut être définies
avec la méthode magique PHP __toString()
. La représentation du texte d'un enregistrement
de catégorie doit être définie dans la classe du modèle JobeetCategory
:
// lib/model/JobeetCategory.php class JobeetCategory extends BaseJobeetCategory { public function __toString() { return $this->getName(); } }
Maintenant, chaque fois que symfony a besoin d'une représentation en texte d'une catégorie, il appelle
la méthode __toString()
qui retourne le nom de catégorie. Comme nous aurons besoin
d'une représentation en texte de toutes les classes du modèle à un moment ou un autre, nous allons
définir une méthode __toString()
pour chaque classe du modèle:
// lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation()); } } // lib/model/JobeetAffiliate.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.
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/propel/tags/release_day_03/ jobeet/
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.