Консольные команды на PHP

    У многих, равно как и у меня, периодически возникает потребность в реализации каких-то небольших задач. Например распарсить сайт/API и сохранить данные в xml/json/csv, произвести какие-либо расчеты/пересчеты, перегнать данные из одного формата в другой, собрать статистику и т.д. и т.п. Замечу, что речь о задачах не связанных с текущими проектами.



    Собирать тяжелый фреймворк ради удобных фич, лень, а реализовывать в рамках кода текущих проектов как-то не эстетично. Поэтому для экономии своего времени приходится создавать скрипт, копипастить в него куски кода из предыдущих наработок, подключать разнообразные библиотеки и запускать скрипт из консоли. При этом часто требуется некоторая интерактивность работы скрипта: обработка опций/аргументов, а то и диалоговое взаимодействие. Здесь главное чтобы не было настроения, которое хорошо описывается выражением «Аппетит приходит во время еды», тогда вообще не понятно к чему приведет работа над простой задачкой =)

    В такие моменты я вспоминал удобную симфоническую консоль, к которой успел привыкнуть работая с проектами на
    Symfony 2. Не в обиду другим консолям (zend, yii, django, ror etc), все хороши, просто так сложилось.

    Когда в очередной раз потребовалось что-то распарсить, я опять вспомнил про консоль Symfony (Console Component) и тот факт, что это независимый компонент все больше подтолкнул меня к мысли использовать ее возможности.

    За пару часов получилась простая тулза, в основе которой:

    и менеджер зависимостей 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 Buzz\Client\FileGetContents;
    use Buzz\Message\Request;
    use Buzz\Message\Response;
    
    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 Parser\YandexRSSNewsParser;
    use Symfony\Component\Console\Command\Command;
    use Symfony\Component\Console\Input\InputArgument;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Input\InputOption;
    use Symfony\Component\Console\Output\OutputInterface;
    
    /**
    * NewsInternetCommand
    */
    class NewsInternetCommand extends Command
    {
        /**
         * Configuration of command
         */
        protected function configure()
        {
            $this
                ->setName("news:internet")
                ->setDescription("Command for parsing internet news")
            ;
        }
    
        /**
         * Execute command
         *
         * @param \Symfony\Component\Console\Input\InputInterface $input
         * @param \Symfony\Component\Console\Output\OutputInterface $output
         */
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $parser = new YandexRSSNewsParser();
    
            $output->writeln("<info>Start parsing</info>\n"));
    
            // 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("\n<info>Done!</info>"));
        }
    }
    

    Теперь выполним подготовленную команду:
    $ app/console news:internet
    

    Результат:


    Получился очень простой, а за счет symfony/console и composer-а гибкий и удобный инструмент для организации консольных команд на PHP.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 30
    • +1
      Неадекватный размер картинок в посте — по полмегабайта каждая. Зачем?!
      • 0
        Сорри!
        Пережал в jpeg. Мак ось неадекватные скрины делает…
        • +2
          Просто для информации, на будущее. Идеальный вариант для скриншотов с текстом — PNG (или GIF). Прозраность консоли или обои рабочего стола при этом, естественно, нужно отключить, чтобы фон был однотонным.

          JPEG при отображении текста дает гадкие и весьма заметные артефакты, даже уровень сжатия выставлять не очень большой.
          • 0
            Спасибо
            В первом варианте были png.
            Тут в другом похоже косяк, мак ось с ретиной делает скрины 144dpi, меняешь на 72 dpi, качество портиться.
            Да и habrastorage походу еще раз пережимает…
            • 0
              Даже скриншот с ретины в PNG занимал бы меньше JPEG-а аналогичного разрешения, если речь идет об одноцветном фоне. Цветные обои на заднем плане в PNG обходятся очень дорого. :) А вот насчет habrastorage, к сожалению, не подскажу, т.к. не использовал.
              • 0
                Чуть позже, постараюсь сделать скрины по новой…
                • +2
                  Ну, может в этом топике уже нет смысла с этим морочиться. Всё видно, объем адекватный. Таких зануд эстетов, как я, здесь не так уж много. :)
              • +1
                • 0
                  Конечно портиться портится
                  Спасибо!
        • +5
          <?php
          passtru("wget news.yandex.ru/internet.rss -qO — | grep -e '<\(title\|pubDate\)>' | tail -n+3 | tac — | sed 's/<pubDate>\(.*\)<\/pubDate>/[\1]/' | sed 's/<title>\(.*\)<\/title>/\1|/' | tr -d '\n' | tr '|' '\n'");
          • +2
            Зачем этот пример из PHP вызывать?
            • +8
              Ну так пост, вроде, про PHP и про то, как добавить энтропии в рутиные задачи…
              • –1
                Если что-то нужно распарсить, то вам по всей видимости проще средствами wget|grep обойтись )))
            • +3
              [sarkasm]да вы что?
              а как же повторное использование библиотек и красивое документирование кода. [/sarkasm]
              • +2
                а теперь вам сюда нужно прикрутить запись в базу, оповещение на мыло и логирование
                • 0
                  Допишу ещё три строчки. Можно было бы и в одну, но чересчур сложно получится. Мы же запростоту решений, да?
                  • 0
                    Угу, а потом нужно будет отправлять почту через smtp, а настройки этого smtp должны вытягиваться из xml… ну и т.д. И через пару итераций в этих трех строчках черт ногу сломит.

                    Я полностью согласен, что этот код избыточен для выведения в консоль 5 строчек. Но, когда речь идет о клиентском проекте с часто меняющимися и/или добавляющимися требованиями, подход «написать пару строчек кода без учета возможных изменений или расширений» просто неприемлем.
                    • 0
                      Тут уже в одной этой строчке без поллитры не разберёшься :) Я не призываю так делать ни в коем случае, лишь хотел продемонстрировать, что автор поста нарезает холодец болгаркой.
                      • 0
                        В принципе согласен. Но для ознакомления с компонентом Console статья вполне сгодится:)
                        • 0
                          Я не знаю где вы тут разглядели такого рода процесс)))
                          В этом комменте написал, что все должно занимать 3 команды + написание кода.

                          По большому счету мое предложение — попытка сделать инструмент для системного использования, вы же зацепились за конкретный пример для статьи, набросали быстро свою реализацию в одну строчку и с вами сложно спорить о ее простоте))
                • 0
                  >// ./src/Command/NewsInternetCommand.php

                  >namespace Command;

                  >use Parser\YandexRSSNewsParser;
                  >use Symfony\Component\Console\Command\Command;
                  >use Symfony\Component\Console\Input\InputArgument;
                  >use Symfony\Component\Console\Input\InputInterface;
                  >use Symfony\Component\Console\Input\InputOption;
                  >use Symfony\Component\Console\Output\OutputInterface;


                  реализовывать скриптовые задачи на «этом» — facepalm.jpg
                  • 0
                    Не совсем понял в чем проблема использовать неймспейсы?
                    • +3
                      суть не в неймспейсах, а в количестве кода для выполнения простейшей задачи.

                      github.com/Herzult/SimplePHPEasyPlus#readme
                      • +1
                        Приведенный вами пример все же более раздут

                        Весь процесс должен выполниться 3-мя командами:
                        $ composer create-project suncat/console-commands ./cmd
                        $ cd cmd
                        $ app/console generate
                        

                        После чего открываете сгенерированный класс команды и пишете тело метода execute()

                        Не вижу здесь такого переизбытка кода, как вы это представляете…
                        • 0
                          выполните следующее:

                          find $projectRoot -type f -name '*.php' -exec cat {}\; | wc -l

                          поймёте, что такое переизбыток кода
                          считайте кодом не только количество строк, написанных лично вами, а весь код, исполняемый интерпретатором :)
                          • 0
                            А вам жалко интерпретатор, да? :) «Ванильный» PHP явно не хайлоад ориентирован.
                  • –1
                    Я бы чуть упростил возможность форматированного вывода.
                    То что сразу бросается в глаза — вывод секций

                           $output->writeln(array(
                                "",
                                "<info>Start parsing</info>",
                                ""
                            ));
                    


                    и строк с несколькими параметрами.

                                $output->writeln(sprintf(
                                   "<info>[%s]</info> <comment>%s</comment>",
                                   $item['datetime'],
                                   $item['title']
                                ));
                    


                    Всё это стоит упросить и сделать код более компактным )

                    А так идея и реализация очень прикольные. Спасибо
                    • 0
                      Спасибо
                      Этот код чисто для примера, но по вашему совету немного скорректировал.
                      • +1
                        То что всегда хочется сделать в первую очередь в форматировании вывода команд симфони — добавить кастомных методов и хукуов. Ибо использовать эти их XML теги для стандартных вещей крайне нерационально. Лучше добавить предустановленных методов вывода сразу с красивым форматированием.
                        • 0
                          Вы в целом о форматировании, а я подумал речь о конкретном примере.
                          Да было бы более удобно, если бы вывод формировался посредством некого OutputBuilder-а.
                          Стоит подумать над этим…

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