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 12 : L'Admin Generator

1.2 / Doctrine

Avec les ajouts que nous avons faits hier sur Jobeet, l'application frontend est maintenant pleinement utilisable par les chercheurs d'emploi et les posteurs d'emplois. Il est temps de parler un peu de l'application backend.

Aujourd'hui, grâce à la fonctionnalité de l'admin generator de symfony, nous allons développer une interface backend complète pour Jobeet en seulement une heure.

La création du backend

La toute première étape consiste à créer l'application backend. Si vous vous souvenez bien, vous devriez vous rappeler comment le faire avec la tâche generate:app :

$ php symfony generate:app --escaping-strategy=on --csrf-secret=UniqueSecret1 backend

Même si l'application backend ne sera utilisée que par les administrateurs de Jobeet, nous avons activé toutes les fonctions intégrées de sécurité de symfony.

tip

Si vous souhaitez utiliser des caractères spéciaux dans le mot de passe, comme un signe dollar ($) par exemple, vous avez besoin de l'échapper correctement sur la ligne de commande :

$ php symfony generate:app --csrf-secret=Unique\$ecret backend

L'application backend est maintenant disponible à l'adresse http://jobeet.localhost/backend.php/ pour l'environnement de prod et http://jobeet.localhost/backend_dev.php/ pour l'environnement de dev.

note

Lorsque vous avez créé l'application frontend, le contrôleur frontal de la production a été nommé index.php. Comme vous ne pouvez avoir qu'un seul fichier index.php par répertoire, symfony crée un fichier index.php pour le premier contrôleur de la production et les noms des autres après le nom d'application.

Si vous essayez de recharger les données de tests avec la tâche doctrine:data-load, elle ne fonctionnera plus. C'est parce que la méthode JobeetJob::save() a besoin d'accéder au fichier de configuration app.yml depuis l'application frontend. Comme nous avons maintenant deux applications, symfony utilise le premier qu'il trouve, qui est maintenant le backend.

Mais comme on le voit pendant le jour 8, les paramètres peuvent être configurés à différents niveaux. En déplaçant le contenu du fichier apps/frontend/config/app.yml vers config/app.yml, les paramètres seront partagés par toutes les applications et le problème sera réglé. Changez le maintenant car nous allons utiliser les classes du modèle assez largement dans l'admin generator, et donc nous aurons besoin des variables définies dans app.yml dans l'application backend.

tip

La tâche doctrine:data-load prend également en charge une option --application. Donc, si vous avez besoin de paramètres spécifiques d'une application ou une autre, c'est la marche à suivre :

$ php symfony doctrine:data-load --application=frontend

Les modules du backend

Pour l'application frontend, la tâche doctrine:generate-module a été utilisé pour démarrer un module de base CRUD basé sur une classe modèle. Pour le backend, la tâche doctrine:generate-admin sera utilisé car il génère une interface de travail complète de backend pour une classe de modèle :

$ php symfony doctrine:generate-admin backend JobeetJob --module=job
$ php symfony doctrine:generate-admin backend JobeetCategory --module=category

Ces deux commandes créent un module job et un module category pour les classes respectives du modèle JobeetJob et JobeetCategory.

L'option facultative --module remplace le nom module généré par défaut par la tâche (ce qui aurait été autrement jobeet_job pour la classe JobeetJob).

Derrière le rideau, la tâche a également créé une route personnalisée pour chaque module :

# apps/backend/config/routing.yml
jobeet_job:
  class: sfDoctrineRouteCollection
  options:
    model:                JobeetJob
    module:               job
    prefix_path:          job
    column:               id
    with_wildcard_routes: true

Il ne faut donc pas s'étonner que la classe de la route utilisée par l'admin generator|Admin Generator est sfDoctrineRouteCollection, car l'objectif principal d'une interface d'administration est la gestion du cycle de vie des objets du modèle.

La définition de la route définit également certaines options que nous n'avons pas vu auparavant :

  • prefix_path: Définit le chemin du préfixe pour la route générée (par exemple, la page d'édition sera quelque chose comme /job/1/edit).
  • column: Définit la colonne de table à utiliser dans l'URL pour les liens faisant référence à un objet.
  • with_wildcard_routes: Comme l'interface d'administration aura plus que des opérations classiques du CRUD, cette option permet de définir plus d'objet et une collection d'actions sans modifier la route.

tip

Comme toujours, c'est une bonne idée de lire l'aide avant d'utiliser une nouvelle tâche.

$ php symfony help doctrine:generate-admin

Elle vous donnera tous les arguments de la tâche et les options ainsi que quelques exemples d'utilisation classique.

Backend Look and Feel

D'emblée, vous pouvez utiliser les modules générés :

http://jobeet.localhost/backend_dev.php/job
http://jobeet.localhost/backend_dev.php/category

Les modules admin ont beaucoup plus de fonctionnalités que les modules simples que nous avons générés les jours précédents. Sans écrire une seule ligne de PHP, chaque module fournit ces fonctionnalités exceptionnelles :

  • La liste des objets est paginée
  • La liste est triable
  • La liste peut être filtrée
  • Les objets peuvent être créés, édités et supprimés
  • Les objets sélectionnés peuvent être supprimés dans un batch
  • La validation du formulaire est activée
  • Les messages flash donne immédiatement un retour à l'utilisateur
  • ... et plus encore

L'admin generator offre toutes les fonctions dont vous avez besoin pour créer une interface backend dans un simple package à configurer.

Pour rendre l'expérience utilisateur un peu mieux, nous avons besoin de personnaliser le backend par défaut. Nous allons aussi ajouter un menu simple pour le rendre facile pour naviguer entre les différents modules.

Remplacez le contenu du fichier ~layout|Layout~.php par défaut avec le code ci-dessous :

// apps/backend/templates/layout.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Jobeet Admin Interface</title>
    <link rel="shortcut icon" href="/favicon.ico" />
    <?php use_stylesheet('admin.css') ?>
    <?php include_javascripts() ?>
    <?php include_stylesheets() ?>
  </head>
  <body>
    <div id="container">
      <div id="header">
        <h1>
          <a href="<?php echo url_for('@homepage') ?>">
            <img src="/legacy/images/logo.jpg" alt="Jobeet Job Board" />
          </a>
        </h1>
      </div>
 
      <div id="menu">
        <ul>
          <li>
            <?php echo link_to('Jobs', '@jobeet_job_job') ?>
          </li>
          <li>
            <?php echo link_to('Categories', '@jobeet_category_category') ?>
          </li>
        </ul>
      </div>
 
      <div id="content">
        <?php echo $sf_content ?>
      </div>
 
      <div id="footer">
        <img src="/legacy/images/jobeet-mini.png" />
        powered by <a href="/">
        <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a>
      </div>
    </div>
  </body>
</html>

Cette mise en page utilise une feuille de style admin.css. Ce fichier doit être déjà présent dans web/css/ car il a été installé avec les autres feuilles de style au cours de jour 4.

L'aspect de l'admin generator

Éventuellement, changez la page d'accueil par défaut de symfony dans routing.yml :

# apps/backend/config/routing.yml
homepage:
  url:   /
  param: { module: job, action: index }

Le cache de symfony

Si vous êtes curieux, vous avez probablement déjà ouvert les fichiers générés par la tâche sous le répertoire apps/backend/modules/. Sinon, s'il vous plaît ouvrez-les maintenant. Surprise ! Les répertoires templates sont vides et les fichiers actions.class.php sont assez vides :

// apps/backend/modules/job/actions/actions.class.php
require_once dirname(__FILE__).'/../lib/jobGeneratorConfiguration.class.php';
require_once dirname(__FILE__).'/../lib/jobGeneratorHelper.class.php';
 
class jobActions extends autoJobActions
{
}

Comment peut-il fonctionner ? Si vous regardez d'un peu plus près, vous remarquerez que la classe jobActions étend autoJobActions. La classe autoJobActions est automatiquement généré par symfony si elle n'existe pas. Elle se trouve dans le répertoire cache/backend/dev/modules/autoJob/, qui contient le module «réel» :

// cache/backend/dev/modules/autoJob/actions/actions.class.php
class autoJobActions extends sfActions
{
  public function preExecute()
  {
    $this->configuration = new jobGeneratorConfiguration();
 
    if (!$this->getUser()->hasCredential(
      $this->configuration->getCredentials($this->getActionName())
    ))
    {
 
// ...

La manière dont l'admin generator fonctionne devrait vous rappeler certains comportements connus. En fait, il est assez semblable à ce que nous avons appris sur le modèle et les classes du formulaire. Basé sur la définition du schéma du modèle, symfony génère le modèle et les classes du formulaire. Pour l'admin generator, le module généré peut être configuré en éditant le fichier config/generator.yml trouvé dans le module :

# apps/backend/modules/job/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           JobeetJob
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_job
    with_doctrine_route:   1
 
    config:
      actions: ~
      fields:  ~
      list:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Chaque fois que vous mettez à jour le fichier generator.yml, symfony régénère le cache. Comme nous le voyons aujourd'hui, la personnalisation de l'admin des modules générés est simple, rapide et amusant.

note

La re-génération automatique des fichiers de cache se produit uniquement dans l'environnement de développement. En production, vous devez effacer le cache manuellement avec la tâche cache:clear.

La configuration du backend

Un module admin peut être personnalisé en modifiant la clé config du fichier generator.yml. La configuration est organisée en sept sections :

  • actions: La configuration par défaut pour les actions figurant sur la liste et dans les formulaires
  • fields: La configuration par défaut pour les champs
  • list: La configuration pour la liste
  • filter: La configuration pour les filtres
  • form: La configuration pour le formulaire new/edit
  • edit: La configuration spécifique pour la page edit
  • new: La configuration spécifique pour la page new

Commençons la personnalisation.

Configuration du titre

Les titres des sections list, edit et new du module category peuvent être personnalisés en définissant l'option title :

# apps/backend/modules/category/config/generator.yml
config:
  actions: ~
  fields:  ~
  list:
    title: Category Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Category "%%name%%"
  new:
    title: New Category

Le title pour la section edit contient des valeurs dynamiques : toutes les chaînes entourées par %% sont remplacées par les valeurs de la colonne pour l'objet correspondant.

Titres

La configuration pour le module job est assez similaire :

# apps/backend/modules/job/config/generator.yml
config:
  actions: ~
  fields:  ~
  list:
    title: Job Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Job "%%company%% is looking for a %%position%%"
  new:
    title: Job Creation

Configuration des champs

Les différentes vues (list, new et edit) sont composés de champs. Un champ peut être une colonne de la classe du modèle, ou une colonne virtuelle comme nous le verrons plus tard.

La configuration des champs par défaut peut être personnalisée avec la section fields :

# apps/backend/modules/job/config/generator.yml
config:
  fields:
    is_activated: { label: Activated?, help: Whether the user has activated the job, or not }
    is_public:    { label: Public?, help: Whether the job can also be published on affiliate websites, or not }

Configuration des champs

La section fields remplace la configuration des champs pour toutes les vues, ce qui signifie que le label pour le champ is_activated sera changé pour les vues list, edit et new.

La configuration de l'admin generator est basé sur un principe de configuration en cascade. Par exemple, si vous souhaitez modifier un label pour la vue list uniquement, définissez une option fields sous la section list :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    fields:
      is_public:    { label: "Public? (label for the list)" }

Toute configuration qui est définie dans la section principale fields peut être substituée par la configuration de la vue spécifique. Les règles de substitution sont les suivantes :

  • new et edit héritent de form qui hérite fields
  • list hérite fields
  • filter hérite fields

note

Pour les sections de formulaire (form, edit et new), les options label et help substituent celles définies dans les classes de formulaires.

Configuration de la vue list

display

Par défaut, les colonnes de la vue de la liste sont toutes des colonnes du modèle, dans l'ordre du fichier du schéma. L'option display substitue l'ordre par défaut des colonnes à afficher :

# apps/backend/modules/category/config/generator.yml
config:
  list:
    title:   Category Management
    display: [=name, slug]

Le signe = avant la colonne name est une convention visant à convertir la chaîne en un lien.

Liste de la table

Faisons de même pour le module job pour le rendre plus lisible :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    title:   Job Management
    display: [company, position, location, url, is_activated, email]

layout

La liste peut être affichée avec des layouts différents. Par défaut, le layout est ~tabular|Layout tabular~, ce qui signifie que chaque valeur de colonne est la colonne dans sa propre table. Mais pour le module job, il serait préférable d'utiliser le layout ~stacked|Layout stacked~, qui est l'autre layout intégré :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    title:   Job Management
    layout:  stacked
    display: [company, position, location, url, is_activated, email]
    params:  |
      %%is_activated%% <small>%%category_id%%</small> - %%company%%
       (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

Dans un layout stacked, chaque objet est représenté par une seule chaîne, qui est definie par l'option params.

note

L'option display est toujours nécessaire, car elle définit les colonnes qui seront triables par l'utilisateur.

Les colonnes "Virtuelles"

Avec cette configuration, le segment %%category_id%% sera remplacée par la clé primaire de la catégorie. Mais il serait plus utile d'afficher le nom de la catégorie.

Chaque fois que vous utilisez la notation %%, la variable ne doit pas nécessairement correspondre à une colonne réelle dans le schéma de la base de données. L'admin generator a seulement besoin de trouver un getter associé dans la classe du modèle.

Pour afficher le nom de la catégorie, on peut définir une méthode getCategoryName() dans la classe du modèle JobeetJob et remplacer %%category_id%% par %%category_name%%.

Mais la classe JobeetJob a déjà une méthode getJobeetCategory() qui retourne l'objet catégorie associé. Et si vous utilisez %%jobeet_category%%, cela fonctionne comme la classe JobeetCategory. Cette dernière a une méthode magique __toString() qui convertit l'objet en une chaîne.

# apps/backend/modules/job/config/generator.yml
%%is_activated%% <small>%%jobeet_category%%</small> - %%company%%
 (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

Layout stacked

sort

En tant qu'administrateur, vous serez probablement plus intéressés de voir les dernières offres d'emploi postées. Vous pouvez configurer la colonne de tri par défaut en ajoutant l'option sort :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    sort: [expires_at, desc]

max_per_page

Par défaut, la liste est paginée et chaque page contient 20 articles. Ceci peut être modifié avec l'option ~max_per_page~ :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    max_per_page: 10

Maximum par page

batch_actions

Sur une liste, une action peut être exécutée sur plusieurs objets. Ces actions batch ne sont pas nécessaires pour le module category, donc nous allons les supprimer :

# apps/backend/modules/category/config/generator.yml
config:
  list:
    batch_actions: {}

Supprime les actions batch

L'option batch_actions définit la liste des actions batch. Le tableau vide permet l'élimination de la fonctionnalité.

Par défaut, chaque module a une action batch delete définie par le framework, mais pour le module job, supposons que nous devons trouver un moyen de prolonger la validité de certains emplois sélectionnés pour 30 jours supplémentaires :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    batch_actions:
      _delete:    ~
      extend:     ~

Toutes les actions commençant par un _ sont intégrés dans les actions fournies par le framework. Si vous actualisez votre navigateur et sélectionnez les actions batch supplémenaires, symfony va lever une exception en vous invitant à créer une méthode executeBatchExtend() :

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeBatchExtend(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
 
    $q = Doctrine_Query::create()
      ->from('JobeetJob j')
      ->whereIn('j.id', $ids);
 
    foreach ($q->execute() as $job)
    {
      $job->extend(true);
    }
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('@jobeet_job_job');
  }
}

Les clés primaires sélectionnées sont stockées dans le paramètre de requête ids. Pour chaque emploi sélectionné, la méthode JobeetJob::extend() est appelée avec un argument supplémentaire pour contourner le contrôle d'expiration.

Mettez à jour la méthode extend() pour prendre ce nouvel argument en compte :

// lib/model/doctrine/JobeetJob.class.php
class JobeetJob extends BaseJobeetJob
{
  public function extend($force = false)
  {
    if (!$force && !$this->expiresSoon())
    {
      return false;
    }
 
    $this->setExpiresAt(date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days')));
    $this->save();
 
    return true;
  }
 
  // ...
}

Après que tous les emplois aient été étendus, l'utilisateur est redirigé vers la page d'accueil du module job.

Personnalise les actions batch

object_actions

Dans la liste, il y a une colonne supplémentaire pour les actions que vous pouvez exécuter sur un seul objet. Pour le module category, retirons les car nous avons un lien sur le nom de catégorie pour la modifier, et nous n'avons pas vraiment besoin d'être en mesure d'en supprimer une directement dans la liste :

# apps/backend/modules/category/config/generator.yml
config:
  list:
    object_actions: {}

Pour le module job, gardons les actions existantes et ajoutons un nouvel extend d'une action semblable à celle que nous avons ajouté pour les actons batch :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    object_actions:
      extend:     ~
      _edit:      ~
      _delete:    ~

Quant aux actions batch, les actions _delete et _edit sont celles définies par le framework. Nous avons besoin de définir l'action listExtend() pour faire fonctionner le lien extend :

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListExtend(sfWebRequest $request)
  {
    $job = $this->getRoute()->getObject();
    $job->extend(true);
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('@jobeet_job_job');
  }
 
  // ...
}

Personnalise l'action d'un objet

actions

Nous avons déjà vu comment lier une action à une liste d'objets ou à un objet unique. L'option actions définit les actions qui ne prennent pas d'objet du tout, comme la création d'un nouvel objet. Enlevons l'action par défaut new et ajoutons une nouvelle action, qui supprime tous les emplois qui n'ont pas été activés par l'employeur pendant plus de 60 jours :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    actions:
      deleteNeverActivated: { label: Delete never activated jobs }

Jusqu'à présent, toutes les actions que nous avons définies ont ~, ce qui signifie que symfony configure automatiquement l'action. Chaque action peut être personnalisée en définissant un tableau de paramètres. L'option label substitue le label par défaut généré par symfony.

Par défaut, l'action exécutée lorsque vous cliquez sur le lien est le nom de l'action préfixée par list.

Créez l'action listDeleteNeverActivated dans le module job :

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListDeleteNeverActivated(sfWebRequest $request)
  {
    $nb = Doctrine::getTable('JobeetJob')->cleanup(60);
 
    if ($nb)
    {
      $this->getUser()->setFlash('notice', sprintf('%d never activated jobs have been deleted successfully.', $nb));
    }
    else
    {
      $this->getUser()->setFlash('notice', 'No job to delete.');
    }
 
    $this->redirect('@jobeet_job_job');
  }
 
  // ...
}

Nous avons réutilisé la méthode JobeetJobTable::cleanup() définie hier. C'est un autre excellent exemple de la réutilisation fournies par le modèle MVC.

note

Vous pouvez également modifier l'action à exécuter en passant le paramètre action :

deleteNeverActivated: { label: Delete never activated jobs, action: foo }

Actions

table_method

Le nombre de requêtes nécessaires vers la base de données pour afficher la page de la liste des emplois est de 14, comme illustré par le web debug toolbar.

Si vous cliquez sur le nombre, vous verrez que la plupart des requêtes sont utilisées pour récupérer le nom de la catégorie pour chaque emploi :

Nombre de requête avant

Pour réduire le nombre de requête, nous pouvons changer la méthode par défaut utilisé pour récupérer les emplois en utilisant l'option table_method :

# apps/backend/modules/job/config/generator.yml
config:
  list:
    table_method: retrieveBackendJobList

Maintenant vous devez créer la méthode retrieveBackendJobList dans JobeetJobTable situé dans lib/model/doctrine/JobeetJobTable.class.php.

// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  public function retrieveBackendJobList(Doctrine_Query $q)
  {
    $rootAlias = $q->getRootAlias();
    $q->leftJoin($rootAlias . '.JobeetCategory c');
    return $q;
  }
 
  // ...

La méthode retrieveBackendJobList() ajoute une jointure entre les tables job et category et crée automatiquement l'objet catégorie lié à chaque emploi.

Le nombre de requêtes est maintenant réduit à quatre:

Nombre de requête après

Configuration des vues du formulaire

La configuration des vues du formulaire se fait dans trois sections : form, edit et new. Elles ont tous les mêmes capacités de configuration et la section form existe seulement en tant que solution de repli pour les sections edit et new.

display

Comme pour la liste, vous pouvez changer l'ordre des champs affichés avec l'option display. Mais comme le formulaire affiché est définie par une classe, n'essayez pas de supprimer un champ car cela pourrait conduire à des erreurs inattendues.

L'option display pour les vues du formulaire peut également être utilisé pour organiser des champs en groupes :

# apps/backend/modules/job/config/generator.yml
config:
  form:
    display:
      Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email]
      Admin:   [_generated_token, is_activated, expires_at]

La configuration ci-dessus définit deux groupes (Content et Admin), contenant chacune un sous-ensemble de champs du formulaire.

Regroupement des champs

note

Les colonnes dans le groupe Admin n'apparaissent pas dans le navigateur pour le moment car elles ont été mis hors d'usage dans la définition du formulaire emploi. Elles apparaîtront dans quelques sections où nous aurons défini une classe de formulaire d'emplois personnalisée pour l'application admin.

L'admin generator a un support intégré pour la relation plusieurs vers plusieurs. Sur le formulaire de catégorie, vous disposez d'une entrée pour le nom, d'une pour le slug et une liste déroulante pour les affiliés connexes. Comme cela n'a pas de sens de modifier cette relation sur cette page, nous allons la supprimer :

// lib/form/doctrine/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['created_at'], $this['updated_at'], $this['jobeet_affiliates_list']);
  }
}

Les colonnes "Virtuelles"

Dans l'option display pour le formulaire d'emplois, le _generated_token commence par un caractère de soulignement (_). Cela signifie que le rendu de ce champ sera traitée par un partial personnalisé nommé _generated_token.php.

Créer ce partial avec le contenu suivant :

// apps/backend/modules/job/templates/_generated_token.php
<div class="sf_admin_form_row">
  <label>Token</label>
  <?php echo $form->getObject()->getToken() ?>
</div>

Dans le partial, vous avez accès au formulaire actuel ($form) et l'objet connexe est accessible via la méthode getObject().

note

Vous pouvez aussi déléguer le rendu d'un composant en faisant précéder le nom du champ par un tilde (~).

class

Comme le formulaire sera utilisé par les administrateurs, nous avons affiché plus d'informations que pour le formulaire emploi de l'utilisateur. Mais pour l'instant, certains d'entre eux ne figurent pas sur le formulaire car ils l'ont été enlevés dans la classe JobeetJobForm.

Pour avoir des formulaires différents pour le frontend et le backend, nous avons besoin de créer deux classes de formulaire. Nous allons créer une classe BackendJobeetJobForm qui étend la classe JobeetJobForm. Comme nous n'aurons pas les mêmes champs cachés, nous devons aussi un peu refactoriser la classe JobeetJobForm pour déplacer l'instruction unset() dans une méthode qui substituera dans BackendJobeetJobForm :

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    $this->removeFields();
 
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $this->validatorSchema['email'],
      new sfValidatorEmail(),
    ));
 
    // ...
  }
 
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated'],
      $this['token']
    );
  }
}
 
// lib/form/doctrine/BackendJobeetJobForm.class.php
class BackendJobeetJobForm extends JobeetJobForm
{
  public function configure()
  {
    parent::configure();
  }
 
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['token']
    );
  }
}

La classe du formulaire par défaut utilisé par l'admin generator peut être surchargée en définissant l'option class :

# apps/backend/modules/job/config/generator.yml
config:
  form:
    class: BackendJobeetJobForm

note

Comme nous avons ajouté une nouvelle classe, n'oubliez pas de vider le cache.

Le formulaire edit a toujours un petit ennui. Le logo de téléchargement ne s'affiche pas partout et vous ne pouvez pas supprimer l'actuel. Le widget sfWidgetFormInputFileEditable ajoute des possibilités d'édition à une simple saisie de fichier :

// lib/form/doctrine/BackendJobeetJobForm.class.php
class BackendJobeetJobForm extends JobeetJobForm
{
  public function configure()
  {
    parent::configure();
 
    $this->widgetSchema['logo'] = new sfWidgetFormInputFileEditable(array(
      'label'     => 'Company logo',
      'file_src'  => '/uploads/jobs/'.$this->getObject()->getLogo(),
      'is_image'  => true,
      'edit_mode' => !$this->isNew(),
      'template'  => '<div>%file%<br />%input%<br />%delete% %delete_label%</div>',
    ));
 
    $this->validatorSchema['logo_delete'] = new sfValidatorPass();
  }
 
  // ...
}

Le widget sfWidgetFormInputFileEditable a plusieurs options pour peaufiner ses caractéristiques et le rendu :

  • file_src: Le chemin web pour télécharger le fichier
  • is_image: Si true, le fichier sera rendu comme une image
  • edit_mode: Si le formulaire est en mode édition ou non
  • with_delete: S'il faut afficher la case à cocher pour supprimer
  • template: Le Template à utiliser pour rendre le widget

Téléchargement de fichier

tip

Le look de l'admin generator peut être modifié très facilement car les Templates générés définissent beaucoup d'attributs class et id. Par exemple, le champ du logo peut être personnalisé en utilisant la classe sf_admin_form_field_logo. Chaque champ a également une classe en fonction du type du champ, comme sf_admin_text ou sf_admin_boolean.

L'option edit_mode utilise la méthode sfDoctrineRecord::isNew().

Elle retourne true si l'objet du modèle du formulaire est nouveau, sinon false. Ceci est d'une grande aide lorsque vous avez besoin d'avoir différents widgets ou validateurs en fonction du statut de l'objet incorporé.

Configuration des filtres

La configuration des filtres est tout à fait la même que la configuration des vues du formulaire. En fait, les filtres ne sont que des formulaires. Et comme pour les formulaires, les classes ont été générés par la tâche doctrine:build --all. Vous pouvez également les re-générer avec la tâche doctrine:build --filters.

Les classes de filtre de formulaire sont situées sous le répertoire lib/filter/ et chaque classe du modèle est associée à une classe de filtre de formulaire (JobeetJobFormFilter pour JobeetJobForm).

Supprimons-les complètement pour le module category :

# apps/backend/modules/category/config/generator.yml
config:
  filter:
    class: false

Pour le module job, nous allons supprimer certains d'entre eux :

# apps/backend/modules/job/config/generator.yml
filter:
  display: [category_id, company, position, description, is_activated, is_public, email, expires_at]

Comme les filtres sont toujours facultatifs, il n'y a pas besoin de surcharger la classe du formulaire de filtre pour configurer les champs à afficher.

Filtres

Personnalisation des actions

Lorsque la configuration n'est pas suffisante, vous pouvez ajouter de nouvelles méthodes pour la classe de l'action comme nous l'avons vu avec la fonctionnalité d'extension, mais vous pouvez aussi surcharger les méthodes de l'action générées :

Méthode Description
executeIndex() Action de la vue list
executeFilter() Mettre à jour les filtres
executeNew() Action de la vue new
executeCreate() Créer un nouvel emploi
executeEdit() Action de la vue edit
executeUpdate() Mettre à jour un emploi
executeDelete() Supprimer un emploi
executeBatch() Executer une action batch
executeBatchDelete() Executer l'action batch _delete
processForm() Processer le formulaire emploi
getFilters() Retourner le filtre actuel
setFilters() Définir le filtre
getPager() Retourner la pagination de la liste
getPage() Obtenir la page de la pagination
setPage() Définir la page de la pagination
buildCriteria() Construire le Criteria pour la liste
addSortCriteria() Ajouter le tri Criteria pour la liste
getSort() Retourner la colonne triée actuelle
setSort() Définit la colonne triée actuelle

Comme chaque méthode générée ne fait qu'une chose, il est facile de changer un comportement sans avoir à copier et coller trop de code.

Personnalisation des Templates

Nous avons vu comment personnaliser les templates générés grâce aux attributs class et id ajoutés par l'admin generator dans le code HTML.

Quant aux classes, vous pouvez également remplacer les Templates originaux. Comme les Templates sont des simples fichiers PHP et non des classes PHP, un Template peut être substituée en créant un Template du même nom dans le module (par exemple dans le répertoire apps/backend/modules/job/templates/ pour le module de l'admin job) :

Template Description
_assets.php Rendre lees CSS et les JS pour les utiliser dans les Templates
_filters.php Rendre la zone des filtres
_filters_field.php Rendre un seul champ du filtre
_flashes.php Rendre les messages flash
_form.php Afficher le formulaire
_form_actions.php Afficher les actions du formulaire
_form_field.php Afficher un seul champ du formulaire
_form_fieldset.php Afficher un jeu de champs du formulaire
_form_footer.php Afficher le formulaire pied de page
_form_header.php Afficher le formulaire d'entête
_list.php Afficher la liste
_list_actions.php Afficher les actions de la liste
_list_batch_actions.php Afficher les actions batch de la liste
_list_field_boolean.php Afficher un seul champ booléen dans la liste
_list_footer.php Afficher le pied de page de la liste
_list_header.php Afficher l'entête de la liste
_list_td_actions.php Afficher les actions d'un objet pour une ligne
_list_td_batch_actions.php Afficher le checkbox pour une ligne
_list_td_stacked.php Afficher le layout stacked pour une ligne
_list_td_tabular.php Afficher un seul champ pour la liste
_list_th_stacked.php Afficher un seul nom de colonne pour l'entête
_list_th_tabular.php Afficher un seul nom de colonne pour l'entête
_pagination.php Afficher la pagination de la liste
editSuccess.php Afficher la vue edit
indexSuccess.php Afficher la vue list
newSuccess.php Afficher la vue new

Configuration finale

La configuration finale pour l'admin de Jobeet admin se présente comme suit :

# apps/backend/modules/job/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           JobeetJob
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_job
    with_doctrine_route:   1
 
    config:
      actions: ~
      fields:
        is_activated: { label: Activated?, help: Whether the user has activated the job, or not }
        is_public:    { label: Public? }
      list:
        title:         Job Management
        layout:        stacked
        display:       [company, position, location, url, is_activated, email]
        params:  |
          %%is_activated%% <small>%%JobeetCategory%%</small> - %%company%%
           (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)
        max_per_page:  10
        sort:          [expires_at, desc]
        batch_actions:
          _delete:    ~
          extend:     ~
        object_actions:
          extend:     ~
          _edit:      ~
          _delete:    ~
        actions:
          deleteNeverActivated: { label: Delete never activated jobs }
        table_method: retrieveBackendJobList
      filter:
        display: [category_id, company, position, description, is_activated, is_public, email, expires_at]
      form:
        class:     BackendJobeetJobForm
        display:
          Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email]
          Admin:   [_generated_token, is_activated, expires_at]
      edit:
        title: Editing Job "%%company%% is looking for a %%position%%"
      new:
        title: Job Creation
 
# apps/backend/modules/category/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           JobeetCategory
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_category
    with_doctrine_route:   1
 
    config:
      actions: ~
      fields:  ~
      list:
        title:   Category Management
        display: [=name, slug]
        batch_actions: {}
        object_actions: {}
      filter:
        class: false
      form:
        actions:
          _delete: ~
          _list:   ~
          _save:   ~
      edit:
        title: Editing Category "%%name%%"
      new:
        title: New Category

Avec seulement ces deux fichiers de configuration, nous avons développé une interface backend idéal pour Jobeet en quelques minutes.

tip

Vous savez déjà que lorsque quelque chose est configurable dans un fichier YAML, il y a aussi la possibilité d'utiliser du code PHP. Pour l'admin generator, vous pouvez modifier le fichier apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php. Il vous donne les mêmes options que le fichier YAML mais avec une interface PHP. Pour apprendre les noms des méthodes, jetez un oeil à la classe de base générée en cache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.class.php.

À demain

En une heure seulement, nous avons construit entièrement une interface backend pour le projet Jobeet. Et dans l'ensemble, nous avons écrit au plus 50 lignes de code PHP. Pas trop mal pour de nombreuses fonctionnalités !

Demain, nous allons voir comment sécuriser l'application backend avec un identifiant et un mot de passe. Ce sera également l'occasion de parler de la classe user de symfony.