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 / Propel
Symfony version
1.2
Language ORM

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.

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 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 --and-load para actualizar las tablas de la base de datos, y llenar la base de datos con nuestros datos:

$ php symfony propel: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 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" 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 sfPropelPager usados en esta plantilla:

  • getResults(): Devuelve un array objetos Propel 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