Essentials

Console commands

Learn how to write console commands with a modern, minimal syntax. In Tempest, this is done using attributes, which are automatically discovered by the framework.

Overview

Tempest leverages discovery to find class methods tagged with the #[ConsoleCommand] attribute. Such methods will automatically be available as console commands through the ./tempest executable.

Additionally, Tempest supports console middleware, which makes it easier to build some console features.

Creating console commands

A console command is defined by adding the #[ConsoleCommand] attribute to any class method. Usually, this is done in a dedicated command class, but it can be any method in any class.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand(name: 'aircraft:track')]
    public function __invoke(): void
    {
        // …
    }
}

The command will be named after the class name and the method name. If you prefer, you may add a name argument to the #[ConsoleCommand] attribute to give a dedicated name to the command.

You may learn more about configuring commands in the dedicated section.

Writing to the output

You may use the Tempest\Console\Console interface to write to the output. You can do this by injecting it into your command class, or by using the Tempest\Console\HasConsole trait, which provides a $console property.

The console methods are documented, but you might use the following ones most often:

// Writes a line to the output.
$this->console->writeln('Hello from Tempest!');

 // Writes an informational, error, or warning message.
$this->console->info('This is an informational message.');
$this->console->error('This is an error message.');
$this->console->warning('This is a warning.');

// Prompts for user input. Supports validation and multiple choices.
$this->console->ask('What should be the email?', validation: [new Email()]);

// Executes and reports the progress of a closure.
$this->console->task('Syncing...', $this->synchronize(...));

Specifying an exit code

Optionally, console may return an exit code. By default, Tempest will infer the correct exit code, depending on whether the command was successful or not.

If you want more control over which exit code is returned, you may return an integer between 0 and 255. For convenience, Tempest comes with an Tempest\Console\ExitCode enumeration that has a handful of predefined exit codes, which are generally accepted to be standard.

use Tempest\Console\ExitCode;

public function __invoke(): ExitCode
{
    if (! $this->hasBeenSetup()) {
        return ExitCode::ERROR;
    }

    // …

    return ExitCode::SUCCESS;
}

Command arguments

The command definition is inferred by the method's parameters. This way, there is no need to remember a framework-specific syntax—this is simple, modern PHP.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand('aircraft:track')]
    public function __invoke(AircraftType $type, ?int $radius = null): void
    {
        // …
    }
}

All built-in types are supported, including enums. When a parameter is nullable, it is also optional when invoking the console command.

Negating boolean arguments

You may negate boolean flags by prefixing them with --no.

For instance, if the command has a $validate parameter with a default value of true, using the --no-validate flag would set the value of $validate to false.

Adding a description or an alias

You may provide the #[ConsoleArgument] to any argument of the method definition. This may be used to describe the argument, change its name or specify an alias.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand(
        name: 'aircraft:track',
        description: 'Updates operating aircraft in the database'
    )]
    public function __invoke(
        #[ConsoleArgument(description: 'Specifies the type of aircraft to track')]
        AircraftType $type,
        #[ConsoleArgument(
            description: 'Specifies the maximum radius around HQ to track aircraft in',
            aliases: ['r']
        )]
        ?int $radius = null
    ): void
    {
        // …
    }
}

Argument description are visible when using the --help flag during command invokation.

./tempest aircraft:track --help

// AIRCRAFT:TRACK
Updates operating aircraft in the database

// USAGE
aircraft:track <type {pc12|pc24}> [radius=null]

type
Specifies the type of aircraft to track

radius (r)
Specifies the maximum radius around HQ to track aircraft in

Configuring commands

The #[ConsoleCommand] attribute accepts a few arguments that may provide more context to the user or affect its functionality.

For instance, the middleware argument accepts a list of middleware classes for this command.

Adding a description

You may use the description argument on the #[ConsoleCommand] attribute to provide context to users regarding the functionality of the command.

This description is shown when listing console commands or when calling it with the --help argument.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand(description: 'Updates operating aircraft in the database')]
    public function __invoke(): void
    {
        // …
    }
}

Hiding the command

A command may be completely hidden from the command list by setting the hidden argument to true. The command will remain invokable, but will not be visible to the user when listing commands.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand(hidden: true)]
    public function __invoke(): void
    {
        // …
    }
}

Specifying a name

The name argument of the #[ConsoleCommand] attribute allows for configuring the command name. This is the name used for the command invokation, and the name that is displayed when listing all commands.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand('aircraft:track')]
    public function __invoke(): void
    {
        // …
    }
}

Specifying aliases

When a command is used a lot, you may add aliases instead of shortening its name. To do this, use the aliases argument of the #[ConsoleCommand] attribute.

final readonly class TrackOperatingAircraft
{
    #[ConsoleCommand('aircraft:track', aliases: ['track'])]
    public function __invoke(AircraftType $type): void
    {
        // …
    }
}

You may then call the command by using this alias.

Preventing usage in production

Some commands are dangerous to use in a non-local environment. You may add the CautionMiddleware to a command to prevent it from being invoked in production. When this happens, the user will be alerted and provided with the choice to continue or abort the command execution.

final readonly class SynchronizeAircraft
{
    #[ConsoleCommand('aircraft:sync', middleware: [CautionMiddleware::class])]
    public function __invoke(): void
    {
        // …
    }
}

Middleware

Console middleware can be applied globally or on a per-command basis. Global console middleware will be discovered and applied automatically, by priority order.

Building your own middleware

You may implement the Tempest\Console\ConsoleMiddleware interface to build a console middleware.

app/InspireMiddleware.php
use Tempest\Console\ConsoleMiddleware;
use Tempest\Console\ConsoleMiddlewareCallable;

final readonly class InspireMiddleware implements ConsoleMiddleware
{
    public function __construct(
        private InspirationService $inspiration,
        private Console $console,
    ) {}

    public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int
    {
        if ($invocation->argumentBag->get('inspire')) {
            $this->console->writeln($this->inspiration->random());
        }

        return $next($invocation);
    }
}

Middleware classes will be autowired by the container, so you can use the constructor to inject any dependency you'd like. The Invocation object contains everything you need about the context for the current console command invocation:

  • $invocation->argumentBag contains the argument bag with all the input provided by the user.
  • $invocation->consoleCommand an instance of the #[ConsoleCommand] attribute for the matched console command. This property will be null if you're not using ResolveOrRescueMiddleware or if your middleware runs before it.

Middleware priority

All console middleware classes get sorted based on their priority. By default, each middleware gets the normal priority, but you can override it using the #[Priority] attribute:

app/InspireMiddleware.php
use Tempest\Core\Priority;

#[Priority(Priority::HIGH)]
final readonly class InspireMiddleware implements ConsoleMiddleware
{ /* … */ }

Note that priority is defined using an integer. However, the Priority class provides a few constant with predefined priorities: Priority::FRAMEWORK, Priority::HIGHEST, Priority::HIGH, Priority::NORMAL, Priority::LOW, Priority::LOWEST.

Middleware discovery

Global console middleware classes are discovered and sorted based on their priority. You can make a middleware class non-global by using the #[SkipDiscovery] attribute:

use Tempest\Discovery\SkipDiscovery;

#[SkipDiscovery]
final readonly class InspireMiddleware implements ConsoleMiddleware
{ /* … */ }

Built-in middleware

Tempest provides a few built-in middleware that you may use on your console commands. Some of these middleware are used internally on some commands, and some of them are used on all commands.

Scheduling

Console commands—or any public class method—may be scheduled by using the #[Schedule] attribute, which accepts an Interval or Every value. Methods with this attributes are automatically discovered, so there is nothing more to add.

You may read more on the dedicated chapter.

Testing

Tempest provides a console command testing utility accessible through the console property of the IntegrationTest test case. You may learn more about testing in the dedicated chapter.

tests/ExportUsersCommandTest.php
$this->console
    ->call(ExportUsersCommand::class)
    ->assertSuccess()
    ->assertSee('12 users exported');

$this->console
    ->call(WipeDatabaseCommand::class)
    ->assertSee('caution')
    ->submit()
    ->assertSuccess();