Caution: You are browsing the legacy symfony 1.x part of this website.
SymfonyWorld Online 2020
100% online
30+ talks + workshops
Live + Replay watch talks later

Calendario de symfony día siete: manipulación del modelo y las vistas

1.0

Previamente en symfony

Ya han pasado seis días, y algunos de ustedes deben estar pensando que la aplicación no es muy útil aun. Es es porque algunos consideran la utilidad de una aplicación por el número de páginas disponibles, y al ver que askeet solo puede mostrar una lista de preguntas, mostrar las respuestas, y manejar las sesiones de usuario.

La razón por la que no damos mucha importancia al número de páginas es porque es muy fácil agregar nuevas páginas con symfony. Quiere pruebas? Ok, hoy mostraremos de las ultimas preguntas formuladas y una lista de las ultimas respuestas, una lista de usuarios interesados en una pregunta, el perfil del usuario, y vamos a agregar una barra de navegación en cada página para acceder esta característica. Como esto no sería mucho trabajo para una hora, también configuraremos las vistas y repasaremos que se ha hecho durante la semana. Listo? Vamos.

Prefactoring

Vamos a agregar listas paginadas con controles de paginación similares a los que se encuentran en question/templates/_list.php. No nos gusta repetirnos a nosotros mismos, por lo que extraeremos el código de la paginación de este parcial a un helper personalizado. Un helper es una función PHP que se hace accesible a la plantilla (justo como los helpers link_to() y format_date()).

Crear un archivo GlobalHelper.php en askeet/apps/frontend/lib/helper y agregue en él:

<?php
 
function pager_navigation($pager, $uri)
{
  $navigation = '';
 
  if ($pager->haveToPaginate())
  { 
    $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';
 
    // First and previous page
    if ($pager->getPage() != 1)
    {
      $navigation .= link_to(image_tag('first.gif', 'align=absmiddle'), $uri.'1');
      $navigation .= link_to(image_tag('previous.gif', 'align=absmiddle'), $uri.$pager->getPreviousPage()).'&nbsp;';
    }
 
    // Pages one by one
    $links = array();
    foreach ($pager->getLinks() as $page)
    {
      $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);
    }
    $navigation .= join('&nbsp;&nbsp;', $links);
 
    // Next and last page
    if ($pager->getPage() != $pager->getCurrentMaxLink())
    {
      $navigation .= '&nbsp;'.link_to(image_tag('next.gif', 'align=absmiddle'), $uri.$pager->getNextPage());
      $navigation .= link_to(image_tag('last.gif', 'align=absmiddle'), $uri.$pager->getLastPage());
    }
 
  }
 
  return $navigation;
}   

Los helpers de paginación mejoran el código que previamente escribimos: puede utilizar cualquier regla de enrutado, no muestra el enlace 'previous' para la primer página ni el enlace 'next' para la última página. También agregamos cuatro nuevas imágenes (first.gif, previous.gif, next.gif y last.gif) para hacer los enlaces más agradables. Obtenlos desde el repositorio SVN de askeet. Probablemente reutilizaras este helper en el futuro para tus propios proyectos.

Para utilizar este helper en el fragmento question/templates/_list.php llama a la funciona helper como sigue:

<?php use_helper('Text', 'Global') ?>
 
<?php foreach($question_pager->getResults() as $question): ?>
<?php endforeach; ?>

Nota la adición de la 's' en la llamada a use_helper() al comienzo, puesto que ahora necesitamos más de un helper. El nombre Global refiere al archivo GlobalHelper.php que recién creamos.

Verifique que todo funcione como antes yendo a:

http://askeet/frontend_dev.php/

navegación paginada refactorizada

Lista de preguntas recientes

En el módulo question, crea una nueva acción recent:

public function executeRecent()
{
  $this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));
}

Eso es así de simple. Consideramos que la habilidad de obtener las últimas preguntas debería ser un método de la clase QuestionPeer. Las clases -Peer están dedicadas a devolver listas de objetos de una clase dada - esto se explica en detalles en el capitulo del modelo del libro de symfony. Pero el método getRecent() aun debe ser creado. Abre el archivo de la clase askeet/lib/model/QuestionPeer.php y agrega:

public static function getRecentPager($page)
{
  $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
  $c = new Criteria();
  $c->addDescendingOrderByColumn(self::CREATED_AT);
  $pager->setCriteria($c);
  $pager->setPage($page);
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();
 
  return $pager;
}

El criterio (Criteria) de orden descendiente para el día de creación selecciona las últimas preguntas. Este método utiliza self en lugar de parent porque es una función de la clase, no una función del objeto. La razón por lo que hacemos un doSelectJoinUser() aquí en lugar de un simple doSelect() es es porque sabemos que la plantilla necesitará los detalles del autor de la pregunta. Esto significaria una primera pregunta para la lista de preguntas, más una petición por pregunta para obtener el usuario relacionado. El método doSelectJoinUser() hace todo eso en una sola petición: cuando preguntamos

$question->getUser();

...no hay ningún pedido enviado a la base de datos. El método joinUser nos permite reducir el numero de peticiones de 1 + el número de preguntas a solo 1. La base de datos nos agradecerá por esta simple optimización.

La documentación de Propel te dará toda las explicaciones acerca de esta característica.

La plantilla de la lista de preguntas recientes se asemejará mucho al listado de preguntas mostrado en el página de inicio. Cree el archivo askeet/apps/frontend/module/question/templates/recentSuccess.php con:

<h1>recent questions</h1>
 
<?php include_partial('list', array('question_pager' => $question_pager)) ?>

Ahora entenderás porque refactorizamos el listado de las preguntas a un fragmento durante el día cinco. Finalmente, necesita agregar una regla recent_questions en el archivo de configuración frontend/config/routing.yml, como se mostró durante el día cuatro:

recent_questions:
  url:   /question/recent/:page
  param: { module: question, action: recent, page: 1 }

Pero espera: el fragmento questions/_list crea enlaces con la regla de enrutado question/list , así no funcionará para la lista de preguntas recientes. Necesitamos pasar la regla de enrutado como parámetro al fragmento para que pueda ser reutilizada para varias paginaciones. Así que cambia la linea final del archivo recentSuccess.php a:

<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>

y también las últimas lineas del fragmento _list.php a:

<div id="question_pager">
  <?php echo pager_navigation($question_pager, $rule) ?>
</div>

No olvide agregar el parámetro en el llamado al fragmento _list en modules/question/templates/listSuccess.php.

<h1>popular questions</h1>
 
<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>

Limpie el cache (la configuración fue modificada), y eso es todo.

Para mostrar la lista de las preguntas, escribe en la barra del navegador de la URL:

http://askeet/recent

Lista de preguntas frecuentas

Lista de recientes respuestas

Es casi lo mismo que más arriba, así que seremos bastante directos en este:

  • Cree un módulo answer:

    $ symfony init-module frontend answer
    
  • Cree una nueva acción recent:

    public function executeRecent()
    {
      $this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1));
    }  
  • Extiende la clase AnswerPeer:

    public static function getRecentPager($page)
    {
      $pager = new sfPropelPager('Answer', sfConfig::get('app_pager_homepage_max'));
      $c = new Criteria();
      $c->addDescendingOrderByColumn(self::CREATED_AT);
      $pager->setCriteria($c);
      $pager->setPage($page);
      $pager->setPeerMethod('doSelectJoinUser');
      $pager->init();
     
      return $pager;
    }
  • Cree una nueva plantilla recentSuccess.php:

    <?php use_helper('Date', 'Global') ?>
     
    <h1>recent answers</h1>
     
    <div id="answers">
    <?php foreach ($answer_pager->getResults() as $answer): ?>
      <div class="answer">
        <h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?stripped_title='.$answer->getQuestion()->getStrippedTitle()) ?></h2>
        <?php echo count($answer->getRelevancys()) ?> points
        posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?>
        on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
        <div>
          <?php echo $answer->getBody() ?>
        </div>
      </div>
    <?php endforeach ?>
    </div>       
     
    <div id="question_pager">
      <?php echo pager_navigation($answer_pager, 'answer/recent') ?>
    </div>
  • Pruebalo en tu navegador:

    http://askeet/answer/recent
    

lista de ultimas respuestas

Ya te estas acostumbrando, no es cierto?

Nota: Aquellos que prestaron atención en el día 4 probablemente reconozca el trozo de código utilizado para mostrar los detalles de la respuesta. Puesto que este código es utilizado en los últimos dos lugares, vamos a refactorizarlo y crear un parcial, para ser utilizado en question/show y answer/recent. Los detalles se encuentran en el repositorio de SVN de askeet

Perfiles de Usuario

El nombre de usuario en una respuesta va enlazar a la acción user/show aun por por escribirse. Esta será el perfil del usuario, y mostrara las ultimas preguntas y respuestas contribuidas, así como algunos detalles acerca del usuario.

Lo primero por hacer es crear la acción:

public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByPk($this->getRequestParameter('id', $this->getUser()->getSubscriberId()));
  $this->forward404Unless($this->subscriber);
 
  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

Los métodos ->getInterestsJoinQuestion() y ->getAnswersJoinQuestion() son métodos nativos de la clase User. Puedes inspeccionarlos en la clase askeet/lib/model/om/BaseUser.php para ver como trabajan.

La plantilla askeet/apps/frontend/modules/user/template/showSuccess.php no debería darle ningún problema:

<h1><?php echo $subscriber ?>'s profile</h1>

Interests

    getQuestion() ?>
  • getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?>

Contributions

    getQuestion() ?>
  • getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?>
    getBody() ?>

Questions

  • getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?>

Por supuesto, podrías desear limitar el numero de resultados devueltos por cada uno de los métodos ->getInterestsJoinQuestion(), ->getAnswersJoinQuestion() y getQuestion() del objeto User, así como el criterio de ordenamiento. Se puede realizar simplemente sobreescribiendo estos métodos en el archivo askeet/lib/model/User.php, y no lo mostraremos aquí como hacerlo - pero el release de hoy lo incluirá.

Es momento para una prueba final. Veamos lo que el primer usuario hizo:

http://askeet/user/show/id/1

perfil del usuario

Ahora también podemos enlazar al perfil del usuario desde una pregunta. Agregue la siguiente linea a question/templates/showSuccess.php y question/templates/_list.php al principio del tag div question_body:

<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>

No olvide declarar el uso del helper Date en _list.php.

Agregar una barra de navegación

Vamos a cambiar el layout global para agregar una barra lateral. Esta barra contenido dinámico, pero como queremos establecer su posición en el layout, no puede ser parte de cada plantilla. Además, poner el código de la barra en la plantilla significaría repetirlo mucho, y sabes que no nos gusta hacer eso.

Es por eso que la barra será un componente. Un componente es el resultado de una acción (i.e. el código HTML resultante de la ejecución de una plantilla) disponible en una variable. El capítulo de la vista del libro de symfony explica que es un componente, y las diferencias entre un componente y un fragmento.

Agregue el componente al layout

Abre el layout global (askeet/apps/frontend/templates/layout.php). Recuerda Ud. esta parte del código:

<div id="content_bar">
  <!-- Nothing for the moment -->
  <div class="verticalalign"></div>
</div>

Remplzae el comentario por

<?php include_component_slot('sidebar') ?>

Y eso es todo.

Define que acción va en el componente

Hemos decidido utilizar algo más poderoso que un simple componente: un spot componente. Es un componente cuya acción puede ser modificada de acuerdo a la acción llamada - permitiendo contenido contextual. Es la configuración de la vista (escrita en el archivo view.yml) quien define que acción corresponde a un componente spot:

default:
  components:
    sidebar:      [sidebar, default]

En este ejemplo, el componente slot llamado sidebar esta declarado como el resultado de la acción default del modulo sidebar.

La configuración de la vista puede ser definida para toda la aplicación (en el directorio askeet/apps/frontend/config/) o especificada para un módulo (en el directorio askeet/apps/frontend/modules/mymodule/config/). Para nuestro caso, vamos a definirlo para toda la aplicación, y sobreescribirlo cuando sea necesario, para proveer enlaces específicos-por-contexto en la barra de navegación.

Así que abra el archivo askeet/apps/frontend/config/view.yml y agregue la configuración del componente slot mostrado a continuación. Encontrara más información acerca de la configuración de la vista en el capitulo relacionado en el libro de symfony.

Escribe la acción y la plantilla de sidebar/default

Primero, vamos a dejar que symfony inicialize el nuevo modulo sidebar:

$ symfony init-module frontend sidebar

A continuación, necesitamos escribir el componente default. En el directorio askeet/sidebar/actions/, renombre actions.class.phpa componente.class.php, y cambie su contenido por:

<?php
 
class sidebarComponents extends sfComponents
{
  public function executeDefault()
  {
  }
}

Un componente de vista es una plantilla, justo como una acción. La diferencia esta en el nombre: Un componente de vista es nombrado como un fragmento (comenzando con _) en lugar de como una plantilla (terminando con Success). Entonces cree un fragmento askeet/apps/frontend/modules/sidebar/templates/_default.php (y borre el indexSuccess.php que no serà utilizado) con el siguiente contenido:

<?php echo link_to('ask a new question', 'question/add') ?>

Si trata de navegar cualquier página de su website askeet ahora, quizás obtenga un error. Eso es porque esta navegando el sitio en el entorno de producción, donde la configuración se encuentra cacheada y no parseada en cada petición. Hemos modificado el archivo de configuración view.yml, pero las acciones en el entorno de producción no lo ven. Ellas utilizan la versión cacheada, limpia el cache o navega el entorno en desarrollo:

$ symfony clear-cache

or

http://askeet/frontend_dev.php/

La barra de navegación se muestra correctamente en cada página

sidebar

Nota: Este es un efecto de la configuración del entorno de producción. Así que necesita recordarlo utilizar el entorno de desarrollo durante la fase de desarrollo (cuando cambie la configuración un montón), y limpie el cache cuando navegue en el entorno de producción después de cada cambio en la configuración.

Un poco más de configuraciones de vistas

Mientras estamos en ello, veamos el archivo de configuración view.yml en apps/config/:

default:
  http_metas:
    content-type: text/html; charset=utf-8

  metas:
    title:        symfony project
    robots:       index, follow
    description:  symfony project
    keywords:     symfony, project
    language:     en

  stylesheets:    [main, layout]

  javascripts:    []

  has_layout:     on
  layout:         layout

  components:
    sidebar:      [sidebar, default]

Las secciones de metas contiene una configuración para las meta tags de todo el sitio. La clave title también define el titulo que es mostrado en la barra de navegaciones de la ventana del navegador. Esto es muy importante, porque es lo primero que un usuario ve del sitio, si es encontrado por un indice de búsqueda. Es por eso que es necesario cambiarlo a algo más adaptado al sitio askeet:

  metas:
    title:        askeet! ask questions, find answers
    robots:       index, follow
    description:  askeet!, a symfony project built in 24 hours
    keywords:     symfony, project, askeet, php5, question, answer
    language:     en

Recargue la página actual. Si no ve ningún cambio, eso se debe a que se encuentra en el entorno de producción, y deberá limpiar el cache primero, para obtener el apropiado titulo de la ventana:

titulo de ventana

Nota: Además de proveer un titulo por defecto para las páginas del proyecto, symfony crea archivo robots.txt y favicon.ico en el directorio raíz (askeet/web/). No olvide cambiarlos también!

Nota: Quizás necesite el titulo para cada página de su sitio. Puede hacerlo definiendo un archivo view.yml especial para cada modulo, pero eso solo le permitiriá dar títulos estáticos. Alternativamente, puede utilizar un valor dinámico desde una acción con el método `->setTitle(), como lo describe en el capítulo de configuración de la vista:

  [php]
  $this->getResponse()->setTitle($title);

Mire lo que hemos hecho

Es una tradición general detenerse y ver que hemos hecho cuando llega el séptimo día. Es una buena oportunidad para documentar algunas pocas cosas, incluyendo el modelo de datos y las acciones disponibles.

De hecho, deberías documentar tu código mientras lo escribe, por ejemplo utilizando comentario al estilo-PHP doc para cada método. Lo que sucede con un proyecto symfony es que los nombres utilizados en los métodos o funciones usualmente sirve como una explicación de su propósito y uso. Los métodos se mantienen cortos, y así muy legibles. La mayoría del tiempo, las plantillas solo utilizan sentencias foreach y if que son bastante auto-explicativas. Es por eso que el código que encontrara en el repositorio SVN de askeet no contiene mucha documentación - además el hecho que ya hemos escrito siete horas del trabajo que hemos realizado!

Ahora echemos una mirada al diagrama entidad relación actualizada:

ERD

La lista de acciones disponibles es la siguiente:

answer/
  recent
question/
  list
  show
  recent
sidebar/
  default (component)
user/
  show
  login
  logout
  handleErrorLogin

El modelo también contiene los siguiente métodos:

Anwser()
  getRelevancyUpPercent()
  getRelevancyDownPercent()
AnswerPeer::
  getRecentPager()
Interest->
  save()
Question->
  setTitle()
QuestionPeer::
  getQuestionFromTitle()
  getHomepagePager()
  getRecentPager()
Relevancy
  save()
User->
  __toString()
  setPassword()

myUser->
  signIn()
  signOut()
  getSubscriberId()
  getSubscriber()
  getNickName()

...además una clase herramienta customizada y un validador customizado, ubicados en el directorio askeet/apps/frontend/lib.

Eso no esta mál por siete horas de trabajo, no es asi?

Nos vemos mañana

La aplicación progreso un montón hoy, y fue bastante rápido de hacer. Todo esta preparado para inyectar algo de AJAX en las interacciones humano-computador. Mañana, usuarios serán capaces de loguearse y declarar su interés por una pregunta utilizando AJAX. No se lo pierde!

Aun puede bajar todo el código desde el repositorio SVN de askeet, etiquetado release_day_7. La lista-de-emails de askeet responderá cualquier pregunta que tenga más rápido que la velocidad de la luz.