Calendário do advento Symfony dia dez: Alterando dados com formulários Ajax

Anteriormente no symfony

Depois de ontem a revisão das técnicas conhecidas, alguns dos senhores têm fome de interação. Mostrando perguntas e listas formatadas, paginadas, não é o suficiente para uma aplicação. E o coração do conceito do askeet é permitir qualquer usuário registrado fazer uma nova pergunta, e qualquer usuário registrado responder uma pergunta existente. Não é hora de começar a ter isto?

Adicione uma nova questão

A barra lateral construída durante o dia sete contém um link para adicionar uma nova questão. Este link para a action question/add, está a espera para ser desenvolvido.

Restringindo o acesso a usuários registrados

Primeiro de tudo, somente usuários registrados podem adicionar uma nova questão. Para restringir o acesso para a action `question/add, crie um security.yml no diretório askeet/apps/frontend/modules/question/config/:

add:
  is_secure:   on
  credentials: subscriber

all:
  is_secure:   off

Quando um usuário não registrado tenta acessar uma action restrita, o symfony o redireciona para a action de login. Esta action deve ser definida no arquivo de configuração settings.yml, nas chaves login_module e login_action:

all:
  .actions:
    login_module:           user
    login_action:           login

Mais informações sobre restrições de actions podem ser obtidas no livro do symfony, no capítulo de segurança.

O template addSuccess.php

A action question/add será usada tanto para mostrar, quanto para lidar com o formulário. Isto significa que a partir de agora, para mostrar o formulário, você só precisa de uma action vazia. Além disto, o formulário será mostrado novamente em caso de erro de validação:

[php]
public function executeAdd()
{
}

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

Ambas as ações mostrarão o template addSuccess.php:

[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>

Ambos os controles, título e corpo, têm um valor padrão (o segundo argumento do helper de formulário), definidos a partir do pedido parâmetro de mesmo nome. Qual o motivo disto? Porque estamos adicionando um arquivo de validação ao formulário. Se a validação falha, o formulário é mostrado novamente, e a entrada anterior estará nos parâmetros de request. Eles podem ser usados como valores padrões para o formulário.

[[Image(add_question_error.gif)]]

Os dados digitados anteriormente não se perdem em caso de falha na validação. Isto é a última coisa que se espera de uma aplicação amiga do usuário.

Mas, para isso, você precisa de um arquivo de validação formulário.

Validação de Formulário

Crie um diretório validate/ no modulo question, e adicione um arquivo de validação add.yml:

methods:
  post:            [title, body]

names:
  title:
    required:      Yes
    required_msg:  Você deve fornecer um título para sua questão

  body:
    required:      Yes
    required_msg:  Você deve fornecer um conteúdo para sua questão
    validators:    bodyValidator

bodyValidator:
    class:         sfStringValidator
    param:
      min:         10
      min_error:   Por favor, no de mais detalhes

Se você necessita de mais informações sobre validação de formulários, volte ao dia seis ou leia o capitulo de validação de formularios do livro do symfony.

Tratando a submissão do formulário

Agora edite a action question/add para tratar o formulário:

[php]
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());
  }
}

Lembre-se que o método ->setTitle() será responsável pelo stripped_title, e o método ->setBody() será repsonsável pelo campo html_bosy, porque nos sobrescrevemos o metodo no model Question.php. O usuário criando uma questão será declarado como interessado nela. Isto previne questões sem interessados, isto seria muito triste.

O fim da action contém um ->redirect() para o detalhe da questão criada. A principal vantagem sobre um ->forward() nisto é se o usuário usar o atualizar a pagina, o formulário não será reenviado. Além disto, o botão ´voltar´ funcionará como esperado. Esta é uma regra geral: Você não deve terminar a submissão de um formulário com um ->foreard().

A melhor coisa é que a action ainda trabalha para exibir o formulário, isto é se o ´request´ não é um POST. Isto se comportará exatamente como uma action vazia escrita anteriormente, retornando o padrão sfView::SUCCESS e isto carrega o template addSuccess.php.

Não se esqueça de criar o método isInterestedIn() no model User:

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

Como uma refatoração menor, você pode usar este método na action user/interested para substituir o snippet de código que faz a mesma coisa.

Vá em frente, teste-a agora. Usando um usuário de teste, você pode adicionar uma questão.

Adicione uma nova resposta

A adição de resposta será implantada em uma maneira um pouco diferente. Não é necessário redirecionar o usuário para uma pagina com um formulário, e sim para a pagina para a resposta ser mostrada. Assim, o formulário de resposta será em AJAX, e a nova resposta irá aparecer imediatamente nos detalhes da pagina da questão.

Adicione o formulário AJAX

Mude o fim do template modules/question/templates/showSuccess.php template para:

[php]
...    
<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>

Um pequeno refatoramento

A função link_to_login() deve ser adicionado para o helper UserHelper.php:

[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 function é algo que já vimos em outro helpers User: Isto mostra um link para a action se o usuario está autenticado, e se não, o link aponta para oformulario AJAX para login. Então substitua a chamada

This function does something that we already saw in the other User helpers: it shows a link to an action if the user is authenticated, and if not, the link points to the AJAX login form. So replace the link_to_function() calls in the link_to_user_interested() and link_to_user_relevancy() functions by calls to link_to_login(). Don't forget the link to @add_question in the modules/sidebar/templates/defaultSuccess.php. Yes, this is refactoring.

Tratando o envio do formulario

Mesmo que ainda envolva um fragmento, O método escolhido aqui para lidar com o pedido AJAX é levemente diferente da descrita

durante o oitavo dia. Isto é porque nos queremos que o resultado da submissão do formulario o substitua. É por

isso que o parametro update do helper form_remote_tag() aponta para ele mesmo, ao invés de uma area exterior. O fragmento _answer.php será incluido no resultado da action que adiciona a resposta, então o resultado final se parece com:

[php]
...
<div id="answers">
  <!-- Answer 1 -->
  <!-- Answer 2 -->
  <!-- Answer 3 -->
  ...
</div>

<div class="answer" id="add_answer">
  <!-- The new answer -->
</div>

Você provalmete adivinhou como o helper javascritp form_remote_tag() trabalha: Ele gerencia o formulário submetido para a

ação especificada no argumento da url através de um objeto XMLHttpRequest. O resultado da action substitui o elemento

espedificado no argumento update. E, apenas como o helper link_to_remote() do dia oito, ele troca a visibilidade

do indicador de acordo com o envio, e destaca a parte atualizada ao final da tranzação AJAX.

Vamos acrescentar algumas palavras sobre o usuario associado à nova resposta. Anteriormente mencionamos que esta resposta

deve ser linkada ao usuário. Se o usuario está autenticado, o seu user_id é usado para a nova resposta. Em outro caso, o

usuário anonymous é usado, sem o usuário fazer o seu login. O helper link_to_login(), localizado no helper

GlobalHelper.php, inverte a visibilidade do formulario invisivel de login no layout. Navegue no código do askeet para

encontrar este código.

A action answer/add

A regra @add_answer dada como o argumento url do formulario AJAX aponta para a ação answer/add:

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

(No caso de você saber, esta configuração é para ser adicionada ao routing.yml)

Aqui está 0 conteúdo da action:

[php]
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();
}

Primeiro de tudo, se esta action não é chamada no modo POST, isto quer dizer que alguem digitou a url na barra de endereço de

um navegador. A ação não foi concebida para esse tipo de solicitação (hackers), então retornará um erro 404 neste caso.

Para determinar o usuário para definir como autor da resposta, a action checa se o usuário corrente está autenticado. Se este

não é caso, o usuário utilizado é 'Anonymous Coward', graças ao novo metodo ::retrieveByNickname() da classe UsePeer.

Veja o código se houver alguma duvida sobre o que este metodo faz.

Depois disto, tudo está pronto para criar uma nova questão e passar o pedido para o template addSuccess.php. Como esperado,

este template contém somente uma linha, o include_partial:

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

Nos precisamos desativar o layout para esta action em frontend/modules/answer/config/view.yml:

addSuccess:
  has_layout: off

Por ultimo, se algum usuário enviar uma resposta vazia, não salvaremos. Então a manipulação de dados em parte está contornada, e a action não retorna nada - isto simplismente apagará o formulario da pagina. Nos poderiamos ter manejado

este erro no formulario AJAX, mas isso implicaria colocar o próprio formulário em outro fragmento. Isto é um esforço

desnecessario agora.

Testando...

Isto é tudo? Sim, o formulario AJAX está pronto para ser usado, limpo e seguro. Teste mostrando a lista de resposta para uma

questão, e adicionando uma nova resposta. A pagina não precisa ser atualizada, e a nova resposta aparece após a lista das

anteriores. Foi simples, não?

Te vejo amanhã

Formularios tradicionais são igualmente facies de implemetar em uma aplicação symfony. E com estas duas adições, o askeet tem todas as funcionalidades de núcleo necessarias para faze-lô funcionar.

Uma coisa porém: Não detalhamos a forma de registo de um novo usuário. Esta característica foi adicionada ao atual askeet repositório SVN de qualquer maneira, uma vez que é muito semelhante ao que foi feito hoje.

Portanto, dez dias é tudo o que se leva para construir uma versão (muito) beta de uma FAQ reforçada com o symfony usando o AJAX. No entanto, queremos mais do que isso para o askeet. Para ajudar a construir a comunidade askeet, precisamos que o site entregue conteúdo, de modo que uma pessoa que envie uma pergunta pode se cadastrar para receber as respostas em um feed agregador. Isso vai ser feito no tutorial de amanhã.

Alguns de vocês já sugeriu algumas idéias para o 21º dia. Expanda a lista ou apoie as sugestões, visitando askeet forum.

Attachments