bynder-4.0.0-beta1/src/Plugin/media/Source/Bynder.php
src/Plugin/media/Source/Bynder.php
<?php
namespace Drupal\bynder\Plugin\media\Source;
use Drupal\bynder\BynderApiInterface;
use Drupal\bynder\Plugin\Field\FieldType\BynderMetadataItem;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\Core\Utility\Error;
use Drupal\media\MediaInterface;
use Drupal\media\MediaSourceBase;
use Drupal\media\MediaTypeInterface;
use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\File\FileSystemInterface;
/**
* Provides media source plugin for Bynder.
*
* @MediaSource(
* id = "bynder",
* label = @Translation("Bynder"),
* description = @Translation("Provides business logic and metadata for Bynder."),
* default_thumbnail_filename = "bynder-logo.png",
* allowed_field_types = {"string", "string_long"}
* )
*/
class Bynder extends MediaSourceBase {
const TRANSFORMATIONS_FIELD_NAME = 'bynder_transformations';
/**
* Bynder api service.
*
* @var \Drupal\bynder\BynderApiInterface
* Bynder api service.
*/
protected $bynderApi;
/**
* Account proxy.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $accountProxy;
/**
* The url generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Statically cached metadata information for the given assets.
*
* @var array
*/
protected $metadata;
/**
* The logger factory service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $logger;
/**
* The cache service.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected LanguageManagerInterface $languageManager;
/**
* Statically cached metadata attributes.
*
* @var array|null
*/
protected static array|null $metadataAttributes;
/**
* Constructs a new class instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* Entity field manager service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\bynder\BynderApiInterface $bynder_api_service
* Bynder api service.
* @param \Drupal\Core\Session\AccountProxyInterface $account_proxy
* Account proxy.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
* The logger factory service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, BynderApiInterface $bynder_api_service, AccountProxyInterface $account_proxy, UrlGeneratorInterface $url_generator, LoggerChannelFactoryInterface $logger, CacheBackendInterface $cache, TimeInterface $time, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
$this->bynderApi = $bynder_api_service;
$this->accountProxy = $account_proxy;
$this->urlGenerator = $url_generator;
$this->logger = $logger;
$this->cache = $cache;
$this->time = $time;
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('config.factory'),
$container->get('bynder_api'),
$container->get('current_user'),
$container->get('url_generator'),
$container->get('logger.factory'),
$container->get('cache.data'),
$container->get('datetime.time'),
$container->get('module_handler'),
$container->get('language_manager'),
);
}
/**
* {@inheritdoc}
*/
public function getMetadataAttributes() {
if (isset(static::$metadataAttributes)) {
return static::$metadataAttributes;
}
static::$metadataAttributes = [
'uuid' => $this->t('ID'),
'name' => $this->t('Name'),
'description' => $this->t('Description'),
'tags' => $this->t('Tags'),
'type' => $this->t('Type'),
'video_preview_urls' => $this->t('Video preview urls'),
'thumbnail_urls' => $this->t('Thumbnail urls'),
'width' => $this->t('Width'),
'height' => $this->t('Height'),
'created' => $this->t('Date created'),
'modified' => $this->t('Data modified'),
'propertyOptions' => $this->t('Meta-property option IDs'),
];
try {
$meta_properties = $this->bynderApi->getMetaProperties();
foreach ($meta_properties as $meta_property) {
static::$metadataAttributes[$meta_property['name']] = $meta_property['label'];
}
}
catch (\Exception $exception) {
// Ignore exceptions here, might not be set up and the settings form will
// show more information.
}
return static::$metadataAttributes;
}
/**
* Returns a list of remote metadata properties.
*
* @return array
* A list of remote metadata properties.
*/
public function getRemoteMetadataProperties() {
return [
'id',
'name',
'description',
'type',
'videoPreviewURLs',
'thumbnails',
'original',
'width',
'height',
'dateCreated',
'dateModified',
'propertyOptions',
'extension',
];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'source_field' => '',
'enforce_mapped_values' => FALSE,
'mapping_language_overrides' => [],
];
}
/**
* Ensures the given media entity has Bynder metadata information in place.
*
* @param \Drupal\media\MediaInterface $media
* The media entity.
* @param bool $force
* (optional) By default, this will not attempt to check for updated
* metadata if there is local data available. Pass TRUE to always check for
* changed metadata.
*
* @return bool
* TRUE if the metadata is ensured. Otherwise, FALSE.
*/
public function ensureMetadata(MediaInterface $media, $force = FALSE) {
$media_uuid = $this->getSourceFieldValue($media);
if (!empty($this->metadata[$media_uuid]) && !$force) {
return TRUE;
}
// If we cannot get a value for $media_uuid we should better exit early.
// This helps avoid circular references introduced due to ::ensureMetadata()
// calling hook_bynder_media_update_alter(), which in turn may call this
// method in a loop.
if (empty($media_uuid)) {
return FALSE;
}
if (!$media->hasField(BynderMetadataItem::METADATA_FIELD_NAME)) {
$this->logger->get('bynder')->error('The media type @type must have a Bynder metadata field named "bynder_metadata".', [
'@type' => $media->bundle(),
]);
return FALSE;
}
if (!$media->get(BynderMetadataItem::METADATA_FIELD_NAME)->isEmpty() && !$force) {
$metadata = Json::decode($media->get(BynderMetadataItem::METADATA_FIELD_NAME)->value);
if (is_array($metadata)) {
$this->metadata[$media_uuid] = $metadata;
return TRUE;
}
}
try {
$media_uuid = $this->getSourceFieldValue($media);
$item = (array) $this->bynderApi->getMediaInfo($media_uuid);
$this->metadata[$media_uuid] = $item;
$has_changed = FALSE;
if ($this->hasMetadataChanged($media, $this->metadata[$media_uuid])) {
$encoded_metadata = Json::encode($this->metadata[$media_uuid]);
if (!$encoded_metadata) {
$this->logger->get('bynder')->error('Unable to JSON encode the returned API response for the media UUID @uuid.', [
'@uuid' => $media_uuid,
]);
return FALSE;
}
$media->set(BynderMetadataItem::METADATA_FIELD_NAME, $encoded_metadata);
$has_changed = TRUE;
}
// Allow other modules to alter the media entity.
$this->moduleHandler->alter('bynder_media_update', $media, $item, $has_changed);
// If the media entity is still new it will be saved and we don't have
// do that twice.
if ($has_changed && !$media->isNew()) {
$media->save();
}
return $has_changed;
}
catch (GuzzleException $e) {
$this->logger->get('bynder')->error('Unable to fetch info about the asset represented by media @name (@id) with message @message.', [
'@name' => $media->label(),
'@id' => $media->id(),
'@message' => $e->getMessage(),
]);
}
return FALSE;
}
/**
* Returns a list of filtered remote metadata properties.
*
* @param array $metadata
* The metadata items.
*
* @return array
* Filtered list of remote metadata properties.
*/
public function filterRemoteMetadata(array $metadata) {
return array_intersect_key($metadata, array_combine($this->getRemoteMetadataProperties(), $this->getRemoteMetadataProperties()));
}
/**
* Compares the local metadata and the remote metadata in case it changed.
*
* @param \Drupal\media\MediaInterface $media
* The media entity.
* @param array $remote_metadata
* The remote metadata.
*
* @return bool
* TRUE if the remote metadata has changed. Otherwise, FALSE.
*/
public function hasMetadataChanged(MediaInterface $media, array $remote_metadata) {
if ($media->get(BynderMetadataItem::METADATA_FIELD_NAME)->isEmpty() && !empty($remote_metadata)) {
return TRUE;
}
$local_metadata = (array) Json::decode((string) $media->get(BynderMetadataItem::METADATA_FIELD_NAME)->value);
return $local_metadata !== $remote_metadata;
}
/**
* {@inheritdoc}
*/
public function getMetadata(MediaInterface $media, $name) {
$remote_uuid = $this->getSourceFieldValue($media);
if ($name == 'uuid') {
return $remote_uuid;
}
// Ensure the metadata information are fetched.
if ($this->ensureMetadata($media)) {
switch ($name) {
case 'video_preview_urls':
return $this->metadata[$remote_uuid]['videoPreviewURLs'] ?? NULL;
case 'thumbnail_urls':
return $this->metadata[$remote_uuid]['thumbnails'] ?? NULL;
case 'thumbnail_uri':
if (!empty($this->metadata[$remote_uuid]['thumbnails']['webimage'])) {
if ($this->useRemoteImages()) {
return $this->metadata[$remote_uuid]['thumbnails']['webimage'];
} else {
/** @var \Drupal\file\FileRepositoryInterface $file_repository */
$file_repository = \Drupal::service('file.repository');
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$image_url = $this->metadata[$remote_uuid]['thumbnails']['webimage'];
$field_definition = $media->getFieldDefinition('thumbnail');
$location = $field_definition->getFieldStorageDefinition()->getSetting('uri_scheme') . '://' . $field_definition->getSetting('file_directory');
$location = \Drupal::token()->replace($location);
$file_system->prepareDirectory($location, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
try {
$data = (string) \Drupal::httpClient()->get($image_url)->getBody();
$file = $file_repository->writeData($data, ltrim($location, '/') . '/' . $file_system->basename(parse_url($image_url, PHP_URL_PATH)));
return $file->getFileUri();
}
catch (\Exception $exception) {
Error::logException(\Drupal::logger('bynder'), $exception);
}
}
}
return parent::getMetadata($media, 'thumbnail_uri');
case 'created':
return $this->metadata[$remote_uuid]['dateCreated'] ?? NULL;
case 'modified':
return $this->metadata[$remote_uuid]['dateModified'] ?? NULL;
case 'default_name':
return $this->metadata[$remote_uuid]['name'] ?? parent::getMetadata($media, 'default_name');
default:
if (isset($this->metadata[$remote_uuid][$name])) {
return $this->metadata[$remote_uuid][$name];
}
elseif (isset($this->metadata[$remote_uuid]['property_' . $name])) {
return $this->metadata[$remote_uuid]['property_' . $name];
}
else {
return NULL;
}
}
}
return parent::getMetadata($media, $name);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
try {
// Check the connection with Bynder.
$this->bynderApi->getBrands();
$form['enforce_mapped_values'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enforce mapped field values'),
'#description' => $this->t('By default, mapped fields are only set if they are empty. By enabling this, they will always be updated to match the configured property.'),
'#default_value' => $this->configuration['enforce_mapped_values'],
];
$meta_attributes = $this->getMetadataAttributes();
$entity = $form_state->get('type');
if ($this->languageManager->isMultilingual() && $entity && !$entity->isNew()) {
$translatable_fields = [];
$exclude_fields = [$this->configuration['source_field'], static::TRANSFORMATIONS_FIELD_NAME, BynderMetadataItem::METADATA_FIELD_NAME];
foreach ($this->entityFieldManager->getFieldDefinitions('media', $entity->id()) as $field_name => $field_definition) {
if (!in_array($field_name, $exclude_fields) && $field_definition->isTranslatable() && $field_definition instanceof FieldConfigInterface) {
$translatable_fields[$field_name] = $field_definition->getLabel();
}
}
if ($translatable_fields) {
$form['mapping_language_overrides'] = [
'#type' => 'details',
'#title' => $this->t('Per-language mapping'),
'#description' => $this->t('Allows to map different properties to specific translations.'),
'#open' => TRUE,
];
foreach ($this->languageManager->getLanguages() as $langcode => $language) {
$form['mapping_language_overrides'][$langcode] = [
'#type' => 'details',
'#title' => $language->getName()
];
foreach ($translatable_fields as $field_name => $field_label) {
$form['mapping_language_overrides'][$langcode][$field_name] = [
'#type' => 'select',
'#title' => $field_label,
'#options' => $meta_attributes,
'#empty_option' => $this->t('- Default field mapping -'),
'#default_value' => $this->configuration['mapping_language_overrides'][$langcode][$field_name] ?? NULL,
];
}
}
}
}
}
catch (\Exception $exception) {
if ($this->accountProxy->hasPermission('administer bynder configuration')) {
$this->messenger()->addError($this->t('Connecting with Bynder failed. Check if the configuration is set properly <a href=":url">here</a>.', [
':url' => $this->urlGenerator->generateFromRoute('bynder.configuration_form'),
]));
}
else {
$this->messenger()->addError($this->t('Something went wrong with the Bynder connection. Please contact the site administrator.'));
}
}
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
// Clean empty mapping overrides.
if (!empty($this->configuration['mapping_language_overrides'])) {
foreach ($this->configuration['mapping_language_overrides'] as $langcode => $language_mappings) {
$this->configuration['mapping_language_overrides'][$langcode] = array_filter($language_mappings);
}
$this->configuration['mapping_language_overrides'] = array_filter($this->configuration['mapping_language_overrides']);
}
}
/**
* Extend and enforce metadata mapping.
*
* @param \Drupal\media\MediaInterface $media
* The media entity.
*
* @see bynder_media_presave()
* @see \Drupal\media\Entity\Media::prepareSave()
*/
public function prepareSave(MediaInterface $media): void {
$field_map = $media->get('bundle')->entity->getFieldMap();
foreach (array_keys($media->getTranslationLanguages()) as $langcode) {
// Merge the language overrides plus the default field mapping for this
// language, replace possibly existing mapping for a given field
// if a language overide is given.
$language_field_map = $field_map;
foreach ($this->configuration['mapping_language_overrides'][$langcode] ?? [] as $field_name => $metadata_attribute_name) {
$existing_position = array_search($field_name, $language_field_map);
if ($existing_position !== FALSE) {
unset($language_field_map[$existing_position]);
}
$language_field_map[$metadata_attribute_name] = $field_name;
}
$translation = $media->getTranslation($langcode);
foreach ($language_field_map as $metadata_attribute_name => $entity_field_name) {
// Only save value in the entity if the field is empty, if the
// source field changed or if enforced mapping is enabled.
if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $this->hasSourceFieldChanged($translation) || $this->configuration['enforce_mapped_values'])) {
$translation->set($entity_field_name, $this->getMetadata($translation, $metadata_attribute_name));
}
}
}
}
/**
* Determines if the source field value has changed.
*
* @param \Drupal\media\MediaInterface $media
* The media entity.
*
* @return bool
* If the source field has changed.
*
* @see \Drupal\media\Entity\Media::hasSourceFieldChanged())
*/
protected function hasSourceFieldChanged(MediaInterface $media): bool {
return isset($media->original) && $this->getSourceFieldValue($media) !== $this->getSourceFieldValue($media->original);
}
/**
* Creates the metadata field storage definition.
*
* @return \Drupal\field\FieldStorageConfigInterface
* The unsaved field storage definition.
*/
public function createMetadataFieldStorage() {
return $this->entityTypeManager->getStorage('field_storage_config')
->create([
'entity_type' => 'media',
'field_name' => BynderMetadataItem::METADATA_FIELD_NAME,
'type' => 'bynder_metadata',
'cardinality' => 1,
'locked' => TRUE,
]);
}
/**
* Creates the metadata field definition.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type.
*
* @return \Drupal\field\FieldConfigInterface
* The unsaved field definition. The field storage definition, if new,
* should also be unsaved.
*/
public function createMetadataField(MediaTypeInterface $type) {
return $this->entityTypeManager->getStorage('field_config')
->create([
'entity_type' => 'media',
'field_name' => BynderMetadataItem::METADATA_FIELD_NAME,
'bundle' => $type->id(),
'label' => 'Bynder Metadata',
'translatable' => FALSE,
'field_type' => 'bynder_metadata',
]);
}
/**
* Creates the transformations field storage definition.
*
* @return \Drupal\field\FieldStorageConfigInterface
* The unsaved field storage definition.
*/
public function createTransformationsFieldStorage() {
return $this->entityTypeManager->getStorage('field_storage_config')
->create([
'entity_type' => 'media',
'field_name' => static::TRANSFORMATIONS_FIELD_NAME,
'type' => 'string',
'cardinality' => 1,
'locked' => TRUE,
]);
}
/**
* Creates the transformations field definition.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type.
*
* @return \Drupal\field\FieldConfigInterface
* The unsaved field definition. The field storage definition, if new,
* should also be unsaved.
*/
public function createTransformationsField(MediaTypeInterface $type) {
return $this->entityTypeManager->getStorage('field_config')
->create([
'entity_type' => 'media',
'field_name' => static::TRANSFORMATIONS_FIELD_NAME,
'bundle' => $type->id(),
'label' => 'Bynder Transformations',
'translatable' => FALSE,
'field_type' => 'string',
]);
}
/**
* Checks if remote images should be used.
*
* This checks to see if the configuration for using remote images is enabled
* and that the Remote Stream Wrapper module is still enabled.
*
* @return bool
* TRUE if remote images should be used, or FALSE otherwise.
*/
protected function useRemoteImages() {
return $this->configFactory->get('bynder.settings')->get('use_remote_images')
|| $this->moduleHandler->moduleExists('remote_stream_wrapper');
}
}
