Console commands
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.
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 benull
if you're not usingResolveOrRescueMiddleware
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:
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.
- The
ForceMiddleware
adds the--force
flag for skipping$console->confirm()
calls. - The
CautionMiddleware
middleware prevents usage of commands in production. - The
OverviewMiddleware
is responsible from listing all commands when none is provided. - The
ResolveOrRescueMiddleware
middleware provides a list of similar commands when an unknown command is invoked. - The
HelpMiddleware
middleware provides help when the--help
flag is used. - The
ConsoleExceptionMiddleware
middleware catches and properly render console exceptions.
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.
$this->console ->call(ExportUsersCommand::class) ->assertSuccess() ->assertSee('12 users exported'); $this->console ->call(WipeDatabaseCommand::class) ->assertSee('caution') ->submit() ->assertSuccess();