Консольные команды в Yii
Для решения специфических задач часто используются готовые консольные команды. Но намного интереснее не только использовать чужие, но и уметь создавать свои. Это поможет легко автоматизировать любую рутинную работу, на которую обычно тратится довольно много времени. Многие фреймворки имеют встроенные инструменты для написания не только самих веб-приложений, но и для создания инфраструктуры пользовательских консольных команд.
Очень удобно в командной строке перейти в папку приложения на фреймворке Symfony и выполнить:
php app/console cache:clear
и все файловые кэши нашего приложения очистятся в один момент. Аналогично можно выполнить любую из предопределённого списка готовых команд и сразу же увидеть результат.
Можно сделать в админ-панели вашего сайта кнопку для очистки кэша, но консольный вариант мы можем добавить в более продвинутый Bash скрипт, который будет не только очищать кэш, но и делать что-то ещё при загрузке новой версии файлов на рабочий сервер. Такие команды можно автоматически прописать в так называемых «хуках» у системы контроля версий.
Также удобно сделать в папке с проектом скрипт, который при выполнении определённой команды в консоли устанавливает вашу систему в свежую базу данных. Или, например, который запускается планировщиком Cron каждый час и в фоновом режиме пересчитывает и обновляет рейтинги пользователей.
Фантазии нет границ, и некоторые увлечённые люди могут совместить COM-порт, микроконтроллер и дворники от автомобиля, чтобы консольной командой очищать от пыли свой монитор...
Перейдём теперь к изучению консольной работы в PHP.
Консольный режим в PHP
Так называемый интерфейс командной строки (Command Line Interface) представляет собой возможность запускать PHP-файлы в консоли. Если в системе установлен PHP, то можно попробовать открыть консоль и ввести команду:
php -v
Если Windows сообщит нам, что команда php не найдена, то нужно либо перейти в каталог с файлом php.exe
, либо добавить путь этого файла в глобальную переменную PATH
окружения и перезагрузить систему.
Если проблем с этим нет, то команда запустит процесс php
с параметром -v
. В ответ на это нам выведется информация о версии установленного у нас интерпретатора.
Создадим теперь файл console.php:
<?php echo "Console command\n"; echo $argc . PHP_EOL; print_r($argv);
Переменные $argv
и $argc
содержат в себе массив параметров и их количество. Попробуем теперь запустить выполнение этого скрипта в командной строке с помощью нашего интерпретатора:
php console.php
Как альтернативный вариант в Unix системах мы можем добавить аннотацию в первой строке файла как #!/usr/bin/php
или в более универсальном виде:
#!/usr/bin/env php <?php echo "Console command\n"; echo $argc . PHP_EOL; print_r($argv);
после этого можно сделать файл исполняемым и запускать прямым вызовом:
chmod +x console.php ./console.php
В любом из этих случаев наш скрипт выполнится и в консоли будет выведен результат его работы:
Console command 1 Array( [0] => console.php, )
Входящие параматры $argv
и $argc
устроены таким образом, что первым значением передаётся само имя файла.
Попробуем вызвать наш скрипт немного другим образом:
php console.php aaa bbb --ccc=ddd
Результат работы нашего скрипта изменится, так как все дополнения попадут в массив аргументов:
Console command 4 Array ( [0] => console.php [1] => aaa [2] => bbb [3] => --ccc=ddd )
Попробуем выполнить в командной строке и через браузер один и тот же скрипт index.php
:
<?php set_time_limit(0); for ($i=0; $i<100; $++) { echo 'Step ' . $i . PHP_EOL; sleep(1); } echo 'End' . PHP_EOL;
Вкладка браузера «зависнет» на 100 секунд, после чего мы увидим результат. А при выполнении в консоли строки будут выводиться в реальном времени. Нажав в консоли комбинацию Ctrl+C
или Ctrl+Break
мы прервём исполнение в любой момент. Также можно воспользоваться классическими приёмами работы в командной строке и стандартным образом записать результат в файл:
php index.php > log.txt
Интересные возможности предоставляет нам использование функции fgets()
с потоком ввода STDIN
. Этим способом можно запрашивать информацию от пользователя:
<?php set_time_limit(0); echo 'What is your name?' . PHP_EOL; $name = fgets(STDIN); echo 'Hello, ' . trim($name) . '!' . PHP_EOL;
Это удобный инструмент для создания диалогов:
<?php set_time_limit(0); echo 'Are you a man? (Y/n): '; $man = fgets(STDIN); echo strtoupper(trim($man)) == 'Y' ? 'Your are a man!' : 'Your are not a man!'; echo PHP_EOL;
Выполнение скрипта прервётся, пока кто-то не введёт ответ и не нажмёт Enter
.
Таким образом мы можем создавать консольные скрипты не только на Bash, Perl, Ruby или Python, но и на PHP. Нюансы использования CLI-режима можно изучить, например, здесь.
Внутри скрииптов мы можем запускать другие стандартные консольные команды функциями system()
или exec()
. Консольные скрипты легко добавляются в список планировщика Cron:
0 * * * root cd /path/to/app && php console.php
Данный скрипт теперь будет запускаться каждый час. Он может, например, пересчитывать рейтинги пользователей сайта или обновлять курсы валют в базе данных интернет-магазина.
Обратите внимание, что при запуске PHP-скрипта в интерфейсе командной строки классические HTTP элементы $_GET, $_POST, $_REQUEST, $_COOKIE,
$_SERVER['REQUEST_URI']
,$_SERVER['REQUEST_TYPE']
и некоторые другие не будут определены.
Консольные команды в Yii
Вписывать некий функционал в «обычные» PHP-файлы интересно, но до тех пор, пока для работы с базой данных хватит стандартного mysql_query
или ручного подключения PDO. Эффективнее использовать некоторые вещи сразу из Yii, например Yii::app()->db
или свои же модели.
Yii Framework предоставляет нам такую возможность. В консоли можно зайти в папку protected
и запустить файл yiic.php
:
php yiic.php
Это универсальный варинт для любой системы. В Windows с тем же успехом можно запустить BAT-файл yiic.bat
:
yiic
В Linux можно сделать исполняемым файл yiic
:
chmod +x yiic
и запускать уже его
./yiic
В итоге на экране мы увидим приветствие и перечисление встроенных команд, которые мы можем использовать:
Yii command runner (based on Yii v1.1.14) Usage: yiic.php [parameters...] The following commands are available: - message - migrate - shell - webapp To see individual command help, use the following: yiic.php help
Миграции мы обсудим подробно как-нибудь потом, а сейчас попробуем сделать что-то самостоятельное.
Итак, зайдём в папку protected/commands
и создадим файл CacheCommand.php
со следующим содержимым:
class CacheCommand extends CConsoleCommand { public function run($args) { Yii::app()->cache->flush(); echo 'The cache is cleared' . PHP_EOL; } }
Это класс, наследующийся от CConsoleCommand
и переопределяющий базовый метод run($args)
. Вернёмся на уровень выше (в папку protected
) и запустим в командной строке нашу новую команду cache
:
php yiic.php cache
Если все пути в файле yiic.php
указаны верно и в конфигурационном файле protected/config/console.php
настроен компонент cache
:
return array( ... 'components'=>array( ... 'cache'=>array( 'class'=>'CFileCache', ), ), ... );
то на экране должна высветиться фраза «The cache is cleared».
В метод run()
при запуске передаётся массив $args
, который представляет из себя ни что иное, как рассмотренный нами ранее массив параметров $argv
.
Теперь поступим следующим образом: удалим метод run()
, добавим два метода actionClear
и actionCheck
и снова запустим yiic cache
:
class CacheCommand extends CConsoleCommand { public function actionClear() { Yii::app()->cache->flush(); echo 'The cache is cleared' . PHP_EOL; } public function actionCheck() { echo 'Testing of ' . get_class(Yii::app()->cache) . PHP_EOL; Yii::app()->cache->set('test', 'test value'); if (Yii::app()->cache->get('test') == 'test value') { echo 'Storing is valid' . PHP_EOL; } else { echo 'Storing is failed' . PHP_EOL; } Yii::app()->cache->delete('test'); if (empty(Yii::app()->cache->get('test'))) { echo 'Deleting is valid' . PHP_EOL; } else { echo 'Deleting is failed' . PHP_EOL; } } }
На экране вместо сообщения о выполненни мы увидим некое объяснение:
Error: Unknown action: index Usage: yiic.php cache Actions: clear check
Это сообщение сгенерировано методом базового класса CConsoleCommand::run
(который мы теперь не переопределяем). В первой строке идёт сообщение об ошибке (не найден метод по умолчанию actionIndex
). Далее идёт автоматически сгенерированная методом CConsoleCommand::getHelp
справка по команде. Она говорит, что в классе CacheCommand
присутствуют два метода-действия, и нам нужно уточнить, какой из них мы хотим выполнить. Эту же справку мы можем получить и самостоятельно командой help
:
php yiic.php help cache
Фактичестки, классы консольных команд можно строить по тому же принципу, как и контроллеры.
Учтём теперь это замечание и запустим наши команды:
php yiic.php cache clear php yiic.php cache check
Теперь оба действия сработают правильно.
Встроенный генератор справки выводит только список действий и подсказывает способы их вызова. Если вместо данной информации необходимо выводить свой текст, то нужно переопределить метод
getHelp()
и поместить информацию в него.
Команды с параметрами
Родство действий команды с действиями контроллера на этом не заканчивается. Аналогично действия команд могут принимать аргументы. Как система маршрутизации извлекает из адреса и передаёт контроллеру GET-параметры, так и анализатор консольного приложения Yii преобразует введённые значения и передаёт их в аргументы команды.
Создадим команду с аргументом $name
:
class ShowCommand extends CConsoleCommand { public function actionHello($name) { echo 'Hello, ' . $name . '!' . PHP_EOL; } }
и запустим её:
php yiic.php show hello --name=Vasya
Добавим возраст:
class ShowCommand extends CConsoleCommand { public function actionHello($name, $age=18) { echo 'Hello, ' . $name . '! You are ' . $age . ' years old.' . PHP_EOL; } }
и передадим оба значения:
php yiic.php show hello --name=Vasya --age=21
На экран выведется полное приветствие. Возраст здесь объявлен необязательным (указано значение по умолчанию), поэтому его можно не передавать.
А что будет, если не передавать значение имени?
php yiic.php show hello --name --age=21
В данном случае Yii воспримет --name
как логический флаг и присвоит ему значение true
.
А вдруг нам нужно перечислить несколько значений? Для этого объявляем аргумент типа array
:
class ShowCommand extends CConsoleCommand { public function actionHello(array $name) { echo 'Hello, ' . implode(', ', $name) . PHP_EOL; } }
и передаём все имена:
php yiic.php show hello --name=Vasya --name=Petya --name=Dima
Код возврата
При запуске какого-либо скрипта вручную мы сами видим, выполнился ли он успешно или отобразил ошибку. Но чаще консольные команды выполняются самой системой (планировщиком либо другими приложениями). При этом в логах нам бы хотелось видеть отметку об ошибке, если что-то пошло не так.
В мире консольных команд за это отвечают так называемые коды возврата. Порядочное консольное приложение должно вернуть 0 при успешном завершении или любое другое число до 254 при нештатных ситуациях. Если приложение возвратит что-то отличное от нуля, то планировщик непременно запишет это в лог и отправит письмо на электронную почту администратора.
Сама система эти коды не различает, но мы можем их сделать полезными для себя. Расставим в любой нашей команде контрольные точки:
class CalculateCommand extends CConsoleCommand { public function run($args) { ... if (!...) { return 1; } ... try { ... } catch (Exception $e) { return 2; } ... return 0; } }
Это могут быть моменты открытия файлов, подключению к чужим серверам и другие. Теперь по письму с названием команды и кодом ошибки мы можем сразу понять, что пошло не так.
В предыдущих командах мы ни разу не встречали строку
return 0;
. Это нормально, так как Yii по умолчанию делает это за нас.
Выполнение через yiic.php и свои фронт-контроллеры
Всё, что содержит файл protected/yiic.php
– это указание путей к файлу конфигурации protected/config/console.php
и подключение файла framework/yiic.php
, в котором и происходит создание приложения. Изучив исходный код мы можем сделать аналог файла framework/yiic.php
в виде какого-нибудь protected/cli.php
:
<?php $yii = dirname(__FILE__) . '/framework/yii.php'; $config = dirname(__FILE__) . '/config/console.php'; defined('YII_DEBUG') or define('YII_DEBUG', true); require_once($yii); Yii::createConsoleApplication($config)->run();
Этот код практически не отличается от кода файла index.php
, за исключением указания другого конфигурационного файла и создания консольного приложения методом createConsoleApplication
вместо стандартного createWebApplication
. При этом вместо экземпляра CWebApplication
в Yii::app()
будет содержаться немного другой класс CConsoleApplication
.
В итоге ничего особенного не изменится. Как и прежде можно будет запускать любую созданную нами команду:
php cli.php cache clear
Но разница всё же будет. Попробуем выполнить исходный скрипт и наш без указания имени команды:
php yiic.php php cli.php
В первом случае в отчёте мы увидим список cache, message, migrate, shell, webapp
. Во втором же будет отображаться только наша команда cache
. Откуда же берутся все остальные?
Подключение сторонних команд
Если откроем файл framework/yiic.php
, то найдём там строки:
$app = Yii::createConsoleApplication($config); $app->commandRunner->addCommands(YII_PATH . '/cli/commands'); ... $app->run();
Именно здесь и происходит добавление в результирующий список всех имеющихся встроенных команд из папки framework/cli/commands
перед запуском приложения вызовом $app->run()
. Это удобно для работы с целыми папками. То же самое можно организовать и в нашем файле, но это требует изменения самого файла protected/yiic.php
.
Давайте теперь создадим ещё одну команду:
class CalculateCommand extends CConsoleCommand { public $dbConnection = ''; public function run($args) { echo 'Calculate command with connection=' . $this->dbConnection . PHP_EOL; } }
но поместим файл CalculateCommand.php
не в protected/commands
, а в protected/components
и запустим стандартный входной скрипт для консольных команд yiic.php
:
php yiic.php
Естественно, что команда calculate
в списке не появится, так как Yii просмотрит protected/commands
, а этот класс находится в другой папке. Доработаем параметры приложения, добавив в console.php
секцию commandMap
:
return array( 'components' => array( 'db' => array(...), ), 'commandMap' => array( 'calculate' => array( 'class' => 'application.components.CalculateCommand', 'dbConnection' => 'db', ), ), );
Теперь при запуске
php yiic.php
мы увидим нашу команду в списке и сможем её выполнить.
С помощью этого подхода можно добавлять к приложению команды из различных расширений и модулей, не перенося их в папку
protected/commands
, а также настраивать параметры любой команды в файле конфигурации.
Кроме того, данный метод позволяет нам при необходимости заменить класс любой стандартной команды.
Подводные камни
Запуск в командной строке имеет свою специфику. Рассмотрим некоторые трудности, с которыми можно столкнуться при написании консольных команд в Yii.
Глобальные переменные запроса
Как мы уже упоминали, в CLI-режиме некоторые HTTP-переменные массива $_SERVER
, массивы $_SESSION
и $_COOKIE
будут неопределены. Действительно, в командной строке не работают сессии и cookies. При обращении к $_SERVER['REQUEST_URI']
, $_SERVER['HTTP_METHOD']
напрямую или через Yii::app()->request
можно получить пустые значения или ошибки. Поэтому нужно избегать их использования в общих компонентах, работающих как в веб-приложении, так и в консольном. Это ещё один серьёзный довод в пользу отделения пользовательских данных от бизнес-логики моделей и прочих компонентов.
Псевдоним webroot
При использовании загрузки файлов удобно определять корневую директорию сайта вызовом Yii::getPathOfAlias('webroot')
. Расположение папки загрузок получить при этом достаточно легко:
$path = Yii::getPathOfAlias('webroot.upload'); $image = $path . DIRECTORY_SEPARATOR . $model->image;
Простым вызовом
echo Yii::getPathOfAlias('webroot.upload');
мы получаем путь
/home/site/public_html/upload
в соответствии с установленным как dirname($_SERVER['SCRIPT_FILENAME'])
в конструкторе класса CApplication
путём для webroot
.
Если же мы сделаем то же самое в консольной команде, то увидим не то, что нам бы хотелось:
./upload
и абсолютные адреса файлов получить не удастся.
Всё из-за того, что $_SERVER['SCRIPT_FILENAME']
в консольном режиме содержит значение yiic.php
без пути. Ну и с логической точки зрения при запуске в консоли изначально не может быть какого-либо корневого публичного пути.
Соответственно, если необходимо обрабатывать изображения в фоновом режиме (запуская периодически консольную команду по планировщику Cron), то нужно либо переписать все компоненты, работающие с файлами, с учётом невозможности получения корневого пути сайта webroot
(что очень сложно), либо просто переопределить webroot
вручную перед запуском команды:
<?php Yii::setPathOfAlias('webroot', Yii::getPathOfAlias('application') . '/..'); class ImageCommand extends CConsoleCommand { ... }
Если у вас папка protected
вынесена из public_html
, то нужно немного изменить адрес.
Псевдонимы модулей
Если у нас в приложении есть модуль blog
, прописанный в секции modules
конфигурационного файла, то при импортировании его классов по требованию мы можем указывать псевдоним, начинающийся с имени модуля:
Yii::import('blog.models.Post');
или
<?php $this->widget('blog.widgets.RecentPostsWidget');
При попытке импорта модели записи блога таким образом в консольной команде мы получим ошибку. Самый простой способ избавиться от неё – указание полного псевдонима пути:
Yii::import('application.modules.blog.models.Post');
Использование планировщика Cron
Предположим, что мы создали команду для пересчёта рейтингов пользователей и запускаем её вручную так:
cd /home/site/public_html/protected && php yiic.php rating recalculate
Но мы хотим, чтобы она запускалась автоматически каждые 10 минут.
Делаем файл yiic
исполняемым, создаём файл /etc/cron.d/site
и добавляем в него строку:
0 */10 * * www-data cd /home/site/public_html/protected && yiic rating recalculate
или (если файл исполняемым не сделан) одну из двух:
0 */10 * * www-data cd /home/site/public_html/protected && /usr/bin/php yiic.php rating recalculate 0 */10 * * www-data /usr/bin/php -q /home/site/public_html/protected/yiic.php rating recalculate
Порой какая-либо команда может слишком долго ждать ответ от другого сервера или обрабатывать данные. Для таких долгих команд можно сделать так, чтобы пока не завершится предыдущий процесс новые не запускались. Это можно сделать с помощью утилиты flock
:
0 * * * www-data /usr/bin/flock -xn /var/lock/rating.lock -c '/usr/bin/php -q /home/site/public_html/protected/yiic.php rating recalculate'
Теперь на время запуска будет создаваться блокирующий файл /var/lock/rating.lock
. Пока не завершится породивший его процесс новые экземпляры команд запускаться не будут.
Что же дальше?
В текущем уроке мы познакомились с некоторыми нюансами использования консольных команд в PHP в общем и в Yii в частности. Теперь самое время прочесть статью на эту тему из официального руководства.
А пример как в консольном режиме можно настроить полностью автоматическую систему публикации программного кода сайта на сервер можно посмотреть в видео:
В следующей статье мы напишем команду минимизации, местами сильно упрощающую жизнь.
А от опытных разработчиков в комментариях хотелось бы узнать, какие «самодельные» консольные команды вы используете.
Спасибо. О многом не знал.
У вас редко появляются статьи. Я знаю как их трудно писать.
Думаю не плохо было бы сделать раздел коротких заметок. Это позволило бы чаще обновлять блог и получать новых посетителей.
А как эти консольные команды выполнять на виртуальных хостингах? Через exec ?
Вообще вот в статье про миграции вы пишете как ими управлять.
Например я из админки сайта вижу список непримененных миграций. И в админке жму - "Применить". Скрипт же должен запустить консольную команду. Все тут получиться?
Хостинг TimeWeb, например, кроме FTP в панели предоставляет SSH доступ. Им и пользуюсь. А так да, можно exec() или system(), если они доступны.
Столкнулся с обратной задачей: на бесплатном плане у hostinger.ru доступен только мастер "Создать новую Cron-задачу", где прописан префикс "Выполнить команду php -f /home/u123456789/". Позволяет дополнить только именем файла, без каких либо параметров. Не подскажете, как скормить консольному приложению на yii параметры, подключив его в php-файле?
Покопался по исходникам yii: по порядку вызова передача параметров запуска приложения осуществляется
в методе processRequest() класса CConsoleApplication, где массив $_SERVER['argv'] передаётся в метод
run() класса CConsoleCommandRunner. (Первый параметр - название команды, т.е. ключ -f неверно задаёт порядок)
В дальнейшем используется fgets(STDIN), следовательно, теоретически, достаточно подменить $_SERVER['argv']
и подключить файл (Предполагается, что задача для cron лежит на одном уровне с public_html).
Возможно, в статье следует уточнить, что yii работает с $_SERVER['argv'], а не $argv и $argc
Как вариант можно вспомнить об exec()
т.е. пишем:
ну и помним что exec() не всегда разрешена из-за безопасности..
В коде CacheCommand в строке
добавьте точку после PHP_EOL.
Добавил. Спасибо!
В классе CacheCommand ругается на
В api посмотрел: get() method
Изменил на:
-Вроде работает. Причем на mac и linux ошибку не выдает, а в freebsd возвращает
Простой и глупый вопрос.
Написал консольный экшн (yii2), который возвращает какую-то цифирь. Куда она записывается? Как ее посмотреть? И как заставить отправлять письмо администратору?
Смотря куда возвращает. Если возвращаете через return; то это код завершения. Если команда выполняется по Cron, то он запишет это в лог как ошибку, если вернётся что-то отличное от нуля. А если хотите посмотреть при ручном запуске в консоли, то выводите всё через echo.
Спасибо. Еще короткий вопрос. Можно ли как-то в ходе выполнения приложения делать проверку, в каком режиме оно запущено - в консольном или веб? В одной из моделей в beforeSave нужно делать одну вещь только при запуске в веб режиме
Можно проверить класс приложения:
Ещё можно использовать сценарии или явно присваивать значение в контроллере.
Спасибо. Ну еще немного Вас помучаю, если не против :)
Использую консольное приложение в связке с SwiftMailer. Конфигурация SM одинакова для веб приложения и консольного. Из веб приложения все отлично работает, в консольном получаю ошибку
Exception 'Swift_TransportException' with message 'Connection could not be established with host smtp.googlemail.com [ #0]'
Есть ли какая-то тонкость настройки SM для работы с консолью?
Видимо, проблема не в SM и не Yii, а в моем QNAP, на котором хостится сайт. С тестовой машинки все отрабатывает.
Но поскольку Unix/Linux знаю очень плохо, буду благодарен, если есть идеи, куда копать, в каком компоненте/конфигурации может быть проблема. Хотя бы идея, что гуглить...
Дмитрий доброго времени суток.
Подскажите как протестировать консольное приложение в PhpStorm.
Тестирование вебприложения не составляет труда, вызвал дебагер и поставил точку останова.
А как быть с консольным? Спасибо.
Это «отладить», а не «протестировать». А так ответ есть в руководстве:
Спасибо огромное Дмитрий. Точно, "отладить" ;-) !
Рассмотрите вариант написать статью про генерацию круда из консоли.
Дмитрий, спасибо за ваши подробные уроки - очень помогают!
По этой теме не могу разобраться как в yii2 (basic) запускать консольную команду из браузера. Во view сделать кнопку, при нажатии на которую запускалась бы соответствующая команда и отрабатывала в фоновом режиме.
Обычно это делают с помощью очередей Redis/RabbitMQ/Gearman через расширение yii2-queue
Здравствуйте. не могу выполнить команду yii message при том что команда yii init выполняется компосер обновлен yii тоже обновил. пол дня вожусь. при том что эта команда раньше работала успешно. вот скрин. http://prntscr.com/nxx9r4 команда выводит хелпы yii... Помогите пожалуйста, пол дня вожусь.
Заранее спасибо.
Увы, но с таким не сталкивался.
В чем может быть причина на любую команду даже такую (php|./) yii message/config да на любую показывает что на скрине выше... Помогите пожалуйста.