Calendário do advento Symfony dia três entrando na arquitetura do MVC
Anteriormente no symfony...
Durante o segundo dia você aprender como contruir um object model (ex.: BaseQuestion.php) baseado num relational data model (schema.xml), e gerar o scaffolding (crud) para um desses objetos. A propósito, todo código gerado até agora está disponível no repositório SVN do projeto askeet:
O objetivo do terceiro dia é definir um melhor layout para o site, definir a lista de questões como sendo a homepage, mostrar o número de usuários interessados em determinada questão e popular o banco com um arquivo de texto, para testes. Não é muito a fazer, mas um bocado de leitura e entendimento.
Para ler este turorial, você deve estar familiarizado com os conceitos de projeto, applicação, módulo e ação no symfony como explicado no capítulo controller chapter do livro.
O MVC model
Hoje estaremos pela primeira vez entrando na arquitetura MVC. O que isto significa? Simplesmente que o código usado para gerar uma página está dividido em vários arquivos de acordo com seu propósito.
Se o código diz respeito a manipulação de dados independente da página, deverá estar localizado no Modelo (a maioria das vezes em askeet/lib/model). Se o mesmo se refere a apresentação final, deverá estar na Visão; no symfony, a camada de visão depende dos templates (por exemplo: askeet/apps/frontend/modules/question/templates/) e dos arquivos de configuração. Eventualmente, o código dedicado a unir tudo e decifrar a lógica do site no bom e velho PHP está localizado no Controlador, e no symfony o controlador de uma específica página é chamado ação (procure pela classe actions.class.php na pasta askeet/apps/frontend/modules/question/actions/). Você pode ler mais sobre esse modelo no capítulo MVC implementation in symfony do livro oficial do symfony.
Embora nossa aplicação mude aparentemente pouco hoje, nós manipularemos vários arquivos diferentes. Não se desespere caso não entenda a princípio a organização e separação do código em muitas camadas, em breve estará claro e lhe será bastante útil.
Alterando o layout
Na moldes do decorator design pattern, o conteúdo do template chamado por uma ação é integrado num template global, ou layout, em outras palavras, o layout contém todas as partes invariáveis da interface, que por sua vez "adorna" os resultados das ações chamadas (ex.: listar usuários, editar pedidos, etc). Abra o layout padrão (localizado em askeet/apps/frontend/templates/layout.php) e mude para o seguinte para o seguinte código:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<?php echo include_http_metas() ?>
<?php echo include_metas() ?>
<?php echo include_title() ?>
<link rel="shortcut icon" href="/favicon.ico" />
</head>
<body>
<div id="header">
<ul>
<li><?php echo link_to('about', '@homepage') ?></li>
</ul>
<h1><?php echo link_to(image_tag('askeet_logo.gif', 'alt=askeet'), '@homepage') ?></h1>
</div>
<div id="content">
<div id="content_main">
<?php echo $sf_data->getRaw('sf_content') ?>
<div class="verticalalign"></div>
</div>
<div id="content_bar">
<!-- Nothing for the moment -->
<div class="verticalalign"></div>
</div>
</div>
</body>
</html>
Nós tentamos manter as tag o mais semântico possível, e movemos todos os estilos CSS para um arquivo. Como o css não é o propósito maior desse tutorial não iremos comentar sobre eles. Caso precise de algum suporte acesse http://www.maujor.com. Eles estão disponíveis para download em SVN repository.
Nós criamos 2 arquivos (main.css e layout.css). Copie-os para o diretório askeet/web/css/ e edite o frontend/config/view.yml para faze-los carregar automaticamente:
stylesheets: [main, layout]
Este layout está ainda bastante inacabado no momento, mas ele será recontruído mais pra frente (dentro de algumas semanas). Os dois elementos mais importantes do template são a tag <head>, que na maioria das vezes é gerada automaticamente e a variável sf_content, que contém os resultados das ações.
Cheque que as modificações serão mostradas corretamente solicitar a home page - desta vez no ambiente de desenvolvimento:
http://askeet/frontend_dev.php/
updated layout Algumas palavras sobre os ambientes
Se você quer saber qual é a diferença entre http://askeet/frontend_dev.php/ e http://askeet/, você deve provavelmente dar uma olhada no capítulo configuration chapter do livro oficial do symfony. Agora, você apenas precisa saber que eles apontam para a mesma aplicação, só que em ambientes diferentes. Um ambiente seta algumas configurações onde determinadas características oide ser ativadas ou desativadas de acordo com a necessidade.
Neste caso, a url /frontend_dev.php/ aponta para o ambiente de desenvolvimento, onde todas as configurações usam o parse (são interpretadas) em cada requisição, o cache HTML é desativado e as ferramentas de debugação estão todas disponíveis (incluíndo a barra de ferramenta transparente que fica localizada no canto superior direito da tela). A / UTL - equivalente a /index.php/ - aponta para o ambiente de produção, onde as configurações são "compiladas" e as ferramentas de debugação são desativadas para acelerar o processo de carregamento das páginas.
Esses 2 scripts PHP - frontend_dev.php e index.php - são chamados de front controllers, e todas as requisições da aplicação são manipuladas por eles. Você pode acha-los dentro do diretório askeet/web/.
Este dois arquivos PHP - frontend_dev.php e index.php - Sào chamados front controllers, e todas as requisições para a aplicação são manipuladas por eles. Você pode acha-los no diretório askeet/web/. O arquivo index.php deve ser chamado frontend_prod.php, mas como a frontend foi a primeira aplicação que você criou, o symfony deduz que você provavelmente quer ela como sendo a aplicação padrão e renomeia-a para index.php, por isso que você vê sua aplicação no ambiente de produção apenas digitando /. Se você quer aprender mais sobre os front controllers e a camada de controle do modelo MVC em geral, dê uma olhada no capítulo controller chapter do livro.
Uma boa regra para a aplicação é navegar bastante no ambiente de desenvolvimento até você se sentir satisfeito com as funcionalidades que já estão funcionando e em seguida mudar para o ambiente de produção para checar a velocidade e a facilidade de identificação das urls.
: Lembre-se sempre de limpar o cache quando você adiciona alguma classe ou quando você altera algum arquivo de configuração para ver o resultado no ambiente de produção.
Redefinindo a homepage padrão
Caso você requisite até agora a home page do novo website, será mostrado uma página de 'Congratulations'. Uma idéia melhor seria mostrar já a lista de questões (vista através da url question/list e entendida como a ação de listar do módulo de questões). Para isso, abra o arquivo de configuração de roteamento da aplicação frontend askeet/apps/frontend/config/routing.yml e ache a sessão homepage. Altere-a para:
homepage:
url: /
param: { module: question, action: list }
Limpe o cache, e solicite-a no ambiente de desenvolvimento (http://askeet/frontend_dev.php/); perceba que agora aparecerá a lista de questões.
: Se você for uma pessoa curiosa, você deve ter procurado a página que contém a mensagem 'Congratulations'. E deve ter ficado surpreso de não acha-la em no diretório do seu projeto askeet. De fato, o template para a ação default/index está definido no pasta de dados (data) do diretório do symfony e é independente do seu projeto. Se você quiser realmente substituí-lo, você pode ainda criar um módulo padrão (default) para seu projeto.
As possibilidades oferecidas para o sistema de roteamento serão detalhados num futuro próximo, mas se você estiver interessado poderá adiantar lendo o routing chapter do livro oficial. Definindo dados de teste
A lista mostrada na home page permanecerá vazia, até que você adicione novas questões. Quando você desenvolve uma aplicação, é sempre bom ter dados de teste à sua disposição. Entrando dados de teste a mão (tanto através da interface CRUD ou diretamente no banco) pode bastante cansativo, por causa disso o symfony pode usar arquivos de texto para popular as tabelas. Nós criaremos o arquivo de dados dentro da pasta askeet/data/fixtures/ (este diretório deve ser criado). Crie o arquivo test_data.yml com o seguinte conteúdo:
User:
anonymous:
nickname: anonymous
first_name: Anonymous
last_name: Coward
fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier
francois:
nickname: francoisz
first_name: François
last_name: Zaninotto
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 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.
q3:
title: How can I generate traffic to my blog?
user_id: francois
body: |
I have a very swell blog that talks
about my class and mates and pets and favorite movies.
Interest:
i1: { user_id: fabien, question_id: q1 }
i2: { user_id: francois, question_id: q1 }
i3: { user_id: francois, question_id: q2 }
i4: { user_id: fabien, question_id: q2 }
OBS: Em arquivos yml não são aceitos caracteres do tipo "TAB" (caracter de identação). Você pode utilizar 2 caracarectes de espaço para substituilo.
Em primeiro lugar você deve entender o YAML aqui. Se você não é familiar com o symfony, você não deve ter o YAML como seu formato favorito de configuração no framework. Isto não é completamente necessário - você também poderá utilizar arquivos XML ou .ini, é muito fácil adicionar um manipulador para permitir que o symfony os leia. Se você tiver tempo e paciência, leia mais sobre YAML e arquivos de configuração do symfony no configuration in practice chapter do livro do symfony. Mesmo ainda não sendo familiar com a sintaxe do YAML, você deve começar de qualquer forma pelo fato deste tutorial usar extensivamente.
Volte para o arquivo de teste. Este definirá instâncias dos objetos, chamado por um rótulo interno. Estes rótulos são bem usados para relacionar objetos sem precisar definir ids (que são geralmente auto-incrementados e podem não ser setados). Por exemplo, o primeiro objeto criado é da classe Usuário, e é rotulada fabien. A primiera questão é rotulada q1. Esse método torna fácil cirar objetos da classe Interest referenciando o objeto relacionado através do rótulo:
Interest:
i1:
user_id: fabien
question_id: q1
O arquivo de dados visto anteriormente usa a sintaxe YAML comprimida para dizer a mesma coisa. Você pode encontrar mais sobre inserção de dados no capítulo data files chapter do livro do symfony.
: Não há necessidade de definir valores para os campos created_at e updated_at, pelo fato do symfony saber o que preencher por padrão.
Criar um batch para popular a base
Neste próximo passo iremos realmente popular a base, e faremos isto através de um script PHP que será chamado através da linha de comando - um batch. Esqueleto do Batch
Crie um arquivo chamado load_data.php no diretório the askeet/batch/ com o seguinte conteúdo:
<?php
define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..'));
define('SF_APP', 'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG', true);
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
// initialize database manager
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
?>
Este script não significa nada, ou perto de nada: ele define o caminho, uma aplicaão um ambiente para pegar a configuração, carrega esta configuração e inicializa o gerenciamento da banco. Mas é o suficiente: signinifica que todo o código escrito acima irá se aproveitar da característica de auto-carregamento das classes, conexão automática com os objetos do propel e com as classes raizes do symfony.
: Se você analisar os front controllers do symfony (como o askeet/web/index.php), achará o código extremamente familiar. Isto porque toda requisição precisa acessar os mesmos objetos e configurações, bem como no caso dos batchs.
Importando dados
Agora que a camada do batch está pronta, é hora de executa-lo. O batch tem que:
- Ler o arquivo YAML
- Criar instancias dos objetos do propel
- Cirar registros relacionados às tabelas do banco
Isto parece ser complicado, mas o symfony pode fazer em apenas 2 linhas de código, obrigado ao objeto sfPropelData. Apenas adicione o código abaixo antes do ?> no arquivo askeet/batch/load_data.php:
$data = new sfPropelData(); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
É isso. O objeto sfPropelData está criado e diz pra o PHP carregar todos os arquivos de dados do diretório especificado (nosso diretório fixtures), dentro do banco definido no arquivo databases.yml.
: A constante DIRECTORY_SEPARATOR é usada aqui para ser conpatível com windows e *nix.
Execute o batch
At last, you can check if these few lines of code were worth the hassle. Digite na linha de comando:
$ cd /home/sfprojects/askeet/batch $ php load_data.php
Cheque as modificações recarregando a home page no ambiente de desenvolvimento:
http://askeet/frontend_dev.php
loaded data
Os dados estao lá!
: Por padrão, o objeto sfPropelData deleta todos os dados antes de carregar os novos. Você pode também adicionar sem perder os dados anteriores adicionando a linha:
$data = new sfPropelData();
$data->setDeleteCurrentData(false);
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
Acessando os dados no modelo
A página mostrada quando a ação list do módulo questão é o resultado do método executeList() (encontrado no arquivo de ação askeet/apps/frontend/modules/question/actions/action.class.php) passado para o template askeet/apps/frontend/modules/question/templates/listSuccess.php. Esta é a base da convenção de nomeclatura que é explicada no capítulo controller chapter so livro. Vamos dar uma olhada no código executado: actions.class.php:
public function executeList ()
{
$this->questions = QuestionPeer::doSelect(new Criteria());
}
listSuccess.php:
...
<?php foreach ($questions as $question): ?>
<tr>
<td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td>
<td><?php echo $question->getTitle() ?></td>
<td><?php echo $question->getBody() ?></td>
<td><?php echo $question->getCreatedAt() ?></td>
<td><?php echo $question->getUpdatedAt() ?></td>
</tr>
<?php endforeach; ?>
Passo a passo, aqui está o que este código faz:
- Chama o método doSelect() passando um critério vazio, buscando assim todas as questões
- Esta lista de registros é colocada num array ($questions) que é passado automaticamente para o template
- O template roda um foreach varrendo todos os elementos do array $questions
- O template mostra os valores dos registros, formatado em uma tabela
Os métodos ->getId(), ->getTitle(), ->getBody(), etc. foram criados durante a chamada do comando propel-build-model (mostrado ontem, tá lembrado?) para recuperar o valor dos respectivos campos id, title body. Esses são comandos padrão, formados adicionando o prefixo get ao nome do campo de acordo com o modelo camelCase. O propel também dispõe de comandos de atribuição apenas adicionando o prefixo set antes dos mesmos. A Documentação do Propel descreve detalhadamente todos os métodos criados para cada classe relacionada.
Como a chamada ao método QuestionPeer::doSelect(new Criteria()), que não passa de uma chamada padrão do propel para retornar todos os registros da tabela de questões.
Não se preoculpe se você não entendeu o código acima, ele ficará claro em alguns dias. Modificando o template lista / questão
Agora que o banco de dados já possui usuários interressados por questões, vai ser fácil pegar o número de usuários interessados em uma questão. Se você procurar no arquivo na classe BaseQuestion?.php gerada pelo propel que fica na pasta askeet/lib/model/om/ você notará um método getInterests(). O Propel viu o campo question_id como chave estrangeira na definição das tabelas e deduziu que as questões podem ter vários usuários interessados. Este tipo de método faz com que fique fácio mostrar o que queremos apenas modificando o template listSuccess.php, localizado na pasta askeet/apps/frontend/modules/question/templates/. No processo, removeremos as antigas tabelas pelas novas divs (tableless):
<?php use_helper('Text') ?>
<h1>popular questions</h1>
<?php foreach($questions as $question): ?>
<div class="question">
<div class="interested_block">
<div class="interested_mark" id="interested_in_<?php echo $question->getId() ?>">
<?php echo count($question->getInterests()) ?>
</div>
</div>
<h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>
<div class="question_body">
<?php echo truncate_text($question->getBody(), 200) ?>
</div>
</div>
<?php endforeach; ?>
Voê percebe aqui o mesmo loop foreach original do listSuccess.php. As funções link_to() e o truncate_text() são template helpers (comandos de ajuda) do próprio symfony. A primeira cria um link para outra ação num mesmo módulo, e a segunda trunca o corpo da questão para apenas 200 caracteres. A função link_to() é auto-carregada, mas trucante_text() não, por isso neste segundo caso, você deve declarar explicitamente o uso do conjunto de helpers Text para usa-la.
Vamos lá, veja como ficou seu novo template recarregando a página inicial novamente.
http://askeet/frontend_dev.php/
better question list
O número de usuários interessados aparece ao lado de cada questão. Para alterar seu visual para este novo layout, baixe o arquivo main.css e o substitua o mesmo na pasta askeet/web/css/.
Limpando
O comando propel-generate-crud cria algumas ações e templates que não serão necessários. Está na hora de remove-los.
Ações que deverão ser removidas no arquivo askeet/apps/frontend/modules/question/actions/actions.class.php:
- executeIndex
- executeEdit
- executeUpdate
- executeCreate
- executeDelete
Template que deverá ser removido da pasta askeet/apps/frontend/modules/question/templates/:
- editSuccess.php
Te vejo amanhã
Hoje foi um excelente primeiro passo no mundo do paradígma MVC (Model-View-Controller). Manipulamos layouts, templates, ações e objetos do modelo gerado pelo Propel, além de acessarmos todas as camadas de uma real estrutura MVC em uma aplicação. Não se procupe se você não entendeu todas as passagens entre essas camadas, isto ficará claro aos poucos.
Muitos arquivos foram abertos hoje, e se você quiser saber como eles são organizados no seu projeto, acesso o capítulo file structure chapter do livro do symfony.
Amanhã será um outro ótimo dia, modificaremos algumas visões, configuraremos regras mais complexas de roteamento, modificaremos o modelo e entraremos mais profundamente na manipulação de dados e ligações entre as tabelas.
Até então, durma bem, e fique a vontade em navegar pelo código do tutorial de hoje: