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(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 if we are looking at one using the CSV plugin. We're not interested in an Importers that don't have a CSV file upload. If so, 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 EntityInterface $entity
*
* @return int
*/
function _products_importer_get_fid_from_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 also 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 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 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(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 one, 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(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());
}
}
More correctly, we are using the specific variant of this hook which 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.
Remember, when we delete the usage of a file and its usage count reaches zero, the file is marked as temporary. This does not mean it gets moved to the temporary:// file system but that it will be deleted at the next cron run (if this is configured to happen).