Giorno 7: Giocare con la pagina delle categorie
Ieri avete ampliato le conoscenze su symfony in diverse aree: oggetti di ##ORM##, fixture, routing, debug e configurazioni personalizzate. Abbiamo finito con una piccola sfida per oggi.
Spero abbiate lavorato sulla pagina delle categorie di Jobeet in modo che il tutorial di oggi abbia quanto più valore possibile per voi.
Pronti? Parliamo di una possibile implementazione.
La rotta per la categoria
Prima di tutto abbiamo bisogno di aggiungere una rotta per definire un URL carino per la pagina delle categorie. Aggiungere questo all'inizio del file che si occupa del routing:
#Ogni volta che si parte a implementare una nuova feature, è buona norma pensare prima all'argomento ~URL~, creando la rotta associata. Ed è obbligatorio, se le regole predefinite di ~routing~ sono state rimosse.
Una rotta può usare una qualsiasi colonna del suo oggetto correlato come parametro. Può anche usare un altro valore, se c'è un metodo di accesso collegato definito nella classe dell'oggetto. Siccome slug
non ha una colonna corrispondente nella tabella category
, abbiamo bisogno di aggiungere un accesso virtuale a JobeetCategory
per rendere la rotta funzionante:
// lib/model/JobeetCategory.php // lib/model/doctrine/JobeetCategory.class.php public function getSlug() { return Jobeet::slugify($this->getName()); }
Il link alla categoria
Ora editiamo il template indexSuccess.php
del modulo job
per aggiungere il link alla pagina delle categorie:
<!-- some HTML code --> <h1> <?php echo link_to($category, 'category', $category) ?> </h1> <!-- some HTML code --> </table> <?php if (($count = $category->countActiveJobs() - ➥ sfConfig::get('app_max_jobs_on_homepage')) > 0): ?> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div> <?php endif; ?> </div> <?php endforeach; ?> </div>
Aggiungiamo il link solamente se ci sono più di 10 offerte di lavoro da visualizzare per la categoria corrente. Il link contiene il numero di offerte non visualizzate. Per rendere il template funzionante abbiamo bisogno di aggiungere il metodo countActiveJobs()
a JobeetCategory
:
[php] // lib/model/JobeetCategory.php public function countActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
return JobeetJobPeer::countActiveJobs($criteria);
}
Il metodo countActiveJobs()
usa un metodo countActiveJobs()
che non esiste ancora in JobeetJobPeer
: sostituiamo il contenuto del file JobeetJobPeer.php
con il codice seguente: [php] // lib/model/doctrine/JobeetCategory.class.php public function countActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId());
return Doctrine_Core::getTable('JobeetJob')->countActiveJobs($q);
}
Il metodo countActiveJobs()
usa un metodo countActiveJobs()
che non esiste ancora in JobeetJobTable
.
[php] // lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getActiveJobs(Criteria $criteria = null) { return self::doSelect(self::addActiveJobsCriteria($criteria)); }
static public function countActiveJobs(Criteria $criteria = null)
{
return self::doCount(self::addActiveJobsCriteria($criteria));
}
static public function addActiveJobsCriteria(Criteria $criteria = null)
{
if (is_null($criteria))
{
$criteria = new Criteria();
}
$criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
$criteria->addDescendingOrderByColumn(self::CREATED_AT);
return $criteria;
}
static public function doSelectActive(Criteria $criteria)
{
return self::doSelectOne(self::addActiveJobsCriteria($criteria));
}
}
Come si può vedere, abbiamo eseguito la rifattorizzazione di tutto il codice di JobeetJobPeer
per introdurre un nuovo metodo condiviso addActiveJobsCriteria()
per rendere il codice più ~DRY~ (Don't Repeat Yourself). [php] // lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { return $this->addActiveJobsQuery($q)->fetchOne(); }
public function getActiveJobs(Doctrine_Query $q = null)
{
return $this->addActiveJobsQuery($q)->execute();
}
public function countActiveJobs(Doctrine_Query $q = null)
{
return $this->addActiveJobsQuery($q)->count();
}
public function addActiveJobsQuery(Doctrine_Query $q = null)
{
if (is_null($q))
{
$q = Doctrine_Query::create()
->from('JobeetJob j');
}
$alias = $q->getRootAlias();
$q->andWhere($alias . '.expires_at > ?', date('Y-m-d H:i:s', time()))
->addOrderBy($alias . '.expires_at DESC');
return $q;
}
}
Come si può vedere, abbiamo eseguito la rifattorizzazione di tutto il codice di JobeetJobTable
per introdurre un nuovo metodo condiviso addActiveJobsQuery()
, per rendere il codice più ~DRY~ (Don't Repeat Yourself).
La prima volta che una porzione di codice viene riutilizzata, copiare il codice può essere sufficiente. Ma se scopre un nuovo modo per utilizzarlo è sempre meglio eseguire la rifattorizzazione per tutti gli usi verso un metodo od una funzione condivisa, come abbiamo fatto qui.
Nel metodo countActiveJobs()
, invece di usare doSelect()
e poi ~contare~ il numero dei risultati, abbiamo utilizzato il metodo doCount()
, che è più veloce. Nel metodo countActiveJobs()
, invece di usare execute()
e poi ~contare~ il numero dei risultati, abbiamo utilizzato il metodo count()
, che è più veloce.
Abbiamo apportato modifiche a molti file solamente per una feature molto semplice. Ma ogni volta che abbiamo aggiunto del codice, abbiamo provato a metterlo nello strato corretto dell'applicazione, abbiamo inoltre fatto in modo di creare codice riusabile. Durante questo processo abbiamo anche fatto la rifattorizzazione di parti di codice esistente. Questo è il flusso tipico quando si lavora su un progetto symfony. Nella schermata seguente, per questioni di spazio, si vedono solo 5 lavori. In realtà dovrebbero vedersene 10 (secondo le impostazioni max_jobs_on_homepage
):
Creazione del modulo delle categorie
È il momento di creare il ~modulo~ category
:
$ php symfony generate:module frontend category
Se avete creato un modulo, avrete probabilmente utilizzato il comando propel:generate-module
. Questo va bene, ma visto che il 90% del codice generato in questo caso non ci serve, meglio usare il comando generate:module
, che crea un modulo vuoto.
Perché non aggiungere un'azione
category
al modulojob
? Potremmo, ma visto che il soggetto principale della pagina delle categorie è una categoria, sembra più naturale creare un modulocategory
dedicato.
Accedendo alla pagina delle categorie, la rotta category
dovrà trovare la categoria associata alla variabile slug
presente nella richiesta. Ma siccome lo ~slug~ non è memorizzato nel database e siccome non possiamo dedurre la categoria dallo slug, non c'è modo di individuare la categoria associata allo slug.
Aggiornare il database
Dobbiamo aggiungere una colonna slug
alla tabella category
:
[yml] # config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true, index: unique } La colonna slug
può occuparsi di un comportamento di Doctrine chiamato Sluggable
. Basta abilitare i comportamenti (behavior) nel modello JobeetCategory
ed esso si occuperà di tutto.
---
# config/doctrine/schema.yml
JobeetCategory:
actAs:
Timestampable:
Sluggable:
fields: [name]
columns:
name:
type: string(255)
notnull: true
Ora che slug
è una vera colonna, dobbiamo rimuovere il metodo getSlug()
da JobeetCategory
.
Ogni volta che il nome di category
cambia, dobbiamo calcolare e cambiare anche slug
. Ridefiniamo il metodo setName()
:
// lib/model/JobeetCategory.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
L'impostazione della colonna
slug
viene presa in considerazione automaticamente quando si salva una riga. Lo slug è costruito usando il valore del camponame
e impostato nell'oggetto.
usiamo il task propel:build --all --and-load
per aggiornare le tabelle del database e ripopoliamo il database con le nostre fixture:
$ php symfony propel:build --all --and-load --no-confirmation
Ora è tutto pronto per creare il metodo executeShow()
. Sostituiamo il contenuto del file delle azioni category
con il codice seguente:
// apps/frontend/modules/category/actions/actions.class.php class categoryActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); } }
Siccome abbiamo rimosso il metodo generato
executeIndex()
, possiamo rimuovere anche il template generato automaticamenteindexSuccess.php
(apps/frontend/modules/category/templates/indexSuccess.php
).
L'ultimo passo è creare il template showSuccess.php
:
// apps/frontend/modules/category/templates/showSuccess.php <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>
I partial
Notate che abbiamo copiato e incollato il tag <table>
per creare una lista di lavori per il template indexSuccess.php
. Non va bene. È ora di imparare un nuovo trucco. Quando si ha bisogno di riutilizzare alcune parti di un template, occorre creare un ~partial~. Un partial è un pezzo di codice di un ~template~ che può essere condiviso con altri template. Un partial è semplicemente un altro template che inizia con un trattino basso (_
):
Creaiamo il file _list.php
:
// apps/frontend/modules/job/templates/_list.php <table class="jobs"> <?php foreach ($jobs as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>
Si possono includere i partial usando l'helper ~include_partial~()
:
<?php include_partial('job/list', array('jobs' => $jobs)) ?>
Il primo parametro di include_partial()
è il nome del partial (fatto dal nome del modulo, una barra /
e il nome del partial senza il _
iniziale). Il secondo parametro è un array di variabili da passare al partial.
Perché non usare il costrutto
include()
di PHP al posto dell'helperinclude_partial()
? La differenza principale tra i due è il supporto per la cache incluso nell'helperinclude_partial()
.
Sostituiamo il codice HTML delle <table>
in entrambi in template con una chiamata a include_partial()
:
// in apps/frontend/modules/job/templates/indexSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?> // in apps/frontend/modules/category/templates/showSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs())) ?>
~Impaginazione~ delle liste
Dai requisiti del giorno 2:
"La lista è impaginata con 20 lavori per pagina."
Per impaginare una lista di oggetti ##ORM##, symfony fornisce una classe dedicata:sfPropelPager
. Nell'azione category
, invece di passare gli oggetti dei lavori al template, si passa un paginatore:
// apps/frontend/job/modules/category/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); $this->pager = new sfPropelPager( 'JobeetJob', sfConfig::get('app_max_jobs_on_category') );
$this->pager->setCriteria($this->category->getActiveJobsCriteria()); $this->pager->setQuery($this->category->getActiveJobsQuery()); $this->pager->setPage($request->getParameter('page', 1)); $this->pager->init(); }
Il metodo
sfRequest::getParameter()
ha un valore di default nel secondo parametro. Nell'azione vista sopra, il parametro della richiestapage
non esiste, quindigetParameter()
restituirà1
.
Il costruttore di sfPropelPager
prende una classe di modello e il numero massimo di unità da restituire per ogni pagina. Aggiungiamo il secondo valore al file di configurazione:
---
# apps/frontend/config/app.yml
all:
active_days: 30
max_jobs_on_homepage: 10
max_jobs_on_category: 20
Il metodo sfPropelPager::setCriteria()
accetta un oggetto Criteria
da usare quando seleziona le unità dal database. Il metodo sfDoctrinePager::setQuery()
accetta un oggetto Doctrine_Query
da usare quando seleziona le unità dal database.
Aggiungiamo il metodo getActiveJobsCriteria()
: Aggiungiamo il metodo getActiveJobsQuery()
:
[php] // lib/model/JobeetCategory.php public function getActiveJobsCriteria() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
return JobeetJobPeer::addActiveJobsCriteria($criteria);
}
[php] // lib/model/doctrine/JobeetCategory.class.php public function getActiveJobsQuery() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId());
return Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q);
}
Ora che abbiamo definito il metodo getActiveJobsCriteria()
, possiamo rifattorizzare gli altri metodi JobeetCategory
per utilizzarlo: Ora che abbiamo definito il metodo getActiveJobsQuery()
, possiamo rifattorizzare gli altri metodi JobeetCategory
per utilizzarlo:
[php] // lib/model/JobeetCategory.php public function getActiveJobs($max = 10) { $criteria = $this->getActiveJobsCriteria(); $criteria->setLimit($max);
return JobeetJobPeer::doSelect($criteria);
}
public function countActiveJobs()
{
$criteria = $this->getActiveJobsCriteria();
return JobeetJobPeer::doCount($criteria);
}
[php] // lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10) { $q = $this->getActiveJobsQuery() ->limit($max);
return $q->execute();
}
public function countActiveJobs()
{
return $this->getActiveJobsQuery()->count();
}
Infine, aggiorniamo il template:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?> <div class="category"> <div class="feed"> <a href="">RSS Feed</a> </div> <h1><?php echo $category ?></h1> </div> <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?> <?php if ($pager->haveToPaginate()): ?> <div class="pagination"> <a href="<?php echo url_for('category', $category) ?>?page=1"> <img src="/images/first.png" alt="First page" title="First page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>"> <img src="/images/previous.png" alt="Previous page" title="Previous page" /> </a> <?php foreach ($pager->getLinks() as $page): ?> <?php if ($page == $pager->getPage()): ?> <?php echo $page ?> <?php else: ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a> <?php endif; ?> <?php endforeach; ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>"> <img src="/images/next.png" alt="Next page" title="Next page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>"> <img src="/images/last.png" alt="Last page" title="Last page" /> </a> </div> <?php endif; ?> <div class="pagination_desc"> <strong><?php echo count($pager) ?></strong> jobs in this category <?php if ($pager->haveToPaginate()): ?> - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong> <?php endif; ?> </div>
La maggior parte di questo codice ha a che fare con link ad altre pagine. Ecco una lista di metodi sfPropelPager
usati in questo template:
getResults()
: Restituisce un array di oggetti ##ORM## per la pagina correntegetNbResults()
: Restituisce il numero totale di risultatihaveToPaginate()
: Restituiscetrue
se c'è più di una paginagetLinks()
: Restituisce una lista di link alle pagine da mostraregetPage()
: Restituisce il numero della pagina correntegetPreviousPage()
: Restituisce il numero della pagina precedentegetNextPage()
: Restituisce il numero della pagina successivagetLastPage()
: Restituisce il numero dell'ultima pagina
Poiché sfPropelPager
implementa anche le interfaccie Iterator
e Countable
, si può usare la funzione count()
per ottenere il numero dei risultati, invece del metodo getNbResults()
.
A domani
Se ieri avete lavorato sulla vostra implementazione e pensate di non aver imparato molto oggi, vuol dire che vi state abituando alla filosofia di symfony. Il processo di aggiungere una nuove feature a un sito symfony è sempre lo stesso: pensa agli URL, crea alcune azioni, aggiorna il modello, e scrivi qualche template. E se si riescono ad applicare alcune buone pratiche di sviluppo, si padroneggerà symfony in poco tempo.
Domani inizierà una nuova settimana per Jobeet. Per celebrarla, parleremo di un nuovo argomento: i test.
ORM
インデックス
Document Index
関連ページリスト
Related Pages
- Giorno 1: Impostare il progetto
- Giorno 2: Il progetto
- Giorno 3: Il ~Modello dei dati~
- Giorno 4: Il controllore e la vista
- Giorno 5: Il routing
- Giorno 6: Di più sul Modello
- Giorno 7: Giocare con la pagina delle categorie
- Giorno 8: I test unitari
- Giorno 9: I test funzionali
- Giorno 10: Form
- Giorno 11: Testare i Form
- Giorno 12: Admin Generator
- Giorno 13: L'utente
- Giorno 14: Feed
- Giorno 15: Web Service
- Giorno 16: Inviare ~email|Email~
- Giorno 17: Ricerca
- Giorno 18: ~AJAX~
- Giorno 19: Internazionalizzazione e Localizzazione
- Giorno 20: I plugin
- Giorno 21: La Cache
- Giorno 22: Il rilascio
- Giorno 23: Un altro sguardo a symfony
- Appendice B - Licenza
- Riconoscimenti
日本語ドキュメント
Japanese Documents
- 2011/01/18 Chapter 17 - Extending Symfony
- 2011/01/18 The generator.yml Configuration File
- 2011/01/18 Les tâches
- 2011/01/18 Emails
- 2010/11/26 blogチュートリアル(8) ビューの作成