Skip to main content
version 1.0.0

Commands

Introduction

In Athenna there is a more advanced way to create commands than just a closure command inside routes/console file. You just need to extend the abstract class Command and implement her methods.

Writing commands

In addition to the commands provided with Artisan, you may build your own custom commands. Commands are typically stored in the app/Console/Commands directory; however, you are free to choose your own storage location as long as your commands can be imported.

Generating commands

To create a new command, you may use the make:command Artisan command. This command will create a new command class in the app/Console/Commands directory and register it inside commands method of app/Console/Kernel.js file. Don't worry if this directory does not exist in your application - it will be created the first time you run the make:command Artisan command:

node artisan make:command SendEmails

Commands structure

After generating your command, you should define appropriate values for the signature and description properties of the class. These properties will be used when displaying your command on the list screen. The signature property also allows you to define your command's arguments expectations. The handle method will be called when your command is executed. You may place your command logic in this method.

Let's take a look at an example command. Note that we are able to request any dependencies we need via the command's constructor method. The Athenna service container will automatically inject all dependencies inside the container property of constructor:

import { Command } from '@athenna/artisan'

export class SendEmails extends Command {
/**
* The name and signature of the console command.
*
* @return {string}
*/
get signature() {
return 'mail:send <email>'
}

/**
* The console command description.
*
* @return {string}
*/
get description() {
return 'Display welcome object.'
}

/**
* The welcome service dependency.
*
* @type {import('#app/Services/MailgunService').MailgunService}
*/
#mailgunService

/**
* Use the constructor to resolve any dependency of the Ioc container.
*
* @param {any} mailgunService
*/
constructor(mailgunService) {
super()

this.#mailgunService = mailgunService
}

/**
* Execute the console command.
*
* @param {string} email
*/
async handle(email) {
const message = 'Hello from Athenna!'

await this.#mailgunService.send(email, message)
}
}
tip

For greater code reuse, it is good practice keeping your console commands light and let them defer to application services to accomplish their tasks. In the example above, note that we inject a service class to do the "heavy lifting" of sending the e-mails.

Defining input expectations

When writing console commands, it is common to gather input from the user through arguments or options. Athenna makes it very convenient to define the input you expect from the user using the addFlags method on your commands. The addFlags method allows you to define any additional flag from commander. Check out their documentation to see all the flags available for your. See the example:

import { Command } from '@athenna/artisan'

export class SendEmails extends Command {
/**
* The name and signature of the console command.
*
* @return {string}
*/
get signature() {
return 'mail:send <email>'
}

/**
* The console command description.
*
* @return {string}
*/
get description() {
return 'Display welcome object.'
}

/**
* The welcome service dependency.
*
* @type {import('#app/Services/MailgunService').MailgunService}
*/
#mailgunService

/**
* Use the constructor to resolve any dependency of the Ioc container.
*
* @param {any} mailgunService
*/
constructor(mailgunService) {
super()

this.#mailgunService = mailgunService
}

/**
* Set additional flags in the commander instance.
* This method is executed when registering your command.
*
* @param {import('commander').Command} commander
* @return {import('commander').Command}
*/
public addFlags(commander) {
const signature = '-s, --subject <subject>'
const description = 'Set the subject of the email.'
const defaultValue = 'Athenna'

return commander.option(signature, description, defaultValue)
}

/**
* Execute the console command.
*
* @param {string} email
* @param {any} options
*/
async handle(email, options) {
const message = 'Hello from Athenna!'

await this.#mailgunService.send(email, message, options.subject)
}
}

Registering commands

All of your console commands are registered within your application's app/Console/Kernel.js class, which is your application's "console kernel".

The ArtisanLoader will register all the commands that come with Artisan such as node artisan serve and node artisan make:provider. The HttpLoader will load all commands from @athenna/http package, such as, node artisan route:list and node artisan make:controller. We call this type of commands as internal commands.

By default, Athenna will import all commands inside app/Console/Commands folder and import it using Folder helper, so you don't need to mind about registering your command in Kernel. We call this type of commands as application commands.

/**
* Register the commands for the application.
*
* @return {any[]}
*/
get commands() {
const internalCommands = [
...ArtisanLoader.loadCommands(),
...HttpLoader.loadCommands(),
...TestLoader.loadCommands(),
...CoreLoader.loadCommands(),
]

const appCommands = new Folder(Path.console('Commands'))
.loadSync()
.getFilesByPattern('**/*.js', true)
.map(command => import(command.href))

return [...internalCommands, ...appCommands]
}

Registering custom templates

The Artisan console's make commands are used to create a variety of classes, such as controllers, middlewares, commands, and tests. These classes are generated using "templates" files that are populated with values based on your input. However, you may want to make small changes to files generated by Artisan or even set your own templates to use in your custom make commands.

The template:customize command will download all the templates that are set in the get templates method inside app/Console/Kernel.js and copy it to the resources/templates folder in our project root:

node artisan template:customize

Now in our resources/templates folder we have the exception.edge file, let's create a custom template for it that removes all comments of the class:

import { Exception } from '@athenna/core'

export class {{ namePascal }}Exception extends Exception {
constructor(message, statusCode, code, help) {
super(message, statusCode, code, help)
}
}
caution

Remember that the file name needs to be exception.edge to be replaced by Artisan original template.

By default, ArtisanLoader.loadTemplates() method already load all the templates found inside your resources/templates folder:

/**
* Register custom templates files.
*
* @return {any[]}
*/
get templates() {
return [
...HttpLoader.loadTemplates(),
...TestLoader.loadTemplates(),
...ArtisanLoader.loadTemplates(),
]
}

Now if we run the node artisan make:exception command, Artisan will use our custom template to generate the exceptions files.

caution

Remember to always leave ...ArtisanLoader.loadTemplates() at the end of the array to override the already set templates of other loaders like HttpLoader and TestLoader.

Calling commands in runtime

Sometimes you may wish to call other commands from an existing Artisan command or from any other part of your application. You may do so using the call method from Artisan. This call method accepts the command string with it arguments and options:

await Artisan.call('make:controller TestController')

If you want to verify if your command has generated some output in stdout or stderr you can use the callInChild method:

const { stdout, stderr } = await Artisan.callInChild('make:controller TestController')

assert.isTrue(stdout.includes('[ MAKING CONTROLLER ]'))
assert.isUndefined(stderr)