Avançar para a área de conteúdo

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

Todas as informações enviadas são seguras.

  • Fechar [x]

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

Todas as informações enviadas são seguras.

  • Fechar [x]

Introdução à Programação MVC com Agavi, Parte 2: Inclua Formulários e Suporte de Banco de Dados com Agavi e Doctrine

Aprenda como construir aplicativos da Web escaláveis com a estrutura Agavi

Vikram Vaswani, Founder, Melonfire
Photo of Vikram Vaswani
Vikram Vaswani é o fundador e Presidente da Melonfire (este link reside fora de ibm.com), uma empresa de serviços de consultoria com conhecimento especial sobre ferramentas e tecnologias de software livre. Ele também é o autor dos livros PHP Programming Solutions (este link reside fora de ibm.com) e PHP: A Beginners Guide (este link reside fora de ibm.com).

Resumo:  Trabalhe com a estrutura escalável e de software livre Agavi para criar um formulário de entrada, use o Doctrine para gerar automaticamente os modelos de dados para o projeto e integre esses modelos no projeto Agavi na Parte 2 desta série de cinco partes.

Visualizar mais conteúdo nesta série

Data:  28/Jul/2009
Nível:  Intermediário
Atividade:  2643 visualizações
Comentários:  


Introdução

Acrônimos Usados Frequentemente

  • API: Interface do programa de aplicativo
  • CRUD: Create Read Update Delete
  • CSS: Cascading stylesheet
  • HTML: Linguagem de Marcação de Hipertexto
  • MVC: Model-View-Controller
  • ORM: Object-Relational Mapping
  • SQL: Linguagem de Consulta Estruturada
  • SVN: Subversion
  • URL: Localizador Uniforme de Recursos
  • XHTML: Extensible Hypertext Markup Language
  • XML: Linguagem de Marcação Extensível

Na primeira parte desta série de artigos, apresentei o Agavi e expliquei alguns dos recursos que o tornam exclusivamente adequado para construção de aplicativos escaláveis e compatíveis com padrões da Web. Por meio do uso de um aplicativo de exemplo chamado Web Automobiles Sales Platform (WASP), orientei você pelos conceitos básicos de criação de um novo projeto Agavi, compreendendo o layout do sistema de arquivos recomendado do Agavi e familiarizando-se com o script de construção de linha de comandos do Agavi. Também apresentei os componentes fundamentais de qualquer aplicativo Agavi—Ações, Visualizações e Rotas—e mostrei alguns dos validadores de entrada integrado do Agavi.

Enquanto o Agavi pode certamente ser usado para servir conteúdo estático, ele realmente se destaca ao usá-lo para algo mais complexo. E, nesta segunda parte, você fará exatamente isso—ao longo das próximas páginas, você aprenderá como receber, validar e processar a entrada de formulários da Web, bem como conectar seu aplicativo Agavi a um banco de dados MySQL.


Criando Formulários com Agavi

Antes, um rápido lembrete de como se parece a página de índice do aplicativo WASP (A Figura 1):


Figura 1. A página de índice do aplicativo WASP
Captura de Tela da página de índice do aplicativo WASP

Outros Artigos desta Série

Você se lembrará de que já preparou o código para manipular os dois links para o conteúdo estático. Portanto, vamos seguir em frente e trabalhar com o link "Contact Us". Conforme o nome sugere, este link aponta para um formulário de contato que partes interessadas podem usar para contatar o revendedor do automóvel. O procedimento geral que você usará para implementar esta funcionalidade é semelhante àquele usado quando você construiu StaticContentAction no artigo anterior; apenas o código será diferente.

Para começar, inicie seu script de construção Agavi e insira valores como a seguir:

shell> agavi action-wizard
...
Module name: Default
Action name: Contact
Space-separated list of views to create for Contact [Success]: Input Error Success

Isso cria uma nova ContactAction e três visualizações adicionais. Você já está familiarizado com as duas visualizações padrão, ContactSuccessView e ContactErrorView, que são exibidas dependendo se a Ação for bem-sucedida ou falhar. A terceira visualização, ContactInputView, é nova para você; ela é a visualização inicial que o usuário recebe, e ela representa o formulário da Web que aceitará a entrada do usuário.

Inclua uma nova rota para a ContactAction no arquivo $WASP_ROOT/app/routing.xml, como em A Lista 1:


Lista 1. A definição da rota Default/ContactAction
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
       ...
      <!-- action for contact form "/contact" -->
      <route name="contact" pattern="^/contact$" module="Default"
       action="Contact" />

    </routes>
  </ae:configuration>
</ae:configurations>

Enquanto estiver nela, atualize o modelo mestre em $WASP_ROOT/app/templates/Master.php e o hyperlink do link "Contact Us" no menu principal para a rota acima (A Lista 2):


Lista 2. O modelo mestre
            
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
...
      <div id="menu">
        <ul>
          <li><a href="<?php echo $ro->gen('index'); ?>">
           Home</a></li>
          <li><a href="#">For Sale</a></li>
          <li><a href="<?php echo $ro->gen('content',
           array('page' => 'other-services')); ?>">Other
            Services</a></li>
          <li><a href="<?php echo $ro->gen('content',
           array('page' => 'about-us')); ?>">About Us</a></li>
          <li><a href="<?php echo $ro->gen('contact'); ?>">
           Contact Us</a></li>
        </ul>
      </div>
...

Em seguida, dentro do arquivo de classe ContactAction em $WASP_ROOT/app/modules/Default/actions/ContactAction.class.php, especifique que a ContactInputView é exibida por padrão e em todos os pedidos GET, definindo os métodos getDefaultViewName() e executeRead() (Lista 3):


Lista 3. A definição de Default/ContactAction
            
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }

  public function executeRead(AgaviRequestDataHolder $rd)
  {
    return 'Input';
  }
}
?>

Atualize o arquivo de modelo correspondente em $WASP_ROOT/app/modules/Default/templates/ContactInput.php com um formulário da Web simples, como em Lista 4. Observe que você pode localizar as regras CSS para este formulário no archive de códigos que acompanha este artigo. (Consulte Download.)


Lista 4. O modelo de Default/ContactInput
            
<h3>Contact Us</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
  <label for="name" class="required">Name:</label>
  <input id="name" type="text" name="name" />
  <p/>
  <label for="email" class="required">Email address:</label>
  <input id="email" type="text" name="email" />
  <p/>
  <label for="message" class="required">Message body:</label>
  <textarea id="message" name="message" style="width:300px; height:200px">
  </textarea>
  <p/>
  <input type="submit" name="submit" class="submit" value="Send Message" />
</form>

E agora, quando você visitar novamente a página de índice do WASP em seu navegador e clicar no link "Contact Us", você verá um formulário da Web, como em Figura 2:


Figura 2. O formulário de contato do WASP
Captura de tela do formulário de contato do WASP

Você está apenas na metade do caminho. Você ainda precisa definir o que acontece quando um usuário preenche os campos do formulário e submete os dados.


Validando Entrada do Formulário

Quando o usuário submete o formulário, o navegador do cliente envia os dados de entrada para o servidor em uma transação POST. E você se lembrará de anteriormente, que o Agavi opera um filtro de entrada extremamente rigoroso: Quaisquer variáveis GET ou POST que não estão explicitamente especificadas em suas regras de validação são automaticamente eliminadas. Portanto, ao definir como a entrada de formulário é manipulada, a primeira etapa sempre é definir os validadores para os campos de entrada do formulário.

Como o formulário ContactInput contém três campos, é necessário definir validadores para cada um desses campos. Veja a seguir como se parecem as regras de validação em $WASP_ROOT/app/modules/Default/validate/Contact.xml (Lista 5):


Lista 5. O validador de Default/ContactAction
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Default/config/validators.xml"
>
  <ae:configuration>

    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>name</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Name is missing</error>
          <error>ERROR: Name is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="email">
        <arguments>
          <argument>email</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Email address is missing</error>
          <error>ERROR: Email address is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="string">
        <arguments>
          <argument>message</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Message body is missing</error>
          <error>ERROR: Message body is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>

A configuração na Lista 5 configura dois validadores para três campos de formulário:AgaviStringValidators para os campos name e message , e um AgaviEmailValidator para o campo email . Em específico, observe os blocos <error> que acompanham cada validador; esses blocos definem mensagens de erro para diferentes cenários de falha e são um componente principal para algo que verão um pouco mais adiante (acredite, vale a pena esperar!).

Supondo que os dados de entrada passem pela validação, o Agavi procurará por um método executeWrite() na ContactAction que define o que deve ser feito com a entrada inserida com POST. Neste caso, o necessário será formatar a entrada em uma mensagem de e-mail e enviá-la para um endereço de e-mail designado. Portanto, a segunda etapa é incluir um método executeWrite() para a ContactAction que faz exatamente isso, como em Lista 6:


Lista 6. A definição de Default/ContactAction com um novo método executeWrite()
            
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  ...

  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    $name = $rd->getParameter('name');
    $email = $rd->getParameter('email');
    $message = $rd->getParameter('message');
    $subject = 'Contact form submission';
    $to = 'webmaster@wasp.example';
    if (@mail($to, $subject, $message, "From: $name <$email>\r\n")) {
      return 'Success';
    } else {
      return 'Error';
    }
  }
}
?>

Dependendo da transmissão ou não da mensagem de e-mail, o Agavi processará a ContactSuccessView ou a ContactErrorView. Você não precisa de nada chamativo nesses modelos de visualização: uma mensagem simples indicando o resultado do envio do formulário será suficiente. Veja a seguir como se parece o $WASP_ROOT/app/modules/Default/templates/ContactSuccess.php:

Sua mensagem foi enviada com êxito!

E aqui como se parece o $WASP_ROOT/app/modules/Default/templates/ContactError.php:

Ocorreu um erro. Sua mensagem não pôde ser enviada. Tente novamente mais tarde.

E pronto! Experimente e veja o que você acha.


Usando o Filtro de Preenchimento de Formulário

Agora, se você tiver um bom olho, perceberá algo: na forma como as coisas estão configuradas neste momento, o aplicativo mostra o mesmo comportamento (renderizando a ContactErrorView) se houver um erro ao enviar a mensagem de e-mail, ou se um ou mais campos do formulário falharem na validação de entrada. No mundo real, é bastante provável que você desejaria que a resposta do aplicativo para esses dois casos fosse diferente. Mais especificamente, se a entrada do usuário falha na validação, você geralmente deseja que o aplicativo renderize novamente o mesmo formulário com os campos inválidos em destaque, para que o usuário possa corrigi-los e submeter novamente o formulário.

O Agavi acompanha uma ferramenta que é quase mágica, tamanha a facilidade com que ela permite que você faça o mencionado acima: o AgaviFormPopulationFilter. Para vê-lo em ação, volte para sua ContactAction (Lista 3) e inclua o código em Lista 7 nela:


Lista 7. A definição Default/ContactAction com o FPF ativado
            
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
   ...

  public function handleError(AgaviRequestDataHolder $rd)
  {
    return 'Input';
  }
}
?>

O comportamento padrão do Agavi quando falha a validação de um formulário é renderizar a Visualização de Erro correspondente. Incluindo um método handleError() na Ação com o mesmo nome de uma visualização diferente substitui este comportamento. O código acima diz ao Agavi para renderizar novamente a ContactInputView se houver falha na validação de algum dos campos do formulário.

Agora, volte para o formulário "Contact Us" em seu navegador da Web e tente submetê-lo com dados vazios ou inválidos. Em vez da ContactErrorView, o Agavi deverá renderizar novamente a ContactInputView com mensagens de erro abaixo dos campos inválidos, como em A Figura 3:


Figura 3. O formulário de contrato do WASP, com erros de entrada destacados
Captura de tela do formulário de contato do WASP, com eroos de entrada destacados

Este é o trabalho manual do AgaviFormPopulationFilter. Quando há falha na validação de um ou mais campos de entrada de um formulário, esta ferramenta se encarrega da exibição automática das mensagens de erro para os campos inválidos. Você notará que ela também se encarrega de colocar valores das entradas válidas de volta no formulário para que o usuário não precise digitá-las novamente. As próprias mensagens de erro são recuperadas dos validadores (lembra-se dos blocos <error> que apontei na página anterior?).

O AgaviFormPopulationFilter é ativado por padrão em um aplicativo Agavi. Você pode desativá-lo, ou alterar seu comportamento ajustando os parâmetros em $WASP_ROOT/app/config/global_filters.xml. Veja a seguir um exemplo de como se parece a configuração padrão:

<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 xmlns="http://agavi.org/agavi/config/parts/filters/1.0">

  <ae:configuration context="web">
    <filters>
      <filter name="FormPopulationFilter" class="AgaviFormPopulationFilter">

        <!-- only run for request method "write" (=POST on web)
        by default (can be changed at runtime, of course) -->
        <!-- if you omit this, it will never run -->
        <ae:parameter name="methods">
          <ae:parameter>write</ae:parameter>
        </ae:parameter>

        <!-- only run for output type "html" (so it doesn't break on,
         say, JSON data) -->
        <!-- if you omit this, it will run for all output types -->
        <ae:parameter name="output_types">
          <ae:parameter>html</ae:parameter>
        </ae:parameter>

        <!-- error message insertion rules -->
        <!-- they are run in sequence; once the first one matched,
         execution stops -->
        <!--
          errors that belong to more than one field (e.g. date validator)
          can be handled using "multi_field_error_messages"
          "normal" errors are handled through "field_error_messages"
          errors that yield no match and those that have no corresponding
          field are inserted using rules defined in "error_messages".
        -->

        <!-- for all field error messages. -->
        <ae:parameter name="field_error_messages">
          <!-- ${htmlnsPrefix} is either empty (for HTML) or something like
          "html:" for XHTML documents with xmlns="..." notation. Always use this,
          makes your code more bullet proof. XPath needs the namespaces when the document
          is namespaced -->

          <!-- all input fields that are not checkboxes or radios, and
          all textareas -->
          <ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox'
           or @type='radio')] | self::${htmlnsPrefix}textarea">
            <!-- if this rule matched, then the node found by the rule is our
             starting point for inserting the error message(s). -->

            <!-- can be any of "before", "after" or "child" (to insert as prev,
            next sibling or last child) -->
            <ae:parameter name="location">after</ae:parameter>
            <!-- a container groups all errors for one element. ${errorMessages}
            is a string containing all errors (see below) -->
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <!-- this defines the HTML for each individual error message;
            those are then put into the container. ${errorMessage} is the
            error message string -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>

          <!-- all other inputs - note how we select the parent element and
          insert ourselves as last child of it -->
          <ae:parameter name="parent::*">
            <ae:parameter name="location">child</ae:parameter>
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>

        <!--
        <ae:parameter name="multi_field_error_messages">
        </ae:parameter>
        -->

        <!-- everything that did not match any of the rules above,
        or errors that do not belong to a field -->
        <ae:parameter name="error_messages">
          <!-- insert before the element -->
          <!-- that can be an input, or a form, if the error does not belong
          to a field or didn't match anywhere else -->
          <ae:parameter name="self::*">
            <ae:parameter name="location">before</ae:parameter>
            <!-- no container here! we just insert paragraph elements -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>
      </filter>

      ...
    </filters>
  </ae:configuration>
</ae:configurations>

Como o AgaviFormPopulationFilter se encarrega dos erros na validação de entrada, agora você também pode atualizar o $WASP_ROOT/app/modules/Default/templates/ContactError.php para informar:

Ocorreu um erro ao enviar sua mensagem. Tente novamente mais tarde.


Integrando Agavi com Doctrine

Agora que você sabe como os formulários trabalham no Agavi, veja algo um pouco mais complicado. No exemplo anterior, a Ação simplesmente converteu a entrada do usuário em uma mensagem de e-mail e a enviou usando a função mail() do PHP. Entretanto, um outro requisito comum é ler ou gravar a entrada do usuário em um armazém de dados, em um arquivo ou em uma tabela de banco de dados. Como o Agavi é fornecido com conectores para a maioria dos sistema de banco de dados comuns, isso é razoavelmente fácil de realizar.

Assim que você consegue um banco de dados, você também precisa de modelos para interagir com ele. Os modelos tornam possível gerenciar, manipular e executar cálculos nos dados. Apesar de você conseguir gravar seus próprios Modelos, uma forma mais fácil é gerá-los automaticamente usando um mapeador relacional de objeto, como por exemplo o Doctrine ou o Propel. Nesta série de artigos, você usará o Doctrine, que é bastante popular por sua flexibilidade e facilidade de uso.

Para ilustrar isso, crie algumas novas Ações, permitindo que os vendedores listem seus veículos usados para venda no aplicativo WASP, e os compradores visualizem essas listas e enviem consultas de compra para o revendedor. As listas submetidas serão armazenadas em um banco de dados MySQL e se tornarão visíveis e acessíveis no site do WASP após serem aprovadas pelo moderador.

Antes de escrever qualquer código de Ação para executar as tarefas acima, será necessário fazer com que o Doctrine e o Agavi conversem entre si. As seguintes etapas descrevem o processo:

Etapa 1: Criar o banco de dados do aplicativo

Para começar, inicie seu prompt de comandos MySQL e crie um banco de dados novo para o aplicativo:

shell>mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 59
Server version: 5.1.28-rc-community MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE wasp;
Query OK, 1 row affected (0.00 sec)

Enquanto estiver nele, defina uma conta de usuário com os privilégios de acesso para apenas este banco de dados. essa é uma boa prática de segurança, pois ela garante que seus outros bancos de dados permaneçam seguros mesmo se esta conta de usuário estiver comprometida.

mysql> GRANT ALL ON wasp.* TO wasp@localhost IDENTIFIED BY 'wasp';
Query OK, 1 row affected (0.00 sec)

Agora, crie algumas tabelas para armazenar as listas de veículos, como a seguir:

mysql> USE wasp;
Database changed

mysql> CREATE TABLE IF NOT EXISTS `listing` (
    ->   `RecordID` int(10) unsigned NOT NULL AUTO_INCREMENT,
    ->   `RecordDate` date NOT NULL,
    ->   `OwnerName` varchar(255) NOT NULL,
    ->   `OwnerTel` varchar(25) DEFAULT NULL,
    ->   `OwnerEmail` text NOT NULL,
    ->   `VehicleManufacturerID` int(11) NOT NULL,
    ->   `VehicleModel` varchar(255) NOT NULL,
    ->   `VehicleYear` year(4) NOT NULL,
    ->   `VehicleColor` varchar(30) NOT NULL,
    ->   `VehicleMileage` int(11) NOT NULL,
    ->   `VehicleIsFirstOwned` tinyint(1) NOT NULL,
    ->   `VehicleAccessoryBit` int(11) NOT NULL,
    ->   `VehicleIsCertified` tinyint(1) NOT NULL,
    ->   `VehicleCertificationDate` date DEFAULT NULL,
    ->   `VehicleSalePriceMin` int(11) NOT NULL,
    ->   `VehicleSalePriceMax` int(11) NOT NULL,
    ->   `VehicleSalePriceIsNegotiable` tinyint(1) NOT NULL DEFAULT '0',
    ->   `Note` text,
    ->   `OwnerCity` varchar(255) NOT NULL,
    ->   `OwnerCountryID` int(11) NOT NULL,
    ->   `DisplayStatus` tinyint(1) NOT NULL,
    ->   `DisplayUntilDate` date DEFAULT NULL,
    ->   PRIMARY KEY (`RecordID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

Este é o armazém de dados principal para o aplicativo. Ele contém detalhes dos veículos listados para venda, incluindo informações como o fabricante e o modelo do veículo, ano de fabricação, cor, quilometragem atual, local e preço de venda. O registro de cada veículo também inclui dois campos administrativos, DisplayStatus e DisplayUntilDate, que controlam se a lista aparece no site do WASP e por quanto tempo.

Observe que esta tabela usa referências de chaves externas nos dois campos, VehicleManufacturerID e OwnerCountryID, que especifica o fabricante do veículo e o país atual do proprietário, respectivamente. Siga em frente e crie as tabelas de origem para essas referências de chaves:

mysql> CREATE TABLE IF NOT EXISTS `manufacturer` (
    ->   `ManufacturerID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `ManufacturerName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`ManufacturerID`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

mysql> CREATE TABLE IF NOT EXISTS `country` (
    ->   `CountryID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `CountryName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`CountryID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

Enquanto estiver nele, preencha essas tabelas de origem também com alguns registros de exemplo:

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`)
      VALUES(1, 'Ferrari');
Query OK, 1 row affected (0.06 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`)
      VALUES(2, 'Porsche');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`)
      VALUES(3, 'BMW');
Query OK, 1 row affected (0.00 sec)


mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(1, 'United States');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(2, 'United Kingdom');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(3, 'India');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(4, 'Singapore');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(5, 'Germany');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(6, 'France');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(7, 'Italy');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(8, 'Spain');
Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
      VALUES(9, 'Hungary');
Query OK, 1 row affected (0.00 sec)

Etapa 2: Fazer download e incluir bibliotecas Doctrine para o aplicativo

Assim que o banco de dados estiver configurado, a próxima etapa é fazer o download das bibliotecas Doctrine e incluí-las em seu aplicativo. Para instalar o Doctrine, visite sua página inicial, faça o download e descompacte o archive do código de origem e copie o conteúdo do diretório lib/ do archive para $WASP_ROOT/libs/doctrine/. Localize os links para o Web site do Doctrine em Recursos. Este artigo usa o Doctrine V. 1.1.

shell> cd /usr/local/apache/htdocs/wasp/libs
shell> mkdir doctrine
shell> cp -R /tmp/Doctrine-1.1.1/lib/* doctrine/

Etapa 3: Criar e incluir modelos Doctrine para o aplicativo

A próxima etapa é gerar modelos Doctrine para seu aplicativo. O Doctrine pode fazer isso para você automaticamente, por meio de seu método generateModelsFromDb() . Primeiro, crie um diretório temporário para os seus modelos de saída:

shell> cd /tmp
shell> mkdir models

Em seguida, crie um script PHP simples que usa o Doctrine para gerar automaticamente os modelos para os objetos do banco de dados criados anteriormente. A Lista 8 é um exemplo de um script como esse, /tmp/doctrine-gen.php (para obter detalhes sobre como isso funciona, consulte o manual do Doctrine, com link em 1Recursos):


Lista 8. Um script PHP para gerar modelos Doctrine
            
<?php
// include main Doctrine class file
// change this per your system
include '/usr/local/apache/htdocs/wasp/libs/doctrine/Doctrine.php';
spl_autoload_register(array('Doctrine', 'autoload'));

// create Doctrine manager
$manager = Doctrine_Manager::getInstance();

// create database connection
$conn = Doctrine_Manager::connection('mysql://wasp:wasp@localhost/wasp', 'doctrine');

// auto-generate models
Doctrine::generateModelsFromDb('models', array('doctrine'),
 array('generateTableClasses' => true));
?>

Execute este script usando o interpretador PHP a partir da linha de comandos:

shell> php doctrine-gen.php

O Doctrine é iniciado para gerar modelos correspondentes às suas tabelas do banco de dados. Assim que o script for executado, verifique dentro de /tmp/models/ e /tmp/models/generated/, e você deverá ver algo como Figura 4:


Figura 4. Modelos Doctrine gerados automaticamente
Captura de Tela com lista de Modelos Doctrine gerados automaticamente

Os arquivos mostrados na Figura 4 são classes representando seus objetos de banco de dados. As classes dentro do diretório /tmp/models/generated/ são classes base geradas pela Doutrina, enquanto aquelas dentro do /tmp/models são classes-filhas que se pode usar para incluir funcionalidade as classes base functionality. Para ilustrar, verifique dentro do /tmp/models/generated/BaseListing.php, e você verá um objeto Doctrine cujas propriedades correspondem aos campos da tabela do banco de dados MySQL:

<?php

/**
 * BaseListing
 *
 * This class has been auto-generated by the Doctrine ORM Framework
 *
 * @property integer $RecordID
 * @property date $RecordDate
 * @property string $OwnerName
 * @property string $OwnerTel
 * @property string $OwnerEmail
 * @property integer $VehicleManufacturerID
 * @property string $VehicleModel
 * @property integer $VehicleYear
 * @property string $VehicleColor
 * @property integer $VehicleMileage
 * @property integer $VehicleIsFirstOwned
 * @property integer $VehicleAccessoryBit
 * @property integer $VehicleIsCertified
 * @property date $VehicleCertificationDate
 * @property integer $VehicleSalePriceMin
 * @property integer $VehicleSalePriceMax
 * @property integer $VehicleSalePriceIsNegotiable
 * @property string $Note
 * @property string $OwnerCity
 * @property integer $OwnerCountryID
 * @property integer $DisplayStatus
 * @property date $DisplayUntilDate
 *
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
abstract class BaseListing extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('listing');
        $this->hasColumn('RecordID', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'unsigned' => 1,
         'primary' => true, 'autoincrement' => true));
        $this->hasColumn('RecordDate', 'date', null,
         array('type' => 'date', 'notnull' => true));
        $this->hasColumn('OwnerName', 'string', 255,
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerTel', 'string', 25,
         array('type' => 'string', 'length' => 25));
        $this->hasColumn('OwnerEmail', 'string', null,
         array('type' => 'string', 'notnull' => true));
        $this->hasColumn('VehicleManufacturerID', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleModel', 'string', 255,
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('VehicleYear', 'integer', null,
         array('type' => 'integer', 'notnull' => true));
        $this->hasColumn('VehicleColor', 'string', 30,
         array('type' => 'string', 'length' => 30, 'notnull' => true));
        $this->hasColumn('VehicleMileage', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsFirstOwned', 'integer', 1,
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleAccessoryBit', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsCertified', 'integer', 1,
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleCertificationDate', 'date', null,
         array('type' => 'date'));
        $this->hasColumn('VehicleSalePriceMin', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceMax', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceIsNegotiable', 'integer', 1,
         array('type' => 'integer', 'length' => 1, 'default' => '0', 'notnull' => true));
        $this->hasColumn('Note', 'string', null, array('type' => 'string'));
        $this->hasColumn('OwnerCity', 'string', 255,
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerCountryID', 'integer', 4,
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('DisplayStatus', 'integer', 1,
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('DisplayUntilDate', 'date', null,
         array('type' => 'date'));
    }

}
?>

Essa classe BaseListing é estendida pela classe Listing no /tmp/models/Listing.php, que atualmente é semelhante ao seguinte:

<?php

/**
 * Listing
 *
 * This class has been auto-generated by the Doctrine ORM Framework
 *
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
class Listing extends BaseListing
{

}
?>

Essa classe-filha, atualmente vazia, é o local apropriado para incluir quaisquer modelos customizados ou propriedades que você desejar. As classes-filha também são o local para definir relacionamentos entre modelos—uma tarefa que deve ser realizada manualmente. Para ilustrar, atualize a classe Listing vazia acima com o seguinte código (Lista 9):


Lista 9. O modelo Listing estendida
            
<?php
class Listing extends BaseListing
{
    public function setUp()
    {
        $this->hasOne('Manufacturer', array(
                'local' => 'VehicleManufacturerID',
                'foreign' => 'ManufacturerID'
            )
        );
        $this->hasOne('Country', array(
                'local' => 'OwnerCountryID',
                'foreign' => 'CountryID'
            )
        );
    }
}
?>

O código da Lista 9 especifica que cada Lista possui um Fabricante e um País.

Você também deve especificar o relacionamento reverso nos modelos Fabricante e País, conforme abaixo (Listas 10 e na 11):


Lista 10. O modelo Country estendido
            
<?php
class Country extends BaseCountry
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'CountryID',
                'foreign' => 'OwnerCountryID'
            )
        );
    }
}
?>


Lista 11. O modelo Manufacturer estendido
            
<?php
class Manufacturer extends BaseManufacturer
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'ManufacturerID',
                'foreign' => 'VehicleManufacturerID'
            )
        );
    }
}
?>

Localize um link para o manual do Doctrine, o qual explica questões internas dos modelos Doctrine e relacionamentos de modelos com maiores detalhes, em Recursos.

Para incluir esses modelos gerados para seu aplicativo Agavi, copie-os para o diretório $WASP_ROOT/app/lib/doctrine sob a raiz do aplicativo:

shell> cd /usr/local/apache/htdocs/wasp/app/lib
shell> mkdir doctrine
shell> cp /tmp/models/* doctrine/
shell> cp /tmp/models/generated/* doctrine/

Ao final desta etapa, as bibliotecas Doctrine são instaladas em $WASP_ROOT/app/libs/doctrine, enquanto os modelos são instalados em $WASP_ROOT/app/lib/doctrine.

Etapa 4: Configure o Agavi para o trabalho com o Doctrine

A etapa final (etapas, na verdade) está relacionada a informar o Agavi sobre seus modelos Doctrine e a configurar seu aplicativo para usar o adaptador Doctrine do Agavi para consultas de banco de dados. Isso envolve uma série de etapas:

Para carregar automaticamente a classe Doctrine principal, edite $WASP_ROOT/app/config/autoload.xml e inclua uma entrada para ela, como em Lista 12:


Lista 12. Configuração Agavi para carregamento automático de Doctrine
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0"
 xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 parent="%core.system_config_dir%/autoload.xml">
  <ae:configuration>

    ...
    <autoload name="Doctrine">
     %core.app_dir%/../libs/doctrine/Doctrine.php
    </autoload>

  </ae:configuration>
</ae:configurations>

Edite o arquivo de configuração do aplicativo principal em $WASP_ROOT/app/config/settings.xml e ative o suporte ao banco de dados em seu aplicativo Agavi, como em Lista 13:


Lista 13. Configuração Agavi para ativar o suporte ao banco de dados
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 xmlns="http://agavi.org/agavi/config/parts/settings/1.0">
  <ae:configuration>
    ...
    <settings>
      <setting name="app_name">WASP</setting>

      <setting name="available">true</setting>
      <setting name="debug">false</setting>

      <setting name="use_database">true</setting>
      <setting name="use_logging">false</setting>
      <setting name="use_security">true</setting>
      <setting name="use_translation">false</setting>
    </settings>

  </ae:configuration>

  ...
</ae:configurations>

Edite o arquivo de configuração do banco de dados do aplicativo em $WASP_ROOT/app/config/databases.xml e configure o Doctrine como o adaptador de banco de dados padrão. Configure o DSN para o banco de dados MySQL e configure também o caminho ($WASP_ROOT/app/lib/doctrine) para os modelos Doctrine instalados na Etapa 3 (Lista 14). Isso assegura que o Agavi carregará automaticamente os modelos necessários.


Lista 14. Configuração Agavi para o DSN do Doctrine
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">

  <ae:configuration>
    <databases default="doctrine">
      
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://wasp:wasp@localhost/wasp</ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/doctrine</ae:parameter>
      </database>
      
    </databases>
  </ae:configuration>

</ae:configurations>


Recuperando Registros do Banco de Dados

Agora que as bases para comunicação entre Agavi, Doctrine e MySQL estão preparadas, você poderá escrever uma ViewAction que recuperará e exibirá listagens individuais a partir do banco de dados MySQL. Primeiro, preencha manualmente a tabela listing com alguns registros de exemplo; isso tornará mais fácil o teste de sua ação em seus estágios iniciais de desenvolvimento:

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel,
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor,
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified,
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax,
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus,
      DisplayUntilDate) VALUES  (1, '2009-06-08', 'John Doe', '00123456789876',
      'john@wasp.example.com', 2, 'Boxster', 2005, 'Yellow', 15457, 1, 23, 1,
      '2008-01-01', 35000, 40000, 1, 'Well cared for. In good shape, no scratches
      or bumps. Has prepaid annual service contract till 2009.', 'London', 2,
      1, '2009-10-15');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel,
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor,
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified,
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax,
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus,
      DisplayUntilDate) VALUES (2, '2009-06-08', 'Jane Doe', '00987654321236',
      'jane@wasp.example.com', 2, '911 Turbo', 2003, 'Black', 17890, 1, 23, 1,
      '2008-06-19', 17000, 25000, 1, '', 'Cambridge', 2, 1, '2009-10-15');
Query OK, 1 row affected (0.00 sec)

Agora, inclua a funcionalidade necessária para o WASP seguindo o processo usual, conforme a seguir:

Etapa 1: Criar Classes Marcadoras

As listagens de veículos podem ser certamente consideradas um componente independente da funcionalidade do aplicativo WASP e como tal, colocar Ações e Visualizações relacionadas a este componente em um módulo separado. Inicie seu script de construção Agavi e crie um novo módulo, conforme abaixo:

shell> agavi module-create
...
Module name: Listing

Enquanto estiver nele, inclua uma nova DisplayAction para manipular a exibição de listagens individuais. Para vincular esta Ação com duas Visualizações, DisplayErrorView e DisplaySuccessView, forneça os seguintes valores quando solicitados:

shell> agavi action-wizard
...
Module name: Listing
Action name: Display
Space-separated list of views to create for Display [Success]: Error Success

Agora, o Agavi gerará os arquivos de classes necessários e os colocará nos locais corretos.

Etapa 2: Definir Rotas

Inclua uma nova rota que faça referência à sua Ação recém-criada, como em (Lista 15):


Lista 15. A definição de rota de Listing/DisplayAction
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      ...
      <!-- action for listing pages "/listing" -->
      <route name="listing" pattern="^/listing" module="Listing">
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
      </route>

    </routes>
  </ae:configuration>
</ae:configurations>

Esta definição de rota espera que uma variável id seja incluída como parte do pedido GET de URL, indicado pelo uso de um grupo de captura na definição da rota. Esta variável representa o identificador exclusivo para a lista de veículos, correspondendo ao campo de chave primária listing.RecordID no banco de dados MySQL. Lembre-se de que você precisará incluir esta variável no validador da DisplayAction para que ela passe pelo filtro de validação de entrada do Agavi.

Este também é o primeiro exemplo que você viu de uma rota aninhada. Em uma definição de rota aninhada, a rota interna herda o padrão correspondido pela rota externa, e pode, então, posteriormente modificar e incluir neste padrão. Este recurso é extremamente conveniente ao implementar a funcionalidade CRUD, em que URLs geralmente possuem uma base comum mas sufixos diferentes, conforme a seguir:

/object/display/23
/object/add
/object/edit/23
/object/delete/23

Usando a definição de rota acima, as URLs contendo o padrão /listing serão correspondidas primeiro pela rota externa. Então, o Agavi examinará o restante do padrão e, dependendo do que ele contiver, decidir qual das rotas-filha oferece a melhor correspondência e direcionar o pedido para a Ação dessa rota. Claro, a definição acima contém apenas uma única rota-filha no momento...mas tenha um pouco de paciência, pois você incluirá mais nela muito em breve.

Etapa 3: Definir Regras de Validação

Como apenas uma variável de entrada é transmitida para DisplayAction, a validação é muito simples—tudo o que você precisa é um AgaviNumberValidator (Lista 16):


Lista 16. O validador de Listing/DisplayAction
            
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Listing/config/validators.xml"
>
  <ae:configuration>

    <validators method="read">
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="type">int</ae:parameter>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>

Etapa 4: Gravar Código da Ação

With routing and validation handled, the next step is to specify the View for the DisplayAction. Since the DisplayAction will only handle GET requests, you must specify an executeRead() method with the name of the View to be generated. Lista 17 shows what the Action looks like:


Lista 17. The Listing/DisplayAction definition
            
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{

  public function getDefaultViewName()
  {
    return 'Success';
  }

  public function executeRead(AgaviRequestDataHolder $rd)
  {
    return 'Success';
  }
}
?>

Etapa 5: Gravar Código da Visualização

Agora vamos para a parte fácil desta seção: configuração do DisplaySuccessView para exibição de listagens de veículos individuais. Lista 18 mostra como se parece a Visualização:


Lista 18. A definição de Listing/DisplaySuccessView
            
<?php
class Listing_DisplaySuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'View Listing');
    $id = $rd->getParameter('id');
    $q = Doctrine_Query::create()
          ->from('Listing l')
          ->leftJoin('l.Manufacturer m')
          ->leftJoin('l.Country c')
          ->where('l.RecordID = ?', $id);
      $result = $q->fetchArray();
      if (count($result) == 1) {
        $this->setAttribute('listing', $result[0]);
        return 'Success';
      } else {
        return $this->createForwardContainer(
         AgaviConfig::get('actions.error404_module'),
         AgaviConfig::get('actions.error404_action'));
      }
  }
}
?>

As primeiras linhas do método executeHtml() configuram o modelo de Visualização e recuperam o valor da variável de entrada $_GET['id']. Este valor é, então, interpolado em uma consulta do Doctrine que tenta localizar uma lista correspondente no banco de dados (para obter detalhes sobre a sintaxe da consulta do Doctrine, consulte Recursos para obter um link para a página do manual correspondente). Se a consulta retornar um único registro como resultado, este resultado será designado para a variável de modelo $t['listing'] como uma matriz associativa. Se nenhuma correspondência, ou múltiplas correspondências, forem localizadas, a Visualização encaminha automaticamente para a ação Error404 padrão do aplicativo.

Também é uma boa idéia nesse momento configurar a DisplayErrorView (Listing 19) para especificar qual comportamento deve ocorrer quando a validação da entrada falhar.


Lista 19. A definição de Listing/DisplayErrorView
            
<?php

class Listing_DisplayErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    return $this->createForwardContainer(
     AgaviConfig::get('actions.error404_module'),
     AgaviConfig::get('actions.error404_action'));
  }
}

?>

Muito simples—apenas encaminhe para a ação padrão Error404 novamente.

Finalmente, configure o modelo DisplaySuccess para exibir as informações recebidas do banco de dados (Lista 20):


Lista 20. O modelo Listing/DisplaySuccess
            
<h3>FOR SALE: <?php printf('%d %s %s (%s)',
 $t['listing']['VehicleYear'], $t['listing']['Manufacturer']['ManufacturerName'],
 ucwords(strtolower($t['listing']['VehicleModel'])),
 ucwords(strtolower($t['listing']['VehicleColor']))); ?></h3>

  <div id="container">
    <div id="specs">
      <table cellspacing="5">
        <tr>
          <td class="key">Listing ID: </td>
          <td class="value"><?php echo $t['listing']['RecordID']; ?></td>
        </tr>
        <tr>
          <td class="key">Year of manufacture: </td>
          <td class="value"><?php echo
           $t['listing']['VehicleYear']; ?></td>
        </tr>
        <tr>
          <td class="key">Color: </td>
          <td class="value"><?php echo
            $t['listing']['VehicleColor']; ?></td>
        </tr>
        <tr>
          <td class="key">Mileage: </td>
          <td class="value"><?php echo
           $t['listing']['VehicleMileage']; ?></td>
        </tr>
        <tr>
          <td class="key">Ownership: </td>
          <td class="value"><?php echo
           ($t['listing']['VehicleIsFirstOwned'] == 1) ? 'First owner' :
           'Multiple owners'; ?></td>
        </tr>
        <tr>
          <td class="key">Certification: </td>
          <td class="value"><?php echo
           ($t['listing']['VehicleIsCertified'] == 1) ? 'Certified, as of '
           . date('d M Y', strtotime($t['listing']['VehicleCertificationDate']))
           : 'Not certified'; ?></td>
        </tr>
        <tr>
          <td class="key">Accessories: </td>
          <td class="value">
          <?php echo ($t['listing']['VehicleAccessoryBit'] == 0) ? 
           'None <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 1) ? 
           'Power steering <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 2) ? 
           'Power windows <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 4) ? 
           'Audio system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 8) ? 
           'Video system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 16) ? 
           'Keyless entry system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 32) ? 
           'GPS <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 64) ? 
           'Alloy wheels <br/>' : null; ?>
          </td>
        </tr>
        <tr>
          <td class="key">Location: </td>
          <td class="value"><?php echo
           $t['listing']['OwnerCity']; ?>,
          <?php echo $t['listing']['Country']['CountryName']; ?></td>
        </tr>
        <tr>
          <td class="key">Sale price: </td>
          <td class="value"> $<?php echo
           $t['listing']['VehicleSalePriceMin']; ?> - $<?php echo
           $t['listing']['VehicleSalePriceMax']; ?> <?php echo
           ($t['listing']['VehicleSalePriceIsNegotiable'] == 1) ? '(negotiable)'
           : null; ?></td>
        </tr>
        <tr>
          <td class="key">Description: </td>
          <td class="value"><?php echo
           $t['listing']['Note']; ?></td>
        </tr>
      </table>
  </div>
</div>

Este modelo lê os vários elementos do registro do banco de dados como chaves da matriz associativa $t['listing'] (lembra-se da chamada para setAttribute() na DisplaySuccessView?) e apresenta estas informações como uma tabela HTML organizadamente formatada. Para vê-lo em ação, abra seu navegador da Web e tente acessar os dois registros de exemplo incluídos anteriormente no banco de dados MySQL, verificando http://wasp.localhost/listing/display/1 ou http://wasp.localhost/listing/display/2. Você deverá ver algo como Figura 5.


Figura 5. Uma lista de veículos
Captura de Tela de uma amostra de lista de veículos

Observe que, se você tentar passar para a URL um ID inválido ou ausente, o Agavi encaminhará você para a página de erro "Page not found" padrão, como na Figura 6. Essa é sua DisplayErrorView em ação


Figura 6. A página de erro gerada em um ID de lista inválido
Captura de tela do de erro de página gerado em um ID de lista inválido

Conclusão

Outros Artigos desta Série

E isso é tudo para este segundo artigo. Nas últimas páginas, aprofundamos ainda mais no mundo do Agavi, explicando como aceitar e validar a entrada do usuário submetida por meio de formulários da Web e apresentando o filtro de preenchimento de formulário do Agavi. Também mostrei como possibilitar o acesso ao banco de dados em um aplicativo Agavi, criando um banco de dados MySQL, usando Doctrine ORM para gerar modelos, e usando esses modelos para conexão e execução de consultas, no banco de dados.

Seu aplicativo de exemplo agora está um pouco mais inteligente que antes: ele possui um formulário de contato, ele sabe como enviar e-mail e ele pode puxar as listas de veículos de um banco de dados MySQL. Entretanto, ainda não há uma interface apropriada para os usuários incluírem diretamente suas próprias listas no banco de dados. Esta função, juntamente com algumas outras, serão discutidas em detalhes na terceira parte desta série.

Consulte Download para obter todo o código implementado neste artigo. Recomendo que o obtenha, comece a brincar com ele e, talvez, tente incluir novas coisas nele. Garanto que não quebrará nada e, com certeza, complementará seu aprendizado. Até a próxima vez...boas experiências!



Download

DescriçãoNomeTamanhoMétodo de download
Archive of the WASP app with functions to datewasp-02.zip3797KBHTTP

Informações sobre métodos de download


Recursos

Aprender

Obter produtos e tecnologias

Discutir

Sobre o autor

Photo of Vikram Vaswani

Vikram Vaswani é o fundador e Presidente da Melonfire (este link reside fora de ibm.com), uma empresa de serviços de consultoria com conhecimento especial sobre ferramentas e tecnologias de software livre. Ele também é o autor dos livros PHP Programming Solutions (este link reside fora de ibm.com) e PHP: A Beginners Guide (este link reside fora de ibm.com).

Ajuda para Relatar Abuso

Relatar abuso

Obrigado. Esta entrada foi sinalizada para atenção do moderador.


Ajuda para Relatar Abuso

Relatar abuso

Falha no envio do Relatório de abuso. Tente novamente mais tarde.


developerWorks: Registre-se


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

Selecione seu nome de exibição

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

(Deve possuir de 3 a 31 caracteres.)


Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Classificar este artigo

Comentários

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Software livre
ArticleID=422928
ArticleTitle=Introdução à Programação MVC com Agavi, Parte 2: Inclua Formulários e Suporte de Banco de Dados com Agavi e Doctrine
publish-date=07282009
author1-email=vikram.melonfire@gmail.com
author1-email-cc=

Conheça a IBM da sua cidade

Virtual Branch Office Brasil

A IBM está mais perto do que você imagina!


Tags

Help
Use o campo de pesquisa para encontrar todos os tipos de conteúdo no My developerWorks com essa tag.

Use a barra de rolagem para ver mais ou menos tags.

Tags populares mostra as principais tags para esta zona de conteúdo em particular (por exemplo, Java technology, Linux, WebSphere).

Minhas tags mostra suas tags para esta zona de conteúdo em particular (por exemplo, Java technology, Linux, WebSphere).

Use o campo de pesquisa para localizar todos os tipos de conteúdo no Meu developerWorks com essa tag. Tags populares mostra as tags principais para essa zona de conteúdo particular (por exemplo, tecnologia Java, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere). Minhas tags mostra as suas tags para essa zona de conteúdo em particular (por exemplo, tecnologia Java, Linux, WebSphere).