Giorno 7: Giocare con la pagina delle categorie
Ieri avete ampliato le conoscenze su symfony in diverse aree: oggetti di Propel, 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:
# apps/frontend/config/routing.yml category: url: /category/:slug class: ~sfPropelRoute~ param: { module: category, action: show } options: { model: JobeetCategory, type: object }
tip
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 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
:
// 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:
// 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 rifattorizzato tutto il codice di
JobeetJobPeer
per introdurre un nuovo metodo condiviso addActiveJobsCriteria()
per rendere il codice più DRY (Don't Repeat Yourself).
tip
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.
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.
tip
Perché non aggiungere un'azione category
al modulo job
? Potremmo, ma
visto che il soggetto principale della pagina delle categorie è una categoria,
sembra più naturale creare un modulo category
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
:
# config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true, index: unique }
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)); }
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(); } }
note
Siccome abbiamo rimosso il metodo generato executeIndex()
, possiamo
rimuovere anche il template generato automaticamente
indexSuccess.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.
note
Perché non usare il costrutto include()
di PHP al posto dell'helper
include_partial()
? La differenza principale tra i due è il supporto
per la cache incluso nell'helper include_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 Propel, 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->setPage($request->getParameter('page', 1)); $this->pager->init(); }
tip
Il metodo sfRequest::getParameter()
ha un valore di default nel
secondo parametro. Nell'azione vista sopra, il parametro della
richiesta page
non esiste, quindi getParameter()
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.
Aggiungiamo il metodo getActiveJobsCriteria()
:
// lib/model/JobeetCategory.php public function getActiveJobsCriteria() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::addActiveJobsCriteria($criteria); }
Ora che abbiamo definito il metodo getActiveJobsCriteria()
,
possiamo rifattorizzare gli altri metodi JobeetCategory
per utilizzarlo:
// 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); }
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="/legacy/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="/legacy/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="/legacy/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="/legacy/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 Propel 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 interfacce 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.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.