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

Día 7: Jugando con la Página de Categoría

1.4 / Doctrine
Symfony version
1.2
Language ORM

Ayer expandiste tus conocimiento de symfony sobre un montón de áreas diferentes: consultas con Doctrine, 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:    sfDoctrineRoute
  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/doctrine/JobeetCategory.class.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/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);
}

El método countActiveJobs() emplea un método countActiveJobs() que aún no existe en JobeetJobTable. Reemplaza el contenido del archivo JobeetJobTable.php con el siguiente código: [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;
  }
}

Como puedes ver por ti mismo, tenemos que refactorizar todo el código de JobeetJobTable para introducir un nuevo método compartido addActiveJobsQuery() 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 execute() y recién contar el número de resultados, usamos el método mas rápido count().

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.

Página de Inicio

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 doctrine: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:

Esta columna slug puede ser tomada con cuidado por un comportamiento Doctrine llamado Sluggable. Simplemente necesitamos habilitar el comportamiento sobre nuestro modelo JobeetCategory y este se encargará de todo por tí.

# config/doctrine/schema.yml
JobeetCategory:
  actAs:
    Timestampable: ~
    Sluggable:
      fields: [name]
  columns:
    name:
      type: string(255)
      notnull:  true

Ahora que slug es una columna real, es necesario eliminar el método getSlug() de JobeetCategory.

note

La configuración de la columna slug es tomada automáticamente cuando guardas un registro. El slug es armado usando el valor del campo name y se lo da al objeto.

Usa la tarea doctrine:build --all --and-load para actualizar las tablas de la base de datos, y llenar la base de datos con nuestros datos:

$ php symfony doctrine:build --all --and-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 Doctrine, symfony proporciona una clase dedicada a ello: sfDoctrinePager. 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 sfDoctrinePager(
    'JobeetJob',
    sfConfig::get('app_max_jobs_on_category')
  );
  $this->pager->setQuery($this->category->getActiveJobsQuery());
  $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 sfDoctrinePager 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 sfDoctrinePager::setQuery() toma un objeto Doctrine_Query para utilizarlo a la hora de seleccionar los elementos de la base de datos.

Agrega el método getActiveJobsCriteria():

// 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);
}

Ahora que tenemos el método getActiveJobsQuery(), podemos refactorizar otros métodos de JobeetCategory para usarlos:

// 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();
}

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" 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 $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 sfDoctrinePager usados en esta plantilla:

  • getResults(): Devuelve un array objetos Doctrine para la página actual
  • getNbResults(): Devuelve el número total de resultados
  • haveToPaginate(): Devuelve true si hay más de una página
  • getLinks(): Devuelve una lista de los enlaces de la página a mostrar
  • getPage(): Devuelve el número de la página actual
  • getPreviousPage(): Devuelve el número de la página anterior
  • getNextPage(): Devuelve el número de la siguiente página
  • getLastPage(): Devuelve el número de la última página

Paginación

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.

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