Мне нужен был обработчик для migration API, который создаёт media entity по URL и возвращает его ID.
Обработчики представляют из себя расширения ProcessPluginBase, которые нужно размещать в папке /src/Plugin/migrate/process вашего модуля.
Файл /src/Plugin/migrate/process/MediaGenerate.php
Т.к. media сущность настраивается разработчиком, то в качестве параметров нужно указать bundle сущности (destination_bundle) и поле (destination_field), в которое будет записываться file reference. Ещё один параметр — это подкаталог pubic://, куда будет записана картинка — public_dir.
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112  | 
						<?php namespace Drupal\custom_migration\Plugin\migrate\process; use Drupal\Core\File\FileSystemInterface; use Drupal\media\Entity\Media; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateExecutableInterface; use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; /**  * Generates a media entity from a file and returns the media id.  *  * @MigrateProcessPlugin(  *   id = "media_generate"  * )  *  * To generate the entity it is best to this in a subprocess:  *  * @code  *      process:  *        field_blog_image/target_id:  *          source: thumbnail  *          plugin: media_generate  *          destination_bundle: image  *          destination_field: field_media_image  *          public_dir: test-public  * @endcode  */ class MediaGenerate extends ProcessPluginBase {   /**    * {@inheritdoc}    */   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {     if (!isset($this->configuration['destination_field'])) {       throw new MigrateException('Destination field must be set.');     }     if (!isset($this->configuration['destination_bundle'])) {       throw new MigrateException('Destination bundle must be set.');     }     $field = $this->configuration['destination_field'];     $bundle = $this->configuration['destination_bundle'];     $imageContent = FALSE;     if (!empty($value)) {       $destinationDir = 'public://' . ($this->configuration['public_dir'] ?? 'migration_uploads');       $destinationFile = $destinationDir . '/' . basename(parse_url($value, PHP_URL_PATH));       // Prepare FILE.       $files = \Drupal::entityTypeManager()         ->getStorage('file')         ->loadByProperties(['uri' => $destinationFile]);       $file = reset($files) ?: NULL;       if ($file === NULL) {         try {           $imageContent = file_get_contents($value);         } catch (\Exception $e) {           return NULL;         }         \Drupal::service('file_system')           ->prepareDirectory(           $destinationDir,           FileSystemInterface:: CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS         );         $file = \Drupal::service('file.repository')           ->writeData($imageContent, $destinationDir . '/' . basename($value),           FileSystemInterface::EXISTS_REPLACE);       }       // Prepare MEDIA.       $query = \Drupal::entityTypeManager()->getStorage('media')->getQuery();       $query->condition('status', 1)         ->condition($field, $file->id())         ->accessCheck(FALSE);       $mids = $query->execute();       $mid = reset($mids) ?: NULL;       if ($mid === NULL) {         $alt = $row->getSourceProperty('alt');         if (empty($alt)) {           $alt = "Media Name: " . $file->label();         }         $media = Media::create([           'bundle' => $bundle,           'uid' => $file->getOwner()->id(),           'status' => '1',           'name' => $file->label(),           $field => [             'target_id' => $file->id(),             'alt' => $alt,           ],         ]);         $media->save();         return $media->id();       } else {         return $mid;       }     }     return NULL;   } }  | 
					
Логика работы следующая: сначала определяется место, куда будет записан файл, если такой файл уже есть в Drupal, то повторно его копировать не будем. Далее идет поиск/создание media entity. Если media элемент, указывающий на файл уже есть — вернем его ID, если нет, то создаём и получаем ID нового media entity.
Пример использования:
| 
					 1 2 3 4 5 6 7  | 
						process:   field_blog_image/target_id:     source: thumbnail     plugin: media_generate     destination_bundle: image     destination_field: field_media_image     public_dir: test-public  | 
					
