Ayer expandiste tus conocimiento de symfony sobre un montón de áreas diferentes: consultas con Propel, archivos de datos, enrutamiento, depuración, y personalizar la configuración. Y terminamos con un pequeño desafío para hoy.
Espero que hayas trabajado en la Página de Categoría de Jobeet así el tutorial de hoy será mucho más productivo para ti.
¿Listo? Vamos a hablar acerca de una posible implementación.
La Ruta Category
Primero, necesitamos agregar una ruta para definir una URL amigable para la página de la categoría. Agrégalo al inicio del archivo de enrutamiento:
# apps/frontend/config/routing.yml category: url: /category/:slug class: sfPropelRoute param: { module: category, action: show } options: { model: JobeetCategory, type: object }
tip
Si vas a comenzar la implementación de una nueva funcionalidad, es una buena práctica primero pensar acerca de la URL y crear la ruta asociada. Y esto es obligatorio si quitas las reglas de enrutamiento por defecto.
Como slug
no es una columna de la tabla category
, necesitamos para agregar un método virtual en JobeetCategory
para que la ruta funcione:
Un ruta puede usar cualquier columna de su objeto asociado como parámetro. También puede usa cualquier otro valor si hay un método asociado definido en la clase del objeto. Debido a que el parámetro slug
no tiene una columna correspondiente en la tabla category
, necesitamos agregar un método de acceso virtual en JobeetCategory
para que la ruta funcione:
// lib/model/JobeetCategory.php public function getSlug() { return Jobeet::slugify($this->getName()); }
El Enlace Categoría
Ahora, edita la plantilla indexSuccess.php
del módulo job
para agregar el enlace a la página de la categoría:
<!-- 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>
Solo agregamos el enlace si hay más de 10 puestos de trabajo a mostrar para la categoría actual. El enlace tiene el número de puestos de trabajo no mostrados. Para que esta plantilla funcione, necesitamos agregar el método countActiveJobs()
a JobeetCategory
:
// lib/model/JobeetCategory.php public function countActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::countActiveJobs($criteria); }
El método countActiveJobs()
emplea un método countActiveJobs()
que aún no existe en JobeetJobPeer
. Reemplaza el contenido del archivo JobeetJobPeer.php
con el siguiente código:
// 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)); } }
Como puedes ver por tí mismo, tenemos que refactorizar todo el código de JobeetJobPeer
para introducir un nuevo método compartido addActiveJobsCriteria()
para hacer el código más DRY (Don't Repeat Yourself).
tip
La primera vez, un trozo de código es re-usado, copiando el código puede ser suficiente. Pero si encuentras otra función para él necesitas refactorizar para reutilizar la función o método, como hemos hecho aquí.
En el método countActiveJobs()
, en vez de usar doSelect()
y recién contar el número de resultados, usamos el método mas rápido doCount()
.
Hemos cambiado un montón de archivos, recién para esta simple funcionalidad. Pero cada vez que debas agregar algún código, tenemos que tratar de ponerlo en la capa correcta de la aplicación y también tratar de hace el código más reusable. En el proceso, tenemos que rectorizar algún código existente. Ese es el típico workflow cuando trabajamos en un proyecto symfony.
Creación del Módulo Category
Es hora de crear el módulo category
:
$ php symfony generate:module frontend category
Si has creado un módulo, probablemente has utilizado el propel:generate-module
. Eso está bien, pero como no es necesario el 90% del código generado, usa generate:module
para crear un módulo vacío.
tip
¿Por qué no añadir una acción category
al módulo job
? Podríamos, pero como el
tema principal de la página de categoría es una categoría, se siente más natural
crear un módulo dedicado category
.
Al acceder a la página de categoría, la ruta category
tendrá que encontrar la categoría asociada con la variable slug
de la petición. Pero como slug no se almacena en la base de datos, y porque no podemos deducir el nombre de la categoría del slug, no hay forma de encontrar la categoría asociada con el slug.
Actualizar la Base de Datos
Tenemos que añadir una columna slug
para la tabla category
:
# config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true, index: unique }
Ahora que slug
es una columna real, es necesario eliminar el método getSlug()
de JobeetCategory
.
Cada vez que el nombre category
cambia, tenemos que calcular el cambio así como para slug
. Vamos a sobreescribir el método setName()
:
// lib/model/JobeetCategory.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
Usa la tarea propel:build-all-load
para actualizar las tablas de la base de datos, y llenar la base de datos con nuestros datos:
$ php symfony propel:build-all-load --no-confirmation
Tenemos ahora todo en su lugar para crear el método executeShow()
. Reemplaza el contenido del archivo de acciones category
con el siguiente código:
// apps/frontend/modules/category/actions/actions.class.php class categoryActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); } }
note
Debido a que quitamos el método generado executeIndex()
, también puedes
quitar la platilla automáticamente generada indexSuccess.php
(apps/frontend/modules/category/templates/indexSuccess.php
).
El último paso es crear la plantilla 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>
Elementos Parciales o Partials
tip
Nota del Traductor
Los Partials o Parciales, son elementos que se usan en las plantillas, haciendo uso del helper include_partial()
, es por eso que su traducción literal no es muy amigable, ya que debemos pensar no en parcial sino en porción (snippet de código de la capa Vista)
Observa que hemos copiado y pegado la etiqueta <table>
que crear una lista de puestos de trabajo en la plantilla indexSuccess.php
. Eso esta mal. Es tiempo para aprender un nuevo truco. Cuando necesites volver a utilizar una parte de una plantilla, lo que necesitas es crear un partial. Un partial es un snippet de código de plantilla que puede ser compartido entre varias plantillas. Un partial es sólo otra plantilla que comienza con un guión bajo (_
).
Crea el archivo _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>
Puedes incluir un partial utilizando el helper include_partial()
:
<?php include_partial('job/list', array('jobs' => $jobs)) ?>
El primer argumento de include_partial()
es el nombre del partial (hecho del nombre del módulo, una /
, y el nombre del partial sin el _
). El segundo argumento es un array de las variables a pasar al partial.
note
¿Por qué no utilizar el método include()
incluído en PHP en lugar del
helper include_partial()
? La principal diferencia entre los dos es el
soporte de cache incluído del helper include_partial()
.
Reemplaza el HTML <table>
de ambas plantillas con la llamada 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())) ?>
Lista Paginada
De los requisitos del día 2:
"La lista es paginada, con 20 puestos de trabajo por página."
Para paginar una lista de un Objetos Propel, symfony proporciona una clase dedicada a ello:
sfPropelPager
.
En la acción category
en lugar de pasar los objetos (jobs) de los puestos de trabajo a la plantilla, pasamos un paginador:
// apps/frontend/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
El método sfRequest::getParameter()
toma un valor por defecto como segundo argumento. En
la acción anterior, si el parámetro de la petición page
no existe, entonces
getParameter()
devolverá 1
.
El constructor de sfPropelPager
tiene una clase del modelo y el número máximo de elementos a regresar por página. Añade este último valor a tu archivo de configuración:
# apps/frontend/config/app.yml all: active_days: 30 max_jobs_on_homepage: 10 max_jobs_on_category: 20
El método sfPropelPager::setCriteria()
toma un objeto Criteria
para utilizarlo a la hora de seleccionar los elementos de la base de datos.
Agrega el método getActiveJobsCriteria()
:
// lib/model/JobeetCategory.php public function getActiveJobsCriteria() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::addActiveJobsCriteria($criteria); }
Ahora que tenemos el método getActiveJobsCriteria()
, podemos refactorizar otros métodos de
JobeetCategory
para usarlos:
[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); }
Por último, vamos a actualizar la plantilla:
<!-- 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> <?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" /> </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 $pager->getNbResults() ?></strong> jobs in this category <?php if ($pager->haveToPaginate()): ?> - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong> <?php endif; ?> </div>
La mayor parte de este código se refiere a los enlaces a otras páginas. Aquí está la lista de métodos sfPropelPager
usados en esta plantilla:
getResults()
: Devuelve un array objetos Propel para la página actualgetNbResults()
: Devuelve el número total de resultadoshaveToPaginate()
: Devuelvetrue
si hay más de una páginagetLinks()
: Devuelve una lista de los enlaces de la página a mostrargetPage()
: Devuelve el número de la página actualgetPreviousPage()
: Devuelve el número de la página anteriorgetNextPage()
: Devuelve el número de la siguiente páginagetLastPage()
: Devuelve el número de la última página
Nos vemos mañana
Si trabajaste en tu propia implementación de ayer puedes sentir que no aprendiste mucho hoy, esto significa que te estás acostumbrando a la filosofía symfony. El proceso para añadir una nueva característica a un sitio web de symfony es siempre el mismo: pensar en las URLs, crear algunas acciones, actualizar el modelo, y escribir algunas plantillas. Y, si puedes aplicar algunas de estas buenas prácticas de desarrollo, te convertirás en un maestro symfony muy rápido.
Mañana será el comienzo de una nueva semana para Jobeet. Para celebrar, vamos a hablar de un nuevo tema: las Pruebas.
note
Si deseas comprobar el código del día de hoy, o de cualquier otro día, el código esta
disponible día a día en el repositorio SVN oficial de Jobeet
(http://svn.jobeet.org/propel/
).
Por ejemplo, puedes obtener el código de hoy de la
etiqueta release_day_07
:
$ svn co http://svn.jobeet.org/propel/tags/release_day_07/ jobeet/
Feedback
tip
Este capítulo ha sido traducido por Roberto Germán Puentes Díaz. Si encuentras algún error que deseas corregir o realizar algún comentario, no dudes en enviarlo por correo a puentesdiaz [arroba] gmail.com
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.