symfony advent calendar dia 9: local improvements

Anteriormente no Symfony

Durante o dia 8, nós adicionamos interações AJAX ao askeet sem dores. A aplicação está, agora, bem utilizável, mas poderia ter algumas melhoras. Rich text deveria ser permitido nas perguntas body, e chaves primárias não deveriam aparecer nas URIs. Tudo isso não é difícil de fazer com o symfony: hoje será uma boa ocasião para praticar o que você já aprendeu, e para avaliar que você sabe como manipular todas as camadas da arquitetura MVC.

Permitindo formatação rich text nas perguntas e respostas

Markdown

O corpo das perguntas e respostas, até agora, só aceitam texto plano (plain text). Para permitir formatação básica - negrito, itálico, links, imagens, etc. - nós iremos usar uma biblioteca externa ao invés de reinventar a roda.

Se você deu uma olhada na documentação do symfony em formato de texto, você, provavelmente, sabe que nós somos grandes fãs de Markdown. Markdown é uma ferramenta de conversão de texto para HTML, e uma sintaxe para formatação de texto. A grande vantagem do Markdown sobre, por exemplo, a sintaxe do Wiki ou fórum, é que um arquivo markdown em texto plano é ainda muito legível:

Test Markdown text
------------------

This is a **very simple** example of [Markdown][1].
The best thing about markdown is its _auto-escape_ feature for code chunks:

    <a href="http://www.symfony-project.com">link to symfony</a>

>The `<` and `>` are properly escaped as `&lt;` and `&gt;`,
>and are not interpreted by any browser

[1]: http://daringfireball.net/projects/markdown/   "Markdown"

Este Markdown renderiza como a seguir:

Test Markdown text ------------------

This is a very simple example of Markdown. The best thing about markdown is its auto-escape feature for code chunks:

 <a href="http://www.symfony-project.com">link to symfony</a>

The < and > are properly escaped as &lt; and &gt;, and are not interpreted by any browser

Biblioteca Markdown

Embora originalmente escrito em Perl, Markdown está disponível como uma biblioteca PHP em PHP Markdown. É o que nós iremos usar. Faça o download do arquivo markdown.php e coloque-o no diretório lib do projeto askeet (askeet/lib/). Isso é tudo: Ele está agora disponível para todas as classes das aplicações do askeet, desde que você faça a requisição dele primeiro:

[php]
require_once('markdown.php');

Nós poderíamos chamar o conversor do Markdown cada vez que nós mostramos o corpo da mensagem, mas isso iria exigir muita carga de nossos servidores. Nós poderíamos, preferivelmente, converter o corpo do texto para um corpo (body) HTML quando a pergunta é criada, e guardar a versão HTML do corpo (body) na tabela Question. Você está, provavelmente, acostumando-se a isto, então a extensão do model não será uma novidade.

Estendendo o model

Primeiro, adicione uma coluna na tabela Question no schema.xml:

[php]
<column name="html_body" type="longvarchar" />

Então, gere novamente o model e atualize o banco de dados:

$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql

Sobrescreva o método setBody

Quando o método ->setBody() da classe Question é chamado, a coluna html_body deve, também, ser atualizada com a conversão Markdown do texto do corpo (body). Abra o arquivo askeet/lib/model/Question.php do model, e crie:

[php]
public function setBody($v)
{
  parent::setBody($v);


require_once('markdown.php');

  // strip all HTML tags
  $v = htmlentities($v, ENT_QUOTES, 'UTF-8');

  $this->setHtmlBody(markdown($v));
}

Aplicando a função htmlentities() antes de setar o HTML body protege o askeet de ataques XSS (cross-site-scripting) desde que todas as tags <script> are escaped.

Atualize os dados de teste

Nós iremos adicionar alguma formatação Markdown para algumas perguntas dos dados de teste (em askeet/data/fixtures/test_data.yml), para ser possível verificar que a conversão funciona adequadamente:

Question:
  q1:
    title: What shall I do tonight with my girlfriend?
    user_id: fabien
    body:  |
      We shall meet in front of the __Dunkin'Donuts__ before dinner, 
      and I haven't the slightest idea of what I can do with her. 
      She's not interested in _programming_, _space opera movies_ nor _insects_.
      She's kinda cute, so I __really__ need to find something 
      that will keep her to my side for another evening.

  q2:
    title: What can I offer to my step mother?
    user_id: anonymous
    body:  |
      My stepmother has everything a stepmother is usually offered
      (watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account). 
      Her birthday comes next week, I am broke, and I know that 
      if I don't offer her something *sweet*, my girlfriend 
      won't look at me in the eyes for another month.

Você pode, agora, repovoar o banco de dados:

$ php batch/load_data.php

Modificando os templates

O template showSuccess.php do módulo question pode ser sensivelmente modificado:

[php]
...
<div class="question_body">
  <?php echo $question->getHtmlBody() ?>
</div>
...

O fragmento list template (_list.php) também mostra o corpo (body), mas em uma versão truncada:

[php]
<div class="question_body">
  <?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?>
</div>

Tudo, agora, está pronto para o teste final: mostrar as três páginas que foram modificadas, e observar o texto formatado que veio dos dados de teste:

http://askeet/question/list
http://askeet/recent
http://askeet/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend

markdown text

O mesmo vai para o corpo (body) de Answer: Uma coluna html_body tem que ser criada no model, o método ->setBody() precisa ser sobrescrito, e as respostas mostradas em question/show tem que usar o método ->getHtmlBody() ao invés de ->getBody(). Como o código é exatamente o mesmo de acima, nós não o descreveremos aqui, mas você irá encontrá-lo no código SVN de hoje.

Esconda todos os ids

Outra boa prática em ações (actions) do symfony é evitar o quanto possível passar chaves primárias como parâmetros de requisição. Isto porque nossas chaves primárias são, essencialmente, auto incrementais, e isso dá a crackers informações demais sobre os registros de nosso banco de dados. Além disso, a URI mostrada não significa nada, e isso é mau para ferramentas de busca.

Use a página do perfil de usuário, por exemplo. Até agora, ela usa o id do usuário como parâmetro. Mas se nós tivermos certeza de que o nickname é único, este poderia, também, ser o parâmetro para uma requisição. Vamos fazer isso.

Mudando a ação (action)

Edite a ação (action) user/show:

[php]
public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByNickname($this->getRequestParameter('nickname'));
  $this->forward404Unless($this->subscriber);

  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

Mude o model

Adicione o seguinte método à classe UserPeer no diretório askeet/lib/model/.

[php]
public static function retrieveByNickname($nickname)
{
  $c = new Criteria();
  $c->add(self::NICKNAME, $nickname);

  return self::doSelectOne($c);
}

Mude o template

As páginas que mostrar um link para o perfil de usuário devem, agora, exibir o nickname do usuário, ao invés de seu id.

Nos templates question/showSuccess.php, question/_list.php, substitua:

[php]
<?php echo link_to($question->getUser(), 'user/show?id='.$question->getUserId()) ?>

por:

[php]
<?php echo link_to($question->getUser(), 'user/show?nickname='.$question->getUser()->getNickname()) ?>

O mesmo tipo de moficação vai para o template answer/_answer.php.

Adicione uma regra de roteamento

Adicione uma nova regra, para esta ação, à configuração do roteamento, para esta ação (action) de modo que a url padrão mostre um parâmetro de requisição nickname:

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }

Após um symfony clear-cache, a última coisa a fazer é testar suas novas modificações.

Roteamento

Não considerando as adições de hoje, muitas das ações (actions) escritas até agora usam o roteamento padrão, assim, o nome do módulo e o nome da ação (action) são, freqüentemente, mostradas na barra de endereços do browser. Você já aprendeu como consertar isso, assim, vamos definir URL padrões para todas as ações. Edite o arquivo askeet/apps/frontend/config/routing.yml:

# question
question:
  url:   /question/:stripped_title
  param: { module: question, action: show }

popular_questions:
  url:   /index/:page
  param: { module: question, action: list, page: 1 }

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

add_question:
  url:   /add_question
  param: { module: question, action: add }

# answer
recent_answers:
  url:   /recent/answers/:page
  param: { module: answer, action: recent, page: 1 }

# user
login:
  url:   /login
  param: { module: user, action: login }

logout:
  url:   /logout
  param: { module: user, action: logout }

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }

# default rules
homepage:
  url:   /
  param: { module: question, action: list }

default_symfony:
  url:   /symfony/:action/*
  param: { module: default }

default_index:
  url:   /:module
  param: { action: index }

default:
  url:   /:module/:action/*

Se você navegar no ambiente de produção, você é fortemente aconselhado a limpar o cache antes de testar esta modificação na configuração.

Uma boa prática do roteamento do symfony é usar nomes de regras em um helper link_to() ao invés de module/action. Não somente é mais rápido (o motor (engine) de roteamento não precisa interpretar a configuração de roteamento para encontrar a regra a ser aplicada), mas ele (helper), também, te permite modificar a ação, posteriormente, através de uma regra. O capítulo sobre roteamento do livro do symfony explica isso em mais detalhes.

[php]
<?php link_to('@user_profile?id='.$user->getId()) ?>
// is better than
<?php link_to('user/show?id='.$user->getId()) ?>

Askeet segue as boas práticas com symfony, assim o código que você irá fazer o download no final do tutorial deste dia, contém somente nomes de regras em link helpers. Como trocar action/module por @rule em todos os templates e um helper customizado não é muito divertido de fazer, assim nosso último conselho a respeito de roteamento é: Escreva regras de roteamento como você cria ações, e use nomes de regras em link helpers desde o começo.

Vejo você amanhã

As mudanças de hoje foram mais demoradas de ler do que de entender. Além disso, as modificações descritas no tutorial foram repetidas para casos semelhantes no código como um todo. Embora nenhuma funcionalidade nova foi adicionada hoje, o código mudou muito.

Se você sente que, hoje, não aprendeu muito sobre symfony, isto significa que você está ficando pronto para começar seu próprio projeto. O processo de criar uma ação, modificando o model para que este sirva à ação (action) como necessário, escrever um templates simples para mostrar (output) a ação (action) e editar a configuração para integrar a nova ação (action) na lógica da aplicação são o básico do desenvolvimento no symfony.

Todas as boas práticas expostas aqui (usando bibliotecas externas ao invés de reescrevê-las no symfony, não mostrando as chaves primárias na aplicação, usando nomes de regras de roteamento ao invés de module/action) irão manter sua aplicação limpa, segura, rápida e de fácil manutenção.

Mas a aplicação askeet está longe de estar terminada! A funcionalidade que faz mais falta é a habilidade de adicionar uma nova pergunta e adicionar uma nova resposta. É o que nós iremos desenvolver amanhã.

Você tem uma sugestão para a funcionalidade extra do 21º dia ? Mande-a para a mailing-list do askeet. Fique ligado!