Чат Telegram
Группа ВКонтакте
CLI в PHP

Command Line Interface в PHP

До этого момента мы с Вами знали, что PHP работает на сервере. Клиент обращается к серверу по протоколу HTTP с каким-либо запросом, запрос на сервере обрабатывается и формируется ответ. После этого клиенту снова по протоколу HTTP в ответе отдаётся сформированный ответ. Однако, если взять какой-нибудь более-менее продвинутый сайт, то мы увидим, что есть задачи, которые не решаются стандартным клиент-серверным путем. Например: поздравлять пользователей с днём рождения и дарить им скидку на какой-нибудь продукт. Для того, чтобы это сделать, нам придется обновлять раз в день php-скрипт в браузере, чтобы он выбирал пользователей, у которых сегодня ДР, затем создавал для них скидки, и отправлял им сообщения по почте. Согласитесь, неудобно это делать вручную и в браузере. Для таких случаев в PHP предусмотрен Command Line Interface (CLI) – интерфейс командной строки.

CLI позволяет запускать программы на PHP не через привычную нам клиент-серверную архитектуру, а как простые программы в командной строке. Давайте создадим простейший скрипт, чтобы показать, как это работает. Создаём новую папку bin в корне проекта, а в ней файл – cli.php.

CLI контроллер

Пишем простейший код:

bin/cli.php

<?php

echo 2 + 2;

А теперь запускаем консоль из OpenServer:
OpenServer cmd

Переходим в папку с нашим проектом, выполнив:

cd domains\myproject.loc

И пишем следующую команду:

php bin/cli.php

В ответ получаем:
Вывод результата в терминал

Написали простейшее консольное приложение! Уже неплохо. Но что если мы захотим сложить 2 числа, которые нужно передать скрипту? Как Вы понимаете, сделать это с помощью GET- или POST- запросов уже не получится. Так как же быть?

Аргументы консольного приложения

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

А для того, чтобы получить к ним доступ из php-скрипта используется магическая переменная $argv. Она представляет собой массив, в котором нулевой элемент – это путь до скрипта, а все последующие – это его аргументы в консоли.

bin/cli.php

<?php

var_dump($argv);

Давайте теперь запустим наш скрипт с параметрами:
Вывод аргументов

Как видим, наши аргументы попали в этот массив. Давайте напишем простейший скрипт, который будет складывать все переданные ему аргументы.

<?php

unset($argv[0]);

$sum = 0;

foreach ($argv as $item) {
    $sum += $item;
}

echo $sum;

Запустим его, и убедимся, что все работает:
Сумма всех аргументов

И он действительно работает: 3 + 4 + 5 = 12.

А что если мы хотим передавать аргументы с именами? Вроде такого:
Именованные аргументы

И затем в коде получать их в коде по их именам? Для этого нам следует написать простейший парсер, который будет находить вот такие именованные параметры и их значения. Пишем.

<?php

unset($argv[0]);

$params = [];

foreach ($argv as $argument) {
    preg_match('/^-(.+)=(.+)$/', $argument, $matches);
    if (!empty($matches)) {
        $paramName = $matches[1];
        $paramValue = $matches[2];

        $params[$paramName] = $paramValue;
    }
}

var_dump($params);

И проверяем его работу:
аргументы в массиве

Отлично, теперь мы можем обращаться к элементам массива params, чтобы выяснить, были ли нам переданы какие-то аргументы или нет.

CLI и ООП

Мы с вами изучили некоторые основы работы с CLI. Давайте теперь перенесем эти знания на объектно-ориентированный подход и научимся работать через интерфейс командной строки с объектами.

Для этого нам понадобится создать отдельную директорию под «команды». Команды – так мы будем называть наши специальные классы, которые будут выполнять какой-то код через запуск из командной строки. Создаем новую директорию: src/MyProject/Cli.
Папка для команд

И теперь создадим наш первый класс, который будет заниматься тем, что считает сумму переданных в него аргументов: -a и -b.

src/MyProject/Cli/Summator.php

<?php

namespace MyProject\Cli;

use MyProject\Exceptions\CliException;

class Summator
{
    /** @var array */
    private $params;

    public function __construct(array $params)
    {
        $this->params = $params;
        $this->checkParams();
    }

    public function execute()
    {
        echo $this->getParam('a') + $this->getParam('b');
    }

    private function checkParams()
    {
        $this->ensureParamExists('a');
        $this->ensureParamExists('b');
    }

    private function getParam(string $paramName)
    {
        return $this->params[$paramName] ?? null;
    }

    private function ensureParamExists(string $paramName)
    {
        if (!isset($this->params[$paramName])) {
            throw new CliException('Param with name "' . $paramName . '" is not set!');
        }
    }
}

В конструкторе класса мы принимаем список параметров, сохраняем их, а затем вызываем метод checkParams(), который проверяет наличие обязательных параметров для этого скрипта. В нём просто поочередно вызывается метод для проверки в массиве нужных ключей. Если их нет – метод кинет исключение. И, наконец, есть метод execute(), который содержит бизнес-логику. В нем используется метод getParam(), который вернет параметр (при его наличии), либо вернет null (при его отсутствии).

И также создаём исключение, специально для ошибок, возникающих при работе с CLI.

src/MyProject/Exceptions/CliException.php

<?php

namespace MyProject\Exceptions;

class CliException extends \Exception
{
}

Теперь давайте снова вернемся в нашу точку входа для консольных приложений cli.php. Этот файл можно назвать фронт-контроллером для консольных команд, он как index.php в случае с клиент-серверным подходом будет создавать другие объекты и запускать весь процесс.

Дополним этот код так, чтобы он создавал экземпляр нужного класса и передавал ему аргументы.

bin/cli.php

<?php

try {
    unset($argv[0]);

    // Регистрируем функцию автозагрузки
    spl_autoload_register(function (string $className) {
        require_once __DIR__ . '/../src/' . $className . '.php';
    });

    // Составляем полное имя класса, добавив нэймспейс
    $className = '\\MyProject\\Cli\\' . array_shift($argv);
    if (!class_exists($className)) {
        throw new \MyProject\Exceptions\CliException('Class "' . $className . '" not found');
    }

    // Подготавливаем список аргументов
    $params = [];
    foreach ($argv as $argument) {
        preg_match('/^-(.+)=(.+)$/', $argument, $matches);
        if (!empty($matches)) {
            $paramName = $matches[1];
            $paramValue = $matches[2];

            $params[$paramName] = $paramValue;
        }
    }

    // Создаём экземпляр класса, передав параметры и вызываем метод execute()
    $class = new $className($params);
    $class->execute();
} catch (\MyProject\Exceptions\CliException $e) {
    echo 'Error: ' . $e->getMessage(); 
}

Теперь мы можем запустить наш скрипт с помощью вот такой команды:
Сумматор

Если мы захотим создать еще один класс, в котором мы будем вычитать из аргумента a аргумент b, то нам нужно будет продублировать довольно большой объем кода. Но ведь если присмотреться – большую часть кода из класса Summator можно вынести в отдельный класс и использовать его повторно.

Давайте создадим абстрактный класс, который будет заниматься тем, что будет сохранять переданные в него параметры и запускать метод для их проверки.

src/MyProject/Cli/AbstractCommand.php

<?php

namespace MyProject\Cli;

use MyProject\Exceptions\CliException;

abstract class AbstractCommand
{
    /** @var array */
    private $params;

    public function __construct(array $params)
    {
        $this->params = $params;
        $this->checkParams();
    }

    abstract public function execute();

    abstract protected function checkParams();

    protected function getParam(string $paramName)
    {
        return $this->params[$paramName] ?? null;
    }

    protected function ensureParamExists(string $paramName)
    {
        if (!isset($this->params[$paramName])) {
            throw new CliException('Param with name "' . $paramName . '" is not set!');
        }
    }
}

Теперь нам в классе Summator достаточно отнаследоваться от этого класса и он значительно упростится:

src/MyProject/Cli/Summator.php

<?php

namespace MyProject\Cli;

class Summator extends AbstractCommand
{
    protected function checkParams()
    {
        $this->ensureParamExists('a');
        $this->ensureParamExists('b');
    }

    public function execute()
    {
        echo $this->getParam('a') + $this->getParam('b');
    }
}

Запустим скрипт снова и убедимся, что все успешно отработало:
Результат суммы в терминале

Давайте создадим по аналогии скрипт, который будет вычитать из аргумента x аргумент y.

src/MyProject/Cli/Minusator.php

<?php

namespace MyProject\Cli;

class Minusator extends AbstractCommand
{
    protected function checkParams()
    {
        $this->ensureParamExists('x');
        $this->ensureParamExists('y');
    }

    public function execute()
    {
        echo $this->getParam('x') - $this->getParam('y');
    }
}

Проверим его в деле:
Разница в консоли

А теперь давайте попробуем не указать один из аргументов – получим ошибку.
Ошибка об отсутствии обязательного аргумента

Вот таким вот нехитрым образом мы с вами научились создавать простейшие программы для запуска в консоли на PHP. А в следующем уроке мы с вами научимся запускать эти команды по расписанию.

loader
Домашнее задание

В файле cli.php добавьте проверку на то, что класс, указанный в качестве аргумента, является наследником класса AbstractCommand. Проверку нужно осуществлять ещё до создания объекта, имея только имя класса.

Курс программирования на PHP
Подготовка до уровня устройства на работу!
Начать бесплатно
Логические задачи с собеседований