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.
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
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
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
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. |
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
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.
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
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
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!
| Descrição | Nome | Tamanho | Método de download |
|---|---|---|---|
| Archive of the WASP app with functions to date | wasp-02.zip | 3797KB | HTTP |
Informações sobre métodos de download
Aprender
- Intro to MVC Programming with Agavi: Open a whole new world with Agavi (Vikram Vaswani, developerWorks, julho de 2009): Na primeira de uma série de cinco partes, explore os conceitos básicos da estrutura do Agavi e compare-a com outras estruturas. Leia tudo sobre visualizações, ações, modelos e rotas e comece a construir seu próprio aplicativo Agavi escalável.
- A visualização Web site oficial do Agavi (este link reside fora de ibm.com) e na o Guia do Agavi (este link reside fora de ibm.com): Aprenda mais sobre essa estrutura de aplicativo PHP5 escalável que segue o paradigma MVC.
- A visualização documentação da API do Agavi (este link reside fora de ibm.com): Dê uma olhada mais de perto nas classes base do Agavi.
- Doctrine ORM para PHP - Introdução aos Modelos (este link reside fora de ibm.com): Compreenda como gerar e usar modelos Doctrine.
- Doctrine
ORM para PHP - Trabalhando com Modelos (este link reside fora de ibm.com): Visualizar consultas de amostra usando Doctrine.
- A visualização blog do Agavi (este link reside fora de ibm.com): Leia as notícias sobre o Agavi.
- A visualização listas de correspondência e os canais IRC do Agavi (este link reside fora de ibm.com): Participe da comunidade do Agavi, faça perguntas e receba respostas.
- Área de Software Livre no
developerWorks: Aprenda sobre o Desenvolvimento de Aplicativos com CakePHP (Duane O'Brien, junho de 2009), Outras Estruturas PHP (Duane O'Brien, outubro de 2007) e Como Construir um Wiki PHP (Duane O'Brien, fevereiro de 2007).
- Certificação em XML da IBM: Saiba como você pode se tornar um Desenvolvedor Certificado pela IBM em XML e tecnologias relacionadas.
- Biblioteca Técnica de XML: Consulte a Zona de XML do developerWorks para obter uma ampla gama de artigos técnicos e dicas, tutoriais, padrões e IBM Redbooks.
- Eventos Técnicos e Webcasts do developerWorks: Fique atualizado com a tecnologia nessas sessões.
- A visualização livraria de tecnologia: Procure livros sobre esses e outros tópicos técnicos.
- Podcasts do developerWorks: Ouça entrevistas e discussões interessantes para desenvolvedores de software.
Obter produtos e tecnologias
- A visualização estrutura do Agavi (este link reside fora de ibm.com): Faça download dessa estrutura de aplicativo padrão PHP5-MVC para obter um código extensível mais limpo.
- A visualização Servidor de banco de dados MySQL (este link reside fora de ibm.com): Faça o download de um banco de dados de software livre popular.
- A visualização pacote Doctrine ORM (este link reside fora de ibm.com): Faça download deste mapeador relacional de objeto para PHP.
- Versões de avaliação de produtos IBM: Faça download ou explore as avaliações on-line no IBM SOA Sandbox e entre em contato com ferramentas de desenvolvimento de aplicativos e produtos de middleware do DB2®, Lotus®, Rational®, Tivoli®e WebSphere®.
Discutir
- Fóruns de Discussão da Zona XML: Participe de qualquer uma das diversas discussões relacionadas a XML.
- Blogs do developerWorks: Verifique esses blogs e envolva-se na comunidade do developerWorks.

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).