Использование Zend GData в Symfony2-проекте



    Ведущий разработчик фреймворка 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. Благодарю всех, кто помог в публикации данного поста.

    Ссылки


    1. Symfony & Zend Framework Together — 2009
    2. GData API
    3. Ignoring the vendor directory
    4. Requirements for running Symfony2
    5. Вопрос про использование Zend_GData в Symfony2 на Stackoverflow
    6. Zend GData download page
    7. Symfony2 Glossary
    8. How to Use a Factory to Create Services
    9. How to create Console/Command-Line Commands

    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 10
    • +2
      В целом хочется добавить, что сейчас Symfony2 уже вырос из статуса «впечатляющая, но нестабильная штука». Фреймворк готов к серьезным проектам.

      Подтверждаю
      • +3
        А почему ZF — это как бы девочка, а SF — как бы мальчик?
        • +1
          Не ищи во всем символического значения, а то ведь найдешь.

          Виктор Пелевин, Поколение «П».
          • +1
            Хотелось, чтобы Symfony2 был слева. По сути же главное, чтобы сущности интегрировались друг с другом :)
            • 0
              Ну вы же интегрируете часть ZF с Symfony. Я думаю, подходящей метафорой был бы, например, пирог (шарлотка — поскольку тут речь идет о Гугле… да и в сторону Корпорации Бабла ненавязчиво плюнуть можем… а тот факт, что речь идет о каком-никаком, а о пироге подразумевает, что девочка корпела над ним), который девочка дарит парню… И вот на девушке написано ZF, на парне Symfony 2 (кстати, да — он же маленький пока еще совсем), а на пироге GData…

              Никому полит технолог не нужен?
          • –1
            Интересно, что будет с использованием ресурсов (в частности, памяти) у такого гибрида. Ведь обе библиотеки весят немало даже сами по себе.
            • 0
              Доверьтесь Service Container'у. Экземпляр Zend_Gdata будет создан только при запросе сервиса. Согласно документации, даже от объявления огромного числа сервисов оверхэд отсутствует или очень мал.
            • 0
              Вместо echo в командах лучше все таки использовать использовать объект $output
              • 0
                Спасибо, полностью согласен. Это что-то вроде рудимента от первого рабочего наброска. Исправлю, как буду за каналом пошире, чтобы не подавать дурной пример.
              • +2
                Love is… Простите, дальше не смог читать. :)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.