Использование Zend_Amf и Adobe Flex SDK.
Опубликовано: 28.03.2009
|
Содержание
- Введение
- Рекомендации по началу изучения Flex
- Формат AMF
- Разработка приложения.
- Разработка приложения. Программирование веб-сервиса
- Разработка приложения. Flex
- Итоги
Введение
Одним из важных организационных моментов в разработке RIA является вопрос выбора и реализации механизма «общения» приложения в браузере с серверными компонентами. В данной статье, написанной по мотивам англоязычного руководства
Для тех кто не знает что такое Adobe Flex, или знаком с этой технологией «понаслышке», автор должен заметить, что знакомство стоит потраченного на него времени. Flex – это набирающая популярность технология разработки веб-ориентированных и прикладных (посредством Adobe AIR) интерфейсов и многофункциональных приложений. Достаточно часто Flex сравнивают с JavaScript, потому что время от времени эти технологии сменяют друг друга в решении каких-то задач по реализации сложных интерфейсов. Неплохой обзор на тему такого сравнения можно прочитать вот в этой статье:
Рекомендации по началу изучения Flex
Для тех кто знаком с этой технологией и интересуется только аспектами настройки Zend_Amf сервиса, автор предлагает пропустить данный раздел и перейти к части Разработка приложения.
Для начала знакомства с
Вплане инструментария, существует несколько подходов к разработке Flex приложений. О различных IDE, редакторах и ручной компиляции в swf файлы читатель сможет, при желании, без труда найти информацию самостоятельно. Однако автор настоятельно рекомендует использовать IDE от Adobe – Adobe Flex Builder (пример разрабатываемый в статье написан с использованием версии 3.0.2), потому что этот вариант наиболее комфортен. В случае если для разработки на PHP и Zend Framework читатель использует Zend Studio for Eclipse, то он может установить Adobe Flex Builder в виде плагина,- вероятно, это будет наиболее удобным вариантом.
Перед тем как приступить к чтению оставшихся частей статьи, автор должен заметить, что он предполагает знакомство читателя с тем, что такое Zend Framework, как его установить и использовать его компоненты и, также, имеет некоторый базовый опыт разработки приложений с использованием Adobe Flex.
Формат AMF
AMF – это бинарный формат передачи данных, позволяющий вызывать методы серверных объектов удаленно из Flex приложения. Использование этого формата более предпочтительно чем использование XML или JSON с точки зрения скорости передачи данных. Также он более удобен, так как на сервере не придется делать никаких дополнительных действий для преобразования объектов или данных, ими возвращаемых (которые в свою очередь тоже могут быть объектами) в XML или JSON.
PHP не поддерживает AMF формат по умолчанию, поэтому для реализации связки PHP-Flex необходимо использовать одно из существующих решений: php расширение AMFPHP, библиотека WebORB или компонент Zend_Amf. В данной статье будут рассматриваться аспекты реализации данной связки только с использованием компонента Zend_Amf.
Компонент Zend_Amf предназначен для работы с протоколом AMF и доступен как часть Zend Framework начиная с версии 1.7.
Разработка приложения
В качестве задания на разработку было решено выбрать задачу программирования интерфейса управления пользователями, данные о которых хранятся в базе данных. Приложение, разработанное в ходе статьи не претендует на какую либо применимость в реальных программных комплексах, единственное назначение этого приложения - показать на примере принципы использования Flex в связке с Zend_Amf.
Перед началом разработки необходимо произвести следующие подготовительные действия:
- Подготовка каталога для размещения файлов. Необходимо настроить веб-сервер так, чтобы каталог открывался по адресу http://zendamf/ (этот адрес использовался в приложении статьи, читатель может использовать любой другой, например http://localhost)
- Необходимо включить каталог с Zend Framework в indlude_path. Автор сделал это путем добавления полного пути к фреймворку в php.ini. Также возможно использование функции set_include_path непосредственно в скрипте Zend_Amf сервиса.
- Так как серверная компонента приложения работает с базой данных, необходимо её предварительно создать. Автор использовал в качестве СУБД MySql версии 5.0.45-community-nt, хотя, так как для работы с базой данных используется компонент Zend_Db, выбор СУБД может быть и другим. В качестве имени базы данных было использовано 'testdb'. Имя базы данных также может быть любым другим, разумеется.
- Для хранения пользователей нужно создать таблицу 'users' с четырьмя полями:
- id – Идентификатор пользователя, int(11)
- name — Имя пользователя, varchar(255)
- login — Логин пользователя, varchar(255)
- email — Адрес электронной почты пользователя, varchar(255)
- Добавить одну тестовую запись в таблицу с любыми данными.
Sql запросы на создание таблицы с тестовой записью для MySql:
CREATE TABLE `users` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
`login` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
INSERT INTO `users` VALUES (1, 'Alexander Steshenko', 'lcf', 'lcf@example.com');
Реализацию поставленной задачи можно условно разделить на две части: проектирование и программирование серверной части приложения, и разработка клиентской части на Flex.
Разработка приложения. Программирование веб-сервиса
Для работы с пользователями реализованы два класса: User — класс представляющий одного пользователя и класс Users, предоставляющий функционал по управлению пользователями. Функционал классов достаточно примитивен, и полностью понятен из тщательно прокомментированных исходных кодов
Файл User.php
<?php
/**
* Класс представляющий модель пользователя.
*
* @author Steshenko Alexander (http://lcf.name)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class User
{
/**
* Идентификатор пользователя
*
* @var integer
*/
public $id;
/**
* Имя пользователя
*
* @var string
*/
public $name;
/**
* Логин (никнейм) пользователя
*
* @var string
*/
public $login;
/**
* Адрес электронной почты
*
* @var string
*/
public $email;
/**
* Конструктор. В качестве необзатятельного параметра принимает
* массив с данными пользователя.
*
* @param array $userData данные пользователя
*/
public function __construct($userData = null)
{
if ($userData != null) {
$this->id = $userData['id'];
$this->name = $userData['name'];
$this->login = $userData['login'];
$this->email = $userData['email'];
}
}
}
Файл Users.php
<?php
/** User */
require_once 'User.php';
/**
* Класс для управления пользователями
*
* @author Steshenko Alexander (http://lcf.name)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Users
{
/**
* Подключение к бд
*
* @var Zend_Db_Adapter_Abstract
*/
protected $_db;
/**
* Конструктор. Получает из регистра и сохраняет
* для внутренних нужд ссылку на объект подключения к бд.
*
*/
public function __construct()
{
$this->_db = Zend_Registry::get('db');
}
/**
* Добавление нового пользователя
*
* @param User $user
*/
public function add(User $user)
{
$userData = array(
'name' => $user->name,
'login' => $user->login,
'email' => $user->email
);
$this->_db->insert('users', $userData);
}
/**
* Сохранение измененного пользователя в базе данных
*
* @param User $user
*/
public function save(User $user)
{
$userData = array(
'name' => $user->name,
'login' => $user->login,
'email' => $user->email
);
$this->_db->update('users', $userData, 'id = ' . $user->id);
}
/**
* Удаление пользователя по его идентификатору
*
* @param integer $userId
*/
public function deleteById($userId)
{
$this->_db->delete('users', 'id = ' . $userId);
}
/**
* Получение массива с пользователями
*
* @return array
*/
public function getAll()
{
$usersArray = $this->_db->fetchAll(
$this->_db->select()->from('users', '*')
);
$users = array();
foreach ($usersArray as $userData) {
$users[] = new User($userData);
}
return $users;
}
}
Главный файл серверной части приложения, service.php в котором используется Zend_Amf_Server также тщательно прокомментирован и выглядит следующим образом:
<?php
/**
* Zend_Amf сервис предоставляющий доступ к объекту Users
* Для управления пользователями
*
* @copyright 2009 Steshenko Alexander (http://lcf.name)
* @license http://www.zend.com/license/3_0.txt PHP License 3.0
*/
/*
* Подключение необходимых компонент Zend Framework
*/
/** Zend_Aml_Server */
require_once('Zend/Amf/Server.php');
/** Zend_Db */
require_once('Zend/Db.php');
/** Zend_Config */
require_once('Zend/Config.php');
/** Zend_Registry */
require_once('Zend/Registry.php');
/**
* Подключение объекта по управлению пользователями
*/
require_once('Users.php');
// Определяем настройки подключения к базе данных
$config = new Zend_Config(array(
'adapter' => 'pdo_mysql',
'params' => array(
'host' => 'localhost',
'username' => 'user',
'password' => 'pass',
'dbname' => 'testdb',
'driver_options'=> array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES UTF8')
)
));
// Создание объекта подключения к базе данных
$db = Zend_Db::factory($config);
// Сохранине объекта подключения в регистре
Zend_Registry::set('db', $db);
// Создание сервера Zend_Amf
$server = new Zend_Amf_Server();
// Добавляем класс к серверу
$server->setClass("Users");
// Связываем php класс User с одноименным action script классом User
$server->setClassMap("User", "User");
// И, наконец, позволяем AMF серверу сделать всю остальную работу
echo $server->handle();
Положив все три файла в каталог вебсервера, убедимся что компонент amf сервера работает. Для этого откроем в браузере страницу http://zendamf/service.php. Результат должен быть следующим:

Термин Endpoint в данном случае обозначает ресурс, которому могут быть адресованы сообщения.
Если результат другой и отображаются какие-либо ошибки, их желательно исправить до того как приступить к разработке клиентской части приложения.
Разработка приложения. Flex
Автор для наглядности демонстрации предмета статьи решил не рассматривать аспекты структурирования приложения на Flex и подходы к реализации тех или иных шаблонов проектирования. Поэтому весь основной код программы будет расположен в главном файле проекта — zendamf.mxml. В целом, структура каталогов и файлов представляет из себя следующее:

Назначение каждого файла будет рассмотрено ниже, так как логично будет начать описание процесса разработки с подготовки визуальной части приложения.
Flex приложение должно предоставлять следующие возможности по управлению серверными объектами:
- обеспечивать просмотр записей о всех пользователях
- предоставлять возможность добавления нового пользователя
- редактировать данные о пользователе
- удалять любого пользователя из списка
С помощью декларативного языка описания интерфейсов MXML и режима 'Design' в среде Flex Builder IDE, реализовать необходимый интерфейс не составит абсолютно никакого труда. У автора, он представляет из себя панель, содержащую таблицу для отображения данных, форму для добавления и пару управляющих кнопок. Читатель может собрать такой интерфейс с учетом личных пожеланий по внешнему виду, у автора же получилось следующее:

Тут стоит заметить, что автор использовал в качестве компонента для отображения таблицы с данными не обычный DataGrid, а его расширенный вариант DoubleClickDataGrid (класс которого описывается в файле /classes/controls/DoubleClickDataGrid.as), нужный для того чтобы редактирование элементов в таблице начиналась по двойному, а не по одинарному клику на ячейке. В принципе, это не является обязательным условием и носит чисто эстетический характер, поэтому пользователь может использовать обычный DataGrid. В таком случае, файл DoubleClickDataGrid.as из проекта можно удалить.
MXML описание получившейся панели:
<mx:Panel width="484"
height="400"
layout="vertical"
title="Управление пользователями"
paddingBottom="5"
paddingLeft="5"
paddingRight="5"
paddingTop="5"
horizontalAlign="center"
horizontalCenter="-5"
verticalCenter="17"
borderColor="#2A2929"
color="#F5F5F5">
<controls:DoubleClickDataGrid id="usersDataGrid"
width="446"
height="100%"
editable="true"
itemEditEnd="onSaveClick(event)"
color="#171717"
alternatingItemColors="[#DAD9D9, #FFFFFF]">
<controls:columns>
<mx:DataGridColumn headerText="Имя"
dataField="name"/>
<mx:DataGridColumn headerText="Логин"
dataField="login"/>
<mx:DataGridColumn headerText="Email"
dataField="email"/>
</controls:columns>
</controls:DoubleClickDataGrid>
<mx:HBox width="453">
<mx:Form color="#0B0B0B">
<mx:FormItem label="Имя"
horizontalAlign="left">
<mx:TextInput id="nameTextInput"/>
</mx:FormItem>
<mx:FormItem label="Логин">
<mx:TextInput id="loginTextInput"/>
</mx:FormItem>
<mx:FormItem label="Email">
<mx:TextInput id="emailTextInput"/>
</mx:FormItem>
<mx:Button label="Добавить пользователя"
click="onAddClick(event)"
color="#141414"/>
</mx:Form>
<mx:Button label="Удалить пользователя"
click="onDeleteClick(event)"
color="#141414"
textAlign="right"
width="172"/>
</mx:HBox>
</mx:Panel>
Далее автор решил рассмотреть создание конфигурационного файла с описанием расположения веб сервиса для разрабатываемого Flex приложения.
Файл services-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service id="amfphp-flashremoting-service" class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="zend">
<channels>
<channel ref="my-zend"/>
</channels>
<properties>
<source>*</source>
</properties>
</destination>
</service>
</services>
<channels>
<channel-definition id="my-zend" class="mx.messaging.channels.AMFChannel">
<endpoint uri="http://zendamf/service.php" class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
Читателю может потребоваться отредактировать строчку <endpoint uri="http://zendamf/service.php" class="flex.messaging.endpoints.AMFEndpoint" /> с учетом специфических настроек веб-сервера.
Данный конфигурационный файл подключается в приложение с помощью установки параметра компиляции -service с полным значением пути месторасположения конфигурационного service-config.xml файла. У автора эта строка имеет следующий вид: -services "D:\articles\zendamf\public\services-config.xml". В Flex Builder IDE такие параметры можно указывать в разделе Flex Compiler свойств проекта.
Для использования удаленного сервиса в главном файле проекта необходимо добавить его описание. Автор сделал это с помощью MXML:
<mx:RemoteObject id="remoteUsers"
destination="zend"
source="Users"
showBusyCursor="true">
<mx:method name="getAll"
result="onGetAllResult(event)"/>
</mx:RemoteObject>
Метод getAll – один из методов удаленного объекта (класса Users). Описывать таким образом все методы удаленного объекта не обязательно, однако может быть полезно, если необходимо определить обработчик для события result. В данном случае, автор таким образом определил обработчик onGetAllResult, для того чтобы назначить полученные методом данные как источник данных для DataGrid.
На сервере имеется еще один тип объектов - класс User, предназначенный для представления данных одного пользователя. (Объекты таких классов являются так называемыми Value Object, поэтому часто именуются как: UserVO,- возможно читатель сочтет удобным такой подход). Для Flex части приложения такой класс тоже должен быть описан, как модель для хранения данных о пользователе.
Файл /models/User.as:
package classes.models
{
[RemoteClass(alias="User")]
[Bindable]
public class User
{
public var id:int;
public var name:String;
public var login:String;
public var email:String;
}
}
Все что осталось сделать — написать функции обработчики для следующих событий:
- загрузка данных о пользователях с сервера
- нажатие кнопки удаления пользователя
- нажатие кнопки добавления пользователя
- завершение редактирования ячейки с данными о пользователе
Обработчики должны вызывать методы удаленного объекта (например remoteUsers.deleteById(user.id)) и читатель может вполне реализовать эти функции сам, либо изучить код автора, содержащий подробные комментарии:
<mx:Script>
<![CDATA[
import mx.controls.Button;
import classes.controls.DoubleClickDataGrid;
import mx.events.DataGridEvent;
import classes.models.User;
import mx.controls.DataGrid;
import mx.controls.dataGridClasses.DataGridColumn;
import mx.controls.Alert;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.collections.ArrayCollection;
/**
* Обработчик события DataGridEvent.ITEM_EDIT_END
*/
private function onSaveClick(event:DataGridEvent):void
{
// Получаем объект DataGrid
var dataGrid:DataGrid=DataGrid(event.target);
// Получаем значение ячейчки
var newPropertyValue:String=dataGrid.itemEditorInstance["text"];
// Получаем редактируемое свойство (name, email или login)
var dataField:String=event.dataField;
// Создаем объект User на основе данных _до_редактирования_
var user:User=User(event.itemRenderer.data)
// Сравниваем старое значение свойства с новым
if (newPropertyValue == user[dataField])
{
// Если данные не поменялись, выходим из функции
return;
}
// Устанавливаем значение нового свойства
user[dataField]=newPropertyValue;
// Сохранение пользователя, используя метод удаленного объекта
remoteUsers.save(user);
// Отображение окна с сообщением об успешном сохранении
Alert.show("Пользователь изменен", "Zend Amf Example");
}
/**
* Обработчик события ResultEvent.RESULT
*/
private function onGetAllResult(event:ResultEvent):void
{
// Устанавливаем данные возвращенные сервером в DataGrid
usersDataGrid.dataProvider=event.result as Array;
}
/**
* Обработчик события нажатия на кнопку добавления пользователя.
*/
private function onAddClick(event:Event):void
{
// Создаем объект User
var user:User=new User();
// Определяем его свойства
user.name=nameTextInput.text;
user.login=loginTextInput.text;
user.email=emailTextInput.text;
// Добавляем используя удаленный вызов метода
remoteUsers.add(user);
// Сбрасываем значения
nameTextInput.text="";
loginTextInput.text="";
emailTextInput.text="";
// Обновляем значения DataGrid
remoteUsers.getAll();
// Отображение окна с сообщением об успешном добавлении
Alert.show("Пользователь добавлен", "Zend Amf Example");
}
/**
* Обработчик события нажатия на кнопку удаления пользователя
*/
public function onDeleteClick(event:Event):void
{
// Берем пользователя который отмечен в DataGrid
var user:User=User(usersDataGrid.selectedItem);
// Вызываем метод удаленного объекта для удаления пользователя
// по его идентификатору
remoteUsers.deleteById(user.id);
// Обновляем данные DataGrid
remoteUsers.getAll();
// Отображение окна с сообщением об успешном удалении
Alert.show("Пользователь удален", "Zend Amf Example");
}
]]>
</mx:Script>
В заключение, автор приводит полный код файла zendamf.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application pageTitle="Zend AMF sample"
layout="absolute"
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:controls="classes.controls.*"
color="#000000"
backgroundGradientAlphas="[1.0, 1.0]"
backgroundGradientColors="[#878484, #A79797]"
borderColor="#8D9295"
creationComplete="remoteUsers.getAll()">
<mx:RemoteObject id="remoteUsers"
destination="zend"
source="Users"
showBusyCursor="true">
<mx:method name="getAll"
result="onGetAllResult(event)"/>
</mx:RemoteObject>
<!--
Adobe Flex & Zend_Amf Demo
Alexander Steshenko | Mar 2009
http://lcf.name/
-->
<mx:Script>
<![CDATA[
import mx.controls.Button;
import classes.controls.DoubleClickDataGrid;
import mx.events.DataGridEvent;
import classes.models.User;
import mx.controls.DataGrid;
import mx.controls.dataGridClasses.DataGridColumn;
import mx.controls.Alert;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.collections.ArrayCollection;
/**
* Обработчик события DataGridEvent.ITEM_EDIT_END
*/
private function onSaveClick(event:DataGridEvent):void
{
// Получаем объект DataGrid
var dataGrid:DataGrid=DataGrid(event.target);
// Получаем значение ячейчки
var newPropertyValue:String=dataGrid.itemEditorInstance["text"];
// Получаем редактируемое свойство (name, email или login)
var dataField:String=event.dataField;
// Создаем объект User на основе данных _до_редактирования_
var user:User=User(event.itemRenderer.data)
// Сравниваем старое значение свойства с новым
if (newPropertyValue == user[dataField])
{
// Если данные не поменялись, выходим из функции
return;
}
// Устанавливаем значение нового свойства
user[dataField]=newPropertyValue;
// Сохранение пользователя, используя метод удаленного объекта
remoteUsers.save(user);
// Отображение окна с сообщением об успешном сохранении
Alert.show("Пользователь изменен", "Zend Amf Example");
}
/**
* Обработчик события ResultEvent.RESULT
*/
private function onGetAllResult(event:ResultEvent):void
{
// Устанавливаем данные возвращенные сервером в DataGrid
usersDataGrid.dataProvider=event.result as Array;
}
/**
* Обработчик события нажатия на кнопку добавления пользователя.
*/
private function onAddClick(event:Event):void
{
// Создаем объект User
var user:User=new User();
// Определяем его свойства
user.name=nameTextInput.text;
user.login=loginTextInput.text;
user.email=emailTextInput.text;
// Добавляем используя удаленный вызов метода
remoteUsers.add(user);
// Сбрасываем значения
nameTextInput.text="";
loginTextInput.text="";
emailTextInput.text="";
// Обновляем значения DataGrid
remoteUsers.getAll();
// Отображение окна с сообщением об успешном добавлении
Alert.show("Пользователь добавлен", "Zend Amf Example");
}
/**
* Обработчик события нажатия на кнопку удаления пользователя
*/
public function onDeleteClick(event:Event):void
{
// Берем пользователя который отмечен в DataGrid
var user:User=User(usersDataGrid.selectedItem);
// Вызываем метод удаленного объекта для удаления пользователя
// по его идентификатору
remoteUsers.deleteById(user.id);
// Обновляем данные DataGrid
remoteUsers.getAll();
// Отображение окна с сообщением об успешном удалении
Alert.show("Пользователь удален", "Zend Amf Example");
}
]]>
</mx:Script>
<mx:Panel width="484"
height="400"
layout="vertical"
title="Управление пользователями"
paddingBottom="5"
paddingLeft="5"
paddingRight="5"
paddingTop="5"
horizontalAlign="center"
horizontalCenter="-5"
verticalCenter="17"
borderColor="#2A2929"
color="#F5F5F5">
<controls:DoubleClickDataGrid id="usersDataGrid"
width="446"
height="100%"
editable="true"
itemEditEnd="onSaveClick(event)"
color="#171717"
alternatingItemColors="[#DAD9D9, #FFFFFF]">
<controls:columns>
<mx:DataGridColumn headerText="Имя"
dataField="name"/>
<mx:DataGridColumn headerText="Логин"
dataField="login"/>
<mx:DataGridColumn headerText="Email"
dataField="email"/>
</controls:columns>
</controls:DoubleClickDataGrid>
<mx:HBox width="453">
<mx:Form color="#0B0B0B">
<mx:FormItem label="Имя"
horizontalAlign="left">
<mx:TextInput id="nameTextInput"/>
</mx:FormItem>
<mx:FormItem label="Логин">
<mx:TextInput id="loginTextInput"/>
</mx:FormItem>
<mx:FormItem label="Email">
<mx:TextInput id="emailTextInput"/>
</mx:FormItem>
<mx:Button label="Добавить пользователя"
click="onAddClick(event)"
color="#141414"/>
</mx:Form>
<mx:Button label="Удалить пользователя"
click="onDeleteClick(event)"
color="#141414"
textAlign="right"
width="172"/>
</mx:HBox>
</mx:Panel>
</mx:Application>
Также следует отметить, что в статье не приводится код дополнительного компонента DoubleClickDataGrid. Если читатель решит его использовать, то ему следует скачать исходный код примера, ссылка на который приведена ниже.
Итоги
В статье мы рассмотрели пример использования компонента Zend_Amf в связке с приложением, разработанным с использованием Adobe Flex SDK.
Полный код приложения, включая php файла, файлы клиентского приложения, дамп для создания базы данных можно скачать здесь: zend_amf_example_sources.zip.
Результат работы разработанного приложения можно увидеть по ссылке http://articles.lcf.name/zend_amf_flex_example/ (база данных периодически очищается).
- Спрашивать почему у вас не работает код, для этого есть тема форума закрепленная за статьей.
- Спрашивать как реализовать ту или иную функциональность, для этого необходимо создать свою тему на форуме.
Комментарии для того чтобы: высказать свое аргументированное мнение о статье, указать какие участки вызывают непонимание, что нужно исправить/улучшить, просто сказать спасибо.
Комментарии имеют древовидную структуру.
Если вы хотите ответить на определенный комментарий - нажмите на ссылку "Ответить" возле этого комментария.