So our logic is in place, but we will need to create a handy way we can trigger the imports. One option is to create an administration form where we go and press a button. However, a more typical example is a command that can be added to the crontab and that can be run at specific intervals automatically. So that's what we are going to do now, and we will do so using Drush.
The Drush command we are going to write will take an optional parameter for the ID of the importer configuration entity we want to process. This will allow the use of the command for more than one just importer. Alternatively, passing no options will process each importer (in case this is something we want to do later on). One thing to note is that we won't focus on performance in this example. This means the command will work just fine for smaller sets of data (as big as one request can process) but will be probably better to use a queue and/or batch processing for larger sets. Also, we will have a chapter dedicated for these subsystems later on, but, for now, let's get on with our example.
Before we actually write our new Drush command, let's make some alterations to our logic as they will make sense in the context of what we want to do. First, let's add a getter method to the Importer plugins to retrieve the corresponding configuration entities. We start with the interface like so:
/**
* Returns the Importer configuration entity.
*
* @return \Drupal\products\Entity\ImporterInterface
*/
public function getConfig();
Then, on the ImporterBase class, we can add the implementation (it will be the same for all individual plugin instances):
/**
* {@inheritdoc}
*/
public function getConfig() {
return $this->configuration['config'];
}
As you can see, it's not rocket science.
Second, let's add a createInstanceFromAllConfigs() method to the ImporterManager that will return an array of plugin instances for each existing Importer configuration entities:
/**
* Creates an array of ImporterInterface from all the existing Importer
* configuration entities.
*
* @return ImporterPluginInterface[]
*/
public function createInstanceFromAllConfigs() {
$configs = $this->entityTypeManager->getStorage('importer')->loadMultiple();
if (!$configs) {
return [];
}
$plugins = [];
/** @var ImporterInterface $config */
foreach ($configs as $config) {
$plugin = $this->createInstanceFromConfig($config->id());
if (!$plugin) {
continue;
}
$plugins[] = $plugin;
}
return $plugins;
}
Here, we use the loadMultiple() method on the entity storage handler, which if we use without any arguments will load all existing entities, and if we have any, we use our existing createInstanceFromConfig() method to instantiate the plugins based on each configuration entity. That's it; we can now go ahead and create our Drush command.
Drush commands are still being written using procedural code and are defined inside the my_module.drush.inc file of our module, so in our case, it will be products.drush.inc. The definition of a Drush command is made up of two parts--the implementation of hook_drush_command() and of a callback for each defined commands. Both of these go inside the same *.drush.inc file. So let's define our command:
/**
* Implements hook_drush_command().
*/
function products_drush_command() {
$items = [];
$items['products-import-run'] = [
'description' => 'Run the product importers',
'options' => [
'importer' => 'The Importer configuration entity to run.',
],
'aliases' => ['pir'],
'callback' => 'products_products_import_run'
];
return $items;
}
This hook returns an array of command definitions, keyed by the actual command name. Also, a command can have certain definition options. In our case, we have a description, a list of options, a list of aliases for the command name, and a callback function to be run. If we omit the callback, the function name defaults to drush_[command_name_with_underscores_instead_of_dashes]. In our case, it would have been drush_products_import_run. The aliases are an array of typically shorter command names by which we can refer to this command.
We also have a named option to this command, which as you may have guessed, is optional. When running the command, it will be used like this:
drush command --option-name=option_value
However, the command should run just as well with the option omitted from the call. This is unlike "arguments" that a Drush command can also have and are mandatory. In our case, we don't need any arguments, though.
Let's now write the callback function that will be called when this command runs:
/**
* Callback for the products-import-run Drush command.
*/
function products_products_import_run() {
$importer = drush_get_option('importer', NULL);
/** @var \Drupal\products\Plugin\ImporterManager $manager */
$manager = \Drupal::service('products.importer_manager');
if (!is_null($importer)) {
$plugin = $manager->createInstanceFromConfig($importer);
if (is_null($plugin)) {
drush_log(t('The specified importer does not exist'), 'error');
return;
}
_products_products_import_run_plugin($plugin);
return;
}
$plugins = $manager->createInstanceFromAllConfigs();
if (!$plugins) {
drush_log(t('There are no importers to run'), 'error');
return;
}
foreach ($plugins as $plugin) {
_products_products_import_run_plugin($plugin);
}
}
The way we retrieve the value of an option within a Drush command is using drush_get_option(), whose second parameter sets a default value in case one is not found in the command. So what we did in the preceding code then is get the option (the Importer ID), and if one is found, we do what we did before--get a plugin instance based on that ID and run the import using the _products_products_import_run_plugin() function:
/**
* Runs an individual Importer plugin.
*
* @see products_products_import_run().
*
* @param \Drupal\products\Plugin\ImporterInterface $plugin
*/
function _products_products_import_run_plugin(\Drupal\products\Plugin\ImporterInterface $plugin) {
$result = $plugin->import();
$message_values = ['@importer' => $plugin->getConfig()->label()];
$message = $result ? t('The "@importer" importer has been run.', $message_values) : t('There was a problem running the "@importer" importer.', $message_values);
$message_status = $result ? 'success' : 'error';
drush_log($message, $message_status);
}
We are also setting a success or error message depending on the results we are getting from the importer. In this message, we use the actual Importer entity label rather than the ID, which is nicer.
Instead of the regular drupal_set_message() that we normally use to print out messages to the user, in this case--since we are in the Drush command-line environment--we use the special Drush function for logging messages--drush_log().
One other thing to note is that the name of this function starts with an underscore. This is to denote that it is private and not supposed to be used by anyone else, except our command callback. It's of course not enforceable, but rather a standard naming convention to denote this (used also in other programming languages).
Going back to our command callback, we use our new createInstanceFromAllConfigs() method to get our hands on all the available Importers to run. Then, we simply defer back to our helpful function that runs the import and prints the relevant message. That's pretty much it.
Going to our terminal, we first have to clear the Drush cache in order to pick this command up using the following command:
drush cc drush
Then, we can run either of these command variations for processing all the available importers:
drush products-import-run
drush pir
Alternatively, we can run a specific importer:
drush products-import-run --importer=my_json_product_importer
drush pir --importer=my_json_product_importer
Better yet, we can now add these commands to our crontab and have them run at specific intervals, once a day, for example.