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

Giorno 12: Admin Generator

1.4 / Propel
Symfony version
1.2
Language ORM

Con l'aggiunta fatta ieri su Jobeet, l'applicazione frontend è ora totalmente utilizzabile da chi cerca e da chi offre lavoro. È giunto il momento di parlare un po' dell'applicazione backend.

Oggi, grazie alla funzionalità di symfony rappresentata dall'admin generator, svilupperemo un'interfaccia di backend completa per Jobeet in una sola ora.

Creazione del Backend

Il primissimo passo è creare l'applicazione backend. Se la vostra memoria funziona bene, dovreste ricordare come farlo con il task generate:app:

$ php symfony generate:app backend

Anche se l'applicazione backend sarà usata solamente dagli amministratori di Jobeet, abbiamo abilitato tutte le feature integrate in symfony per la sicurezza.

tip

Se si vogliono usare caratteri speciali nella password, come ad esempio il simbolo del dollaro ($), serve un escape appropriato nella linea di comando:

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

L'applicazione backend è ora disponibile all'indirizzo http://www.jobeet.com.localhost/backend.php per l'ambiente prod e all'indirizzo http://www.jobeet.com.localhost/backend_dev.php per l'ambiente dev.

note

Quando avete creato l'applicazione frontend il front controller di produzione è stato chiamato index.php. Siccome si può avere un solo file index.php per cartella, symfony crea un file index.php per il primo front controller di produzione e chiama gli altri con il nome dell'applicazione.

Se provate a ricaricare i dati delle fixture con il task propel:data-load, non funzionerà ancora. Questo è dovuto al fatto che il metodo JobeetJob::save() necessita di accedere al file di configurazione app.yml dall'applicazione frontend. Visto che abbiamo ora due applicazioni, symfony utilizza la prima che trova, che attualmente è quella di backend`.

Ma abbiamo visto nel giorno 8 che le impostazioni possono essere configurate a diversi livelli. Spostando il contenuto del file apps/frontend/config/app.yml in config/app.yml, le impostazioni saranno condivise da tutte le applicazioni e il problema sarà risolto. Fate questo cambiamento ora, visto che useremo le classi del modello molto spesso nell'admin generator, quindi avremo bisogno delle variabili definite in app.yml nell'applicazione backend.

tip

Il task propel:data-load considera anche l'opzione --application. Quindi, se avete bisogno di qualche impostazione particolare da un'applicazione piuttosto che un'altra, la strada da usare è la seguente:

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

Moduli di Backend

Per l'applicazione di frontend il task propel:generate-module è stato utilizzato per creare un semplice modulo CRUD basato sul modello di una classe. Per il backend il task propel:generate-admin verrà usato per generare un'interfaccia completamente funzionante per il modello di una classe:

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

Questi due comandi creano un modulo job e un modulo category per i modelli JobeetJob e JobeetCategory.

Dietro le quinte il task ha creato anche una rotta personalizzata per ogni modulo:

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

Non dovrebbe sorprendere che la classe usata per la rotta dall'admin generator sia sfPropelRouteCollection dato che il principale obiettivo dell'interfaccia di admin è la gestione del ciclo di vita degli oggetti del modello.

La definizione della rotta definisce inoltre alcune opzioni che non abbiamo visto prima:

  • prefix_path: Definisce il prefisso al path per le rotte generate (per esempio la pagina dell'edit sarà qualcosa come /job/1/edit).
  • column: Definisce la colonna della tabella da utilizzare negli URL per i link che fanno riferimento a un oggetto.
  • with_wildcard_routes: Dato che l'interfaccia principale avrà più che le classiche operazioni di CRUD, questa opzione permette di definire ulteriori azioni senza modificare la rotta.

tip

Come sempre è una buona idea leggere l'help prima di usare un nuovo task.

$ php symfony help propel:generate-admin

Vi fornirà tutti gli parametri e le opzioni del task, oltre ad alcuni classici esempi di utilizzo.

Aspetto del Backend

Immediatamente potete usare i moduli generati:

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

I moduli di amministrazione hanno molte più feature di quelli più semplici generati nei giorni scorsi. Senza scrivere una riga di PHP, ogni modulo mette a disposizione queste grandi funzionalità:

  • La lista degli oggetti è paginata
  • La lista è ordinabile
  • La lista può essere filtrata
  • Gli oggetti possono essere creati, modificati e cancellati
  • Gli oggetti selezionati possono essere cancellati in modo batch
  • La validazione dei form è abilitata
  • Messaggi rapidi danno feedback immediati all'utente
  • ...e molto altro

L'admin generator offre tutte le funzionalità di cui avete bisogno per creare un'interfaccia di backend semplice da configurare.

If you have a look at our two generated modules, you will notice there is no activated webdesign whereas the symfony built-in admin generator feature has a basic graphic interface by default. For now, assets from the sfPropelPlugin are not located under the web/ folder. We need to publish them under the web/ folder thanks to the plugin:publish-assets task:

$ php symfony plugin:publish-assets

Per rendere migliore la user experience, il layout di default del backend può essere personalizzato. Abbiamo inoltre aggiunto un semplice menù per rendere più semplice la navigazione tra i differenti moduli

Rimpiazzate il contenuto del file di default layout.php con il seguente:

// 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') ?>
          </li>
          <li>
            <?php echo link_to('Categories', 'jobeet_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>

Questo layout usa un foglio di stile admin.css. Questo file deve essere già presente in web/css/, perché installato con gli altri fogli di stile durante il giorno 4.

aspetto admin generator

Alla fine cambiate l'homepage di default nel file routing.yml:

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

La Cache di symfony

Se siete abbastanza curiosi, avrete probabilmente già aperto i file generati dai task che trovate nella cartella apps/backend/modules/. Se non l'avete fatto, apriteli ora. Sorpresa! Le cartelle dei templates sono vuote e i file actions.class.php sono anch'essi quasi vuoti:

// 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
{
}

Come fa a funzionare? Se guardate più attentamente, noterete che la classe jobActions estende autoJobActions. La classe autoJobActions è generata automaticamente da symfony, se non esiste. La trovate nella cartella cache/backend/dev/modules/autoJob/, che contiene il "vero" modulo:

// 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())
    ))
    {
 
// ...

Il modo di lavorare dell'admin generator dovrebbe farvi tornare in mente qualcosa di già visto. Infatti è abbastanza simile a quanto abbiamo imparato sul modello e sulle classi dei form. Basandosi sullo schema del modello, symfony genera le classi del modello e dei form. Per l'admin generator i moduli generati possono essere configurati modificando il file config/generator.yml che trovate nel modulo:

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

Ogni volta che aggiornate il file generator.yml, symfony rigenera la cache. Come vedremo oggi, personalizzare i moduli generati dall'admin generator è facile, veloce e divertente.

note

La rigenerazione automatica dei file di cache avviene solamente nell'ambiente di sviluppo. In quello di produzione avrete bisogno di ripulire la cache manualmente con il task cache:clear.

Configurazione del Backend

Un modulo di amministrazione può essere personalizzato modificando il valore di config del file generator.yml. La configurazione è organizzata in sette sezioni:

  • actions: Configurazione di default per le azioni trovate nella lista e nei form
  • fields: Configurazione di default per i campi
  • list: Configurazione per la lista
  • filter: Configurazione per i filtri
  • form: Configurazione per il form new/edit
  • edit: Configurazioni specifiche per la pagina di modifica
  • new: Configurazioni specifiche per la pagina di creazione

Iniziamo la personalizzazione.

Configurazione del titolo

I titoli delle sezioni list, edit e new del modulo category possono essere personalizzati definendo l'opzione title:

config:
  actions: ~
  fields:  ~
  list:
    title: Category Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Category "%%name%%" (#%%id%%)
  new:
    title: New Category

Il title per la sezione edit contiene valori dinamici: tutte le stringhe racchiuse tra %% sono rimpiazzate dai valori delle colonne dell'oggetto corrispondenti.

Titoli

La configurazione per il modulo job è simile:

config:
  actions: ~
  fields:  ~
  list:
    title: Job Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Job "%%company%% is looking for a %%position%% (#%%id%%)"
  new:
    title: Job Creation

Configurazione Campi

Le viste sono composte da campi. Un campo può essere una colonna di un modello, o una colonna virtuale, come vedremo in seguito.

La configurazione dei campi di default può essere personalizzata nella sezione fields:

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 }

Configurazione dei campi

La sezione fields sovrascrive la configurazione dei campi per tutte le viste, ciò significa che la label per il campo is_activated sarà cambiata per le viste list, edit e new.

La configurazione dell'admin generator è basata su un principio di configurazione a cascata. Per esempio, se si vuol cambiare una label solo per la vista list, definire un'opzione fields sotto la sezione list:

config:
  list:
    fields:
      is_public:    { label: "Public? (label for the list)" }

Ogni configurazione definita sotto la sezione fields principale può essere sovrascritta da configurazioni specifiche per ogni vista. Le regole di sovrascrittura sono le seguenti:

  • new e edit ereditano da form che eredita da fields
  • list eredita da fields
  • filter eredita da fields

note

Per le sezioni del form (form, edit e new), le opzioni label e help sovrascrivono quelle definite nelle classi dei form.

Configurazione della lista

display

Di default, le colonne della vista della lista sono tutte le colonne del Modello, nello stesso ordine del file di schema. L'opzione display sovrascrive quella di default, definendo le colonne da visualizzare ed il loro ordine:

config:
  list:
    title:   Category Management
    display: [=name, slug]

Il segno = prima della colonna name è una convenzione per convertire la stringa in un link.

Tabella della lista

Facciamo lo stesso per rendere il modulo job più leggibile:

config:
  list:
    title:   Job Management
    display: [company, position, location, url, is_activated, email]

layout

La lista può venir visualizzata da differenti layout. Di default, il layout è tabular, ciò significa che il valore di ogni colonna è nella corrispondente colonna della tabella. Ma per il modulo job, sarebbe meglio usare il layout stacked, che è l'altro layout a disposizione:

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%%)

Nel layout stacked, ogni oggetto è rappresentato da una singola stringa, che è definita dall'opzione params.

note

L'opzione display è ancora necessaria, dato che definisce le colonne che saranno ordinabili nella vista.

Colonne "Virtuali"

Con questa configurazione, il segmento %%category_id%% sarà rimpiazzato dalla chiave primaria della categoria. Ma sarebbe molto più significativo visualizzare il nome della categoria.

Utilizzando la notazione %%, la variabile non deve necessariamente corrispondere a una colonna dello schema del database. L'admin generator ha semplicemente bisogno di trovare il relativo getter nella classe modello.

Per visualizzare il nome della categoria, possiamo definire il metodo getCategoryName() nel modello JobeetJob e rimpiazzare %%category_id%% con %%category_name%%.

Ma la classe JobeetJob ha già il metodo getJobeetCategory() che restituisce il relativo oggetto delle categoria. E se usate %%jobeet_category%%, funzionerà dato che la classe JobeetCategory ha il metodo magico __toString(), che converte l'oggetto in una stringa.

%%is_activated%% <small>%%jobeet_category%%</small> - %%company%%
 (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

Layout impilato

sort

Come amministratore, sarà probabilmente più interessante vedere gli ultimi lavori inseriti. Si può configurare l'ordinamento di default con l'opzione sort:

config:
  list:
    sort: [expires_at, desc]

max_per_page

Di default, la lista è paginata e ogni pagina contiene 20 oggetti. Questo può essere modificato con l'opzione max_per_page:

config:
  list:
    max_per_page: 10

Max per pagina

batch_actions

Su una lista, un'azione può venir eseguita su alcuni oggetti. Queste azioni batch non sono necessarie per il modulo category, così rimuoviamole:

config:
  list:
    batch_actions: {}

Rimuovere le azioni batch

L'opzione batch_actions definisce la lista di azioni batch. Un array vuoto rimuoverà questa opzione.

Di default, ogni modulo ha un'azione delete definita da framework, ma per il modulo job fingiamo di avere un modo di estendere la validità di alcuni lavori per altri 30 giorni:

config:
  list:
    batch_actions:
      _delete:    ~
      extend:     ~

Tutte le azioni inizianti con _ sono azioni fornite dal framework. Se si aggiorna la pagina del browser e si seleziona l'azione extend, symfony genererà un'eccezione dicendo di creare il metodo executeBatchExtend():

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeBatchExtend(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
 
    $criteria = new Criteria();
    $criteria->add('jobeet_job.ID', $ids, Criteria::IN);
 
    foreach (JobeetJobPeer::doSelect($criteria) as $job)
    {
      $job->extend(true);
      $job->save();
    }
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('jobeet_job');
  }
}

La chiave primaria selezionata sarà immagazzinata nel parametro ids. Per ogni lavoro selezionato, il metodo JobeetJob::extend() sarà chiamato con un parametro ulteriore per aggirare alcuni controlli nel metodo. Abbiamo bisogno di aggiornare il metodo extend() per utilizzare questo nuovo parametro:

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

Dopo che tutti i lavori saranno stati estesi, l'utente sarà reindirizzato all'homepage del modulo job.

Azioni batch personalizzate

object_actions

Nella lista, c'è una colonna in più per le azioni che possono essere eseguite su un singolo oggetto. Rimuoviamole per il modulo category, perché abbiamo già un link sul nome della categoria per modificarla e non ci serve cancellarne una direttamente dalla lista:

config:
  list:
    object_actions: {}

Per il modulo job, teniamo le azioni esistenti e aggiungiamo una nuova azione extend, simile a quella che abbiamo aggiunto come azione di batch:

config:
  list:
    object_actions:
      extend:     ~
      _edit:      ~
      _delete:    ~

Come per le azioni di batch, le azioni _delete e _edit sono quelle definite nel framework. Dobbiamo definire l'azione listExtend() per far funzionare il link 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);
    $job->save();
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('jobeet_job');
  }
 
  // ...
}

Azioni personalizzate

actions

Abbiamo già visto come collegare un'azione a una lista di oggetti o a un singolo oggetto. L'opzione actions definisce delle azioni che non interessano nessun oggetto, come la creazione di un oggetto nuovo. Rimuoviamo l'azione new e aggiungiamo una nuova azione che cancelli tutti i lavori che non sono stati attivati dal relativo utente per oltre 60 giorni:

config:
  list:
    actions:
      deleteNeverActivated: { label: Delete never activated jobs }

Finora, tutte le azioni che abbiamo definito avevano un ~, che vuol dire che symfony configura automaticamente l'azione. Ogni azione può essere personalizzata definendo un array di parametri. L'opzione label sovrascrive la label generata da symfony.

Di default, l'azione eseguita al click sul link è il nome dell'azione preceduto da list.

Creiamo l'azione listDeleteNeverActivated nel modulo job:

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListDeleteNeverActivated(sfWebRequest $request)
  {
    $nb = JobeetJobPeer::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');
  }
 
  // ...
}

Abbiamo riutilizzato il metodo JobeetJobPeer::cleanup() definito ieri. Questo è un altro grande esempio della riusabilità fornita dal pattern MVC.

note

Si può anche cambiare l'azione da eseguire passando un parametro action:

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

Azioni

peer_method

Il numero di richieste al database necessarie per mostrare la pagina della lista dei lavori è 14, come mostrato dalla web debug toolbar.

Se si clicca sul numero, si vedrà che la maggior parte delle richieste è per recuperare il nome della categoria per ogni lavoro.

Numero di richieste prima

Per ridurre il numero di richieste, possiamo cambiare il metodo di default usato per recuperare i lavori, usando l'opzione peer_method:

config:
  list:
    peer_method: doSelectJoinJobeetCategory

Il metodo doSelectJoinJobeetCategory() aggiunge una join tra le tabelle job e category e crea automaticamente l'oggetto categoria associato a ogni lavoro.

Il numero di richieste ora è ridotto a quattro:

Numero di richieste dopo

Configurazione delle viste del form

La configurazione delle viste del form si esegue in tre sezioni: form, edit, e new. Hanno tutte la stessa capacità di configurazione e la sezione form esiste solo come ripiego per le sezioni edit e new.

display

Come per la lista, si può cambiare l'ordine dei campi mostrati, con l'opzione display. Ma siccome il form mostrato è definito da una classe, non provate a rimuovere un campo, perché potrebbe portare a errori di validazione inattesi.

L'opzione display per le viste del form può anche essere usata per raggruppare i campi:

config:
  form:
    display:
      Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email]
      Admin:   [_token, is_activated, expires_at]

La configurazione qui sopra definisce due gruppi (Content e Admin), ciascuno dei quali contiene un sottoinsieme dei campi del form.

Gruppi di campi

L'admin generator ha un supporto incluso per le relazioni molti a molti. Nel form della categoria, si ha un input per il nome, uno per lo slug e un menù a tendina per gli affiliati correlati. Siccome non ha senso modificare tale relazione in questa pagina, rimuoviamolo:

// lib/form/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['jobeet_category_affiliate_list']);
  }
}

Colonne "Virtuali"

Nelle opzioni display per la form del lavoro, il campo _generated_token inizia con un trattino basso (_). Questo significa che la resa per questo campo è gestita da un partial personalizzato chiamato _generated_token.php:

Creiamo questo partial con il seguente contenuto:

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

Nel partial si ha accesso al form corrente ($form) e agli oggetti relativi, tramite il metodo getObject().

note

È possibile delegare la resa di un componente anteponendo al nome del campo una tilde (~).

class

Dato che il form sarà utilizzato dagli amministratori, abbiamo visualizzato più informazioni rispetto al form dei lavori utilizzato dagli utenti. Ma per il momento alcuni di questi non appaiono nel form dato che sono stati rimossi dalla classe JobeetJobForm.

Per avere dei form differenti per il frontend e il backend, dobbiamo creare due classi form differenti. Creiamo la classe BackendJobeetJobForm che estende la classe JobeetJobForm. Dato che non vogliamo avere gli stessi campi nascosti, dobbiamo rifattorizzare la classe JobeetJobForm per spostare la dichiarazione di unset() in un metodo che sarà ridefinito in BackendJobeetJobForm:

// lib/form/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['token'],
      $this['is_activated']
    );
  }
}
 
// lib/form/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 form di default usata dall'admin generator può essere sovrascritta settando l'opzione class:

config:
  form:
    class: BackendJobeetJobForm

Il form edit ha ancora qualche piccola seccatura. Il logo caricato non appare da nessuna parte e non è possibile rimuoverlo. Il widget sfWidgetFormInputFileEditable aggiunge la possibilità di editare un semplice widget per il caricamento di file:

// lib/form/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>',
    ));
  }
 
  // ...
}

Il widget sfWidgetFormInputFileEditable accetta diverse opzioni per modificare le sue caratteristiche e la sua resa:

  • file_src: Il percorso web del file caricato
  • is_image: Se true, il file verrà reso come un'immagine
  • edit_mode: Se il form è in modalità di modifica o meno
  • with_delete: Se visualizzare il checkbox per la cancellazione
  • template: Il template da usare per rendere il widget

Invio di file

tip

L'aspetto dell'admin generator può essere modificato in modo veramente facile, dato che il template generato contiene molti attributi class e id. Per esempio, il campo del logo può esser modificato utilizzando la classe sf_admin_form_field_logo. Ogni campo ha una classe che dipende dal tipo di campo, come sf_admin_text o sf_admin_boolean.

L'opzione edit_mode utilizza il metodo sfPropel::isNew().

Il metodo restituisce true se l'oggetto del modello del form è nuovo, false altrimenti. Questo è un grosso aiuto quando si ha la necessità di avere widget o validatori differenti che dipendono dallo stato degli oggetti inclusi.

Configurazione dei filtri

Configurare i filtri è praticamente identico al configurare le viste per i form. Come dato di fatto, i filtri sono esattamente dei form. E come per i form, le relative classi sono state generate dal task propel:build --all. I filtri possono essere rigenerati con il task propel:build --filters.

Le classi dei filtri per i form sono posizionate nella cartella lib/filter e a ogni classe del modello è associata una classe filtro per il form (JobeetJobFormFilter per JobeetJobForm).

Rimuoviamole completamente per il modulo category:

config:
  filter:
    class: false

Per il modulo job, rimuoviamone alcune:

filter:
  display: [category_id, company, position, description, is_activated,
   ‚û• is_public, email, expires_at]

Dato che i filtri sono sempre opzionali non c'è nessuna necessità di sovrascrivere le classi dei filtri per i form per configurare quali campi devono essere visualizzati.

Filtri

Personalizzazione delle azioni

Quando la configurazione non è sufficiente, è possibile aggiungere nuovi metodi alle classi delle azioni come abbiamo visto con le caratteristiche estese, ma è anche possibile sovrascrivere i metodi generati:

Metodo Descrizione
executeIndex() azione della vista list
executeFilter() Aggiorna i filtri
executeNew() azione della vista new
executeCreate() Crea un nuovo lavoro
executeEdit() azione della vista edit
executeUpdate() Aggiorna un lavoro
executeDelete() Cancella un lavoro
executeBatch() Esegue un'azione batch
executeBatchDelete() Esegue l'azione batch _delete
processForm() Processa il form Job
getFilters() Restituisce i filtri correnti
setFilters() Imposta i filtri
getPager() Restituisce la lista del paginatore
getPage() Restituisce la pagina del paginatore
setPage() Imposta la pagina del paginatore
buildCriteria() Costruisce i Criteria per la lista
addSortCriteria() Aggiunge i Criteria di ordinamento per la lista
getSort() Restituisce la colonna di ordinamento corrente
setSort() Imposta la colonna di ordinamento corrente

Siccome ogni metodo generato esegue solo una cosa, è semplice cambiarne un comportamento senza dover copiare e incollare troppo codice.

Personalizzazione dei Template

Abbiamo visto come sia possibile personalizzare i template generati grazie agli attributi class e id aggiunti nel codice HTML dall'admin generator.

Come per le classi, è anche possibile sovrascrivere i template originali. Dato che i template sono semplici file PHP e non classi PHP, un template può essere sovrascritto creando nel modulo un template con lo stesso nome (per esempio nella cartella apps/backend/modules/job/templates/ per il modulo di amministrazione job):

Template Descrizione
_assets.php Rende i CSS e JS da usare per i template
_filters.php Rende i riquadri dei filtri
_filters_field.php Rende un singolo filtro del campo
_flashes.php Rende i messaggi flash
_form.php Visualizza il form
_form_actions.php Visualizza le azioni dei form
_form_field.php Visualizza un singolo campo del form
_form_fieldset.php Visualizza un fieldset di un form
_form_footer.php Visualizza il footer del form
_form_header.php Visualizza l'header del form
_list.php Visualizza la lista
_list_actions.php Visualizza le azioni della lista
_list_batch_actions.php Visualizza le azioni batch della lista
_list_field_boolean.php Visualizza un singolo campo booleano nella lista
_list_footer.php Visualizza il footer della lista
_list_header.php Visualizza l'header della lista
_list_td_actions.php Visualizza le azioni dell'oggetto per una riga
_list_td_batch_actions.php Visualizza i checkbox per una riga
_list_td_stacked.php Visualizza il layout impilato per una riga
_list_td_tabular.php Visualizza un singolo campo per la lista
_list_th_stacked.php Visualizza il nome della singola colonna per l'header
_list_th_tabular.php Visualizza il nome della singola colonna per l'header
_pagination.php Visualizza la paginazione della lista
editSuccess.php Visualizza la vista edit
indexSuccess.php Visualizza la vista list
newSuccess.php Visualizza la vista new

Configurazioni finali

Le configurazioni finali per l'admin di jobeet sono le seguenti:

# apps/backend/modules/job/config/generator.yml
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>%%jobeet_category%%</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 }
    peer_method:  doSelectJoinJobeetCategory
  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:   [_token, is_activated, expires_at]
  edit:
    title: Editing Job "%%company%% is looking for a %%position%% (#%%id%%)"
  new:
    title: Job Creation
 
# apps/backend/modules/category/config/generator.yml
generator:
  class: sfPropelGenerator
  param:
    model_class:           JobeetCategory
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_category
    with_propel_route:     true
 
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%%" (#%%id%%)
  new:
    title: New Category

Con solo questi due file di configurazione, in una manciata di minuti abbiamo sviluppato un'ottima interfaccia di backend per Jobeet.

tip

Già sapete che quando qualcosa è configurabile da un file YAML, c'è anche la possibilità di usare del semplice codice PHP. Per l'admin generator si può modificare il file apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php. Questo file dà le stesse opzioni del file YAML, ma con un'interfaccia PHP. Per imparare i nomi dei metodi, guardate le classi base generate in cache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.class.php.

A domani

In solo un'ora, abbiamo costruito un'interfaccia per il backend del progetto Jobeet perfettamente funzionante. E per di più abbiamo scritto meno di 50 linee di codice PHP. Non male per così tante feature!

Domani, vedremo come mettere in sicurezza il backend, proteggendolo con uno username e una password. Sarà anche l'occasione per parlare della classe user di symfony.