Jobeet FR

Le tutoriel pour Symfony2 en français

Vue normale

Le modèle relationnel

Les scénarios du précédent chapitre décrivent les objets principaux de notre projet: les offres, les partenaires et les catégories. Voici le schéma des entités correspondant:

En plus des colonnes décrites dans les scénarios, nous avons également ajouté les colonnes created_at et updated_at. Nous allons configurer Symfony2 pour définir leur valeur automatiquement quand un objet est enregistré ou mis à jour.


La Base De Données

Pour stocker les offres, les partenaires et les catégories dans la BDD, Symfony2 utilise Doctrine ORM. Pour définir les paramètres de connexion, vous devez éditer le fichier app/config/parameters.ini (pour ce tutoriel, nous allons utiliser MySQL):

;app/config/parameters.ini
[parameters]
    database_driver   = pdo_mysql
    database_host     = localhost
    database_name     = jobeet
    database_user     = root
    database_password = password

Maintenant que Doctrine connaît votre BDD, vous pouvez l'utiliser pour créer la BDD pour vous (si vous ne l'avez pas déjà créée):

php app/console doctrine:database:create

Le schéma

Pour que Doctrine connaisse nos objets, nous allons créer des fichiers "métadonnées" qui décrivent la façon dont nos objets seront stockés dans la BDD:

# src/Ens/JobeetBundle/Resources/config/doctrine/Category.orm.yml
Ens\JobeetBundle\Entity\Category:
  type: entity
  table: category
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    name:
      type: string
      length: 255
      unique: true
  oneToMany:
    jobs:
      targetEntity: Job
      mappedBy: category
    category_affiliates:
      targetEntity: CategoryAffiliate
      mappedBy: category
# src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
Ens\JobeetBundle\Entity\Job:
  type: entity
  table: job
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    type:
      type: string
      length: 255
      nullable: true
    company:
      type: string
      length: 255
    logo:
      type: string
      length: 255
      nullable: true
    url:
      type: string
      length: 255
      nullable: true
    position:
      type: string
      length: 255
    location:
      type: string
      length: 255
    description:
      type: text
    how_to_apply:
      type: text
    token:
      type: string
      length: 255
      unique: true
    is_public:
      type: boolean
      nullable: true
    is_activated:
      type: boolean
      nullable: true
    email:
      type: string
      length: 255
    expires_at:
      type: datetime
    created_at:
      type: datetime
    updated_at:
      type: datetime
      nullable: true
  manyToOne:
    category:
      targetEntity: Category
      inversedBy: jobs
      joinColumn:
        name: category_id
        referencedColumnName: id
  lifecycleCallbacks:
    prePersist: [ setCreatedAtValue ]
    preUpdate: [ setUpdatedAtValue ]
# src/Ens/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml
Ens\JobeetBundle\Entity\Affiliate:
  type: entity
  table: affiliate
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    url:
      type: string
      length: 255
    email:
      type: string
      length: 255
      unique: true
    token:
      type: string
      length: 255
    created_at:
      type: datetime
  oneToMany:
    category_affiliates:
      targetEntity: CategoryAffiliate
      mappedBy: affiliate
  lifecycleCallbacks:
    prePersist: [ setCreatedAtValue ]
# src/Ens/JobeetBundle/Resources/config/doctrine/CategoryAffiliate.orm.yml
Ens\JobeetBundle\Entity\CategoryAffiliate:
  type: entity
  table: category_affiliate
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  manyToOne:
    category:
      targetEntity: Category
      inversedBy: category_affiliates
      joinColumn:
        name: category_id
        referencedColumnName: id
    affiliate:
      targetEntity: Affiliate
      inversedBy: category_affiliates
      joinColumn:
        name: affiliate_id
        referencedColumnName: id

L'ORM

Maintenant Doctrine peut générer les classes qui définissent nos objets pour nous avec la commande:

php app/console doctrine:generate:entities EnsJobeetBundle
Generating entities for bundle "EnsJobeetBundle"
  > backing up Job.php to Job.php~
  > generating Ens\JobeetBundle\Entity\Job
  > backing up Category.php to Category.php~
  > generating Ens\JobeetBundle\Entity\Category
  > backing up CategoryAffiliate.php to CategoryAffiliate.php~
  > generating Ens\JobeetBundle\Entity\CategoryAffiliate
  > backing up Affiliate.php to Affiliate.php~
  > generating Ens\JobeetBundle\Entity\Affiliate

Si vous regardez dans le répertoire Entity de EnsJobeetBundle, vous trouverez les classes nouvellement créées, là-dedans: Affiliate.php, Category.php, CategoryAffiliate.php et Job.php. Ouvrez Job.php et définissez les valeurs created_at et updated_at comme ci-dessous:

// src/Ens/JobeetBundle/Entity/Job.php
// ...
public function setCreatedAtValue()
{
  if(!$this->getCreatedAt())
  {
    $this->created_at = new \DateTime();
  }
}
// ...
public function setUpdatedAtValue()
{
  $this->updated_at = new \DateTime();
}

Faites de même pour la valeur created_at de la classe Affiliate:

// src/Ens/JobeetBundle/Entity/Affiliate.php
// ...
public function setCreatedAtValue()
{
  $this->created_at = new \DateTime();
}

Cela rendra Doctrine capable de définir les valeurs created_at et updated_at lors de l'enregistrement ou de la mise à jour des objets. Ce comportement a été défini dans les fichiers Job.orm.yml et Affiliate.orm.yml énumérés ci-dessus.

Nous allons également demander à Doctrine de créer nos tables de BDD (ou les mettre à jour afin de prendre en compte notre configuration) avec la commande:

php app/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "7" queries were executed

Cette tâche ne doit être vraiment utilisée qu'au cours du développement. Pour une méthode plus stable de mise à jour systématique de votre BDD de production, lisez les migrations Doctrine.


Les données initiales

Les tables ont été créées dans la BDD, mais elles ne contiennent pas de données. Pour toute application web, il existe trois types de données: les données initiales (ce qui est nécessaire pour que l'application fonctionne, dans notre cas, nous avons quelques catégories initiales et un utilisateur admin), les données test (nécessaires pour tester l'application) et les données utilisateur (créées par les utilisateurs au fil du temps).

Pour remplir la BDD avec des données initiales, nous allons utiliser DoctrineFixturesBundle. Pour installer ce paquet, suivez les étapes suivantes:

1. Ajoutez les lignes suivantes à votre fichier deps:

[doctrine-fixtures]
    git=http://github.com/doctrine/data-fixtures.git
 
[DoctrineFixturesBundle]
    git=http://github.com/doctrine/DoctrineFixturesBundle.git
    target=/bundles/Symfony/Bundle/DoctrineFixturesBundle
    version=origin/2.0

2. Mettez à jour les vendors:

php bin/vendors install --reinstall

3. Enregistrez le namespace Doctrine\Common\DataFixtures dans app/autoload.php "avant" Doctrine\Common:

// ...
$loader->registerNamespaces(array(
    // ...
    'Doctrine\\Common\\DataFixtures' => __DIR__.'/../vendor/doctrine-fixtures/lib',
    'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib',
    // ...
));

4. Enregistrez le paquet DoctrineFixturesBundle dans app/AppKernel.php:

// ...
public function registerBundles()
{
    $bundles = array(
        // ...
        new Symfony\Bundle\DoctrineFixturesBundle\DoctrineFixturesBundle(),
        // ...
    );
    // ...
}

Maintenant que tout est en place, nous allons créer de nouvelles classes pour charger les données dans un nouveau dossier dans notre paquet: src/Ens/JobeetBundle/DataFixtures/ORM.

// src/Ens/JobeetBundle/DataFixtures/ORM/LoadCategoryData.php
namespace Ens\JobeetBundle\DataFixtures\ORM;
 
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Ens\JobeetBundle\Entity\Category;
 
class LoadCategoryData extends AbstractFixture implements OrderedFixtureInterface
{
  public function load(ObjectManager $em)
  {
    $design = new Category();
    $design->setName('Design');
 
    $programming = new Category();
    $programming->setName('Programming');
 
    $manager = new Category();
    $manager->setName('Manager');
 
    $administrator = new Category();
    $administrator->setName('Administrator');
 
    $em->persist($design);
    $em->persist($programming);
    $em->persist($manager);
    $em->persist($administrator);
 
    $em->flush();
 
    $this->addReference('category-design', $design);
    $this->addReference('category-programming', $programming);
    $this->addReference('category-manager', $manager);
    $this->addReference('category-administrator', $administrator);
  }
 
  public function getOrder()
  {
    return 1; // the order in which fixtures will be loaded
  }
}
// src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
namespace Ens\JobeetBundle\DataFixtures\ORM;
 
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Ens\JobeetBundle\Entity\Job;
 
class LoadJobData extends AbstractFixture implements OrderedFixtureInterface
{
  public function load(ObjectManager $em)
  {
    $job_sensio_labs = new Job();
    $job_sensio_labs->setCategory($em->merge($this->getReference('category-programming')));
    $job_sensio_labs->setType('full-time');
    $job_sensio_labs->setCompany('Sensio Labs');
    $job_sensio_labs->setLogo('sensio-labs.gif');
    $job_sensio_labs->setUrl('http://www.sensiolabs.com/');
    $job_sensio_labs->setPosition('Web Developer');
    $job_sensio_labs->setLocation('Paris, France');
    $job_sensio_labs->setDescription('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.');
    $job_sensio_labs->setHowToApply('Send your resume to fabien.potencier [at] sensio.com');
    $job_sensio_labs->setIsPublic(true);
    $job_sensio_labs->setIsActivated(true);
    $job_sensio_labs->setToken('job_sensio_labs');
    $job_sensio_labs->setEmail('job@example.com');
    $job_sensio_labs->setExpiresAt(new \DateTime('2012-10-10'));
 
    $job_extreme_sensio = new Job();
    $job_extreme_sensio->setCategory($em->merge($this->getReference('category-design')));
    $job_extreme_sensio->setType('part-time');
    $job_extreme_sensio->setCompany('Extreme Sensio');
    $job_extreme_sensio->setLogo('extreme-sensio.gif');
    $job_extreme_sensio->setUrl('http://www.extreme-sensio.com/');
    $job_extreme_sensio->setPosition('Web Designer');
    $job_extreme_sensio->setLocation('Paris, France');
    $job_extreme_sensio->setDescription('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.');
    $job_extreme_sensio->setHowToApply('Send your resume to fabien.potencier [at] sensio.com');
    $job_extreme_sensio->setIsPublic(true);
    $job_extreme_sensio->setIsActivated(true);
    $job_extreme_sensio->setToken('job_extreme_sensio');
    $job_extreme_sensio->setEmail('job@example.com');
    $job_extreme_sensio->setExpiresAt(new \DateTime('2012-10-10'));
 
    $em->persist($job_sensio_labs);
    $em->persist($job_extreme_sensio);
 
    $em->flush();
  }
 
  public function getOrder()
  {
    return 2; // the order in which fixtures will be loaded
  }
}

Une fois vos fixtures écrites, vous pouvez les charger via la ligne de commande suivante:

php app/console doctrine:fixtures:load

Vous pouvez maintenant vérifier votre BDD, vous devriez voir les données chargées dans les tables.

Le fichier de fixtures des offres fait référence à deux images. Vous pouvez les télécharger (sensio-labs.gif, extreme-sensio.gif) et les mettre dans le répertoire web/uploads/jobs/.


Voyez-le en action dans le navigateur

Maintenant, nous allons utiliser un peu de magie! Exécutez à l'invite de commande:

php app/console doctrine:generate:crud --entity=EnsJobeetBundle:Job --route-prefix=ens_job --with-write --format=yml

Cela va créer un nouveau contrôleur src/Ens/JobeetBundle/Controllers/JobController.php avec des actions pour lister, créer, modifier et supprimer des offres (et leurs modèles, formulaires et routes correspondants). Pour le voir dans le navigateur, nous devons importer les nouvelles routes qui ont été créées dans src/Ens/JobeetBundle/Resources/config/routing/job.yml dans notre le fichier de routage principal de notre paquet:

# src/Ens/JobeetBundle/Resources/config/routing.yml
EnsJobeetBundle_job:
    resource: "@EnsJobeetBundle/Resources/config/routing/job.yml"
    prefix: /job
 
EnsJobeetBundle_homepage:
    pattern:  /hello/{name}
    defaults: { _controller: EnsJobeetBundle:Default:index }

Nous aurons aussi besoin d'ajouter une méthode __toString() à notre classe Category pour être utilisée par le menu déroulant Catégorie du formulaire de modification d'offre:

// src/Ens/JobeetBundle/Entity/Category.php
// ...
public function __toString()
{
  return $this->getName();
}

Videz le cache:

php app/console cache:clear --env=prod
php app/console cache:clear --env=dev

Vous pouvez maintenant tester le contrôleur Job dans un navigateur: http://jobeet.local/job/ ou, dans l'environnement de développement, http://jobeet.local/app_dev.php/job/

Vous pouvez désormais créer et modifier des offres. 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 BDD.


Réflexions finales

Nous avons à peine écrit du code PHP, mais nous avons un module web de travail pour le modèle Job, prêt à être modifié et personnalisé. Rappelez-vous : pas de code PHP signifie pas de bugs!


Chapitre précédent Chapitre suivant


Une question ? Une réaction ?

comments powered by Disqus