Прочитав первую статью серии, вы познакомились с инфраструктурой Agavi, узнав об ее уникальных возможностях для создания масштабируемых Web-приложений, соответствующих современным стандартам. На примере приложения WASP (Web Automobiles Sales Platform) были продемонстрированы основные шаги создания нового проекта Agavi, рассмотрена структура каталогов, а также скрипт сборки системы. Кроме того, вы познакомились с базовыми компонентами любого Agavi-приложения: обработчиками (Actions), представлениями (Views) и маршрутами (Route) и увидели в действии некоторые из встроенных валидаторов пользовательского ввода.
Разумеется, Agavi может с успехом использоваться для обслуживания статических Web-страниц, однако её лучшие качества проявляются при создании более сложных приложений. Именно этому будет посвящена статья: вы узнаете о получении, валидации и обработке содержимого Web-форм, а также о соединении с базой данных MySQL.
Создание Web-форм для приложения на основе Agavi
Во-первых, необходимо напомнить, что представляет собой главная страница приложения WASP (рисунок 1).
Рисунок 1. Главная страница приложения WASP
В предыдущей статье мы создали обработчик нажатия на две ссылки, указывающие на статические страницы. Теперь необходимо сделать то же самое в отношении ссылки "Контакты". Как следует из названия, эта ссылка указывает на форму ввода, которую необходимо заполнить для связи с автодилером. В целом процедура создания нужного обработчика выглядит аналогично той, которая использовалась для StaticContentAction. Единственная разница заключается в коде самого класса.
Выполните скрипт сборки Ant, указав следующие входные параметры:
shell> agavi action-wizard ... Module name: Default Action name: Contact Space-separated list of views to create for Contact [Success]: Input Error Success |
Скрипт создаст класс ContactAction и три представления. С первыми двумя (ContactSuccessView и ContactErrorView) вы уже знакомы, они показываются пользователю в случае успешного и аварийного завершения обработки соответственно. Третье представление (ContactInputView) нам ранее не встречалось. Оно представляет собой Web-форму, предлагающую пользователю ввести необходимую информацию.
Далее добавьте в файл $WASP_ROOT/app/routing.xml маршрут к обработчику ContactAction, как показано в листинге 1.
Листинг 1. Описание маршрута 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>
...
<!-- объявление обработчика для формы с контактными данными ("/contact") -->
<route name="contact" pattern="^/contact$" module="Default"
action="Contact" />
</routes>
</ae:configuration>
</ae:configurations>
|
При этом следует обновить базовый шаблон, находящийся в файле $WASP_ROOT/app/templates/Master.php, добавив гиперссылку "Контакты" в главном меню. Она должна указывать на только что созданный маршрут (листинг 2).
Листинг 2. Базовый шаблон
<!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>
...
|
Теперь откройте исходный код класса ContactAction (файл $WASP_ROOT/app/modules/Default/actions/ContactAction.class.php) и укажите, что по умолчанию после обработки всех запросов типа GET должно выводиться представление ContactInputView. Для этого служат методы getDefaultViewName() и executeRead(), показанные в листинге 3.
Листинг 3. Обработчик Default/ContactAction
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
public function getDefaultViewName()
{
return 'Input';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
return 'Input';
}
}
?>
|
Далее измените шаблон в файле $WASP_ROOT/app/modules/Default/templates/ContactInput.php, добавив в него простую Web-форму, как показано в листинге 4. Стили CSS, использующиеся в этой форме, можно найти в архиве с исходным кодом к статье (см. раздел Загрузка).
Листинг 4. Шаблон 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>
|
После этого, обновив главную страницу приложения WASP в браузере и перейдя по ссылке "Контакты", вы должны увидеть Web-форму, показанную на рисунке 2.
Рисунок 2. Контактная форма приложения WASP
Однако это только полдела. Далее необходимо запрограммировать поведение приложения после того, как пользователь заполнил форму и отправил данные на сервер.
Валидация пользовательских данных
После отправки пользователем Web-формы браузер передает ее содержимое серверу при помощи запроса типа POST. Как вы помните, Agavi использует очень строгий фильтр для входных данных: любые параметры запросов типа GET и POST, которые не были объявлены в правилах валидации, будут проигнорированы. Таким образом, обработка пользовательского ввода должна начинаться с определения валидаторов для всех полей Web-формы.
Форма ContactInput содержит три поля, для которых должны быть объявлены валидаторы. Соответствующие правила находятся в файле $WASP_ROOT/app/modules/Default/validate/Contact.xml, приведенном в листинге 5.
Листинг 5. Валидаторы данных для обработчика 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>
|
В конфигурационном файле, показанном в листинге 5, описаны два валидатора, проверяющих три поля ввода: AgaviStringValidator используется для полей name и message, а AgaviEmailValidator — для поля email. Обратите внимание, что для каждого валидатора указан блок <error>, в котором задаются сообщения для различных ошибок валидации. Эти конструкции будут совершенно незаменимы чуть ниже.
Если данные успешно прошли валидацию, Agavi сделает попытку вызвать метод executeWrite() класса ContactAction, который отвечает за обработку POST-запроса. В нашем случае обработка заключается в формировании сообщения e-mail, которое затем отправляется по определенному адресу. Метод executeWrite(), выполняющий эти действия, показан в листинге 6.
Листинг 6. Обработчик Default/ContactAction, содержащий метод 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';
}
}
}
?>
|
Далее Agavi выведет представление ContactSuccessView или ContactErrorView в зависимости от того, был передан текст сообщения или нет. Шаблоны этих представлений очень просты, их цель заключается только в выводе простого сообщения, информирующего пользователя о результате операции. Например, шаблон в файле $WASP_ROOT/app/modules/Default/templates/ContactSuccess.php выглядит следующим образом:
Your message was successfully sent! |
В свою очередь, в файле $WASP_ROOT/app/modules/Default/templates/ContactError.php находится следующее сообщение:
There was an error. Your message could not be sent. Please try again later. |
Это все, что касается валидации, так что вы можете опробовать её в деле.
Работа с фильтром заполнения Web-форм
Внимательные читатели, вероятно, заметили, что приложение реагирует одинаково на разные типы ошибок, т.е. одно представление (ContactErrorView) выводится как в случае проблемы с отправкой e-mail, так и при отрицательном результате валидации пользовательского ввода. В реальности приложение должно по-разному обрабатывать подобные ситуации. В частности, при возникновении ошибок валидации, как правило, необходимо отобразить ту же форму ввода, указав на проблемные поля, чтобы пользователь мог их исправить и повторно отправить данные на сервер.
В состав Agavi входит компонент AgaviFormPopulationFilter, позволяющий реализовать подобное поведение с практически волшебной легкостью. Чтобы увидеть его в работе, вернитесь к классу ContactAction (см. листинг 3) и добавьте фрагмент кода, приведенный в листинге 7.
Листинг 7. Обработчик Default/ContactAction с включенным фильтром заполнения
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
...
public function handleError(AgaviRequestDataHolder $rd)
{
return 'Input';
}
}
?>
|
По умолчанию в случае отрицательного результата валидации Agavi выводит соответствующее представление Error View. Подобное поведение можно изменить, добавив в обработчик метод handleError(), возвращающий имя другого представления. Например, во фрагменте кода, приведенном выше, указывается, что в случае ошибок в любом из полей формы должно выводиться представление ContactInputView.
Теперь вернитесь к форме "Контакты" в браузере, введите в некоторые поля некорректные значения или оставьте их пустыми и попробуйте отправить её на сервер. В результате Agavi вновь выведет ContactInputView вместо ContactErrorView и добавит сообщения об ошибках рядом с соответствующими полями (рисунок 3).
Рисунок 3. Контактная форма WASP с выделением полей, содержащих некорректные или пустые значения
Это поведение реализуется классом AgaviFormPopulationFilter, который автоматически выводит сообщения об ошибках в случае, если одно или несколько полей формы не проходят валидацию. Кроме того, обратите внимание, что он также сохраняет значения полей, успешно прошедших валидацию, поэтому пользователю не приходится вводить их повторно. Сами сообщения об ошибках берутся из объявлений валидаторов (помните элементы <error> в предыдущем разделе?).
AgaviFormPopulationFilter используется в приложениях Agavi по умолчанию. Вы можете отключить либо изменить его поведение при помощи настроек в файле $WASP_ROOT/app/config/global_filters.xml. Пример конфигурации по умолчанию приведен ниже.
<?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">
<!-- По умолчанию фильтр должен работать только для
метода "write" (POST-запросов). Этот параметр может быть изменен
на этапе выполнения -->
<!-- Если этот параметр не задан, фильтр не будет выполняться никогда -->
<ae:parameter name="methods">
<ae:parameter>write</ae:parameter>
</ae:parameter>
<!-- Фильтр должен выполняться только для вывода в формате HTML
(это позволит избежать ошибок при использовании, например, JSON) -->
<!-- Если этот параметр не задан, фильтр будет выполняться для всех форматов -->
<ae:parameter name="output_types">
<ae:parameter>html</ae:parameter>
</ae:parameter>
<!-- Правила вставки сообщений об ошибках -->
<!-- Эти правила применяются последовательно,
процесс останавливается после первого применения -->
<!--
Ошибки, относящиеся к нескольким полям (например, в случае
валидации дат), могут обрабатываться при помощи блока
"multi_field_error_messages", а "обыкновенные" ошибки -
при помощи "field_error_messages". Ошибки, для которых не заданы правила,
а также те, которые не относятся ни к одному полю, обрабатываются
в соответствии с правилами, заданными в блоке "error_messages".
-->
<!-- Правила вставки всех сообщений -->
<ae:parameter name="field_error_messages">
<!-- ${htmlnsPrefix} является либо пустой строкой (в случае HTML),
либо строкой вида "html:" для документов XHTML с конструкцией
xmlns="...". Использование этого выражения повышает надежность вашего кода.
В частности, XPath требует префиксов пространств имен, если они заданы
в документе. -->
<!-- Ошибки, относящиеся ко всем полям (в том числе текстовым областям),
кроме флагов и переключателей -->
<ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox'
or @type='radio')] | self::${htmlnsPrefix}textarea">
<!-- При применении этого правила сообщение(я) об ошибке
вставляется относительно вершины, соответствующей выражению выше -->
<!-- Значением этого параметра могут быть "before", "after" или "child".
При этом сообщение вставляется в качестве предыдущей, следующей или последней
вершины того же уровня. -->
<ae:parameter name="location">after</ae:parameter>
<!-- Контейнер служит для группировки всех ошибок,
относящихся к одному элементу. ${errorMessages}
представляет собой строку, содержащую все ошибки (см. ниже) -->
<ae:parameter name="container">
<![CDATA[<div class="errors">${errorMessages}</div>]]>
</ae:parameter>
<!-- Этот блок описывает HTML-код каждого сообщения об ошибке.
Затем сообщения помещаются в контейнер. Строка с текстом сообщений
содержится в переменной ${errorMessage} -->
<ae:parameter name="markup">
<![CDATA[<p class="error">${errorMessage}</p>]]>
</ae:parameter>
</ae:parameter>
|
<!-- Все остальные элементы. Обратите внимание на выбор
родительского элемента и вставку сообщения в конец списка
его дочерних элементов. -->
<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>
-->
<!-- Этот блок описывает вывод сообщений для ошибок,
для которых не применимо ни одно из правил выше, а также ошибок,
которые не относятся к полям ввода. -->
<ae:parameter name="error_messages">
<!-- Вставка перед элементом -->
<!-- Элемент может представлять собой поле ввода или форму
(если ошибка не относится к полю ввода или к ней не применимо
ни одно из правил) -->
<ae:parameter name="self::*">
<ae:parameter name="location">before</ae:parameter>
<!-- Сообщения вставляются в виде абзацев без использования контейнеров -->
<ae:parameter name="markup">
<![CDATA[<p class="error">${errorMessage}</p>]]>
</ae:parameter>
</ae:parameter>
</ae:parameter>
</filter>
...
</filters>
</ae:configuration>
</ae:configurations>
|
Поскольку об обработке ошибок валидации позаботится AgaviFormPopulationFilter, в файле $WASP_ROOT/app/modules/Default/templates/ContactError.php можно просто оставить следующую строку:
There was an error sending your message. Please try again later. |
Теперь, разобравшись с формами ввода, пришло время перейти к более сложным аспектам работы с Agavi. В предыдущем примере обработчик просто преобразовывал информацию, полученную от пользователя, в сообщение e-mail и отправлял его при помощи функции mail() в PHP. Однако в реальных приложениях часто приходится сохранять пользовательский ввод в некотором хранилище данных, как правило, либо в файле, либо в базе данных. В этом нет ничего сложного, поскольку в состав Agavi входят компоненты для подключения ко многим распространенным СУБД.
Приложения, работающие с базой данных, должны включать классы модели (Model) для взаимодействия с ней. Модели отвечают за управление, манипулирование и выполнение вычислений над данными. Разумеется, вы можете создавать свои собственные модели, однако проще генерировать их автоматически с использованием технологии объектно-реляционного отображения, такой как Doctrine или Propel. В статьях этой серии мы будем использовать Doctrine, популярность которой объясняется ее простотой и гибкостью.
Для иллюстрации работы с базой данных мы создадим несколько новых обработчиков, позволяющих продавцам предоставлять списки их подержанных автомобилей для продажи через WASP, а покупателям — просматривать эти списки и отправлять дилерам запросы на покупку. Списки автомобилей будут храниться в базе данных MySQL и будут доступны через Web-интерфейс WASP сразу после одобрения модератором.
Перед тем, как переходить к написанию кода обработчиков, необходимо связать Agavi и Doctrine, выполнив описанные ниже действия.
Шаг 1: создание базы данных приложения
Вначале перейдите в консоль управления MySQL и создайте новую, пустую базу данных приложения.
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) |
Далее создайте учетную запись пользователя, имеющего права доступа только к этой базе данных. Подобная практика гарантирует, что другие базы данных будут в безопасности в случае несанкционированного доступа к этой записи.
mysql> GRANT ALL ON wasp.* TO wasp@localhost IDENTIFIED BY 'wasp'; Query OK, 1 row affected (0.00 sec) |
Затем создайте несколько таблиц для хранения информации об автомобилях, как показано ниже.
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)
|
Эта таблица будет хранить основные данные приложения. В ней будет содержаться подробная информация о каждом продаваемом автомобиле, включающая его марку, модель, год выпуска, цвет, пробег, местонахождение и цену. Записи в этой таблице также включают два административных поля, DisplayStatus и DisplayUntilDate, указывающие, должен ли автомобиль выводиться на сайте WASP и если да, то в течение какого промежутка времени.
Обратите внимание на два внешних ключа: VehicleManufacturerID и OwnerCountryID, которые указывают на производителя автомобиля и страну его настоящего владельца. Далее необходимо создать соответствующие таблицы для хранения этой информации.
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)
|
Создав таблицы, можно сразу наполнить их тестовыми данными.
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)
|
Шаг 2: загрузка библиотек Doctrine и их добавление в приложение
После создания базы данных следует загрузить библиотеки Doctrine и добавить их к нашему приложению. Зайдите на официальный сайт проекта, загрузите Doctrine, распакуйте архив с исходным кодом и скопируйте содержимое директории lib/ в каталог $WASP_ROOT/libs/doctrine/. Ссылка на Web-сайте Doctrine приведена в разделе Ресурсы. Мы будем использовать Doctrine версии 1.1.
shell> cd /usr/local/apache/htdocs/wasp/libs shell> mkdir doctrine shell> cp -R /tmp/Doctrine-1.1.1/lib/* doctrine/ |
Шаг 3: создание моделей Doctrine и их добавление в приложение
Следующий шаг заключается в генерации моделей Doctrine для приложения. Это можно сделать автоматически при помощи метода generateModelsFromDb(). Вначале следует создать временную директорию для сгенерированных моделей, как показано ниже.
shell> cd /tmp shell> mkdir models |
Далее создайте простой скрипт на PHP для автоматической генерации моделей на основе ранее созданной базы данных при помощи Doctrine. Пример такого скрипта приведен в листинге 8 (его также можно найти в файле /tmp/doctrine-gen.php). Подробное объяснение принципов работы скрипта можно найти в руководстве по Doctrine, ссылка на которое приведена в разделе Ресурсы.
Листинг 8. PHP-скрипт для генерации моделей данных Doctrine
<?php
// Включение главного класса Doctrine
// Подставьте путь к соответствующему файлу в вашей системе
include '/usr/local/apache/htdocs/wasp/libs/doctrine/Doctrine.php';
spl_autoload_register(array('Doctrine', 'autoload'));
// Создание менеджера Doctrine
$manager = Doctrine_Manager::getInstance();
// Открытие соединения с базой данных
$conn = Doctrine_Manager::connection('mysql://wasp:wasp@localhost/wasp', 'doctrine');
// Генерация моделей
Doctrine::generateModelsFromDb('models', array('doctrine'),
array('generateTableClasses' => true));
?>
|
Выполните этот скрипт из командной строки при помощи интерпретатора PHP.
shell> php doctrine-gen.php |
В результате Doctrine начнет генерировать модели, соответствующие таблицам базы данных. После окончании работы скрипта в директориях /tmp/models/ и /tmp/models/generated/ должны находиться файлы, подобные показанным на рисунке 4.
Рисунок 4. Автоматическая генерация моделей Doctrine
Файлы, показанные на рисунке 4, представляют собой классы, соответствующие объектам базы данных. В директории /tmp/models/generated/ находятся базовые классы, сгенерированные Doctrine, а в директории /tmp/models – их дочерние классы, которые можно использовать для расширения функциональности базовых классов. В качестве примера откройте файл /tmp/models/generated/BaseListing.php, и вы увидите объект Doctrine, свойства которого соответствуют полям таблицы базы данных 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'));
}
}
?>
|
Этот класс наследуется классом Listing, находящимся в файле /tmp/models/Listing.php. На настоящий момент он выглядит следующим образом:
<?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
{
}
?>
|
В этот изначально пустой дочерний класс следует помещать все необходимые приложению методы и свойства. В дочерних классах также должны описываться отношения между моделями (этот этап выполняется вручную). В качестве иллюстрации добавим в класс Listing фрагмент кода, приведенный в листинге 9.
Листинг 9. Расширение класса Listing
<?php
class Listing extends BaseListing
{
public function setUp()
{
$this->hasOne('Manufacturer', array(
'local' => 'VehicleManufacturerID',
'foreign' => 'ManufacturerID'
)
);
$this->hasOne('Country', array(
'local' => 'OwnerCountryID',
'foreign' => 'CountryID'
)
);
}
}
?>
|
В коде, показанном в листинге 9, указывается, что каждая запись типа Listing связана с одной записью типа Manufacturer и одной записью типа Country.
Кроме того, необходимо добавить зеркальные связи в модели Manufacturer и Country, как показано в листингах 10 и 11.
Листинг 10. Расширение класса Country
<?php
class Country extends BaseCountry
{
public function setUp()
{
$this->hasMany('Listing', array(
'local' => 'CountryID',
'foreign' => 'OwnerCountryID'
)
);
}
}
?>
|
Листинг 11. Расширение класса Manufacturer
<?php
class Manufacturer extends BaseManufacturer
{
public function setUp()
{
$this->hasMany('Listing', array(
'local' => 'ManufacturerID',
'foreign' => 'VehicleManufacturerID'
)
);
}
}
?>
|
В разделе Ресурсы приведена ссылка на руководство по Doctrine, в котором объясняется внутреннее устройство моделей и связей между ними.
Чтобы добавить эти сгенерированные модели в наше Agavi-приложение скопируйте их в директорию $WASP_ROOT/app/lib/doctrine.
shell> cd /usr/local/apache/htdocs/wasp/app/lib shell> mkdir doctrine shell> cp /tmp/models/* doctrine/ shell> cp /tmp/models/generated/* doctrine/ |
Итак, на данный момент библиотеки Doctrine находятся в директории $WASP_ROOT/app/libs/doctrine, а модели данных - в $WASP_ROOT/app/lib/doctrine.
Шаг 4: настройка Agavi для работы с Doctrine
В завершение необходимо настроить Agavi для работы с моделями Doctrine, а также сконфигурировать приложение для использования адаптера Doctrine, входящего в состав Agavi, при выполнении запросов к базе данных. Для этого придется выполнить ряд действий.
Вначале отредактируйте файл $WASP_ROOT/app/config/autoload.xml и добавьте в него запись, показанную в листинге 12, которая необходима для автозагрузки главного класса Doctrine.
Листинг 12. Настройка Agavi для автоматической загрузки 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>
|
Далее откройте конфигурационный файл $WASP_ROOT/app/config/settings.xml и включите поддержку базы данных для приложения Agavi, как показано в листинге 13.
Листинг 13. Конфигурация Agavi, включающая настройки для работы с базой данных
<?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>
|
После этого откройте файл настроек работы с базой данных ($WASP_ROOT/app/config/databases.xml) и укажите Doctrine в качестве адаптера по умолчанию. Задайте DSN базы данных и укажите путь к моделям данных Doctrine, созданным на шаге 3 ($WASP_ROOT/app/lib/doctrine), как показано в листинге 14. В результате Agavi будет автоматически загружать модели данных приложения.
Листинг 14. Конфигурация Agavi, включающая DSN 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>
|
Выборка записей из базы данных
Теперь, настроив взаимодействие между Agavi, Doctrine и MySQL, можно переходить к созданию обработчика ViewAction, который будет запрашивать автомобили из базы данных для их последующего отображения на Web-странице. Вначале следует поместить в таблицу listing несколько тестовых записей, которые помогут в тестировании обработчика на начальных стадиях разработки.
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)
|
Далее мы перейдем к разработке необходимой функциональности приложения. Для этого предстоит выполнить следующие шаги.
Шаг 1: создание классов-заготовок
Управление данными о продаваемых автомобилях можно рассматривать в качестве самостоятельного компонента приложения WASP, поэтому соответствующие обработчики и представления следует вынести в собственный модуль. Для этого выполните скрипт сборки приложения и создайте новый модуль, как показано в примере ниже.
shell> agavi module-create ... Module name: Listing |
Кроме того, создайте новый обработчик DisplayAction, который будет управлять отображением данных об автомобилях. Этот обработчик должен быть связан с двумя представлениями: DisplayErrorView и DisplaySuccessView. Для этого задайте следующие параметры при выполнении скрипта:
shell> agavi action-wizard ... Module name: Listing Action name: Display Space-separated list of views to create for Display [Success]: Error Success |
В результате Agavi сгенерирует необходимые файлы классов и поместит их в соответствующие директории.
Добавьте новый маршрут, ссылающийся на только что созданный обработчик, как показано в листинге 15.
Листинг 15. Описания маршрута 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>
...
<!-- обработчик для выборки автомобилей ("/listing") -->
<route name="listing" pattern="^/listing" module="Listing">
<route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
</route>
</routes>
</ae:configuration>
</ae:configurations>
|
Группа захвата (capture group) в этом определении маршрута говорит о том, что URL GET-запросов должны включать обязательный параметр id. Эта переменная должна содержать уникальный идентификатор автомобиля, соответствующий первичному ключу listing.RecordID в базе данных. Не забудьте включить этот параметр в определение валидатора обработчика DisplayAction, иначе его значение будет отброшено фильтром Agavi.
На данном этапе нам впервые встретилось определение вложенного маршрута. В этом случае внутренний маршрут наследует шаблон URL внешнего маршрута с возможностью его дальнейшего уточнения. Эта возможность очень полезна при реализации операций CRUD, так как в этом случае URL, как правило, имеют общую часть, но разные окончания, как показано в примере ниже.
/object/display/23 /object/add /object/edit/23 /object/delete/23 |
Для URL, содержащих префикс /listing, будет сначала установлено соответствие внешнему маршруту. Затем Agavi проанализирует остальную часть описания маршрута и определит, какой из внутренних маршрутов точнее соответствует данному URL, после чего передаст запрос соответствующему обработчику. Маршрут в примере выше пока содержит только один внутренний маршрут, но очень скоро мы добавим еще несколько.
Шаг 3: описание правил валидации
В этом примере валидация будет крайне простой, поскольку в обработчик DisplayAction передается всего одна переменная запроса. Таким образом, нам будет достаточно одного валидатора типа AgaviNumberValidator (листинг 16).
Листинг 16. Валидатор данных для обработчика 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>
|
Шаг 4: написание кода обработчика
После описания маршрутов и правил валидации следует задать имя представления для обработчика DisplayAction. Этот класс обрабатывает только запросы типа GET, поэтому необходимо добавить метод executeRead(), возвращающий имя нужного представления (пример приведен в листинге 17).
Листинг 17. Обработчик Listing/DisplayAction
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
return 'Success';
}
}
?>
|
Шаг 5: написание кода представления
Теперь можно переходить к главному этапу: программированию представления DisplaySuccessView, служащему для вывода информации об автомобилях. Код представления показан в листинге 18.
Листинг 18. Представление 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'));
}
}
}
?>
|
Первые несколько строк метода executeHtml() настраивают шаблон представления, а также получают значение входного параметра $_GET['id']. Это значение далее передается в запрос Doctrine, целью которого является поиск автомобиля в базе данных (подробное описание синтаксиса запросов содержится в руководстве по Doctrine, ссылка на которое находится в разделе Ресурсы). Если результатом запроса является единственная запись, то она сохраняется в переменной шаблона $t['listing'] в виде ассоциативного массива. Если найдено несколько записей или не найдено ни одной, то управление автоматически передается стандартному обработчику Error404.
На этом этапе также имеет смысл создать представление DisplayErrorView, отвечающее за поведение приложения в случае отрицательного результата валидации (листинг 19).
Листинг 19. Представление 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'));
}
}
?>
|
Как видите, в этом нет ничего сложного: управление просто передается стандартному обработчику Error404.
Наконец, нам осталось создать шаблон DisplaySuccess для отображения информации, полученной из базы данных (листинг 20).
Листинг 20. Шаблон 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>
|
Этот шаблон считывает различные поля записи базы данных, представляющие собой ключи в ассоциативном массиве $t['listing'] (помните вызов setAttribute() в классе DisplaySuccessView?) , и выводит их в виде аккуратной таблицы на странице HTML. Для наглядной иллюстрации откройте Web-браузер и попробуйте найти тестовые записи, ранее добавленные в базу данных, обратившись по URL http://wasp.localhost/listing/display/1 или http://wasp.localhost/listing/display/2. Результат должен выглядеть подобно показанному на рисунке 5.
Рисунок 5. Вывод информации об автомобиле
Обратите внимание, что если вы попробуете передать неверный или несуществующий идентификатор, то Agavi автоматически перенаправит вас на страницу со стандартным сообщением "The page you requested could not be found" (запрашиваемая страница не найдена), показанную на рисунке 6. Это происходит благодаря представлению DisplayErrorView.
Рисунок 6. Страница, показываемая при попытке запроса с неверным идентификатором автомобиля
На этом мы заканчиваем вторую статью серии. Прочитав ее, вы глубже погрузились в мир Agavi, ознакомившись с обработкой и валидацией данных, отправленных при помощи Web-форм, а также фильтром автозаполнения. Кроме того, в статье рассказывалось о работе с базами данных в приложениях Agavi на примере БД MySQL, использовании ORM Doctrine для автоматической генерации моделей. Наконец, было продемонстрировано использование этих моделей для соединения с базой данных и выполнения запросов.
Возможности демонстрационного приложения, начатого в первой статье, были несколько расширены. Теперь оно содержит форму для обращения, умеет отправлять сообщения по e-mail, а также извлекать информацию об автомобилях из базы данных MySQL. Однако оно все еще не предоставляет удобного пользовательского интерфейса для добавления новых записей в базу данных. Эта возможность вместе с некоторыми другими будет подробно обсуждаться в третьей статье серии.
Ссылка на архив, включающий весь исходный код этой статьи, находится в разделе Загрузка. Я советую вам загрузить и поэкспериментировать с приложением, попробовав добавить в него различные функции. Так вы ничего не испортите, но это существенно поможет вам в изучении Agavi. До следующего раза, а пока – удачных экспериментов!
| Описание | Имя | Размер | Метод загрузки |
|---|---|---|---|
| Исходный код текущей версии приложения WASP | wasp-02.zip | 3797 KБ | HTTP |
Научиться
- Оригинал статьи: Introduction to MVC programming with Agavi, Part 2: Add forms and database support with Agavi and Doctrine (Викрам Васвани, developerWorks, июль 2009 г.). (EN)
- Прочитайте первую статью Введение в создание MVC-приложений при помощи Agavi. Часть 1: Откройте мир новых возможностей вместе с Agavi (Викрам Васвани, developerWorks, июль 2009 г.), в которой рассматриваются базовые понятия инфраструктуры Agavi в сравнении с альтернативными технологиями. В частности, вы узнаете о работе с представлениями, обработчиками, шаблонами, маршрутами, а также начнете создавать ваше первое масштабируемое Agavi-приложение. (EN)
- В статье Введение в создание MVC-приложений при помощи Agavi. Часть 3: Разработка механизма аутентификации и административных функций при помощи Agavi (Викрам Васвани, developerWorks, август 2009 г.) рассматриваются вопросы реализации дополнительных функций Web-платформы для продажи автомобилей, в том числе возможности добавления, удаления и редактирования объявлений. Из этой статьи вы узнаете об отделении административных функций от обычных при помощи механизма аутентификации (третья часть серии из пяти статей). (EN)
- В статье Введение в создание MVC-приложений при помощи Agavi. Часть 4: Создание поисковой машины с выводом результатов в XML, RSS или SOAP при помощи Agavi (Викрам Васвани, developerWorks, август 2009 г.) описывается реализация простого механизма поиска, а также использование различных форматов представления результатов, в том числе XML, RSS и SOAP (четвертая часть серии из пяти статей). (EN)
- В последней статье Введение в создание MVC-приложений при помощи Agavi. Часть 5: Добавление постраничного вывода, возможности загрузки файлов и специализированных валидаторов в приложение на основе Agavi (Викрам Васвани, developerWorks, сентябрь 2009 г.) рассматриваются завершающие штрихи к приложению Agavi, а именно: возможность загрузки файлов, хранение пользовательских данных в сессии, интеграция со сторонними библиотеками и создание специализированных валидаторов (пятая часть серии из пяти статей). (EN)
- Ознакомьтесь с руководством по Agavi на официальном сайте проекта: узнайте больше об этой масштабируемой инфраструктуре для создания приложений на PHP5 в соответствии с принципами MVC. (EN)
- Подробная информация о базовых классах инфраструктуры содержится в документации по API Agavi. (EN)
- Прочитайте главу руководства по Doctrine под названием ORM Doctrine для PHP: введение в модели данных, в которой рассказывается о генерации и использовании моделей Doctrine. (EN)
- Прочитайте главу руководства по Doctrine под названием ORM Doctrine для PHP: работа с моделями данных, в которой можно найти примеры запросов с использованием Doctrine. (EN)
- Узнайте о новостях из мира Agavi в блоге проекта. (EN)
-
Список рассылки и IRC-каналы Agavi: присоединяйтесь к сообществу Agavi, в котором вы сможете получить ответы на интересующие вас вопросы. (EN)
- В разделе Open Source на сайте developerWorks вы можете получить информацию о создании приложений при помощи CakePHP (Дуэйн О'Брайен, Duane O'Brien, июнь 2009 г.), других инфраструктур PHP (Дуэйн О'Брайен, октябрь 2007 г.), а также о создании wiki-приложения на PHP (Дуэйн О'Брайен, февраль 2007 г.). (EN)
-
Сертификация по XML корпорации IBM: узнайте, как стать сертифицированным разработчиком IBM в области XML и связанных с ним технологий. (EN)
-
Технические мероприятия и Web-трансляции developerWorks: в этих разделах можно получить самую актуальную информацию о современных технологиях. (EN)
- Обратитесь к магазину технической литературы, в котором представлены книги на данную и другие темы. (EN)
- Слушайте интервью и обсуждения вопросов, интересующих разработчиков программного обеспечения, в трансляциях developerWorks. (EN)
Получить продукты и технологии
- Загрузите Agavi – инфраструктуру для создания приложения на PHP5 по принципам MVC, которая поможет повысить качество и упростит дальнейшее развитие ваших приложений. (EN)
- Загрузите MySQL - популярную бесплатную СУБД. (EN)
- Загрузите пакет Doctrine - инфраструктуру объектно-реляционного отображения для PHP. (EN)
- Загрузите ознакомительные версии продуктов IBM, а также опробуйте средства разработки приложений и промежуточное программное обеспечение IBM, в частности DB2®, Lotus®, Rational®, Tivoli® и WebSphere® на сайте online-тестирования IBM SOA Sandbox. (EN)
Обсудить
- Примите участие в обсуждении материала на форуме.
- Читайте блоги developerWorks и становитесь членами нашего сообщества. (EN)
Викрам Васвани (Vikram Vaswani) – основатель и президент консалтинговой фирмы Melonfire, специализирующейся на технологиях и инструментах с открытым исходным кодом. Также является автором книг Решения по программированию на PHP and Как сделать все что угодно с помощью PHP и MySQL .