Skip to content

Console commands

naf/cli runs named commands inside the application's bootstrapped environment. Commands can use configured services, plugins and logging for imports, maintenance and workers. Run the examples from the application root unless another directory is specified.

Required packages

In an existing NAF application:

composer require naf/cli

List and run commands

vendor/bin/naf command:list

In an application with naf/cli and the hello:say command from this chapter, the output looks like this:

 ---------------------
  Registered commands
 ---------------------

 Say hello
 hello:say .  .  .  .  .Greets a person by name

 Plugin boot order
 plugins:debug .  .  .  Displays plugin boot order, declared prerequisites and absent optional targets.

 Route debug
 route:debug .  .  .  . Displays current routes for an application.

With no arguments at all, the binary prints the same list. Every installed plugin contributes its commands, so what you see depends on what you have installed.

The binary works from any directory: it finds its autoloader through Composer rather than through the directory you happen to be standing in.

With naf/cli 0.2.3+, an application can install a root-level shortcut. From the directory containing its composer.json, run:

vendor/bin/naf-install
bin/naf command:list

The installer creates bin/naf and adds a post-autoload-dump hook to restore it after later Composer installs. Commit the shortcut and manifest change with your application. It leaves a conflicting existing file or symlink untouched. If the application provides an executable bin/naf-runtime, the shortcut delegates runtime selection to that file; otherwise it runs the Composer binary with local PHP. A missing or non-executable adapter is an error. The usual vendor/bin/naf remains available.

To inspect plugin startup, run vendor/bin/naf plugins:debug (or bin/naf plugins:debug when the shortcut is installed). It shows the order, declared prerequisites and optional targets that are not installed. This command requires framework 0.2.7+.

Define a command

A command is a class extending AbstractCommand, with a name, a configure() that declares its arguments, and a run() that does the work. In a starter application, create this file:

app/Commands/HelloCommand.php
<?php

declare(strict_types=1);

namespace App\Commands;

use Naf\CLI\Core\AbstractCommand;
use Naf\CLI\Core\Input;
use Naf\CLI\Core\Output;

final class HelloCommand extends AbstractCommand
{
    public const string NAME = 'hello:say';

    protected function configure(): void
    {
        $this->setTitle('Say hello')
            ->setDescription('Greets a person by name')
            ->addArgument('name')
            ->addOption('help', 'h');
    }

    public function run(Input $input, Output $output): int
    {
        if ($input->getOption('help') || $input->getOption('h')) {
            $this->showHelp($output);
            return static::SUCCESS;
        }

        $name = $input->getArgument('name');
        if (!is_string($name) || trim($name) === '') {
            $output->writeLine('A name is required.', 'error');
            return static::ERROR;
        }

        $output->writeLine('Hello, ' . trim($name) . '!', 'ok');

        return static::SUCCESS;
    }
}

Register a command

use App\Commands\HelloCommand;
use function Naf\app;
use function Naf\CLI\command;

app(); // Boots NAF and its plugins; naf/cli defines command() during this boot.
command()->add(HelloCommand::class);

Add this registration to root bootstrap.php, after requiring Composer's autoloader and before app()->run(). The app() call matters: naf/cli loads its helper functions while NAF boots its plugins, so calling command() before the first app() fails with Call to undefined function Naf\CLI\command(). Commands are not discovered by scanning app/Commands/; application and plugin bootstraps register them explicitly.

vendor/bin/naf hello:say World

Expected output, with exit status 0:

 -----------
  Say hello
 -----------

 Hello, World!

The command explicitly rejects a missing or empty name; validate inputs in run() rather than relying on argument metadata alone.

vendor/bin/naf hello:say --help prints the title, description, arguments and options declared in configure(), because the example calls showHelp() for --help. CLI 0.2.4+ prints plain text with a vendor/bin/naf usage line:

 -----------
  Say hello
 -----------


 Say hello
   Greets a person by name

 Usage:
   vendor/bin/naf hello:say <name> [options]

 Arguments:
   name

 Options:
   -h, --help
CLI 0.2.3 and older

Help prints literal <info> and <comment> markers and an outdated php cli.php hint. The argument and option definitions still apply; update CLI for corrected output.

Arguments and options

protected function configure(): void
{
    $this->addArgument('name')                      // required
        ->addArgument('greeting', optional: true)   // optional
        ->addOption('shout', 's')                   // a flag
        ->addOption('repeat', 'r', expectsValue: true);
}
$input->getArgument('name');      // ?string
$input->getOption('shout');       // true when present, null when not
$input->getOption('repeat');      // the value, or an array when given more than once

Repeated options can return an array. Validate option types and reject repeats when a command accepts only one value.

Interactive input

$name = $input->ask('What is your name? ');

This blocks for interactive input. Commands used by cron or CI should receive required values as arguments or options and must not depend on an interactive terminal.

Writing output

$output->writeLine('Done.', 'ok');        // green
$output->writeLine('Careful.', 'warning'); // yellow
$output->writeLine('Failed.', 'error');    // red
$output->writeLine('Report', 'title');     // light green on a black background
$output->writeLine('Section', 'headline'); // light blue on a black background
$output->writeLine('Plain text');          // no colour
$output->writeEmptyLine();
$output->drawStroke(40);                   // a line of dashes

Exit status

run() returns an integer, and that integer becomes the process's exit status. static::SUCCESS is 0, static::ERROR is 1.

Return a nonzero status on failure so cron, CI and shell conditionals can detect it. Test commands through vendor/bin/naf as well as their underlying services.

If the result is different

What you see What to check
Command "name" not found The registration runs in bootstrap.php and the name matches NAME; command:list shows what is registered
Call to undefined function Naf\CLI\command() Call app() before command() in bootstrap.php; see Register a command
The command is missing after adding a package The package is installed with composer require and registers its commands in its bootstrap
A cron job reports success although the command failed run() returns static::ERROR (or another nonzero value) on failure