У многих, равно как и у меня, периодически возникает потребность в реализации каких-то не больших задач. Например распарсить сайт/API и сохранить данные в xml/json/csv, произвести какие-либо расчеты/пересчеты, перегнать данные из одного формата в другой, собрать статистику и т.д. и т.п. Замечу, что речь о задачах не связанных с текущими проектами.
Собирать тяжелый фреймворк ради удобных фич, лень, а реализовывать в рамках кода текущих проектов как-то не эстетично. Поэтому для экономии своего времени приходится создавать скрипт, копипастить в него куски кода из предыдущих наработок, подключать разнообразные библиотеки и запускать скрипт из консоли. При этом часто требуется некоторая интерактивность работы скрипта: обработка опций/аргументов, а то и диалоговое взаимодействие. Здесь главное чтобы не было настроения, которое хорошо описывается выражением «Аппетит приходит во время еды», тогда вообще не понятно к чему приведет работа над простой задачкой =)
В такие моменты я вспоминал удобную симфоническую консоль, к которой успел привыкнуть работая с проектами на
Symfony 2. Не в обиду другим консолям (zend, yii, django, ror etc), все хороши, просто так сложилось.
Когда в очередной раз потребовалось что-то распарсить, я опять вспомнил про консоль Symfony (Console Component) и тот факт, что это независимый компонент все больше подтолкнул меня к мысли использовать ее возможности.
За пару часов получилась простая тулза, в основе которой:
- symfony/console — сама консоль
- symfony/finder — для поиска и подключения к приложению наших комманд
- suncat/symfony-console-extra — несколько плюшек для того чтобы это все работало
и менеджер зависимостей Composer, который нам поможет все это быстро собрать, добавлять новые либы, а также возьмет на себя автозагрузку классов.
Предположим, что нам очень понадобилось собрать список последних новостей, «Интернет» тематики. И в качестве источника нас вполне устраивает RSS сервиса Яндекс.Новости.
С помощью Сomposer-а создаем новый проект:
$ composer create-project suncat/console-commands ./cmd
У меня Composer установлен глобально, поэтому он всегда доступен. Если вы им еще не пользуетесь, то для проверки примера его необходимо установить.
После скачивания приложения и всех зависимостей переходим в созданную директорию:
$ cd cmd # для примера, при создании проекта задайте имя директории на свое усмотрение
Структура следующая:
app/
console # консоль
src/ # автозагрузка psr-0
Command/ # классы ваших команд
vendor/ # сторонние библиотеки
Проверяем состояние:
$ app/console list
Если видим справочную информацию и список доступных команд значит все ок. Выглядит это так:
Теперь создадим шаблон класса команды, которую мы планируем использовать для реализации задачи:
$ app/console generate
В появившемся диалоге указываем название будущего класса:
Please enter the name of the command class: NewsInternetCommand
В ответ получим уведомление:
Generated new command class to "./cmd/src/Command/NewsInternetCommand.php"
Собственно все, команда готова, она появилась в списке доступных команд:
Но пока она не делает того что нужно (здесь можно открыть созданный класс в IDE или любимом редакторе и написать код команды).
Так как для нашего примера необходимо получать внешний контент и нам нравится ООП, поставим еще одну библиотеку:
$ composer require kriswallsmith/buzz 0.9
Buzz — легкий HTTP клиент на PHP5.3. Будем использовать его для выполнения запросов к сервису новостей.
Создадим отдельный класс — YandexRSSNewsParser, который будет предоставлять классу команде подготовленный контент:
// ./src/Parser/YandexRSSNewsParser.php
namespace Parser;
use BuzzClientFileGetContents;
use BuzzMessageRequest;
use BuzzMessageResponse;
use DOMDocument;
use DOMXPath;
class YandexRSSNewsParser
{
private $method;
private $host;
/**
* Construct
*/
public function __construct()
{
$this->method = 'GET';
$this->host = 'http://news.yandex.ru';
}
/**
* Get news
*
* @param $resource
*
* @return mixed
*/
public function getNews($resource)
{
// content
$xml = $this->getData($resource);
if (false === $xml) {
return array();
}
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXpath($doc);
// items
$items = $xpath->query('.//item');
$news = array();
foreach ($items as $item) {
$news[] = array(
'datetime' => $xpath->evaluate("./pubDate", $item)->item(0)->nodeValue,
'title' => $xpath->evaluate("./title", $item)->item(0)->nodeValue
);
}
return $news;
}
/**
* Get data
*
* @return mixed
*/
protected function getData($resource)
{
$request = new Request($this->method, $resource, $this->host);
$response = new Response();
$client = new FileGetContents();
// processing get data
$attempt = 0;
do {
if ($attempt) {
sleep($attempt);
}
try {
$client->send($request, $response);
} catch (Exception $e) {
continue;
}
} while (false === ($response instanceof Response) && ++$attempt < 5);
if (false === ($response instanceof Response) || false === $response->isOk()) {
return false;
}
$data = $response->getContent();
return $data;
}
}
И отредактируем класс команды, для вывода в консоль заголовков последних новостей, рубрики «Интернет»:
// ./src/Command/NewsInternetCommand.php
namespace Command;
use ParserYandexRSSNewsParser;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleOutputOutputInterface;
/**
* NewsInternetCommand
*/
class NewsInternetCommand extends Command
{
/**
* Configuration of command
*/
protected function configure()
{
$this
->setName("news:internet")
->setDescription("Command for parsing internet news")
;
}
/**
* Execute command
*
* @param SymfonyComponentConsoleInputInputInterface $input
* @param SymfonyComponentConsoleOutputOutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$parser = new YandexRSSNewsParser();
$output->writeln(array(
"",
"<info>Start parsing</info>",
""
));
// News
$news = $parser->getNews('/internet.rss');
foreach ($news as $item) {
$output->writeln(sprintf(
"<info>[%s]</info> <comment>%s</comment>",
$item['datetime'],
$item['title']
));
}
$output->writeln(array(
"",
"<info>Done!</info>",
""
));
}
}
Теперь выполним подготовленную команду:
$ app/console news:internet
Результат:
Получился очень простой, а за счет symfony/console и composer-а гибкий и удобный инструмент для организации консольных команд на PHP.
Автор: k0t0vsky