Managed file usage service

Now that we have an idea of the available entity CRUD hooks, we can implement three of them to handle our managed file problem. Because, if you remember, managed files are actually represented by the File entity type, so the Entity CRUD hooks get fired for these as well.

To mark a file as being used by something, we can use the DatabaseFileUsageBackend service (file.usage), which is an implementation of the FileUsageInterface. This has a few handy methods that allow us to add a usage or delete it. That is actually what we are going to do next.

What we want to do first is add a file usage whenever a new Importer entity gets created (and a file uploaded with it):

/** 
 * Implements hook_ENTITY_TYPE_insert() for the Importer config entity type. 
 */ 
function products_importer_insert(\Drupal\Core\Entity\EntityInterface $entity) { 
  if ($entity->getPluginId() != 'csv') { 
    return; 
  } 
 
  // Mark the current File as being used. 
  $fid = _products_importer_get_fid_from_entity($entity); 
  $file = Drupal::entityTypeManager()->getStorage('file')->load($fid); 
  \Drupal::service('file.usage')->add($file, 'products', 'config:importer', $entity->id()); 
}  

We are implementing the specific version of hook_entity_insert() for our own entity type, and the first thing we are checking is whether we are looking at one using the CSV plugin. We're not interested in any importers that don't have a CSV file upload. If we are, we get the File entity ID from the importer using a private helper function:

/** 
 * Given an Importer entity using the CSV plugin, return the File ID of the CSV 
 * file. 
 * 
 * @param \Drupal\Core\Entity\EntityInterface $entity 
 * 
 * @return int 
 */ 
function _products_importer_get_fid_from_entity(\Drupal\Core\Entity\EntityInterface $entity) { 
  $fids = $entity->getPluginConfiguration()['file']; 
  $fid = reset($fids); 
  return $fid; 
}  

You'll notice that the file key in our plugin configuration array is an array of File IDs, even if we only uploaded one single file. That is just something we need to account for here (we did so also in our configuration schema earlier on).

Then, we load the File entity based on this ID and use the file.usage service to add a usage to it. The first parameter of the add() method is the File entity itself, the second is the module name that marks this usage, the third is the type of thing the file is used by, while the fourth is the ID of this thing. The latter two depend on the use case; we choose to go with our own notation (config:importer) to make it clear that we are talking about a configuration entity of the type importer. Of course, we used the ID of the entity.

With this, a new record will get created in the file_usage table whenever we save such an Importer entity for the first time. Now let's handle the case in which we delete this entity—we don't want this file usage lingering around, do we?

/** 
 * Implements hook_ENTITY_TYPE_delete() for the Importer config entity type. 
 */ 
function products_importer_delete(\Drupal\Core\Entity\EntityInterface $entity) { 
  if ($entity->getPluginId() != 'csv') { 
    return; 
  } 
 
  $fid = _products_importer_get_fid_from_entity($entity); 
  $file = Drupal::entityTypeManager()->getStorage('file')->load($fid); 
  \Drupal::service('file.usage')->delete($file, 'products', 'config:importer', $entity->id()); 
}  

Most of what we are doing in this specific version of hook_entity_delete() is the same as before. However, we are using the delete() method of the file.usage service but passing the same arguments. These $type and $id parameters are actually optional, so we can "un-use" multiple files at once. Moreover, we have an optional fifth parameter (the count) whereby we can specifically choose to remove more than one usage from this file. By default, this is 1, and that makes sense for us.

Finally, we also want to account for the cases in which the user edits the importer entity and changes the CSV file. We want to make sure the old one is no longer marked as used for this Importer. And we can do this with hook_entity_update():

/** 
 * Implements hook_ENTITY_TYPE_update() for the Importer config entity type. 
 */ 
function products_importer_update(\Drupal\Core\Entity\EntityInterface $entity) { 
  if ($entity->getPluginId() != 'csv') { 
    return; 
  } 
 
  /** @var \Drupal\products\Entity\ImporterInterface $original */ 
  $original = $entity->original; 
  $original_fid = _products_importer_get_fid_from_entity($original); 
  if ($original_fid !== _products_importer_get_fid_from_entity($entity)) { 
    $original_file = Drupal::entityTypeManager()->getStorage('file')->load($original_fid); 
    \Drupal::service('file.usage')->delete($original_file, 'products', 'config:importer', $entity->id()); 
  } 
}  

We are using the specific variant of this hook that only gets fired for Importer entities. Just like we've been doing so far. And as I mentioned, we can access the original entity (before the changes have been made to it) like so:

$original = $entity->original;  

And if the File ID that was on the original entity is not the same as the one we are currently saving with it (meaning the file was changed), we can delete the usage of that old File ID.