![]() |
|
The Askeet TutorialCalendario de symfony día siete: manipulación del modelo y las vistas |
WARNING: The SVN source code found in the release_day tags is outdated. Please refer to the current version until each day code is updated.
You are currently reading "The Askeet Tutorial" which is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.
sidebar/default
![]() |
This work is licensed under a
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Translation of this work into another language is explicitly allowed. |
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.
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()).' '; } // Pages one by one $links = array(); foreach ($pager->getLinks() as $page) { $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page); } $navigation .= join(' ', $links); // Next and last page if ($pager->getPage() != $pager->getCurrentMaxLink()) { $navigation .= ' '.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/
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
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
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
yanswer/recent
. Los detalles se encuentran en el repositorio de SVN de askeet
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>
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
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
.
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.
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.
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.
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.php
a 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
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.
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:
Nota: Además de proveer un titulo por defecto para las páginas del proyecto, symfony crea archivo
robots.txt
yfavicon.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);
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:
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?
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.
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the user mailing-list or to the forum.