Главная Услуги Работы Персона Юзабилити анализы
IMG тел. +7(901) 370-1796
CMS на основе стандарта ZF, или построение простого сайта на базе Zend Framework (с кодом)




ПОИСК по сайту


    Полный список статей
/ CMS на основе стандарта ZF / Версия для печати / translit / абракадабра :-)


<-предыдущая следующая ->

 
  google.com bobrdobr.ru del.icio.us technorati.com linkstore.ru news2.ru rumarkz.ru memori.ru moemesto.ru smi2.ru twitter.com Яндекс закладки text20.ru RuSpace RuSpace toodoo

Неоднократно я сталкивался с мнением, что в мануале по Zend Framework очень мало туториалов, то есть в нем хорошо описаны отдельные классы, но тяжело разобраться как их использовать вместе. Сейчас я попытаюсь восполнить этот недостаток. Статья предназначена для начинающего и среднего уровня. После прочтения вы сможете создавать простые сайты на базе Zend Framework. В конце вы сможете скачать полноценный рабочий код мини сайта - примера, разработанного специально под статью. Код был протестирован на Zend Framework 1.5.2, php 5.2.0, mysql 5.0, apache 1.3

В сайте реализовано:
  • Паттерн MVC - на основе Zend_Controller, Zend_View, Zend_Layout и Zend_Db
  • Управление конфигурационными файлами - Zend_Config
  • Работа с базой данных - Zend_Db, Zend_Db_Select, Zend_Db_Table
  • Отделение представления (вида) с помощью системы шаблонов - Zend_View
  • Двухэтапное представление - Zend_Layout
  • Управление маршрутами - Zend_Controller_Router_Rewrite
  • Автоматическая загрузка классов - Zend_Loader
  • Работа с исключениями и обработчик ошибок
Старт
Для получения базовых навыков прочтите предыдущую статью. Там мы располагали php файлы вне document root директории, но учитывая, что наши хостеры часто не дают такой возможности, тепер мы будем располагать все файлы внутри document root.

Итак, скачиваем последнюю стабильную версию фреймворка, разархивируем и выбираем классы для работы. Почти все классы Zend Framework можно использовать независимо, поэтому выберем только нужные. Вот так выглядит наш список файлов и папок библиотеки Zend:

Файловая структура Zend Framework
Рис. 1. Файловая структура Zend Framework


А вот так выглядит дерево папок сайта:

Дерево папок сайта на базе Zend Framework
Рис. 2. Дерево папок сайта


Во многом структура осталась такая же как в прошлой статье. Все названия папок полностью соответствуют файлам, что лежат в них, поэтому, комментировать их не буду. Отмечу только что в папку system я кладу системные файлы, которые трудно отнести к какой-либо части MVC. Для лучшего понимания вы можете сразу скачать код, смотреть и сравнивать. Кроме того код обильно откомментирован.
Общий алгоритм работы сайта
Для общего понимания я схематически изобразил алгоритм работы сайта.

Алгоритм работы сайта на базе Zend Framework
Рис. 3. Алгоритм работы сайта


Как и ранее с помощью файла .htaccess все запросы к сайту перенаправляются на входную точку - файл index.php лежащий в корне document root.


RewriteEngine on

RewriteBase /

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d

RewriteRule ^(.*)$ index.php/$1
Не перенаправляются только запросы на реально существующие файлы, поэтому во избежание неприятных ситуаций сразу закрываем доступ в папку applications где расположены php файлы. Для этого кладем туда .htaccess запрещающий доступ к любым файлам.

Рассмотрим код файла index.php

<?php

// Подключаем файл настроек
require 'applications/settings/config.php';

// Создаем строку путей
$paths = implode(PATH_SEPARATOR,
array (
$config['path']['libs'],
$config['path']['models'],
$config['path']['system']
));

// Пути по которым происходит поиск подключаемых файлов, это папка библиотек, моделей и системных файлов
set_include_path($paths);


// Подключение главного системного класса
require 'Kernel.php';

// Запуск приложения
Kernel::run($config);
Так как мы не хотим каждый раз указывать полный путь к нужным файлам - задаем set_include_path. Подключаемые файлы это библиотеки Zend, системные классы, а также классы модели. index.php запускает Kernel::run() - основной метод нашего "ядреного" класса Kernel.php. Этот класс предназначен для инициализации и настройки окружения.


public static function run($config) {

try {

// Запуск autoload
Zend_Loader::registerAutoload();

// Создание объекта конфигурации
$cnf = new Zend_Config($config);

// Занесение объекта конфигурации в реестр
Zend_Registry::set('cnf', $cnf);

// Подключение к базе данных
self::setDbAdapter();

// Подключение файла с правилами маршрутизации
if (file_exists($cnf->path->system.'routes.php')) {
require($cnf->path->system.'routes.php');
}

// Инициализация Zend_Layout, настройка пути к макетам, а также имени главного макета.
Zend_Layout::startMvc(array(
'layoutPath' => $cnf->path->layouts,
'layout' => 'index',
));

// Получение объекта Zend_Layout
$layout = Zend_Layout::getMvcInstance();

// Инициализация объекта Zend_View
$view = $layout->getView();

// Настройка расширения макетов
$layout->setViewSuffix('tpl');

// Задание базового URL
$view->baseUrl = $cnf->url->base;

// Задание пути для view части
$view->setBasePath($cnf->path->views);

// Установка объекта Zend_View
$layout->setView($view);

// Настройка расширения view скриптов с помощью Action помошников
$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer();
$viewRenderer->setView($view)
->setViewSuffix('tpl');

Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);

// Создание объекта front контроллера
$front = Zend_Controller_Front::getInstance();

// Настройка front контроллера, указание базового URL, правил маршрутизации
$front->setBaseUrl($cnf->url->base)
->setRouter($router);

// Запуск приложения, в качестве параметра передаем путь к папке с контроллерами
Zend_Controller_Front::run($cnf->path->controllers);

}
catch (Exception $e) {
// Перехват исключений
Error::catchException($e);
}
}
Практически каждая строчка откомментирована и уже сейчас вы можете полностью представить себе схему работы сайта-примера.

Далее, предположим мы обратились к следующей странице нашего сайта http://ilovezf/pages/1/ В этом случае будет вызван IndexController и его действие PageAction. (Такое поведение происходит потому что я добавил специальное правило в файл маршрутов routes.php, по умолчанию по такому запросу вызовется контроллер PagesController и действие 1, конечно если они существуют)

В контроллере происходит подключение модели и получение данных необходимых запрошенной странице. Затем происходит определение переменных для вида.

Наконец в главный макет applications/views/scripts/index.tpl подставляются нужные нам данные и весь полученный результат отправляется в браузер.

Далее я более подробно остановлюсь на ключевых моментах построения нашего приложения.

Zend_Loader

Этот класс позволяет использовать одну из замечательных возможностей php5 - автоматическую загрузку объектов, иначе называемую autoload. Если вы пишите ООП приложение то у вас, должно быть, не один десяток различных классов, и на некоторых страницах нужно подключить 5-10 классов. Если таких страниц много, то это очень неприятно. С Zend_Loader вам достаточно всего лишь указать название класса в нужном формате, и он сам произведет подключение.

То есть, в коде например пишем $news = new Text_News_Home();

Символ "_" используется как разделитель, Zend_Loader будет пытаться найти указанный класс по адресу Text/News/Home.php. (В рассматриваемом примере в index.php я указал относительно каких директорий будет происходить поиск)

Для автоматической загрузки можно использовать метод Zend_Loader::loadClass() но я рекомендую выполнять Zend_Loader::registerAutoload(); что бы не писать каждый раз Zend_Loader::loadClass(). Правда registerAutoload() требует наличие модуля spl_autoload.

Zend_Registry

Класс реализует паттерн singleton и таким образом позволяет нам обращаться к нужным объектам из любого места кода. Это альтернатива использованию глобальных переменных. Класс оперирует неким реестром, в который заносятся объекты. Ключевые методы класса это get и set, которые заносят и получают объекты в реестр соответственно. В сайте-примере в реестр заносятся объекты соединения с БД и объект конфигурации. Также важен метод isRegistery проверяющий существование объекта в реестре.

Zend_Config

Класс позволяет в ООП стиле работать с конфигурационными файлами. Поддерживаются конфигурационные файлы различного формата: php-массив, ini - файл, xml - файл. Для рассматриваемого примера я выбрал самый простой вариант - хранить настройки в php массиве. Вы можете использовать любой. В объекте Zend_Config все данные представлены в виде ассоциативного массива. Например таким образом: $cnf->path->system получаем значения физического пути папки с системными файлами.

Например вот так может выглядеть ваш конфигурационный файл:

<?php
$config = array (
// Настройки соединения с БД
'db' => array (
'adapter' => 'PDO_MYSQL',
'params' => array(
'host' => 'localhost',
'username' => 'root',
'password' => 'xxxx',
'dbname' => 'ilovezf',
'driver_options'=> array(PDO::ATTR_EMULATE_PREPARES => true),
'profiler' => false,
),
),
);

Управление маршрутами Zend_Controller_Router_Rewrite

Маршрутизация - это процесс принятия URI (той части URI, которая идет после базового URL) и ее разложения на части для определения того, какой контроллер и какое действие этого контроллера должны получить запрос и выполниться. С помощью Zend_Controller_Router_Rewrite можно задавать различные правила маршрутизации.

<?php

$router = new Zend_Controller_Router_Rewrite();

$router->addRoute('articles',
new Zend_Controller_Router_Route('articles/:articleId',
array('module' => 'default', 'controller' => 'articles', 'action' => 'view'))
);

$router->addRoute('pages',
new Zend_Controller_Router_Route('pages/:pageId',
array('module' => 'default', 'controller' => 'index', 'action' => 'page'))
);
С помощью указанных правил мы добиваемся например того что по запросу http://ilovezf/articles/1/ будет вызван контроллер articlesController и действие viewAction, кроме того значение идентификатора статьи (в указанном запросе это 1) будет доступно по параметру articleId. Кроме Zend_Controller_Router_Route который используется для динамических маршрутов, есть еще Zend_Controller_Router_Route_Static для постоянных неизменных маршрутов, например для URL типа http://ilovezf/exit/. Также существует Zend_Controller_Router_Route_Regex основанный на регулярных выражениях, это самый гибкий способ определения маршрутов.

Front контроллер

Zend_Controller_Front реализует паттерн Front Controller, используемый в приложениях MVC. Его назначение состоит в инициализации окружения запроса, проложении маршрута приходящего запроса и последующем запуске выявленных действий. Он агрегирует все ответы и возвращает их по завершении процесса.

// Создание объекта front контроллера
$front = Zend_Controller_Front::getInstance();

// Настройка front контроллера, указание базового URL, правил маршрутизации
$front->setBaseUrl($cnf->url->base)
->setRouter($router);

// Запуск приложения, в качестве параметра передается путь к папке с контроллерами
Zend_Controller_Front::run($cnf->path->controllers);

Работа с исключениями и обработчик ошибок

Zend Framework предлагает несколько вариантов работы с исключениями. Во первых это плагин Zend_Controller_Plugins_ErrorHandler, он дает возможность перехватывать исключения вызванные отсутствием контроллера или действия, кроме того он перехватывает исключения выброшенные в контроллерах. Никакие другие исключения, например исключения маршрутизации, или исключения выброшенные в других плагинах он не перехватывает. Этот плагин хорош именно для работы с ошибками типа 404, он позволяет в рамках общей структуры макетов отобразить страницу 404 с необходимыми сообщениями. За плагин отвечает контроллер ErrorController и действие errorAction.

Для того же что бы все таки обрабатывать остальные ошибки в рассматриваемом примере используется еще один обработчик ошибок /applications/system/Error.php.

<?php

class Error {

/**
* Управление ошибками
*
* @param exception $exception Перехватываемое исключение
*/

public static function catchException(Exception $exception) {

// Получение текста ошибки
$message = $exception->getMessage();
// Получение трейса ошибки как строки
$trace = $exception->getTraceAsString();
$str = 'ERROR: ' . $message . "n" . $trace;

$cnf = Zend_Registry::get('cnf');

// Если включен режим отладки отображаем сообщение о ошибке на экран
if($cnf->debug->on) {
Zend_Debug::dump($str);
}
// Иначе выводим сообщение об ошибке
else {
// Здесь может происходить логирование ошибки, уведомление вебмастера и т д
die('System error! Please try later');
}
}
}
В Kernel.php с помощью try и catch все ошибки перехватываются и передаются этому обработчику. Далее в зависимости от настроек режима дебага в конфигурационном файле, либо показывается информация об ошибке в браузере, либо выдается сообщение о временных неполадках.

Для того что бы все ошибки, включая ошибки не найденных контроллеров и действий, перехватывать своим обработчиком, необходимо при настройке front контроллера выполнить $front->throwexceptions(true).

Работа с контроллером

Контроллер является связующим звеном между видом и моделью. Основная задача контроллера это проверить данные что пришли от пользователя, выполнить те или иные действия с данными, используя методы модели, и наконец определить переменные для вида. Для иллюстрации привожу откоментированный код контроллера ArticlesController

class ArticlesController extends Zend_Controller_Action {

/**
* Список статей
*/

public function indexAction() {

// Создание объекта модели, благодаря autoload нам нет необходимости подключать класс через require
$articles = new ArticlesModel();
// Выполнения метода модели по получению списка статтей
$articlesList = $articles->getArticles();
// Определение переменных для вида
$this->view->articlesList = $articlesList;

}

/**
* Выбранная статья
*/

public function viewAction() {

// Получение параметра пришедшего от пользователя
$articleId = $this->_getParam('articleId');
// Создание объекта модели
$articles = new ArticlesModel();
// Выполнения метода модели по получению информации о статье
$articleInfo = $articles->getArticles($articleId);
// Определение переменных для вида
$this->view->articleInfo = $articleInfo;

}

}

Работа с моделью

Работа с моделью практически не освещена в мануале, в файловой структуре предпологается папка models но не дается конкретных рекомендаций по ее и спользованию. Кроме того можно сказать что понятия модели в Zend Framework как такового нет. Реализация этой части MVC возлагается на плечи разработчика. Я рассматриваю модель как набор классов оперирующих с данными, это может быть получение данных, изменения данных, вставка данных в базу данных или другое хранилище. Я не допускаю составления и выполнения sql-запросов в классах контроллеров. Название каждого класса модели состоит из двух слов, самого названия сущности данных и слова "Model", по аналогии к названиям контроллеров. В рассматриваемом сайте-примере существует два класса модели, это PagesModel и ArticlesModel. В первом классе собраны функции работающие с сущностью "страница", во втором с сущностью "статьи". Классы модели являются наследниками Zend_Db_Table_Abstract. Методы работы с базой и пример класса модели смотрите ниже.

Работа с базой данных

Для гибкой работы с БД Zend Framework предлагает несколько классов. Нам понадобятся Zend_Db, Zend_Db_Select, Zend_Db_Table, Zend_Db_Table_Select. В Kernel.php задается соединение с БД, задается адаптер используемый по умолчанию, и объект соединения заносится в реестр.

// Подключение к БД, так как Zend_Db "понимает" Zend_Config,
// нам достаточно передать специально сформированный объект конфигурации в метод factory
$db = Zend_Db::factory($cnf->db);
// Задание адаптера по умолчанию для наследников класса Zend_Db_Table_Abstract
Zend_Db_Table_Abstract::setDefaultAdapter($db);
Отмечу что если вы используете Zend_Cache вам пригодится метод Zend_Db_Table_Abstract::setDefaultMetadataCache, с его помощью вы можете закешировать метаданные таблиц. Таким образом запрос describe table не будет выполнятся при каждом создании объекта Zend_Db_Table.

Далее вся работа с базой данных у нас будет собрана в классах моделей.
Для примера рассмотрим код модели ArticlesModel

<?php

class ArticlesModel extends Zend_Db_Table_Abstract {

// Имя таблицы
protected $_name = 'articles';

/**
* Получить все статьи или одну
*
* @param int $articleId Идентификатор статьи
* @return array
*/

public function getArticles($articleId = NULL) {

// Создаем объект Zend_Db_Select
$select = $this->getAdapter()->select()
// Таблица из которой делается выборка
->from($this->_name)
// Добавление таблицы с помощью join, указывается поле связи
->join('users','users.id = articles.author_id',array('name'))
// Порядок сортировки
->order('id DESC')
// Количество возвращаемых записей
->limit(2)
;

if (!is_NULL($articleId)) {

// Условие на выборку
$select->where("articles.id = ?",$articleId);
// Выполнение запроса
$stmt = $this->getAdapter()->query($select);
// Получение данных
$res = $stmt->fetch();


}
else {

$stmt = $this->getAdapter()->query($select);
// Получение массива данных
$res = $stmt->fetchAll();

}

return $res;

}

}
Класс ArticlesModel наследует абстрактный класс Zend_Db_Table_Abstract. Таким образом, мы получаем возможность использовать его функционал по работе с базой. Zend_Db_Table реализует паттерн Table Data Gateway. Идея в том что для каждой таблицы создается отдельный класс который предназначен для работы именно с этой таблицей. Таким образом, работа с БД делится на логические части, с которыми, в случае большого объема проекта, работать гораздо удобнее. Например, вся работа со статьями в сайте-примере собрана в модели ArticlesModel, а работа со страницами собрана в модели PagesModel.

Все что нам нужно, это указать название таблицы с которой мы работаем: protected $_name = 'articles';

Для различных сложных выборок из базы используется класс Zend_Db_Select, он предоставляет гибкий функционал для выполнения select запросов. Особенно отмечу объектно-ориентированный подход "покусочного" построения sql-запросов. Например:

$select = $this->getAdapter()->select()
->from('articles')
->where("articles.id = ?",$articleId)
->join('users','users.id = articles.author_id',array('name'))
->order('id DESC')
->limit(2);

"Куски" могут распологаться в произвольном порядке, это очень удобно когда у вас запросы строятся динамически.

Для защиты от sql-инъекций в select запросах предлагается механизм плейсхолдеров where('id = ?',$pageId);
В случае же update, delete запросов использующих условие where c данными полученными от пользователя, необходимо поступать немного по другому. Для защиты от sql-инъекций в таких случаях нужно экранировать спец символы и брать данные в кавычки самому. Я использую следующий метод:
$id = $this->getAdapter()->quote($id);

Если запросы на выборку у вас достаточно простые и не требуют данных из других таблиц, вы можете использовать методы fetchRow, fetchAll класса Zend_Db_Table (обратите внимание что fetchAll и fetchRow класса Zend_Db_Table теперь рекомендуются к использованию с Zend_Db_Table_Select), пример:

<?php
/**
* Работа с страницами
*/

class PagesModel extends Zend_Db_Table_Abstract {

// Имя таблицы
protected $_name = 'pages';

/**
* Получить одну страницу (альтернативой этому методу может быть встроенный метод find)
*
* @param int $pageId Идентификатор страницы
* @return array
*/

public function getPage($pageId) {

// Создание объекта Zend_Db_Table_Select,
// Нам не нужно указывать название таблицы как в Zend_Db_Select
$select = $this->select()
// Накладываем условие
->where('id = ?', $pageId);

// Выполняем запрос и получаем объект Zend_Db_Table_Row в результате
// Нам не нужно предварительно выполнять запрос методом query, как в Zend_Db_Select
$row = $this->fetchRow($select);
// Преобразовываем Zend_Db_Table_Row в массив
$res = $row->toArray();

return $res;

}

Итого, если вам необходимы запросы на вставку, удаление, изменение данных - нужно использовать Zend_Db_Table. Если вам необходимы простые запросы на выборку данных результатом которых есть данные только одной таблицы - можно использовать Zend_Db_Table (а можно и Zend_Db_Select). Если же вам нужны сложные запросы с получением данных из других таблиц - нужно использовать Zend_Db_Select.

Работа с видом Zend_View и Zend_Layout

Для работы с видом Zend Framework предлагает два класса, это Zend_View и Zend_Layout.

При создании объектов этих классов в Kernel.php я произвел небольшую настройку. Изменил расширение этих файлов на привычное tpl, и изменил название главного layout макета. Я дал ему привычное название "index"

Вот так выглядит скрипт вида, отвечающий за список статей.

<ul>
<?php foreach ($this->articlesList as $article): ?>
<li><a href = "/articles/<?php print $article['id']; ?>/"><?php print $article['title']; ?></a></li>
<?php endforeach; ?>
</ul>

Как видно php код здесь отделен от html кода, кроме того используется pascal-подобный синтаксис php для лучшего визуального восприятия.

Zend_Layout реализует паттерн "двухэтапное представление" (Two Step View pattern). Данный паттерн предназначен для максимально гибкой работы с макетами вида. Создается некий главный макет дизайна сайта. Обычно он содержит каркас html страницы

// Doctype
<html>
<head>
// Мета теги
// Подключение css и т д
</head>
<body>
...
</body>
</html>

А вот все что находится между <body></body> разбивается на блоки, причем у каждого блока есть свой маленький макет. Это может быть меню, новости, блок авторизации и т д. Таким образом, если мы хотим изменить html код меню, мы меняем всего лишь один макет. Вот так выглядит код главного макета index.tpl

<?php print $this->doctype(Zend_View_Helper_Doctype::XHTML1_TRANSITIONAL); ?>
<html>
<head>
<?php print $this->headTitle('I LOVE ZEND FRAMEWORK!'); ?>
<?php print $this->headMeta()->appendHttpEquiv('Content-Type', 'text/html; charset=UTF-8'); ?>
<?php print $this->headLink()->appendStylesheet($this->baseUrl.'public/design/css/style.css'); ?>
<?php print $this->headScript(); ?>
</head>
<body>
<div id="menu">
<?php print $this->partial('menu.tpl'); ?>
</div>
<div id="main">
<div id="left">
<div style = "margin:5px;">Лучшие статьи:</div>
<?php print $this->action('index', 'articles'); ?>
<div id = "copy">(c) San <a href = "http://zendframework.ru">http://zendframework.ru</a></div>
</div>
<div id="content"><?php print $this->layout()->content; ?></div>
</div>
</body>
</html>

Кроме двухэтапного представления Zend_Layout дает помощников. Это doctype, headTitle, headMeta, headLink, headScript. Эти помощники выручают если например на какой то особой странице вы захотите подключить особый css или js файл.

С помощью помощника partial можно подключать другие макеты - блоки, например в вышеприведенном коде так подключается меню.

Кроме того можно вызывать различные действия прямо из макета. Именно это дает нам возможность разбить весь шаблон сайта на блоки. Таким образом, я подключаю блок "Лучшие статьи", и когда я захочу убрать его со всех страниц, я удалю только одну строчку, а если я захочу изменить его формат, я отредактирую всего один view скрипт.

Возможно в качестве шаблонизатора вы захотите использовать Smarty или что нибудь в этом роде. Интеграция с Zend Framework не вызовет большого труда. Но хочу отметить что Zend_View и Zend_Layout уже сами по себе отличный шаблонизатор, причем не требующий использования нового специфического языка шаблонизатора.
Заключение
Скачать код без библиотек Zend (16 kb)
Скачать код с библиотеками Zend (368 kb)

Для запуска кода необходимо:
  1. Не забыть что Zend Framework требует php 5.1.4 или выше
  2. Установить расширение PDO с поддержкой драйвера mysql (если оно не установлено)
  3. Скачать архив, разархивировать его в document root виртуального хоста. Сайт будет работать только в document root, но не во вложенной папке.
  4. Создать базу данных в кодировке UTF-8
  5. Выполнить sql dump расположенный в корне архива
  6. Отредактировать конфигурационный файл applications/settings/config.php Необходимо указать название БД, имя пользователя и пароль
Результат работы разработанного сайта примера должен выглядеть примерно следующим образом.

Внешний вид сайта на базе Zend Framework
Рис. 4. Внешний вид сайта-примера


P.S.
Весь вышеизложенный подход построения приложения на базе Zend Framework есть субъективным мнением автора и не претендует на полную объективность и оптимальность. Главной целью данной статьи было показать, как можно использовать Zend Framework для построения web приложения. Не весь код приведенный в статье присутствует именно в таком виде в архиве, в некоторых местах в статье он не существенно изменен, для лучшей наглядности.

P.P.S.
Приношу благодарность Grisha Kostyuk aka naspeh за помощь в написании статьи.

P.P.P.S.
Если статья показалась вам полезной, вы можете помочь развитию русскоязычному сообществу zend framework, поставив ссылку на ресурс http://zendframework.ru. В следующий раз я планирую расширить пример и описать работу с Zend_Auth, Zend_Acl, Zend_Session, Zend_Cache, Zend_Date а также добавить работу с Ajax

Создание эксклюзивных сайтов, юзибилити анализ и бесплатный анализ под запросы основных поисковых машин
Контактная информация :
тел. +7(901) 370-1796

Написать письмо на e-mail
icq 415547094  romverрейтинг на mail.ru сайта romverinbox.ru
© 1997 - 2017 romver.ru

Полная карта сайта Display Pagerank