Alright, since all of our setup is in place, we can now go ahead and create our first importer plugin. As we defined it in the manager, these need to go in the Plugin/Importer namespace of modules. So, let's start with a simple JsonImporter which will use a remote URL resource to import products. This is an example JSON file that will be processed by this plugin, just for testing purposes:
{
"products" : [
{
"id" : 1,
"name": "TV",
"number": 341
},
{
"id" : 2,
"name": "VCR",
"number": 123
},
{
"id" : 3,
"name": "Stereo",
"number": 234
}
]
}
I know, VCR right? We have an ID, a name, and a product number. This is all totally made-up information about products just to illustrate the process. So, let's create our JsonImporter:
namespace Drupal\products\Plugin\Importer; use Drupal\products\Plugin\ImporterBase; /** * Product importer from a JSON format. * * @Importer( * id = "json", * label = @Translation("JSON Importer") * ) */ class JsonImporter extends ImporterBase { /** * {@inheritdoc} */ public function import() { $data = $this->getData(); if (!$data) { return FALSE; } if (!isset($data->products)) { return FALSE; } $products = $data->products; foreach ($products as $product) { $this->persistProduct($product); } return TRUE; } /** * Loads the product data from the remote URL. * * @return \stdClass */ private function getData() { /** @var \Drupal\products\Entity\ImporterInterface $config */ $config = $this->configuration['config']; $request = $this->httpClient->get($config->getUrl()->toString()); $string = $request->getBody()->getContents(); return json_decode($string); } /** * Saves a Product entity from the remote data. * * @param \stdClass $data */ private function persistProduct($data) { /** @var \Drupal\products\Entity\ImporterInterface $config */ $config = $this->configuration['config']; $existing = $this->entityTypeManager->getStorage('product')->loadByProperties(['remote_id' => $data->id, 'source' => $config->getSource()]); if (!$existing) { $values = [ 'remote_id' => $data->id, 'source' => $config->getSource() ]; /** @var \Drupal\products\Entity\ProductInterface $product */ $product = $this->entityTypeManager->getStorage('product')->create($values); $product->setName($data->name); $product->setProductNumber($data->number); $product->save(); return; } if (!$config->updateExisting()) { return; } /** @var \Drupal\products\Entity\ProductInterface $product */ $product = reset($existing); $product->setName($data->name); $product->setProductNumber($data->number); $product->save(); } }
You can immediately spot the plugin annotation where we specify an ID and a label. Next, by extending ImporterBase, we inherit the dependent services and ensure that the required interface is implemented. Speaking of which, we basically just have to implement the import() method. So, let's break down what we are doing:
- Inside the getData() method, we retrieve the product information from the remote resource. We do so by getting the URL from the Importer configuration entity and using Guzzle to make a request to that URL. We expect that to be JSON, so we just decode it as such. Of course, error handling is virtually nonexistent in this example, and that is not good.
- We loop through the resulting product data and call the persistProduct() method on each item. In there, we first check whether we already have the product entity. We do so using the simple loadByProperties() method on the product entity storage and try to find products that have the specific source and remote ID. If one doesn't exist, we create it. This should all be familiar from the previous chapter when we looked at manipulating entities. If the product already exists, we first check whether according to configuration, we can update it and only do so if that allows us to. The loadByProperties() method always returns an array of entities, but since we only expect to have a single product with the same remote ID and source combination, we simply reset() this array to get to that one entity. Then, we just set the name and product number on the entity.
As you can see, instead of using the Entity API/Typed Data set() method to update the entity field values, we use our own interface methods. I find that this is much cleaner, more modern, and an IDE-friendly way because everything is very explicit.
One thing you might notice is the error handling in this import process or more precisely, a lack thereof. This is because I kept things simple for the purpose of focusing on the current topic. Normally, you would want to maybe throw and catch some exceptions and definitely log some messages (both error and success). You know how to do the latter from Chapter 3, Logging and Mailing.
And that is pretty much it. We can now create our first importer entity and make it use this importer plugin (after clearing the cache of course):
The URL in the previous screenshot is just a local URL where the example JSON file is found, and we can see the only plugin available to choose, as well as the other entity fields we created form elements for. By saving this new entity, we can make use of it programmatically (assuming that the products.json file referenced in the URL exists):
$config = \Drupal::entityTypeManager()
->getStorage('importer')
->load('my_json_product_importer');
$plugin = \Drupal::service('products.importer_manager')
->createInstance($config->getPluginId(), ['config' => $config]);
$plugin->import();
We first load the importer entity by ID. Then, we use the ImporterManager service to create a new instance of a plugin using the createInstance() method. Only one parameter is required for it—the ID of the plugin—but as I said earlier, we want to pass the configuration entity to it because it depends on it. So we do just that. Then, we call the import() method on the plugin. After running this code, the product entity listing will show some shiny new products.
Let's, however, improve things a bit. Since the configuration entities and plugins are so tightly connected, let's use the plugin manager to do this entire thing rather than having to first load an entity and request the plugin from it. In other words, let's add a method to the plugin manager where we can pass the configuration entity ID, and it returns an instance of the relevant plugin; something like this:
/**
* Creates an instance of ImporterInterface plugin based on the ID of a
* configuration entity.
*
* @param $id
* Configuration entity ID
*
* @return null|\Drupal\products\Plugin\ImporterInterface
*/
public function createInstanceFromConfig($id) {
$config = $this->entityTypeManager->getStorage('importer')->load($id);
if (!$config instanceof \Drupal\products\Entity\ImporterInterface) {
return NULL;
}
return $this->createInstance($config->getPluginId(), ['config' => $config]);
}
Here, we essentially do the same thing as before, but we return NULL if there is no configuration entity found. You can choose to throw an exception if you want instead. However, as you may have correctly noticed, we also need to inject the EntityTypeManager into this class, so our constructor changes as well to take it as a last parameter and set it as a class property. You should be able to do that on your own. But we also need to alter the service definition for the plugin manager to add the EntityTypeManager as a dependency:
products.importer_manager:
class: Drupal\products\Plugin\ImporterManager
parent: default_plugin_manager
arguments: ['@entity_type.manager']
As you can see, we keep the parent inheritance key so that all the parent arguments are taken in. On top, however, we add our own regular arguments key which will append arguments to the ones that come from the parent.
And with this we have simplified things for the client code:
$plugin = \Drupal::service('products.importer_manager')
->createInstanceFromConfig('my_json_product_importer');
$plugin->import();
All we have to interact with is the plugin manager, and we can directly run the import. This is in some ways better because our configuration entities are not something we designed for being used by anyone else. They are simple configuration storage used by our importer plugins.