symfony advent calendar dia 7: manipulação do model e do view
Anteriormente no symfony
Já fazem seis dias, e alguns de vocês podem estar pensando que a aplicação ainda não é muito usual. Isto é porque alguns consideram a utilidade de uma aplicação pelo número de páginas disponíveis, e eles vêem que o askeet pode, somente, mostrar uma lista de perguntas, mostrar as respostas a elas, e tratar sessões de usuários.
A razão pela qual nós não damos muita importância ao número de páginas é porque é muito fácil adicionar novas páginas com o symfony. Você quer a prova? Ok, hoje nós iremos mostrar uma lista com as últimas perguntas e uma lista das últimas respostas postadas, uma lista de usuários interessados em uma pergunta, o perfil de um usuário, e nós iremos adicionar uma barra de navegação em cada página para acessar essas funcionalidades. Porque isso tudo não é muito trabalho para uma hora, nós iremos, também, ajustar a configuração do view e rever o que foi feito durante esta semana. Prontos? Vamos lá.
Prefactoring
Então, nós iremos adicionar listas paginadas com controles de paginação semelhantes aqueles em question/templates/_list.php. Nós não gostamos de nos repetir, então iremos extrair o código de paginação do partial em um custom helper. Um helper é uma função PHP acessível aos templates (assim como o link_to() e format_date() helpers).
Crie um GlobalHelper.php em askeet/apps/frontend/lib/helper e adicione:
[php]
<?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;
}
O helper de navegação de paginação melhora o código que nós escrevemos anteriormente: ele pode usar qualquer regra de roteamento, não mostra o 'previous links' para a primeira página nem o 'next links' para a última página. Nós também adicionamos quatro novas imagens (first.gif, previous.gif, next.gif e last.gif) para fazerem os links parecerem mais bonitos. Peguem-nos do askeet SVN repository. Vocês irão, provavelmente, reutilizar este helper no futuro, nos seus próprios projetos.
Para usar este helper no fragmento question/templates/_list.php, chame a função helper como a seguir:
[php]
<?php use_helper('Text', 'Global') ?>
<?php foreach($question_pager->getResults() as $question): ?>
<div class="question">
<div class="interested_block">
<?php include_partial('interested_user', array('question' => $question)) ?>
</div>
<h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>
<div class="question_body">
<?php echo truncate_text($question->getBody(), 200) ?>
</div>
</div>
<?php endforeach; ?>
<div id="question_pager">
<?php echo pager_navigation($question_pager, 'question/list') ?>
</div>
O nome Global referencia ao arquivo GlobalHelper.php que nós acabamos de criar.
Verifique se tudo funciona como requisitado:
http://askeet/frontend_dev.php/
Lista de perguntas recentes
No módulo question, crie uma nova ação (action) recent:
[php]
public function executeRecent()
{
$this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));
}
Simples assim. Nós consideramos que a habilidade de buscar as últimas perguntas deveria ser um método da classe QuestionPeer. As classes -Peer são dedicadas para retornar listas de objetos de uma certa classe - isto é explicado em detalhes no capítulo sobre model do livro do symfony. Mas o método da classe getRecent() ainda deve ser criado. Abra a classe askeet/lib/model/QuestionPeer.php e adicione:
[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;
}
O criteria de ordem decrescente de data de criação irá selecionar as últimas perguntas. Este método utiliza self ao invés de parent porque ele é uma função da classe, não um objeto função. A razão pela qual nós fazemos um doSelectJoinUser() aqui, ao invés de um simples doSelect() é porque nós sabemos que o template necessitará de detalhes do autor da pergunta. Isto significaria uma primeira requisição para a lista de perguntas, mais uma requisição por questão, para pegar o usuário relacionado. O método doSelectJoinUser() faz tudo isso em uma única requisição: quando nós perguntamos
[php]
$question->getUser();
...não há nenhuma requisição sendo enviada ao banco de dados. O joinUser nos permite reduzir o número de requisições de 1 + o número de questões, para somente um. O banco de dados irá nos agradecer por esta fácil otimização.
A documentação do Propel nos dará todas as explicações sobre esta grande funcionalidade.
O template da lista de perguntas recentes não irá mais parecer a lista de perguntas mostrada na página principal. Crie o askeet/apps/frontend/module/question/templates/recentSuccess.php com:
[php]
<h1>recent questions</h1>
<?php include_partial('list', array('question_pager' => $question_pager)) ?>
Você, agora, entende porque nós refactored a lista de perguntas em um fragmento durante o quinto dia. Finalmente, você necessita adicionar uma regra recent_questions no arquivo de configuração frontend/config/routing.yml , como ensinado durante o quarto dia:
recent_questions:
url: /recent/:page
param: { module: question, action: recent, page: 1 }
Mas espere: o fragmento question/_list cria links com a regra question/list, então, utilizando-o não irá funcionar para a lista de perguntas recentes. Nós precisamos ter uma regra de roteamento passada como parâmetro para o fragmento para que este possa ser reutilizado para vários pagers. Então, mude a última linha de recentSuccess.php para:
[php]
<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>
e, então, mude as últimas linhas do fragmento _list.php para:
[php]
<div id="question_pager">
<?php echo pager_navigation($question_pager, $rule) ?>
</div>
Não esqueça de, também, adicionar o parâmetro da regra na chamada do fragmento _list em modules/question/templates/listSuccess.php.
[php]
<h1>popular questions</h1>
<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>
Limpe o cache (a configuração foi modificada), e é isso.
Para mostrar a lista das últimas perguntas, digite na barra de navegação do seu browser:
http://askeet/recent
Lista de respostas recentes
É muito parecido com o que foi feito acima, então nós iremos ser diretos para este caso:
Crie o módulo
answer:$ symfony init-module frontend answerCrie uma nova ação
recent:[php] public function executeRecent() { $this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1)); }Extend a classe
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; }Crie um novo template
recentSuccess.php:[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>Teste em seu browser:
http://askeet/answer/recent
Você está ficando acostumado, não?
Note: Aqueles que prestaram atenção no quarto dia provavelmente lembraram do monte de código utilizado para mostrar os detalhes de uma resposta. Desde que este código é usado em, pelo menos, dois lugares, nós iremos refactor it e criar um partial
_answer.php, para ser utilizado tanto porquestion/showquantoanswer/recent. Detalhes podem ser encontrados no repositório SVN do askeet.
Perfil de usuário
O nome do usuário em uma resposta irá apontar para uma ação user/show ainda por ser escrita. Isto irá ser o perfil do usuário, e irá mostrar as últimas perguntas e as últimas respostas contribuidas, assim como alguns detalhes sobre o usuário.
A primeira coisa a se fazer é criar a ação:
[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();
}
Os métodos ->getInterestsJoinQuestion() e ->getAnswersJoinQuestion() são métodos nativos da classe User. Você pode inspecionar a classe askeet/lib/model/om/BaseUser.php para ver como eles funcionam.
O template askeet/apps/frontend/modules/user/templates/showSuccess.php não deveria dar nenhum problema:
[php]
<h1><?php echo $subscriber ?>'s profile</h1>
<h2>Interests</h2>
<ul>
<?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>
<li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>
<h2>Contributions</h2>
<ul>
<?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>
<li>
<?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?><br />
<?php echo $answer->getBody() ?>
</li>
<?php endforeach; ?>
</ul>
<h2>Questions</h2>
<ul>
<?php foreach ($questions as $question): ?>
<li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>
É claro, você gostaria de limitar o número de resultados retornados por cada um dos métodos ->getInterestsJoinQuestion(), ->getAnswersJoinQuestion() e getQuestion() do objeto User, assim como a ordenação. Isto é simplesmente feito sobrescrevendo estes métodos no arquivo da classe askeet/lib/model/User.php, e nós não iremos revelar aqui como fazer isso - mas o lançamento de hoje irá incluí-lo.
É hora do teste final. Vamos ver o que o primeiro usuário fez:
http://askeet/user/show/id/1
Agora nós, também, podemos apontar para um perfil de um usuário de uma questão. Adicione a seguinte linha em question/templates/showSuccess.php e em question/templates/_list.php no começo da div question_body:
[php]
<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>
Não esqueça de declarar o uso do helper Date em _list.php.
Adicionando a barra de navegações
Nós iremos mudar o layout global para adicionar uma barra de navegação (sidebar). Esta barra de navegação irá conter um conteúdo dinâmico, mas como nós queremos estabelecer sua posição no layout, ela não pode ser uma parte de cada template. Além disso, colocando o código da barra de navegação no template poderia parecer muito repetitivo, e você sabe que nós não gostamos de fazer isso.
É por isso que a barra de navegação será um componente. Um componente é uma resultado de uma ação (i.e. o código HTML resultante da execução do template) disponível em uma variável. O capítulo sobre view do livro do symfony explica o que é um componente, e as diferenças entre um componente e um fragmento.
Adicione o componente no layout
Abra o layout global (askeet/apps/frontend/templates/layout.php). Você se lembra desta parte do código:
[php]
<div id="content_bar">
<!-- Nothing for the moment -->
<div class="verticalalign"></div>
</div>
Substitua o comentário por:
[php]
<?php include_component_slot('sidebar') ?>
E é isso.
Defina qual ação vai no componente
Nós decidimos usar algo um pouco mais "potente" que um simples componente: um slot componente. Ele é um componente cuja ação pode ser modificada de acordo com a ação que está chamando - permitindo conteúdo contextual. É a configuração do view (escrita em um arquivo view.yml) que define qual ação corresponde a um slot componente:
default:
components:
sidebar: [sidebar, default]
Neste exemplo, o slot componente chamado sidebar é declarado para ser o resultado da ação default do módulo sidebar.
A configuração do view pode ser definida para toda a aplicação (no diretório askeet/apps/frontend/config/) ou especificamente para um módulo (no diretório askeet/apps/frontend/modules/mymodule/config/). Para o nosso caso, nós iremos definí-lo para toda a aplicação e sobrescrevê-lo quando necessário para prover links para um específico contexto na barra de navegação.
Então, abra o arquivo askeet/apps/frontend/config/view.yml e adicione no slot componente a configuração mostrada acima. Você irá encontrar mais informações sobre a configuração do view no capítulo relacionado do livro do symfony.
Escrevendo a ação sidebar/default e o template
Primeiramente, Nós iremos deixar o symfony inicializar o novo módulo sidebar:
$ symfony init-module frontend sidebar
Em seguida, nós precisamos escrever um componente default. No diretório askeet/apps/frontend/modules/sidebar/actions/ , renomeie actions.class.php para components.class.php, e mude seu conteúdo para:
[php]
<?php
class sidebarComponents extends sfComponents
{
public function executeDefault()
{
}
}
A component view is a template, just like for an action. A diferença é na nomenclatura: Um component view é nomeado como um fragmento (fragment) (começando com _) ao invés do modo de um template comum (terminando com Success). Então, crie um fragmento askeet/apps/frontend/modules/sidebar/templates/_default.php (e apague o indexSuccess.php, o qual não será usado) com o seguinte conteúdo:
[php]
<?php echo link_to('ask a new question', 'question/add') ?>
<ul>
<li><?php echo link_to('popular questions', 'question/list') ?></li>
<li><?php echo link_to('latest questions', 'question/recent') ?></li>
<li><?php echo link_to('latest answers', 'answer/recent') ?></li>
</ul>
Se você tentar navegar para qualquer página do site do askeet, você, provavelmente, irá ter um erro. Isto é porque você está navegando no site usando o ambiente de produção, onde a configuração está em cache e não é recarregada a cada requisição. Nós modificamos o arquivo de configuração view.yml, mas as ações no ambiente de produção não enxergam-no. Elas usam a versão em cache - aquela que não contém a configuração do slot componente. Se você quiser ver as mudanças, ou limpe o cache ou navegue no ambiente de desenvolvimento:
$ symfony clear-cache
ou
http://askeet/frontend_dev.php/
A barra de navegação está devidamente mostrada em cada página
Nota: Este é um efeito geral para a configuração do ambiente de produção. Então, você precisa se lembrar de usar o ambiente de desenvolvimento durante a fase de desenvolvimento (quando você muda muito a configuração), e limpar o cache quando quando você navega no ambiente de produção após cada mudança na configuração.
Um pouco mais da configuração do view
Enquanto nós estamos nele, vamos dar uma olhada no arquivo de configuração da aplicação view.yml em 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]
A seção metas contém a configuração para as meta tags de todo o site. A chave title também define o título que é mostrado na barra de título da janela do browser. Este título é muito importante, porque é a primeira coisa que um usuário vê do site se ele é encontrado numa ferramenta de busca. É, portanto, necessário mudá-lo para algo mais adaptado ao site do 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
Recarregue a página corrente. Se você não vê nenhuma mudança, é porque você está no ambiente de produção, e você deveria limpar o cache primeiro, para ter o devido título:
Nota: Além de prover um título padrão para as suas páginas do projeto, o symfony cria um
robots.txtpadrão e umfavicon.icono diretório raiz da internet (askeet/web/). Não esqueça de mudá-los também!
Nota: Você talvez precise mudar o título de cada página do seu site. Você pode fazer isto definindo um arquivo de configuração
view.ymlcustomizado para cada módulo, mas isso só lhe permite título estáticos. Como alternativa, você pode usar um valor dinâmico, numa ação, com o método->setTitle(), como descrito no capítulo de configuração do view:[php] $this->getResponse()->setTitle($title);
Olhando para o que nós fizemos
É uma tradição geral parar e olhar para o que você fez quando você alcança o sétimo dia. É uma boa oportunidade para documentar algumas coisas, incluindo o atual data model e as ações disponíveis.
De fato, você deveria documentar seu código enquanto você o escreve, por exemplo, usando os comentários ao estilo do PHP doc para cada método. O diferencial de um projeto symfony é que os nomes usados nos métodos ou funções, freqüentemente, servem como uma explicação dos seus objetivos e usos. Os métodos são mantidos pequenos, e, também, muito legíveis. Na maioria do tempo, os templates somente usam declarações foreach e if, as quais são bonitas e auto-explicativas. É por isso que o código que você irá encontrar no repositório SVN do skeet não contém muita documentação - além do fato que nós já escrevemos sete horas sobre o trabalho que nós fizemos!
Agora, olhemos o diagrama entidade relacionamento atualizado:
A lista de ações disponíveis é a seguinte:
answer/
recent
question/
list
show
recent
sidebar/
default (component)
user/
show
login
logout
handleErrorLogin
O model também contém os seguintes métodos:
Answer()
getRelevancyUpPercent()
getRelevancyDownPercent()
AnswerPeer::
getRecentPager()
Interest->
save()
Question->
setTitle()
QuestionPeer::
getQuestionFromTitle()
getHomepagePager()
getRecentPager()
Relevancy
save()
User->
__toString()
setPassword()
myUser->
signIn()
signOut()
getSubscriberId()
getSubscriber()
getNickName()
...mais uma classe de ferramentas customizadas e um validador customizado, localizado no diretório askeet/apps/frontend/lib/.
Nada mal para sete horas, não ?
Vejo você amanhã
A aplicação progrediu muito hoje, e foi até rápido de se fazer. Tudo está, agora, preparado para injetarmos algum AJAX na interação homem-computador. Amanhã, os usuários estarão aptos a fazer login e a declarar seu interesse por uma questão usando AJAX. Não perca !
Você ainda pode fazer o download de todo o código de hoje a partir do repositório SVN do askeet, etiquetado release_day_7. A lista de discussão do askeet irá responder qualquer dúvida mais rápido que a luz.