
Ведущий разработчик фреймворка Symfony — Фабьен Потенсьер (Fabien Potencier) в 2009-м году выступил на Zend/PHP Conference с докладом о извлечении выгоды в совместном использовании Symfony 1.3/1.4 и Zend Framework. Основные тезисы его речи доступны в презентации, опубликованной на его персональном сайте [1].
Как известно, Symfony2 — это практически новый фреймворк, созданный с использованием новейших возможностей языка PHP. На данный момент разработка перешла в RC-цикл, и всё больше разработчиков, имеющих опыт работы на первой ветке Symfony (да и не только они), смотрят в сторону нового флагмана. Но, даже несмотря на значительное количество включенных в стандартное издание компонентов, Symfony2 не покрывает все нужды веб-разработчика, поэтому, рано или поздно, встает вопрос о подключении внешних библиотек.
Очевидно, что в этом свете объемный комплект библиотек Zend (Gdata, Search_Lucene, Pdf и т. д.) нельзя обойти стороной. В данном посте мной будет рассмотрен процесс интеграции Symfony2 и Zend на примере Zend Gdata — библиотеки для взаимодействия с Google Data API [2].
Кратко о установке Symfony2
Как указано в README стандартного дистрибутива symfony2, разработчики больше не рекомендуют использовать git, предлагая вместо этого скачивать архив с официального сайта. На момент написания поста последняя версия — RC4. Все нижесказанное будет работать и в более ранних версиях с некоторыми оговорками, т. к. был произведен значительный рефакторинг в Command, затронувший имена классов, а также появились генераторы кода. Я предпочитаю скачивать архив без вендоров (without vendors), т. к. при использовании GIT директория vendors так или иначе будет добавлена в .gitignore [3].
Для разработки достаточно веб-сервера Apache и PHP 5.3.2 (и выше). Детальные требования подробно изложены в документации [4].
Интеграция Zend Gdata в проект
На представленное в данной статье решение меня натолкнула презентация Фабьена, о которой уже говорилось выше. На просторах интернета можно найти достаточно безумные решения, наподобие обработки дистрибутива Zend регулярными выражениями. Буду рад выслушать иные предложения в комментариях. Также существует обсуждение на stackoverflow [5].
Так как внесение каких-либо изменений в дистрибутив подключаемой библиотеки может привести к сложностям при обновлении (и в целом считается дурным тоном), а привязка к переменным окружения сервера ограничивает разработчика, то предложенное решение использует только стандартные средства Symfony2 и php.
Сначала, следуем на сайт Zend Framework и скачиваем дистрибутив Gdata [6].
Создаем в проекте следующую структуру директорий:
vendor/
-> zend/
--> lib/
---> Zend/
----> [Zend directory from Zend GData package]
--> README
--> LICENSE
Файлы README и LICENSE копируем из дистрибутива Zend GData. Регистрируем префикс 'Zend_' в app/autoload.php:
$loader->registerPrefixes(array(
// ... Предыдущие префиксы, такие как Twig и Twig_Extensions
'Zend_' => __DIR__.'/../vendor/zend/lib',
));
Далее добавляем в конец файла (по аналогии со Swift Mailer) код из презентации Фабьена:
// Zend Framework GData needs a special autoload fix too
set_include_path(__DIR__.'/../vendor/zend/lib'.PATH_SEPARATOR.get_include_path());
Теперь классы, входящие в Zend GData будут определяться загрузчиком Symfony2 по префиксу 'Zend_' и все многочисленные непривязанные require в дистрибутиве Gdata будут корректно работать, благодаря измененному include_path. Возможно, что на нагруженных серверах будет разумно указать значение include_path в php.ini.
Сервисы
Цитируя глоссарий Symfony2 [7], сервис — это общий термин для обозначения любых PHP-объектов, которые выполняют какую-либо задачу, используемую глобально. Примерами могут служить подключение к базе данных или объект, отправляющий электронные письма.
Основываясь на данном определении достаточно легко понять, что, в нашем случае, Zend_Gdata является сервисом.
Далее мы разработаем консольную команду, которая, используя сервис Google GData API, будет загружать записи из блога на blogspot.com в базу данных.
Первым делом создадим бандл GdataTestBundle. Теперь, когда появился генератор кода, это делается предельно просто:
$ php app/console generate:bundle --namespace=Habr/GDataBundle --format=yml
Теперь, в конфигурационный файл src/Habr/GDataBundle/Resources/config/services.yml добавляем следующие строки (необходимо заменить ~ в строках, помеченных комментариями на реальные данные для доступа к блогу на blogger):
parameters:
gdata.class: Zend_Gdata
gdata.http_client.class: Zend_Gdata_HttpClient
gdata.http_client_factory.class: Zend_Gdata_ClientLogin
gdata.username: ~ # <email_address@gmail.com>
gdata.password: ~ # <password>
gdata.blog_id: ~ # <blog ID>
gdata.service_name: blogger
services:
gdata_http_client:
class: %gdata.http_client.class%
factory_class: %gdata.http_client_factory.class%
factory_method: getHttpClient
arguments:
- %gdata.username%
- %gdata.password%
- %gdata.service_name%
gdata:
class: %gdata.class%
arguments: [@gdata_http_client]
Остановимся подробнее на содержимом данного конфигурационного файла. Symfony2 предоставляет множество способов для инициализации сервисов и разрешения зависимостей. В простейшем случае, на основе указанного в конфигурации имени класса создается объект, который возвращается в клиентский код с помощью метода get() из интерфейса ContainerInterface. В реальности же объекты имеют сложные зависимости между собой и, зачастую, инициализация одного сервиса требует экземпляра другого, переданного в качестве аргумента конструктору. Данный случай можно наблюдать выше для сервиса gdata.
Гораздо больший интерес представляет инициализация Zend_Gdata_HttpClient — экземпляр объекта данного класса создается с помощью вызова статического метода класса-фабрики (Zend_Gdata_ClientLogin), которому также передаются аргументы. Подробнее об использовании классов-фабрик для инициализации сервисов рассказано в специальной главе Symfony Cookbook [8].
Протестировать полученную конфигурацию мы сможем, когда создадим консольную команду, использующую определенный нами сервис.
Модель
Создадим простую модель для хранения выгруженных с Blogger.com записей, в чем нам также поможет встроенный генератор (в случае, если вы не хотите вносить собственные изменения в конфигурацию модели, то на все последующие вопросы в интерактивном режиме можно нажимать enter):
$ php app/console doctrine:generate:entity --entity="HabrGDataBundle:Post" --fields="title:string(255) content:text remote_id:string(255) created_at:datetime"
Теперь в директории src/Habr/GDataBundle/Entity находится сгенерированный файл Blog.php, в котором присутствуют все необходимые геттеры и сеттеры. Далее, для отражения внесенных изменений на уровне СУБД, нам необходимо сперва настроить подключение к базе данных.
Для использования MySQL необходимо изменить соответствующую часть app/config/parameters.ini следующим образом, используя специфичные для вас настройки подключения:
[parameters]
; ...
database_driver = pdo_mysql
database_host = localhost
database_name = habr
database_user = root
database_password =
; …
Следующим шагом является создание базы данных:
$ php app/console doctrine:database:create
После создаем таблицы на основе моделей:
$ php app/console doctrine:schema:update --force
Можете проверить результат выполнения команды в phpmyadmin или любом другом клиенте для используемой базы данных.
Команда
Сущность, которая называлась в Symfony 1.4 «task» (задачей), в Symfony2 стала командой. В текущем релизе пока ещё нет генератора для команд, поэтому внутри корневой директории бандла вручную создаем поддиректорию Command, куда кладем файл FetchFeedCommand.php (суффикс Command обязателен). Реализация команд [9] (как и большинства компонентов Symfony2) очень проста, поэтому привожу только исходный код с комментариями:
// FetchFeedCommand.php
<?php
namespace Habr\GDataBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Habr\GDataBundle\Entity\Post;
/**
* FetchFeedCommand
* @author temochka <http://temochka.habrahabr.ru>
* @package HabrGDataBundle
* @subpackage command
* @version 0.1
*/
class FetchFeedCommand extends ContainerAwareCommand
{
/**
* Конфигурация команды
*/
protected function configure()
{
$this->setName('gdata:blogger:fetch-feed');
}
/**
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// Инициализируем экземпляр сервиса gdata (Объект Zend_Gdata)
$gdClient = $this->getContainer()->get('gdata');
// Из настроек получаем
$blogID = $this->getContainer()->getParameter('gdata.blog_id');
// Создаем запрос к API
$query = new \Zend_Gdata_Query('http://www.blogger.com/feeds/' . $blogID . '/posts/default');
$feed = $gdClient->getFeed($query);
// Получаем экземпляр EntityManager'а из сервиса doctrine
$em = $this->getContainer()->get('doctrine')->getEntityManager();
foreach($feed->entries as $entry)
{
// Создаем записи
$post = new Post;
$post->setTitle($entry->title->text);
$post->setRemoteId($entry->id);
$post->setContent($entry->content);
$post->setCreatedAt(new \DateTime($entry->published->text));
$em->persist($post);
$output->writeln(sprintf("\tNew post %s has been added.\n", $entry->title->text));
}
// Сохраняем записи в БД
$em->flush();
}
}
Теперь запустить команду можно, передав ./app/console её имя, определенное в вызове setName.
$ ./app/console gdata:blogger:fetch-feed
Результатом должны стать все записи блога в таблице post базы данных.
Заключение
Приведенный пример является несколько синтетическим, так как для получения открытой ленты записей с Blogger.com не требуется аутентификация. Желающие поупражняться и получить дополнительный опыт с Symfony2 могут реализовать добавление или удаление новой записи в блог самостоятельно. Ссылка на подробную документацию уже была приведена выше.
В целом хочется добавить, что сейчас Symfony2 уже вырос из статуса «впечатляющая, но нестабильная штука». Фреймворк готов к серьезным проектам.
P.S. Благодарю всех, кто помог в публикации данного поста.
Ссылки
- Symfony & Zend Framework Together — 2009
- GData API
- Ignoring the vendor directory
- Requirements for running Symfony2
- Вопрос про использование Zend_GData в Symfony2 на Stackoverflow
- Zend GData download page
- Symfony2 Glossary
- How to Use a Factory to Create Services
- How to create Console/Command-Line Commands