El tutorial Jobeet

16.1. Los afiliados

En los escenarios del tutorial del día 2 establecimos que "un usuario afiliado obtiene la lista de ofertas de trabajo activas".

16.1.1. Los archivos de datos

A continuación vamos a crear un nuevo archivo de datos para la información de los afiliados:

# data/fixtures/030_affiliates.yml
JobeetAffiliate:
  sensio_labs:
    url:       http://www.sensio-labs.com/
    email:     [email protected]
    is_active: true
    token:     sensio_labs
    jobeet_category_affiliates: [programming]

  symfony:
    url:       http://www.symfony-project.org/
    email:     [email protected]
    is_active: false
    token:     symfony
    jobeet_category_affiliates: [design, programming]

Cuando se establecen relaciones muchos-a-muchos, crear los registros de la tabla intermedia es tan sencillo como definir un array cuya clave sea el nombre de la tabla intermedia seguido de una letra s. El contenido del array está formado por los nombres de los objetos que se han definido en los archivos de datos. Puedes utilizar objetos definidos en otros archivos de datos, pero con la condición de que los objetos hayan sido definidos antes de utilizarlos (el orden en el que se cargan los archivos YAML es importante).

El archivo de datos anterior ya incluye el valor del token de cada afiliado para que las pruebas sean más fáciles. En cualquier caso, cuando un usuario real solicita una cuenta, el token se debe generar automáticamente:

// lib/model/JobeetAffiliate.php
class JobeetAffiliate extends BaseJobeetAffiliate
{
  public function save(PropelPDO $con = null)
  {
    if (!$this->getToken())
    {
      $this->setToken(sha1($this->getEmail().rand(11111, 99999)));
    }

    return parent::save($con);
  }

  // ...
}

Después de crear el archivo de datos, ya puedes volver a cargar todos los datos de prueba:

$ php symfony propel:data-load

16.1.2. El servicio web de las ofertas de trabajo

Como ya hemos explicado varias veces, siempre que vayas a añadir alguna nueva funcionalidad a la aplicación, es mejor pensar primero en su URL:

# apps/frontend/config/routing.yml
api_jobs:
  url:     /api/:token/jobs.:sf_format
  class:   sfPropelRoute
  param:   { module: api, action: list }
  options: { model: JobeetJob, type: list, method: getForToken }
  requirements:
    sf_format: (?:xml|json|yaml)

En la ruta anterior, la variable especial sf_format es el último elemento que forma la URL y sus posibles valores son xml, json o yaml.

El método getForToken() se invoca cuando la acción obtiene la colección de objetos relacionados con la ruta. Como es necesario comprobar que el afiliado se encuentra activado, debemos redefinir el comportamiento por defecto de la ruta:

// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public function getForToken(array $parameters)
  {
    $affiliate = JobeetAffiliatePeer::getByToken($parameters['token']);
    if (!$affiliate || !$affiliate->getIsActive())
    {
      throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token']));
    }

    return $affiliate->getActiveJobs();
  }

  // ...
}

Si el token no existe en la base de datos, se lanza una excepción de tipo sfError404Exception. Después, esta clase se convierte automáticamente en una respuesta de error de tipo 404. Esta es por tanto la forma más sencilla de generar una página de error 404 desde una clase del modelo.

El método getForToken() utiliza, a su vez, otros dos nuevos métodos que vamos a crear a continuación.

En primer lugar tenemos que crear el método getByToken() para obtener los datos de un afiliado a partir del token que se indica:

// lib/model/JobeetAffiliatePeer.php
class JobeetAffiliatePeer extends BaseJobeetAffiliatePeer
{
  static public function getByToken($token)
  {
    $criteria = new Criteria();
    $criteria->add(self::TOKEN, $token);

    return self::doSelectOne($criteria);
  }
}

En segundo lugar, el método getActiveJobs() devuelve el listado de las actuales ofertas de trabajo activas para las categorías seleccionadas por el afiliado:

// lib/model/JobeetAffiliate.php
class JobeetAffiliate extends BaseJobeetAffiliate
{
  public function getActiveJobs()
  {
    $cas = $this->getJobeetCategoryAffiliates();
    $categories = array();
    foreach ($cas as $ca)
    {
      $categories[] = $ca->getCategoryId();
    }

    $criteria = new Criteria();
    $criteria->add(JobeetJobPeer::CATEGORY_ID, $categories, Criteria::IN);
    JobeetJobPeer::addActiveJobsCriteria($criteria);

    return JobeetJobPeer::doSelect($criteria);
  }

  // ...
}

El último paso consiste en crear la acción y las plantillas relacionadas con la API. Para ello, crea un módulo vacío llamado api utilizando la tarea generate:module:

$ php symfony generate:module frontend api

Nota Como no vamos a hacer uso de la acción index generada por defecto, la puedes borrar de la clase de las acciones y también puedes borrar su plantilla asociada indexSucess.php

16.1.3. La acción

La misma acción list que se muestra a continuación se utiliza para todos los formatos en los que se pueden obtener los datos de la API:

// apps/frontend/modules/api/actions/actions.class.php
public function executeList(sfWebRequest $request)
{
  $this->jobs = array();
  foreach ($this->getRoute()->getObjects() as $job)
  {
    $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost());
  }
}

En vez de pasar un array de objetos JobeetJob a las plantillas, les pasamos simplemente un array de cadenas de texto. Además, como tenemos tres plantillas diferentes para la misma acción, hemos creado un método llamado JobeetJob::asArray() que contiene la lógica que procesa los valores:

// lib/model/JobeetJob.php
class JobeetJob extends BaseJobeetJob
{
  public function asArray($host)
  {
    return array(
      'category'     => $this->getJobeetCategory()->getName(),
      'type'         => $this->getType(),
      'company'      => $this->getCompany(),
      'logo'         => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null,
      'url'          => $this->getUrl(),
      'position'     => $this->getPosition(),
      'location'     => $this->getLocation(),
      'description'  => $this->getDescription(),
      'how_to_apply' => $this->getHowToApply(),
      'expires_at'   => $this->getCreatedAt('c'),
    );
  }

  // ...
}

16.1.4. El formato XML

Si recuerdas el tutorial de ayer, añadir el soporte del formato xml es tan sencillo como crear una nueva plantilla:

<!-- apps/frontend/modules/api/templates/listSuccess.xml.php -->
<?xml version="1.0" encoding="utf-8"?>
<jobs>
<?php foreach ($jobs as $url => $job): ?>
  <job url="<?php echo $url ?>">
<?php foreach ($job as $key => $value): ?>
    <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>>
<?php endforeach; ?>
  </job>
<?php endforeach; ?>
</jobs>

16.1.5. El formato JSON

De la misma forma, añadir el soporte del formato JSON es muy similar:

<!-- apps/frontend/modules/api/templates/listSuccess.json.php -->
[
<?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?>
{
  "url": "<?php echo $url ?>",
<?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?>
  "<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?>

<?php endforeach; ?>
}<?php echo $nb == $i ? '' : ',' ?>

<?php endforeach; ?>
]

16.1.6. El formato YAML

Cuando el formato que utilizas es uno de los que incluye Symfony por defecto, el framework se encarga de realizar automáticamente algunas tareas como por ejemplo cambiar el Content-Type de la respuesta o deshabilitar el layout.

Como el formato YAML no está incluido entre los formatos que soporta Symfony para la peticiones de los usuarios, debemos modificar el Content-Type de la respuesta y debemos deshabilitar el layout desde la acción:

class apiActions extends sfActions
{
  public function executeList(sfWebRequest $request)
  {
    $this->jobs = array();
    foreach ($this->getRoute()->getObjects() as $job)
    {
      $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost());
    }

    switch ($request->getRequestFormat())
    {
      case 'yaml':
        $this->setLayout(false);
        $this->getResponse()->setContentType('text/yaml');
        break;
    }
  }
}

En una acción, el método setLayout() modifica el layout utilizado por defecto y también permite deshabilitarlo si utilizas el valor false.

A continuación se muestra la plantilla resultante para el formato YAML:

<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php -->
<?php foreach ($jobs as $url => $job): ?>
-
  url: <?php echo $url ?>

<?php foreach ($job as $key => $value): ?>
  <?php echo $key ?>: <?php echo sfYaml::dump($value) ?>

<?php endforeach; ?>
<?php endforeach; ?>

Si realizas una llamada a este servicio web con un token inválido, verás una página de error 404 en formato XML si la petición la realizas en XML y una página de error 404 en formato JSON si tu petición estaba en el formato JSON. Sin embargo, si se produce un error con una petición en formato YAML, symfony no sabe lo que debe mostrar.

Cada vez que creas un nuevo formato, debes crear una plantilla de error asociada. Esta plantilla se utiliza para las páginas del error 404 pero también para todas las demás excepciones.

Como las excepciones deben ser diferentes en el entorno de producción y en el de desarrollo, debes crear dos archivos diferentes: config/error/exception.yaml.php para el entorno de desarrollo y config/error/error.yaml.php para el de producción:

// config/error/exception.yaml.php
<?php echo sfYaml::dump(array(
  'error'       => array(
    'code'      => $code,
    'message'   => $message,
    'debug'     => array(
      'name'    => $name,
      'message' => $message,
      'traces'  => $traces,
    ),
)), 4) ?>
// config/error/error.yaml.php
<?php echo sfYaml::dump(array(
  'error'       => array(
    'code'      => $code,
    'message'   => $message,
))) ?>

Por último, antes de probar estas páginas no te olvides de crear un layout para el formato YAML:

// apps/frontend/templates/layout.yaml.php
<?php echo $sf_content ?>
Página de error 404

Figura 16.1 Página de error 404

Nota Si quieres redefinir las plantillas que incluye Symfony por defecto para el error 404 y las excepciones, tan sólo debes crear los archivos correspondientes en el directorio config/error/.