Консоль (CLI)
Lift поставляет крошечный, дружелюбный к PSR CLI-фреймворк — Lift\Console\Application — и бинарник vendor/bin/lift, который идёт с генераторами (make:controller, make:model, …), инструментами базы данных (migrate, migrate:rollback) и воркером очередей.
Ментальная модель: CLI-приложение — это просто коллекция объектов
Commandс ключами по имени.Applicationпарсит argv, находит совпадающую команду, вызывает её методexecute(Input, Output): int.
Использование vendor/bin/lift
После composer require malinichevvv/lift-php бинарник доступен в PATH для любого проекта:
vendor/bin/lift # список всех команд
vendor/bin/lift list # то же самое
vendor/bin/lift help <command> # справка по одной команде
vendor/bin/lift version # показать версию фреймворка
Настройте алиас оболочки, если будете часто его набирать:
alias lift='vendor/bin/lift'
lift list
Встроенные команды
| Группа | Команда | Назначение |
|---|---|---|
| make | make:controller <Name> |
Сгенерировать класс контроллера |
make:request <Name> |
Сгенерировать подкласс FormRequest |
|
make:resource <Name> |
Сгенерировать подкласс JsonResource |
|
make:model <Name> |
Сгенерировать active-record модель | |
make:middleware <Name> |
Сгенерировать middleware по PSR-15 | |
make:command <Name> |
Сгенерировать подкласс Command |
|
make:job <Name> |
Сгенерировать задачу очереди | |
make:event <Name> |
Сгенерировать класс события | |
make:test <Name> |
Сгенерировать подкласс TestCase |
|
make:migration <name> |
Сгенерировать файл миграции с меткой времени | |
| migrate | migrate |
Выполнить все ожидающие миграции |
migrate:rollback [--steps=N] |
Откатить последние N пакетов (по умолчанию 1) | |
migrate:reset |
Откатить каждую миграцию | |
migrate:fresh |
reset + migrate |
|
migrate:status |
Табличное состояние каждой миграции | |
| queue | queue:work [--queue=...] [--sleep=N] [--max-jobs=N] |
Запустить воркер очереди (см. Очереди) |
queue:table |
Вывести SQL/миграцию для создания таблицы очереди в БД | |
| routes | routes:list [--bootstrap=...] |
Перечислить каждый зарегистрированный маршрут в таблице |
| app | serve [--port=8000] |
Запустить php -S на public/ |
key:generate |
Вывести случайный APP_KEY (32 байта в base64) |
|
repl |
Запустить интерактивный PHP REPL с контекстом приложения |
Большинство генераторов принимают эти флаги:
lift make:controller AdminController --namespace=App\\Admin --path=src/Admin
| Флаг | По умолчанию | Назначение |
|---|---|---|
--namespace=… |
App |
PHP-пространство имён сгенерированного класса |
--path=… |
src |
Каталог записи (относительно CWD) |
--force |
выкл. | Перезаписывать существующие файлы |
Сгенерированные файлы намеренно минимальны — это отправные точки, редактируйте свободно.
make:test — генерация тестовых классов
lift make:test UserTest
# → src/Tests/UserTest.php
lift make:test Feature/OrderFlowTest --namespace=Tests\\Feature
# → src/Tests/Feature/OrderFlowTest.php
Сгенерированная заготовка наследует Lift\Testing\TestCase:
final class UserTest extends TestCase
{
public function testExample(): void
{
$this->assertTrue(true);
}
}
TestCase поставляет HTTP-помощники ($this->get(...), $this->post(...), $this->assertStatus(200)) — см. Тестирование для полного API.
repl — интерактивный PHP REPL
lift repl помещает вас в живой PHP-интерпретатор с уже загруженным приложением:
$ lift repl
Lift REPL — type PHP and press Enter. Type exit or Ctrl+D to quit.
$app is available.
>>> $app->configuration()->get('app.name')
"My App"
>>> $app->db()->table('users')->count()
42
>>> $u = new App\Models\User(); $u->name = 'Alice'
>>> $u
App\Models\User {"name":"Alice"}
>>> exit
Bye!
Как это работает
-
Lift ищет файлы начальной загрузки в таком порядке:
bootstrap/app.phpapp/bootstrap.phpapp.php
Если найден, он подключает файл и делает возвращаемое значение доступным как
$app. -
Каждая строка сначала пробуется как выражение (
return (…);). Если она парсится, возвращаемое значение печатается в компактном виде в стиле var_export. Если не парсится (присваивание, поток управления и т. д.), строка выполняется как оператор. -
Переменные сохраняются между итерациями — задайте
$x = 5на одной строке, используйте$xна следующей. -
Многострочный ввод: завершите строку
\, чтобы продолжить на следующей.
>>> $users = $app->db()
... ->table('users')
... ->where('active', true)
... ->get() \
... ->pluck('email')
["[email protected]","[email protected]"]
Флаги
| Флаг | Назначение |
|---|---|
--bootstrap=path |
Явный файл начальной загрузки (переопределяет автоопределение) |
Пример:
lift repl --bootstrap=app/bootstrap.php
lift repl --bootstrap=/var/www/myapp/bootstrap/app.php
История сохраняется в ~/.lift_repl_history и загружается при следующем запуске, так что вы можете стрелкой вверх пролистывать предыдущие сессии.
Требования: должно быть установлено PHP-расширение readline (php-readline в большинстве дистрибутивов Linux). REPL сообщит вам, если оно отсутствует.
Практические примеры REPL
Проверка конфигурации
>>> $app->configuration()->get('app.name')
"My App"
>>> $app->configuration()->get('database.connections.mysql.host')
"localhost"
>>> $app->environment()
"local"
Запрос к базе данных
>>> $app->db()->table('users')->count()
42
>>> $app->db()->table('users')->where('active', true)->get()
[{"id":1,"email":"[email protected]","name":"Alice"}, ...]
>>> $app->db()->table('orders')->where('status', 'pending')->count()
7
Тестирование модели
>>> $user = $app->container()->get(\App\Models\User::class)
>>> $user->find(1)
App\Models\User {"id":1,"email":"[email protected]","name":"Alice","active":true}
>>> $user->find(9999)
null
Проверка маршрутов
>>> $app->router()->getRoutes()
[{"path":"/","method":"GET","handler":"App\Http\Controllers\HomeController@index","name":"home"}, ...]
>>> $app->router()->match('GET', '/users/42')
{"path":"/users/{id}","handler":"App\Http\Controllers\UserController@show","params":{"id":"42"}}
Работа с кэшем
>>> $cache = $app->container()->get(\Psr\SimpleCache\CacheInterface::class)
>>> $cache->get('user:42:profile')
{"id":42,"name":"Alice","role":"admin"}
>>> $cache->set('test_key', 'test_value', 60)
true
>>> $cache->get('test_key')
"test_value"
Разовая задача — создать пользователя
>>> $app->db()->table('users')->insert([
... 'email' => '[email protected]',
... 'name' => 'Admin',
... 'password_hash' => password_hash('secret', PASSWORD_BCRYPT),
... 'created_at' => time(),
... ])
true
>>> $app->db()->table('users')->where('email', '[email protected]')->first()
{"id":43,"email":"[email protected]","name":"Admin",...}
Тестирование сервиса
>>> $payment = $app->container()->get(\App\Services\PaymentService::class)
>>> $payment->charge(1000, 'tok_visa')
{"id":"ch_1234","amount":1000,"status":"succeeded"}
Проверка привязок контейнера
>>> $app->container()->has(\App\Services\PaymentService::class)
true
>>> $app->container()->make(\Lift\Http\Request::class)
Lift\Http\Request {...}
Отладка переменной
>>> $data = ['foo' => 'bar', 'nested' => ['a' => 1, 'b' => 2]]
>>> $data
{"foo":"bar","nested":{"a":1,"b":2}}
Попробовать вызов API
>>> $client = $app->container()->get(\Lift\Http\HttpClient::class)
>>> $client->get('https://api.github.com/repos/malinichevvv/lift-php')->json()
{"id":123456789,"name":"lift-php","full_name":"malinichevvv/lift-php",...}
Когда использовать REPL vs альтернативы
| Задача | REPL | CLI-команда | Тест | Скрипт |
|---|---|---|---|---|
| Быстрый эксперимент с API | ✅ Лучше всего | ❌ Избыточно | ❌ Медленно | ❌ Шаблонный код |
| Разовое исправление данных | ✅ Хорошо | ✅ Лучше, если переиспользуемо | ❌ Нет | ✅ Хорошо для сложного |
| Отладка продакшена (осторожно) | ✅ Возможно | ✅ Безопаснее | ❌ Нет | ❌ Нет |
| Повторяющаяся задача | ❌ Нет истории | ✅ Идеально | ❌ Нет | ✅ Да |
| Сложная логика | ❌ Нет отмены | ✅ Версионируется | ✅ Проверено | ✅ Версионируется |
Ограничения REPL
- Нет отмены — если вы удалите данные через
$app->db()->table('x')->delete(), они пропали. Будьте осторожны в продакшене. - Нет персистентности — сессии REPL не сохраняют состояние. Перезагрузите, чтобы начать заново.
- Глобальное состояние — изменения затрагивают только процесс PHP. Другие воркеры/процессы их не увидят.
- Долгоживущее состояние — если вы открываете транзакцию и забываете зафиксировать/откатить её, она остаётся открытой до выхода из REPL.
Добавление собственных команд
Lift\Console\Application принимает любой подкласс Lift\Console\Command. Положите файл в bin/:
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Lift\Console\Application;
use Lift\Console\Command;
use Lift\Console\Input;
use Lift\Console\Output;
final class CleanCacheCommand extends Command
{
public function getName(): string { return 'cache:clear'; }
public function getDescription(): string { return 'Wipe the application cache'; }
public function execute(Input $input, Output $output): int
{
$output->write('Clearing cache… ');
// …собственно работа…
$output->success('done');
return 0;
}
}
$app = require __DIR__ . '/../app/bootstrap.php'; // ваше приложение Lift
$cli = new Application('myapp', '1.0.0');
$cli->register($app->make(CleanCacheCommand::class)); // разрешено через DI
exit($cli->run());
Сделайте его исполняемым: chmod +x bin/myapp. Запустите: ./bin/myapp cache:clear.
Соглашение: давайте командам пространства имён
Используйте : в имени команды, чтобы группировать команды в list. lift list напечатает их под заголовками:
cache
cache:clear Wipe the application cache
cache:warmup Pre-build the page cache
db
db:seed Run the seeders
Базовый класс Command
abstract class Command
{
abstract public function getName(): string; // например, 'cache:clear'
abstract public function getDescription(): string; // однострочное резюме
abstract public function execute(Input $i, Output $o): int; // код выхода
public function getHelp(): string { return $this->getDescription(); } // необязательная длинная справка
}
Возвращайте 0 из execute() при успехе, ненулевое значение при неудаче. Код выхода — это то, что возвращает Application::run() — идеально для shell-скриптов:
lift migrate || { echo 'migration failed'; exit 1; }
Input — чтение argv
$input->getCommand(); // 'migrate'
$input->getArgument(0, 'default'); // первый позиционный аргумент
$input->getArguments(); // все позиционные аргументы (кроме команды)
$input->getOption('queue', 'default'); // --queue=foo или 'default'
$input->hasOption('force'); // был ли передан --force?
Правила парсинга argv:
--name=value→ опцияnameсо строковым значением.--name→ опцияnameсо значениемtrue.-X(один символ) → опцияXсо значениемtrue.- Всё остальное, по порядку, становится командой, а затем позиционными аргументами.
Намеренно нет объявлений обязательных аргументов — читайте нужные аргументы, откатывайтесь к значениям по умолчанию, падайте с полезным сообщением:
public function execute(Input $i, Output $o): int
{
$name = $i->getArgument(0);
if ($name === '') {
$o->error('Usage: lift make:foo <name>');
return 1;
}
// …
}
Output — запись в stdout/stderr с цветом
Теги в стиле разметки внутри строк — <green>, <yellow>, <red>, <cyan>, <bold>, <grey> — преобразуются в ANSI-escape только когда stdout это TTY. В пайпе (lift foo | grep …) или в тестах теги вырезаются.
$o->writeln('Hello');
$o->writeln('<green>Success</green> in <bold>0.4s</bold>');
$o->write('Working… '); // без перевода строки
$o->writeln('done');
$o->success('All clear.'); // зелёный
$o->warn('Slow query detected'); // жёлтый
$o->error('Boom'); // красный, → stderr
$o->info('Heads up'); // голубой
Таблицы
$o->table(
headers: ['ID', 'Email', 'Active'],
rows: [
[1, '[email protected]', 'yes'],
[2, '[email protected]', 'no'],
],
);
Автоматически подгоняет столбцы под самую широкую ячейку. Используйте для вывода в стиле migrate:status.
Автономный CLI (без приложения Lift)
Lift\Console\Application не зависит от Lift\App. Вы можете использовать его отдельно:
use Lift\Console\Application;
$cli = new Application('mytool', '0.1.0');
$cli->register(new GenerateReadmeCommand());
$cli->register(new CheckLinksCommand());
exit($cli->run());
Отлично для специфичного для проекта инструментария без HTTP-стека.
Реальный пример — ежедневное cron-задание
bin/daily.php:
#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Lift\Console\Application;
use Lift\Console\Command;
use Lift\Console\Input;
use Lift\Console\Output;
$app = require __DIR__ . '/../app/bootstrap.php';
final class PurgeOldSessions extends Command
{
public function __construct(private readonly \Lift\Database\Connection $db) {}
public function getName(): string { return 'purge:sessions'; }
public function getDescription(): string { return 'Delete sessions older than 30 days'; }
public function execute(Input $i, Output $o): int
{
$days = (int) $i->getOption('days', 30);
$cut = time() - 86400 * $days;
$n = $this->db->execute('DELETE FROM sessions WHERE last_activity < ?', [$cut]);
$o->success("Purged {$n} stale session(s).");
return 0;
}
}
$cli = new Application('daily', '1.0.0');
$cli->register($app->make(PurgeOldSessions::class));
exit($cli->run());
Crontab:
0 3 * * * cd /var/www/myapp && php bin/daily.php purge:sessions --days=30
Тестирование команд
Output принимает собственные потоковые ресурсы, так что тесты могут захватывать stdout/stderr без форка процесса:
public function testItPrintsHello(): void
{
$out = fopen('php://memory', 'r+');
$err = fopen('php://memory', 'r+');
$cmd = new MyCommand();
$exit = $cmd->execute(new Input(['arg']), new Output($out, $err));
rewind($out);
self::assertSame(0, $exit);
self::assertStringContainsString('Hello', stream_get_contents($out));
}
Цветовые теги автоматически вырезаются, когда Output не считает, что он на TTY — ваши утверждения остаются свободными от ANSI-escape.
Частые подводные камни
| Симптом | Причина | Исправление |
|---|---|---|
Command 'foo' not found |
Забыли register() её |
Добавьте её в начальную загрузку вашего CLI. |
Цвета показываются буквально как <green>... |
Output не обнаруживает TTY (пайп, перенаправление) | Ожидаемо — цвета показываются только в реальных терминалах. |
| Вывод длинной команды не сбрасывается | fwrite() в stdout строчно-буферизирован |
fflush(STDOUT) после записи — или используйте Output::writeln(), который пишет целые строки. |
Cannot resolve parameter $db при регистрации |
Начальная загрузка CLI забыла подключить DI | Стройте команды через контейнер: $cli->register($app->make(MyCmd::class)). |
lift queue:work сразу выходит с кодом 0 |
Драйвер очереди не настроен ⇒ SyncQueue::pop() всегда возвращает null, и sleep тикает вечно — но он продолжает работать. Симптом — «ничего не происходит» |
Настройте настоящий драйвер (Redis, БД). |
pcntl_signal not available в воркере |
Скомпилированный PHP без pcntl | Установите php-pcntl; некорректное завершение всё равно работает. |
Шпаргалка
// Создать команду
final class MyCmd extends Command
{
public function getName(): string { return 'my:cmd'; }
public function getDescription(): string { return 'Does X'; }
public function execute(Input $i, Output $o): int { /* … */ return 0; }
}
// Чтение ввода
$i->getCommand() / getArgument(0) / getArguments() / getOption('name') / hasOption('force');
// Запись вывода
$o->writeln('plain');
$o->success('green'); $o->warn('yellow'); $o->error('red, → stderr'); $o->info('cyan');
$o->table(['a','b'], [[1,2]]);
// Запустить CLI
$cli = new Application('myapp', '1.0.0');
$cli->register($cmd);
exit($cli->run());
// Встроенные команды
vendor/bin/lift list / version / help <cmd>
vendor/bin/lift make:controller|request|resource|model|middleware|command|job|event|test <Name>
vendor/bin/lift make:migration <name>
vendor/bin/lift migrate / migrate:rollback / migrate:fresh / migrate:status
vendor/bin/lift queue:work
vendor/bin/lift routes:list
vendor/bin/lift key:generate
vendor/bin/lift serve --port=8000
vendor/bin/lift repl [--bootstrap=path/to/app.php]