DrupalConsole is a great CLI tool for Drupal 8. As we have seen in our Introduction to DrupalConsole, it is designed for generating boilerplate code and interacting with a Drupal site. The DrupalConsole Launcher executes console commands from the terminal. A console command should be as light as possible and not attempt to do too much.

This article assumes you have installed the DrupalConsole Launcher and Drupal 8. Preferably, you should have read our introductory article.

From the command line, change directory to the root of your site. If you run drupal list | grep user: you will see a list of user-related commands. The last one is user:role Adds/removes a role for a given user. Would it not be nice to be able to create new roles from the command line too? That is exactly what we are going to do next, in order to help us understand a bit more about what makes up a command.

In Drupal, a command is a PHP class that lives in a module. So, the first thing we need to do is create a module. We are going to do that the DrupalConsole way by running drupal generate:module, which will take us through interactive prompts.

You only need the new module name. Enter "Console Command Demo" and accept the suggested machine name console_command_demo. You may wish to give it a better description. You can do without module file, feature, composer.json, dependencies, unit tests and template.

Next we generate a command with:

drupal generate:command

You get the next set of prompts:

  • Enter the extension name []:

    Enter the machine name of the module you created a while ago (e.g. console_command_demo) and hit "Enter".

  • Enter the Command name. [console_command_demo:default]:

    console_command_demo:default has been suggested. You should namespace your command with the module machine name followed by a word that best describes your command. It will add a console_command_demo section to your commands when you run drupal list. However, it makes sense to add our command to the "user:role" namespace, so enter "user:role:create" and continue.

  • Enter the Command Class. (Must end with the word 'Command'). [DefaultCommand]:

    Enter "RoleCreateCommand".

  • Is the command aware of the drupal site installation when executed?. (yes/no) [no]:

    Since we want to create user roles with our command, there should be a Drupal site. So we enter "yes".

  • Do you confirm generation? (yes/no) [yes]:

    We confirm code generation with "yes".

Congratulations! You have just created your first console command in Drupal 8. Enable the Console Command Demo module with:

drupal module:install console_command_demo

Check the command under "user" in the list of all available commands, with drupal list:

user:role:create      Drupal Console generated command.

We will change that message later but let us try the command now:

drupal user:role:create

It works! Well, it only prints out a message "I am a new generated command." but it works. Open the module folder in your favourite editor or IDE and note the generated file structure.

console_command_demo.info.yml is required for Drupal to know about the module. console_command_demo.module is optional. console.services.yml makes the command available as a service. console/translations/en/user.role.create.yml contains the messages displayed as a user interacts with the command.

Open src/Command/RoleCreateCommand.php:

// src/Command/RoleCreateCommand.php 

/**
 * Class RoleCreateCommand.
 *
 * @package Drupal\console_command_demo
 *
 * @DrupalCommand (
 *     extension="console_command_demo",
 *     extensionType="module"
 * )
 */

This is a special comment block called annotation. You find this everywhere in Drupal 8 when defining a plugin. It lets DrupalConsole know that this file contains code for a command.

class RoleCreateCommand extends Command { }

A command class name must end in "Command" and extend Symfony\Component\Console\Command\Command, the base class of all commands. You may have a look at that class later.

As you can see from RoleCreateCommand.php, you only need to implement 2 methods. The first is configure():

// src/Command/RoleCreateCommand.php

protected function configure() {
    $this
      ->setName('user:role:create')
      ->setDescription($this->trans('commands.user.role.create.description'));
  }

The name user:role:create is what we entered when generating the command. The description comes from the console/translations/en/user.role.create.yml file. Open it and note how the argument to trans() is written: commands.<FILE_NAME_WITHOUT_EXTENSION>.<KEY_IN_FILE>. Change description to something like "Create a user role." and save it. You may check with drupal list again and see your description displayed.

The second method is execute():

// src/Command/RoleCreateCommand.php

protected function execute(InputInterface $input, OutputInterface $output) {
    $io = new DrupalStyle($input, $output);

    $io->info($this->trans('commands.user.role.create.messages.success'));
  }

This is the method that runs the command. At the moment, it only outputs the success message, but this is where we are going implement the code that creates a new user role.

A quick word about DrupalStyle defined in vendor/drupal/console-core/src/Style/DrupalStyle.php. If you open it, you will see that it extends Symfony\Component\Console\Style\SymfonyStyle, which contains helpers that make the input and output in the terminal conform to the Symfony Style Guide.

DrupalStyle provides a Drupal-specific way of displaying input and output in the terminal.

A role requires only one thing, the name (label). Roles also have a machine name (id), which is optional. If you do not provide it, Drupal generates it from the name. You may also specify the position (weight) when you view the list of roles on the site. Let us define these in the existing configure() method, which should now look like this:

// src/Command/RoleCreateCommand.php

protected function configure() {
    $this
      ->setName('user:role:create')
      ->setDescription($this->trans('commands.user.role.create.description'))
      ->addArgument(
        'label',
        InputOption::VALUE_REQUIRED,
        $this->trans('commands.user.role.create.options.label')
      )
      ->addOption(
        'id',
        null,
        InputOption::VALUE_OPTIONAL,
        $this->trans('commands.user.role.create.options.id')
      )
      ->addOption(
        'weight',
        null,
        InputOption::VALUE_OPTIONAL,
        $this->trans('commands.user.role.create.options.weight'),
        0
      );
  }

Import InputOption class with use Symfony\Component\Console\Input\InputOption; at the top of the file and save it. If you run drupal help user:role:create you will see the following (among other things):

drupal help user:role:create
Usage:
  user:role:create [options] [--] [<label>]

Arguments:
  label                  commands.user.role.create.options.label

Options:
      --id[=ID]          commands.user.role.create.options.id
      --weight[=WEIGHT]  commands.user.role.create.options.weight [default: 0]

We have not defined those strings in our console/translations/en/user.role.create.yml. Add them, so the file now looks like:

description: 'Create a user role.'
options:
  id: 'The ID of the role you would you like to create'
  label: 'The label of the role'
  weight: 'The weight of the role'
arguments: {}
messages:
    success: 'I am a new generated command.'

Run drupal help user:role:create, and see the result. We need to pass an argument (label) and two options (id and weight) to our command. An implementation of the interact() method is needed:

// src/Command/RoleCreateCommand.php

  /**
   * {@inheritdoc}
   */
  protected function interact(InputInterface $input, OutputInterface $output)
  {
    $io = new DrupalStyle($input, $output);

    $label = $input->getArgument('label');
    while (!$label) {
      $label = $io->askEmpty(
        $this->trans('commands.user.role.create.questions.label'),
        null
      );
    }
    $input->setArgument('label', $label);

    $id = $input->getOption('id');
    if (!$id) {
      $id = $io->askEmpty(
        $this->trans('commands.user.role.create.questions.id'),
        null
      );
    }
    $input->setOption('id', $id);

    $weight = $input->getOption('weight');
    if (!$weight) {
      $weight = $io->ask(
        $this->trans('commands.user.role.create.questions.weight'),
        0,
        null
      );
    }
    $input->setOption('weight', $weight);
  }

This method will present you with interactive questions with responses saved in variables which are passed to the execute() method. See how label must be specified before proceeding? Also, the id or weight can not be empty, if the option has been specified on the command line.

The language file needs to be updated. We shall see the rest of the keys in the file later.

// console/translations/en/user.role.create.yml

description: 'Create a user role.'
options:
  id: 'The ID of the role you would you like to create'
  label: 'The label of the role'
  weight: 'The weight of the role'
questions:
    id: 'Enter the ID of the role would you like to create'
    label: 'Enter the label of the role'
    weight: 'Enter the weight of the role'
arguments:
    id: 'The role ID'
    label: 'The role label'
    weight: 'The weight of the role'
messages:
    id: 'Role ID'
    label: 'Label'
    weight: 'Weight'
success: 'Role "%s" was created successfully.'    

You can test it out with drupal user:role:create and respond to the prompts. Now, we implement the code to create a role from the input in the execute() method as follows:

  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output) {
    $io = new DrupalStyle($input, $output);

    $id = $input->getOption('id');
    $label = $input->getArgument('label');
    $weight = $input->getOption('weight');

    $role = $this->createRole($label, $id, $weight);

    $tableHeader = ['Field', 'Value'];

    $tableFields = [
      $this->trans('commands.user.role.create.messages.id'),
      $this->trans('commands.user.role.create.messages.label'),
      $this->trans('commands.user.role.create.messages.weight'),
    ];

    if ($role['success']) {
      $tableData = array_map(
        function ($field, $value) {
          return [$field, $value];
        },
        $tableFields,
        $role['success']
      );

      $io->table($tableHeader, $tableData);
      $io->success(sprintf($this->trans('commands.user.role.create.messages.success'), $role['success']['label']));

      return 0;
    }

    if ($role['error']) {
      $io->error($role['error']['error']);
    }

  }

We read the input into variables and pass them to our helper method createRole(), which we shall see shortly. Finally, we display information about the new role in tabular form. On the other hand, errors will be displayed, if the role was not created.

/**
   * Create a user role.
   *
   * @param string $label
   *  The name of the role.
   * @param string $id
   *  The machine name  of the role/
   * @param int $weight
   *  The weight.
   *
   * @return array
   *  Array of data depending on success or failure.
   */
  private function createRole($label, $id, $weight) {
    $role = Role::create(
      [
        'label' => $label,
        'id' => str_replace(' ', '_', strtolower($id ?: $label)),
        'weight' => $weight
      ]
    );

    $result = [];

    try {
      $role->save();

      $result['success'] = [
        'id' => $role->id(),
        'label' => $role->label(),
        'weight' => $role->getWeight(),
      ];
    } catch (\Exception $e) {
      $result['error'] = [
        'id' => $role->id(),
        'label' => $role->get('label'),
        'error' => 'Error: ' . get_class($e) . ', code: ' . $e->getCode() . ', message: ' . $e->getMessage()
      ];
    }

    return $result;
  }

We need to import the Role class with use Drupal\user\Entity\Role;. Look at what we are doing with id in the array passed to the Role::create() method. If the option has not been specified, we use the label instead, convert it to lowercase, and make sure there are no spaces.

Run the command interactively:

user:role:create command

You may also run the command as follows:

drupal user:role:create Secretary --id=secretary --weight=3

To see what happens when an error occurs, try creating a new role with an existing id. For example:

drupal user:role:create Dummy --id=administrator --weight=-1

This is our command for creating user roles all done.

Conclusion

We have tried the DrupalConsole CLI tool for Drupal 8 and explored some of its features. Having a good grasp of this subject is a solid foundation for other implementations of the Symfony Console component in frameworks such as Symfony itself, Laravel or Sylius.

In addition, we took a closer look at commands by writing a new one. There is a lot more to this great tool that has brought Drupal further in line with other enterprise-grade solutions in PHP. It is still evolving and you can follow the latest developments on the DrupalConsole project website.

Code

The demo module for this article is in the X-Team Drupal 8 Examples Github repository.