English spoken conference

Symfony 5: The Fast Track

A new book to learn about developing modern Symfony 5 applications.

Support this project

Caution: You are browsing the legacy symfony 1.x part of this website.

Jour 10 : Les formulaires

1.2 / Doctrine

La deuxième semaine de Jobeet a pris un bon départ avec l'introduction du framework de test de symfony. Nous allons continuer aujourd'hui avec le framework de formulaire

Le framework de formulaire

Tout site Web a des formulaires : du simple formulaire de contact à celui avec beaucoup plus de champs. L'écriture de formulaire est aussi une des tâches les plus complexes et l'une des plus fastidieuses pour un développeur web : vous devez écrire le formulaire HTML, implémenter des règles de validation pour chaque champ, traiter les valeurs pour les stocker dans une base de données, afficher les messages d'erreur, repeupler les champs dans le cas d'erreurs, et bien plus encore ...

Bien sûr, au lieu de réinventer la roue, encore et encore, symfony fournit un framework pour faciliter la gestion de formulaire. Le framework de formulaire est composé de trois parties :

  • validation: Le sous-framework de validation fournit les classes pour valider les saisies (entier, chaine, adresse email, ...)
  • widgets: Le sous-framework de widget fournit les classes pour la production des champs HTML (input, textarea, select, ...)
  • forms: Les classes formulaire représentent des formulaires faits de widgets et de validateurs et fournit des méthodes pour aider la gestion du formulaire. Chaque champs du formulaire a son propre validateur et son propre widget.

Les formulaires

Un formulaire de symfony est une classe composé de champs. Chaque champ a un nom, un validateur et un widget. Un simple ContactForm peut être défini avec la classe suivante :

class ContactForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'email'   => new sfWidgetFormInput(),
      'message' => new sfWidgetFormTextarea(),
    ));
 
    $this->setValidators(array(
      'email'   => new sfValidatorEmail(),
      'message' => new sfValidatorString(array('max_length' => 255)),
    ));
  }
}

Les champs du formulaire sont configurés dans la méthode configure(), en utilisant les méthodes setValidators() et setWidgets().

tip

Le frameework de formulaire est livré avec un lot de widgets et validators. L'API les décrit assez largement avec toutes les options, les erreurs et les messages d'erreur par défaut.

Les noms des classes des widgets et des validateurs sont assez explicites : le champ email sera rendu par une balise HTML <input> (sfWidgetFormInputText) et sera validé par une adresse email (sfValidatorEmail). Le champ message sera rendu par une balise <textarea> (sfWidgetFormTextarea), et doit être une chaine d'au plus 255 caractères (sfValidatorString).

Par défaut, tous les champs sont obligatoires, car la valeur par défaut pour l'option required est true. Ainsi, la définition de la validation pour email est équivalente à new sfValidatorEmail(array('required' => true)).

tip

Vous pouvez fusionner un formulaire dans un autre en utilisant la méthode mergeForm(), ou incorporer un formulaire à l'aide de la méthode embedForm() :

$this->mergeForm(new AnotherForm());
$this->embedForm('name', new AnotherForm());

Les formulaires de Doctrine

La plupart du temps, un formulaire doit être sérialisé à la base de données. Comme symfony sait déjà tout sur le modèle de votre base de données, il peut générer automatiquement des formulaires basés sur ces informations. En fait, lorsque vous avez lancé la tâche doctrine:build-all au cours du jour 3, symfony a automatiquement appelé la tâche doctrine:build-forms :

$ php symfony doctrine:build-forms

La tâche doctrine:build-forms génère des classes de formulaires dans le répertoire lib/form/. L'organisation de ces fichiers générés est semblable à celle de lib/model/. Chaque classe du modèle a une classe de formulaire correspondante (par exemple JobeetJob a JobeetJobForm), qui est vide par défaut car il hérite d'une classe de base :

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
  }
}

tip

En parcourant les fichiers générés dans le sous-répertoire lib/form/doctrine/base/, vous pourrez voir de nombreux exemples d'usage de symfony intégré dans les widgets et les validateurs.

Personnalisation du formulaire d'emploi

Le formulaire d'emploi est un parfait exemple pour apprendre la personnalisation du formulaires. Voyons comment le personnaliser, étape par étape.

D'abord, changez le lien "Post a Job" dans la mise en page pour être en mesure de vérifier les modifications directement dans votre navigateur :

<!-- apps/frontend/templates/layout.php -->
<a href="<?php echo url_for('@job_new') ?>">Post a Job</a>

Par défaut, un formulaire Doctrine affiche des champs pour toutes les colonnes de la table. Mais pour le formulaire d'emploi, certains d'entre eux ne doit pas être modifiable par l'utilisateur final. La suppression des champs à partir d'un formulaire est aussi simple que d'en désactiver :

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
  }
}

La désactivation d'un champ signifie que le widget et le validateur du champs sont supprimés.

La configuration du formulaire doit parfois être plus précis que l'introspection à partir du schéma de la base de données. Par exemple, la colonne email est un varchar dans le schéma, mais nous avons besoin de cette colonne pour être validé comme un email. Changeons la valeur par défaut sfValidatorString par sfValidatorEmail :

// lib/form/doctrine/JobeetJobForm.class.php
public function configure()
{
  // ...
 
  $this->validatorSchema['email'] = new sfValidatorEmail();
}

Le remplacement de la validation par défaut n'est pas toujours la meilleure solution, comme les règles de validation par défaut introspectés depuis le schéma de la base de données sont perdues (new sfValidatorString(array('max_length' => 255))). Il est presque toujours préférable d'ajouter le nouveau validateur à celui déjà existant à l'aide du validateur spécial sfValidatorAnd :

// lib/form/doctrine/JobeetJobForm.class.php
public function configure()
{
  // ...
 
  $this->validatorSchema['email'] = new sfValidatorAnd(array(
    $this->validatorSchema['email'],
    new sfValidatorEmail(),
  ));
}

Le validateur sfValidatorAnd prend un tableau de validateurs qui doivent tous passer pour que cette valeur soit valide. L'astuce ici est de faire référence au validateur actuel ($this->validatorSchema['email']), et d'ajouter le nouveau.

note

Vous pouvez également utiliser le validateur sfValidatorOr pour forcer une valeur à passer pour au moins un validateur. Et bien sûr, vous pouvez mélanger et fusionner les validateurs sfValidatorAnd et sfValidatorOr pour créer des validateurs complexes sur base de booléen.

Même si la colonne type est également un varchar dans le schéma, nous voulons sa valeur pour se limiter à une liste de choix : temps plein, temps partiel, ou freelance.

D'abord, nous allons définir les valeurs possibles dans JobeetJobTable :

// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  static public $types = array(
    'full-time' => 'Full time',
    'part-time' => 'Part time',
    'freelance' => 'Freelance',
  );
 
  public function getTypes()
  {
    return self::$types;
  }
 
  // ...
}

Puis, utilisez sfWidgetFormChoice pour le widget type :

$this->widgetSchema['type'] = new sfWidgetFormChoice(array(
  'choices'  => Doctrine::getTable('JobeetJob')->getTypes(),
  'expanded' => true,
));

sfWidgetFormChoice représente un widget de choix qui peut être affiché par un widget différent selon certaines options de configuration (expanded et multiple) :

  • Liste déroulante (<select>) : array('multiple' => false, 'expanded' => false)
  • Boîte déroulante (<select multiple="multiple">) : array('multiple' => true, 'expanded' => false)
  • Liste de radiobuttons : array('multiple' => false, 'expanded' => true)
  • List de checkboxes : array('multiple' => true, 'expanded' => true)

note

Si vous voulez que l'un des radiobutton soit sélectionné par défaut (full-time par exemple), vous pouvez changer la valeur par défaut dans le schéma de base de données.

Même si vous pensez que personne peut soumettre un valeur non valide, un pirate peut facilement contourner les choix du widget en utilisant des outils comme curl ou la barre d'outils de développeur web de Firefox. Changeons le validateur pour restreindre les choix possibles :

$this->validatorSchema['type'] = new sfValidatorChoice(array(
  'choices' => array_keys(Doctrine::getTable('JobeetJob')->getTypes()),
));

Comme la colonne logo va stocker le nom du fichier du logo associé à l'emploi, nous devons changer le widget en une balise file input :

$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
  'label' => 'Company logo',
));

Pour chaque champ, symfony génère automatiquement un label (qui sera utilisé dans la balise <label> rendue). Ceci peut être modifié avec l'option label.

Vous pouvez également modifier les labels d'un batch avec la méthode setLabels() du tableau de widget :

$this->widgetSchema->setLabels(array(
  'category_id'    => 'Category',
  'is_public'      => 'Public?',
  'how_to_apply'   => 'How to apply?',
));

Nous devons également changer le validateur par défaut :

$this->validatorSchema['logo'] = new sfValidatorFile(array(
  'required'   => false,
  'path'       => sfConfig::get('sf_upload_dir').'/jobs',
  'mime_types' => 'web_images',
));

sfValidatorFile est assez intéressant car il fait un certain nombre de choses :

  • Valider que le fichier téléchargé est une image dans un format web (mime_types)
  • Renomme le fichier en quelque chose d'unique
  • Stocke le fichier dans le path donné
  • Met à jour la colonne logo avec le nom généré

note

Vous devez créer le répertoire logo (web/uploads/jobs/) et vérifier qu'il est accessible en écriture par le serveur web.

Comme le validateur enregistrera le chemin relatif dans la base de données, changer le chemin utilisé dans le template showSuccess :

// apps/frontend/modules/job/templates/showSuccess.php
<img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />

tip

Si une méthode generateLogoFilename() existe dans le modèle, il sera appelé par le validateur et le résultat remplacera le nom de fichier logo généré par défaut. La méthode est donné à l'objet sfValidatedFile comme argument.

Tout comme vous pouvez remplacer le label généré de n'importe quel champ, vous pouvez aussi définir un message d'aide. Ajoutons en un pour la colonne is_public pour mieux expliquer sa signification :

$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');

La classe JobeetJobForm finale se lit comme suit :

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated']
    );
 
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
 
    $this->widgetSchema['type'] = new sfWidgetFormChoice(array(
      'choices'  => Doctrine::getTable('JobeetJob')->getTypes(),
      'expanded' => true,
    ));
    $this->validatorSchema['type'] = new sfValidatorChoice(array(
      'choices' => array_keys(Doctrine::getTable('JobeetJob')->getTypes()),
    ));
 
    $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array(
      'label' => 'Company logo',
    ));
 
    $this->widgetSchema->setLabels(array(
      'category_id'    => 'Category',
      'is_public'      => 'Public?',
      'how_to_apply'   => 'How to apply?',
    ));
 
    $this->validatorSchema['logo'] = new sfValidatorFile(array(
      'required'   => false,
      'path'       => sfConfig::get('sf_upload_dir').'/jobs',
      'mime_types' => 'web_images',
    ));
 
    $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
  }
}

Le template du formulaire

Maintenant que la classe de formulaire a été personnalisé, nous avons besoin de l'afficher. Le Template du formulaire est le même, que vous vouliez créer un nouvel emploi ou modifier un existant. En fait, les deux Templates newSuccess.php et editSuccess.phpsont assez similaires:

<!-- apps/frontend/modules/job/templates/newSuccess.php -->
<?php use_stylesheet('job.css') ?>
 
<h1>Post a Job</h1>
 
<?php include_partial('form', array('form' => $form)) ?>

note

Si vous n'avez pas ajouté de la feuille de style job pour le moment, il est temps de le faire dans les deux Templates (<?php use_stylesheet('job.css') ?>).

Le formulaire est lui-même rendu dans le partial _form. Remplacez le contenu du partial généré _form par le code suivant :

<!-- apps/frontend/modules/job/templates/_form.php -->
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
 
<?php echo form_tag_for($form, '@job') ?>
  <table id="job_form">
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="submit" value="Preview your job" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>

Les helpers include_javascripts_for_form() et include_stylesheets_for_form() incluent des dépendances de JavaScript et de feuille de style nécessaires pour les widgets du formulaire.

tip

Même si le formulaire emploi n'a pas besoin de JavaScript ou d'un fichier de style, c'est une bonne habitude de garder l'appels de ces helpers "juste au cas où". Il peut sauver votre journée plus tard, si vous décidez de changer un widget qui a besoin de quelques JavaScript ou d'une feuille de style spécifique.

Le helper form_tag_for() génère une balise <form> pour le formulaire et la route donnés et modifie les méthodes HTTP en POST ou en PUT PUT selon si l'objet est nouveau ou non. Il s'occupe aussi de l'attribut ~multipart|Formulaires (Multipart)~ si le formulaire a aucune balise input file.

Finalement, le <?php echo $form ?> rend les widgets du formulaire.

sidebar

Personnalisation de l'aspect d'un formulaire

Par défaut, le <?php echo $form ?> rend les widgets du formulaire en tant que lignes de tableau.

La plupart du temps, vous aurez besoin de personnaliser la présentation de vos formulaires. L'objet du formulaire fournit de nombreuses méthodes utiles pour cette personnalisation :

Méthode Description
render() Rend le formulaire (equivalent à la sortie de
echo $form)
renderHiddenFields() Rend les champs masqués
hasErrors() Retourne true si le formulaire a quelques erreurs
hasGlobalErrors() Retourne true si le formulaire a des erreurs globales
getGlobalErrors() Retourne un tableau des erreurs globales
renderGlobalErrors() Rend les erreurs globales

Le formulaire se comporte aussi comme un tableau de champs. Vous pouvez accéder au champ company avec $form['company']. L'objet retourné fournit des méthodes pour rendre chaque élément du champs :

Méthode Description
renderRow() Rend la ligne du champ
render() Rend le widget du champ
renderLabel() Rend le label du champ
renderError() Rend les messages d'érreur du champ s'il y'en a
renderHelp() Rend le message d'aide du champ

L'instruction echo $form est équivalent à :

<?php foreach ($form as $widget): ?>
  <?php echo $widget->renderRow() ?>
<?php endforeach; ?>

L'action du formulaire

Nous avons maintenant une classe de formulaire et un template qui fait le rendu. Maintenant, il est temps de le faire réellement fonctionner avec certaines actions.

Le formulaire emploi est géré par cinq méthodes dans le module job :

  • new: Affiche un formulaire non renseigné pour créer un nouvel emploi
  • edit: Affiche un formulaire pour modifier un emploi existant
  • create: Crée un nouvel emploi avec les valeurs soumises par l'utilisateur
  • update: Met à jour un emploi avec les valeurs soumises par l'utilisateur
  • processForm: Appelé par create et update, il traite le formulaire (validation, repopulation du formulaire et sérialisation vers la base de données)

Tous les formulaires ont le cycle de vie suivant :

Flux d'un formulaire

Comme nous avons créé une collection de route Doctrine il y a 5 jours pour le module job, nous pouvons simplifier le code pour les méthodes de gestion de formulaire :

// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
}
 
public function executeCreate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm();
  $this->processForm($request, $this->form);
  $this->setTemplate('new');
}
 
public function executeEdit(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
}
 
public function executeUpdate(sfWebRequest $request)
{
  $this->form = new JobeetJobForm($this->getRoute()->getObject());
  $this->processForm($request, $this->form);
  $this->setTemplate('edit');
}
 
public function executeDelete(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $job->delete();
 
  $this->redirect('job/index');
}
 
protected function processForm(sfWebRequest $request, sfForm $form)
{
  $form->bind(
    $request->getParameter($form->getName()),
    $request->getFiles($form->getName())
  );
 
  if ($form->isValid())
  {
    $job = $form->save();
 
    $this->redirect($this->generateUrl('job_show', $job));
  }
}

Lorsque vous naviguez vers la page /job/new, une nouvelle instance du formulaire est créé et transmise au Template (action new).

Lorsque l'utilisateur soumet le formulaire (action create), le formulaire est lié (méthode bind()) avec les valeurs soumises par l'utilisateur et la validation est déclenchée.

Une fois que le formulaire est lié, il est possible de vérifier sa validité en utilisant la méthode isValid() : si le formulaire est valide (elle retourne true), l'emploi est sauvé dans la base de données ($form->save()), et l'utilisateur est redirigé vers la page de prévisualisation d'emploi, sinon, le template newSuccess.php s'affiche à nouveau avec les valeurs soumises par l'utilisateur et les messages d'erreur associés.

tip

La méthode setTemplate() change le Template utilisé pour une action donnée. Si le formulaire soumis n'est pas valide, les méthodes create et update utilisent le même template respectif que l'action new et edit pour ré-afficher le formulaire avec les messages d'erreur.

La modification d'un emploi existant est assez similaire. La seule différence entre l'action new et edit est que l'objet job à modifier, est passé comme premier argument au constructeur du formulaire. Cet objet sera utilisé pour les valeurs par défaut de widget dans le Template (les valeurs par défaut sont un objet pour le formulaire Doctrine, mais un simple tableau pour des formulaires simples).

Vous pouvez également définir des valeurs par défaut pour le formulaire de création. Une solution est de déclarer les valeurs dans le schéma de base de données. Une autre est de passer un objet Job pré-modifié au constructeur du formulaire.

Changer la méthode executeNew() pour définir full-time comme étant la valeur par défaut pour la colonne type :

// apps/frontend/modules/job/actions/actions.class.php
public function executeNew(sfWebRequest $request)
{
  $job = new JobeetJob();
  $job->setType('full-time');
 
  $this->form = new JobeetJobForm($job);
}

note

Lorsque le formulaire est lié, les valeurs par défaut sont remplacées par celles soumises par l'utilisateur. Les valeurs soumises par l'utilisateur seront utilisées pour le repeuplement du formulaire lorsque ce dernier est à nouveau affichée en cas d'erreurs de validation.

Protection du formulaire d'emploi avec un jeton (token)

Tout doit fonctionner correctement maintenant. Pour l'instant, l'utilisateur doit saisir le jeton pour l'emploi. Mais le jeton de l'emploi doit être généré automatiquement quand un nouvel emploi est créé, car nous ne pouvons pas se fier à l'utilisateur pour fournir un jeton unique.

Mettez à jour la méthode save() de JobeetJob pour ajouter la logique qui génère le jeton avant qu'un nouvel emploi soit enregistré :

// lib/model/doctrine/JobeetJob.class.php
public function save(Doctrine_Connection $conn = null)
{
  // ...
 
  if (!$this->getToken())
  {
    $this->setToken(sha1($this->getEmail().rand(11111, 99999)));
  }
 
  return parent::save($conn);
}

Vous pouvez maintenant supprimer le champ token du formulaire :

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated'],
      $this['token']
    );
 
    // ...
  }
 
  // ...
}

Si vous vous souvenez des histoires d'utilisateurs du jour 2, un emploi peut être modifié que si l'utilisateur connaît le jeton associé. À l'heure actuelle, il est assez facile de modifier ou de supprimer n'importe quel emploi, en essayant juste de deviner l'URL. Car l'URL de edit est /job/ID/edit, où ID est la clé primaire de l'emploi.

Par défaut, une route sfDoctrineRouteCollection génère des URL avec la clé primaire, mais elle peut être changée par n'importe quelle colonne unique en faisant passer l'option column :

# apps/frontend/config/~routing|Routing~.yml
job:
  class:        sfDoctrineRouteCollection
  options:      { model: JobeetJob, column: token }
  requirements: { token: \w+ }

Notez que nous avons également modifié le paramètre requirements token pour faire correspondre n'importe quelle chaîne car le requirements de symfony par défaut est \d+ pour la clé unique.

Maintenant, toutes les routes liées aux emplois, à l'exception des job_show_user, incorporent le jeton. Par exemple, la route pour modifier un emploi est maintenant sur le modèle suivant :

http://jobeet.localhost/job/TOKEN/edit

Vous aurez aussi besoin de changer le lien "Edit" dans le Template showSuccess :

<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<a href="<?php echo url_for('job_edit', $job) ?>">Edit</a>

La page de prévisualisation

La page de prévisualisation est la même que l'affichage de la page emploi. Grâce au routage, si l'utilisateur arrive avec le bon jeton, il sera accessible dans le paramètre de la requête token.

Si l'utilisateur entre dans l'URL sous forme de jeton, nous allons ajouter une barre d'admininistrateur en haut. Au début du Template showSuccess, ajoutez un partial pour mettre la barre d'administrateur et supprimer le lien edit en bas :

<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<?php if ($sf_request->getParameter('token') == $job->getToken()): ?>
  <?php include_partial('job/admin', array('job' => $job)) ?>
<?php endif; ?>

Ensuite, créez le partial _admin :

<!-- apps/frontend/modules/job/templates/_admin.php -->
<div id="job_actions">
  <h3>Admin</h3>
  <ul>
    <?php if (!$job->getIsActivated()): ?>
      <li><?php echo link_to('Edit', 'job_edit', $job) ?></li>
      <li><?php echo link_to('Publish', 'job_edit', $job) ?></li>
    <?php endif; ?>
    <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li>
    <?php if ($job->getIsActivated()): ?>
      <li<?php $job->expiresSoon() and print ' class="expires_soon"' ?>>
        <?php if ($job->isExpired()): ?>
          Expired
        <?php else: ?>
          Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days
        <?php endif; ?>
 
        <?php if ($job->expiresSoon()): ?>
         - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days
        <?php endif; ?>
      </li>
    <?php else: ?>
      <li>
        [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.]
      </li>
    <?php endif; ?>
  </ul>
</div>

Il y a beaucoup de code, mais la plupart du code est simple à comprendre.

Pour rendre le Template plus lisible, nous avons ajouté un ensemble de raccourci de méthodes dans la classe JobeetJob :

// lib/model/doctrine/JobeetJob.class.php
public function getTypeName()
{
  $types = Doctrine::getTable('JobeetJob')->getTypes();
  return $this->getType() ? $types[$this->getType()] : '';
}
 
public function isExpired()
{
  return $this->getDaysBeforeExpires() < 0;
}
 
public function expiresSoon()
{
  return $this->getDaysBeforeExpires() < 5;
}
 
public function getDaysBeforeExpires()
{
  return floor((strtotime($this->getExpiresAt()) - time()) / 86400);
}

a barre d'administrateur affiche les différentes actions en fonction de l'état de l'emploi :

Emploi non activé

Emploi activé

note

Vous serez capable de voir la barre "activée" après la section suivante.

Activation et Publication d'un emploi

Dans la section précédente, il y a un lien pour publier l'emploi. Le lien doit être changé pour pointer vers une nouvelle action publish. Au lieu de créer une nouvelle route, on peut juste configurer la route job existante :

# apps/frontend/config/routing.yml
job:
  class:   sfDoctrineRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: put }
  requirements:
    token: \w+

Le object_actions prend un tableau des actions supplélentaires pour un objet donné. Nous pouvons maintenant modifier le lien du lien "Publish" :

<!-- apps/frontend/modules/job/templates/_admin.php -->
<li>
  <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?>
</li>

La dernière étape est de créer l'action publish :

// apps/frontend/modules/job/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $job->publish();
 
  $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));
 
  $this->redirect($this->generateUrl('job_show_user', $job));
}

Le lecteur attentif aura remarqué que le lien "Publish" est soumis avec la méthode HTTP put. Pour simuler la méthode put, le lien est automatiquement converti en un formulaire lorsque vous cliquez dessus.

Et comme nous avons activé la protection CSRF, le helper link_to() embarque un jeton CSRF dans le lien et la méthode checkCSRFProtection() de l'objet de la requête vérifie la validité de celui-ci lors de la soumission.

La méthode executePublish() utilise une nouvelle méthode publish() qui peut être définie comme suit :

// lib/model/doctrine/JobeetJob.class.php
public function publish()
{
  $this->setIsActivated(true);
  $this->save();
}

Vous pouvez maintenant tester la nouvelle fonctionnalité de publication dans votre navigateur.

Mais nous avons encore quelque chose à corriger. Les emplois non-activés ne doivent pas être accessibles. Ce qui signifie qu'ils ne doivent pas apparaître sur la page d'accueil Jobeet et ne doivent pas être accessibles par leur URL. Comme nous avons créé une méthode addActiveJobsQuery() pour limiter une Doctrine_Query aux emplois actifs, nous pouvons le modifier et ajouter des nouvelles exigences à la fin :

// lib/model/doctrine/JobeetJobTable.class.php
public function addActiveJobsQuery(Doctrine_Query $q = null)
{
  // ...
 
  $q->andWhere($alias . '.is_activated = ?', 1);
 
  return $q;
}

C'est tout. Vous pouvez maintenant le tester dans votre navigateur. Tous les emplois non-activés ont disparu de la page d'accueil, même si vous connaissez son URL, ils ne sont plus accessibles. Ils sont cependant accessibles si l'on connait URL du jeton de l'emploi. Dans ce cas, l'aperçu de l'emploi sera affichée avec la barre d'administration.

C'est un des grands avantages du modèle MVC et la refactorisation nous a fait faire du chemin. Un seul changement dans une seule méthode a été nécessaire pour ajouter la nouvelle exigence.

note

Lorsque nous avons créé la méthode getWithJobs(), nous avons oublié d'utiliser la méthode addActiveJobsQuery(). Donc, nous avons besoin de la modifier et ajouter la nouvelle exigence :

class JobeetCategoryTable extends Doctrine_Table
{
  public function getWithJobs()
  {
    // ...
 
    $q->andWhere('j.is_activated = ?', 1);
 
    return $q->execute();
  }

À demain

Le tutoriel d'aujourd'hui a été composé avec beaucoup de nouvelles informations, mais nous espérons que vous avez maintenant une meilleure compréhension du framework de formulaire de symfony.

Je sais que certains d'entre vous ont remarqué que nous avons oublié quelque chose aujourd'hui ... Nous n'avons pas mis en œuvre de test pour les nouvelles fonctionnalités. Parce que l'écriture de test est une partie importante du développement d'une application, c'est la première chose que nous ferons demain.