Caution: You are browsing the legacy symfony 1.x part of this website.

Calendario de symfony día diez: Alterar datos con formularios AJAX

1.0

Previamente en symfony

Después de la revisión de técnicas conocidas de ayer, algunos de Uds. tienen necesidad de interacción. Mostrar preguntas con formato enriquecido y listas, incluso paginadas, no es suficiente para hacer una aplicación viva. Y el corazón del concepto askeet es permitir a cualquier usuario registrado que formule una nueva pregunta, y a cualquier usuario que responda una existente. No es tiempo que lleguemos a esto?

Agregar una nueva pregunta

La barra de navegación construida durante día siete ya contiene un enlace para agregar nuevas pregunta. Enlaza a la acción question/add, que esta esperando por ser desarrollada.

Restringir el acceso a usuarios registrados

Primero de todo, solo usuarios registrados pueden agregar una nueva pregunta. Para restringir el acceso a la acción question/add, crea un security.yml en el directorio askeet/apps/frontend/modules/question/config/:

add:
  is_secure:   on
  credentials: subscriber

all:
  is_secure:   off

Cuando un usuario no registrado intente acceder a una acción restringido, symfony redirecciona el/ella a la acción login. Esta debe ser definida en la aplicación settings.yml bajo las claves login_module y login_action:

all:
  .actions:
    login_module:           user
    login_action:           login

Más información acerca de restringir el acceso a acciones puede ser encontrada en el capítulo de seguridad de el libro de symfony.

La plantilla addSuccess.php

La acción question/add será utilizada para ambos, mostrar el formulario y manejar el formulario. Esto significa que, para mostrar el formulario, solo necesitas un acción vacía. Adicionalmente, el formulario será mostrado nuevamente en caso de error en la validación de datos:

public function executeAdd()
{
}
 
public function handleErrorAdd()
{
  return sfView::SUCCESS;
}

Ambas acciones mostrarán la plantilla addSuccess.php:

<?php echo form_tag('@add_question') ?>
 
  <fieldset>
 
  <div class="form-row">
    <?php echo form_error('title') ?>
    <label for="title">Question title:</label>
    <?php echo input_tag('title', $sf_params->get('title')) ?>
  </div>
 
  <div class="form-row">
    <?php echo form_error('body') ?>
    <label for="label">Your question in details:</label>
    <?php echo textarea_tag('body', $sf_params->get('body')) ?>
  </div>
 
  </fieldset>
 
  <div class="submit-row">
    <?php echo submit_tag('ask it') ?>
  </div>
</form>

Tanto el control title y body tienen un valor por defecto (el segundo argumento del helper de formulario) definido por el parámetro de la petición del mismo nombre. Porqué es esto? Porque vamos a agregar un archivo de validación al formulario. Si la validación falla, el formulario es mostrado nuevamente, y las entradas previas del usuario aun están en los parámetros de la petición. Pueden ser utilizados como los valores por defecto de los elementos del formulario.

error en el formulario con entradas previas mantenidas

Las entradas anteriores no se perdieron en caso de que falle la validación de formularios. Esto es lo ultimo que puede esperar de una aplicación amistosa-con-el-usuario.

Pero, para lograr esto, necesitas un archivo de validación de formulario

Validación de Formularios

Crea un directorio validation/ en el modulo question, y agregue en un archivo de validación add.yml:

methods:
  post:            [title, body]

names:
  title:
    required:      Yes
    required_msg:  You must give a title to your question

  body:
    required:      Yes
    required_msg:  You must provide a brief context for your question
    validators:    bodyValidator

bodyValidator:
    class:         sfStringValidator
    param:
      min:         10
      min_error:   Please, give some more details

Si necesita más información acerca de la validación, dirijase al día seis o lea el capítulo de validación de formularios del libro de symfony.

Manejando el envío del formulario

Ahora edite nuevamente la acción question/add para manejar el envío del formulario:

public function executeAdd()
{
  if ($this->getRequest()->getMethod() == sfRequest::POST)
  {
    // create question
    $user = $this->getUser()->getSubscriber();
 
    $question = new Question();
    $question->setTitle($this->getRequestParameter('title'));
    $question->setBody($this->getRequestParameter('body'));
    $question->setUser($user);
    $question->save();
 
    $user->isInterestedIn($question);
 
    return $this->redirect('@question?stripped_title='.$question->getStrippedTitle());
  }
}

Recuerde que el método ->setTitle() también establecerá el stripped_title, y el método ->setBody() establecerá también el campo ``html_body, porque sobreescrivimos estos métodos en la clase del modeloQuestion.php`. Un usuario creando una pregunta declarar interés en el. Esto es para prevenir preguntas con interés 0, que sería my triste.

El final de la acción contienen un ->redirect() al detalle de una pregunta creada. La mayor ventaja sobre un ->forward() es que si el usuario recarga la página de detalle después, el formulario no será enviado de nuevo. Además, el botón 'atrás' funciona como es esperado. Esa es una regla general: No debería terminar un envío de un formulario con una ->forward().

Lo mejor es que la acción aun funciona para mostrar el formulario, esto es si la petición no esta en el modo POST. Se comportara exactamente como la acción vacía escrita previamente, retornando el valor por defecto sfView::SUCCESS que lanzara la plantilla addSuccess.php.

No olvide crear el método isInterestedIn() en el modelo User:

public function isInterestedIn($question)
{
  $interest = new Interest();
  $interest->setQuestion($question);
  $interest->setUserId($this->getId());
  $interest->save();
}

Como una menor refactorización, puede utilizar este método en la acción user/interested para remplazar el código que hace lo mismo.

Adelantese, pruébelo ahora. Utilizando uno de los usuarios de prueba, puede agregar una pregunta.

Agregue una nueva pregunta

La adición de la pregunta, será implementada de un forma ligeramente diferente. No hay necesidad de redirigir al usuario a una nueva página con el formulario, entonces a otra página nuevamente para que la respuesta sea mostrada. Así que el formulario de respuestas será AJAX, y la nueva respuesta aparecerá inmediatamente en la página de detalles de la pregunta.

Agregue el formulario AJAX

Cambie le final de la plantilla modules/question/template/showSuccess.php por:

...    
<div id="answers">
<?php foreach ($question->getAnswers() as $answer): ?>
  <div class="answer">
  <?php include_partial('answer/answer', array('answer' => $answer)) ?>
  </div>
<?php endforeach; ?>
 
<?php echo use_helper('User') ?>
 
<div class="answer" id="add_answer">
  <?php echo form_remote_tag(array(
    'url'      => '@add_answer',
    'update'   => array('success' => 'add_answer'),
    'loading'  => "Element.show('indicator')",
    'complete' => "Element.hide('indicator');".visual_effect('highlight', 'add_answer'),
  )) ?>
 
    <div class="form-row">
      <?php if ($sf_user->isAuthenticated()): ?>
        <?php echo $sf_user->getNickname() ?>
      <?php else: ?>
        <?php echo 'Anonymous Coward' ?>
        <?php echo link_to_login('login') ?>
      <?php endif; ?>
    </div>
 
    <div class="form-row">
      <label for="label">Your answer:</label>
      <?php echo textarea_tag('body', $sf_params->get('body')) ?>
    </div>
 
    <div class="submit-row">
      <?php echo input_hidden_tag('question_id', $question->getId()) ?>
      <?php echo submit_tag('answer it') ?>
    </div>
  </form>
</div>
 
</div>

Un poco de refactorización

La función link_to_login() debe ser agregada al helper UserHelper.php:

function link_to_login($name, $uri = null)
{ 
  if ($uri && sfContext::getInstance()->getUser()->isAuthenticated())
  {
    return link_to($name, $uri);
  }
  else
  {
    return link_to_function($name, visual_effect('blind_down', 'login', array('duration' => 0.5)));
  }
}

Esta función hace algo que ya hemos visto en otros User helpers: muestra un enlace al formulario AJAX de login. Así que remplace las llamadas link_to_function() en link_to_user_interested() y link_to_user_relevancy() por llamadas a link_to_login(). No olvide el enlace a @add_question en el modules/sidebar/templates/_default.php. Si, esto es refactorización.

Manejando el envío del formulario

Incluso si aun involucra un fragmento, el método elige manejar el AJAX de una forma ligeramente diferente del descrito en el día ocho. Esto es porque queremos que el resultado del envío del formulario remplace el formulario. Es por eso que el parámetro update del helper form_remote_tag() apunta al contenedor del mismo formulario, en lugar de otra zona. El fragmento _answer.php será incluido en el resultado de la respuesta de la acción de adición, así que el resultado final luce como sigue:

...
<div id="answers">
  <!-- Answer 1 -->
  <!-- Answer 2 -->
  <!-- Answer 3 -->
  ...
</div>
 
<div class="answer" id="add_answer">
  <!-- The new answer -->
</div>

Probablemente haya adivinado como el helper javascript form_remote_tag() funciona: Maneja el envío del formulario a la acción especificada en el argumento url mediante un objeto XMLHttpRequest. El resultado de la acción remplaza el elemento especificado en el argumento updated. Y, y justo como el ayudante link_to_remote() del día ocho, que cambia la visibilidad del indicador de actividad entre encendido y apagado de acuerdo con el envío de petición y remarca la parte actualizada al final de la transacción AJAX.

Agregaremos algunas palabras acerca del usuario asociado a la nueva pregunta. Previamente hemos mencionado que las respuestas deben estar relacionadas a un usuario. Si el usuario esta autentificado, entonces su user_id es utilizado para la nueva respuesta. En otro caso, en otro caso el usuario anonymous es utilizado, excepto que el usuario elija loguearse entonces. El helper link_to_login(), ubicado en el helper en GlobalHelper.php, cambie la visibilidad del formulario oculto en el layout. Navegue el código askeet para ver su código.

La acción answer/add

La regla @add_answer dada como el argumento url del formulario AJAX apunta a la acción answer/add:

add_answer:
  url:   /add_anwser
  param: { module: answer, action: add }

(En caso de que se pregunta, esta configuración debe ser agregada al archivo de configuración de la aplicación routing.yml)

Aquí esta el contenido de la acción:

public function executeAdd()
{
  if ($this->getRequest()->getMethod() == sfRequest::POST)
  {
    if (!$this->getRequestParameter('body'))
    {
      return sfView::NONE;
    }
 
    $question = QuestionPeer::retrieveByPk($this->getRequestParameter('question_id'));
    $this->forward404Unless($question);
 
    // user or anonymous coward
    $user = $this->getUser()->isAuthenticated() ? $this->getUser()->getSubscriber() : UserPeer::retriveByNickname('anonymous');
 
    // create answer
    $this->answer = new Answer();
    $this->answer->setQuestion($question);
    $this->answer->setBody($this->getRequestParameter('body'));
    $this->answer->setUser($user);
    $this->answer->save();
 
    return sfView::SUCCESS;
  }
 
  $this->forward404();
}

Primero que nada, esta acción no se llama en modo POST, esto significa que alguien escribió la URI en la barra de direcciones del navegador. La acción no esta diseñada para ese tipo de peticiones (hacker), así que devuelve un error 404 en ese caso.

Para determinar el usuario a establecer como el autor de la respuesta, la acción comprueba si el usuario actual está autenticado. Si no es el caso, la acción utiliza el usuario 'Anonymous Coward' (covarde anónimo), gracias al nuevo método ::retrieveByNickName() de la clase UserPeer. Comprueva el código si tienes alguna duda acerca de lo que hace este método.

Después de eso, todo está listo para crear la nueva pregunta y pasar la respuesta a la plantilla addSuccess.php. Como era de esperar, esta plantilla contiene sólo una linea, el include_partial:

<?php include_partial('answer', array('answer' => $answer)) ?>

También necesitamos desactivar el layout para esta acción en frontend/modules/answer/config/view.yml:

addSuccess:
  has_layout: off

Por último, si el usuario envía una respuesta vacía, no queremos guardarla. Así que la parte de manejo de datos es ignorada, y la acción no devuelve nada - esto simplemente borrara el formulario de la página. Podríamos haber realizado un manajeador de errores en este formulario AJAX, pero esto implicaría poner el formulario en otro fragmento. Esto no vale el esfuerzo por ahora.

Pruébelo

Es eso todo? Si, el formulario AJAX este listo para ser utilizado, limpio y seguro. Pruébelo mostrando las listas de respuestas a una pregunta y agregando una nueva respuesta a ella. La página no necesita refrescarse, y la nueva respuesta aparece al final de la lista de respuestas previas. Eso fue simple, no es así?

Nos vemos mañana

Formulario clásico y AJAX son igualmente fáciles de implementar en una aplicación. Y con estas dos adiciones, la aplicación askeet tiene todas sus características requeridas para hacerla funcionar.

Una cosa más: No hemos detallado el modo de registrar un nuevo usuario. Esta característica fue agregada al repositorio SVN askeet actual de cualquier forma, pues es muy similar a lo que ha sido hecho hoy.

Así que diez días es todo lo que tomo construir una versión (muy) beta de un FAQ mejorado-con-AJAX con symfony. No obstante, queremos que askeet sea más que eso. Para ayudar construir la comunidad askeet, necesitamos que el sitio distribuya canales de sindicación, para que las personas haciendo preguntas puedan registrase para recibir las respuestas en un agregador de canales. Eso será el tutorial de mañana. Algunos de Uds. ya han sugerido algunas ideas para el día 21. Expanda la lista o apoye las sugerencias de otros visitando el el foro de askeet