Commands
Introduction
Writing commands
Registering commands
Registering custom templates
Calling commands in runtime
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)