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');
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc