Calendario de symfony día siete: manipulación del modelo y las vistas ================================================================================== 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] 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](http://svn.askeet.com/tags/release_day_7/web/imagenes/). 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] getResults() as $question): ?>
$question)) ?>

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

getBody(), 200) ?>
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](/images/askeet/pager_navigation_day7.gif) Lista de preguntas recientes ---------------------------- En el módulo `question`, crea una nueva acción `recent`: [php] 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](http://www.symfony-project.com/content/book/page/model.html) 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: [php] 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 [php] $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](http://propel.phpdb.org/docs/user_guide/) 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: [php]

recent questions

$question_pager)) ?> Ahora entenderás porque refactorizamos el listado de las preguntas a un fragmento durante [el día cinco](5.txt). 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](4.txt): 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] $question_pager, 'rule' => 'question/recent')) ?> y también las últimas lineas del fragmento `_list.php` a: [php]
No olvide agregar el parámetro en el llamado al fragmento `_list` en `modules/question/templates/listSuccess.php`. [php]

popular questions

$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](/images/askeet/recent_questions.gif) 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`: [php] public function executeRecent() { $this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1)); } * Extiende la clase `AnswerPeer`: [php] 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]

recent answers

getResults() as $answer): ?>

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

getRelevancys()) ?> points posted by getUser(), 'user/show?id='.$answer->getUser()->getId()) ?> on getCreatedAt(), 'p') ?>
getBody() ?>
* Pruebalo en tu navegador: http://askeet/answer/recent ![lista de ultimas respuestas](/images/askeet/recent_answers.gif) Ya te estas acostumbrando, no es cierto? >**Nota**: Aquellos que prestaron atención en el [día 4](4.txt) 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](http://svn.askeet.com/tags/release_day_7/apps/frontend/modules/answer/templates/) 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: [php] 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: [php]

's profile

Interests

Contributions

Questions

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](/images/askeet/user_profile.gif) 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`: [php]
asked by getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on getCreatedAt(), 'f') ?>
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](http://www.symfony-project.com/content/book/page/view.html) 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: [php]
Remplzae el comentario por [php] 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](http://www.symfony-project.com/content/book/page/templating_configuration.html) 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.php`a `componente.class.php`, y cambie su contenido por: [php] 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](/images/askeet/sidebar.gif) >**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](/images/askeet/window_title.gif) >**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](http://www.symfony-project.com/content/book/page/templating_configuration.html): > > [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](http://www.phpdoc.org/) 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](http://svn.askeet.com/) 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](/images/askeet/mcd2.gif) 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](http://svn.askeet.com/tags/release_day_7/), etiquetado `release_day_7`. La [lista-de-emails de askeet](mailto:askeet-subscribe@symfony-project.com) responderá cualquier pregunta que tenga más rápido que la velocidad de la luz.