media_helper-2.0.0/src/Service/MediaHelper.php

src/Service/MediaHelper.php
<?php

namespace Drupal\media_helper\Service;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\FileInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\media\MediaInterface;
use Drupal\media\MediaSourceInterface;
use Drupal\media_helper\MediaHelperInterface;

use function in_array;

/**
 * The module's main service providing configuration reading and media rendering.
 */
class MediaHelper implements MediaHelperInterface {

  /**
   * A list of other modules for which Media Helper integration is enabled.
   *
   * @var string[]
   */
  protected array $moduleIntegrationsEnabled = [];

  /**
   * A list of supported media source plugin IDs for |media_image.
   *
   * Dynamic instead of constant so that integrations can add to the list.
   *
   * @var string[]
   */
  protected array $supportedImageSourcePluginIds = [
    'image',
  ];

  /**
   * Whether svg_image_field module integration should account for image styles.
   *
   * If this is TRUE, image styles should be taken into account for dimensions. If this is FALSE, they should not.
   */
  protected bool $svgImageFieldModuleOverrideDimensions = FALSE;

  /**
   * Instantiates the MediaHelper service.
   */
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected FileSystemInterface $fileSystem,
    protected ModuleHandlerInterface $moduleHandler,
  ) {
    $config = $this->configFactory->get('media_helper.settings');

    if ($this->moduleHandler->moduleExists('responsive_image')) {
      $this->moduleIntegrationsEnabled[] = 'responsive_image';
    }

    if (
      $config->get('integrations.svg_image.enable')
      && $this->moduleHandler->moduleExists('svg_image')
      && function_exists('svg_image_is_file_svg')
    ) {
      $this->moduleIntegrationsEnabled[] = 'svg_image';
    }

    if (
      $config->get('integrations.svg_image_field.enable')
      && $this->moduleHandler->moduleExists('svg_image_field')
    ) {
      $this->moduleIntegrationsEnabled[] = 'svg_image_field';
      $this->supportedImageSourcePluginIds[] = 'svg';
      $this->svgImageFieldModuleOverrideDimensions = (bool) $config->get('integrations.svg_image_field.override_dimensions');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedImageSourcePluginIds(): array {
    return $this->supportedImageSourcePluginIds;
  }

  /**
   * {@inheritdoc}
   */
  public function isImageSourceSupported(string|MediaSourceInterface $source): bool {
    if ($source instanceof MediaSourceInterface) {
      $source = $source->getPluginId();
    }
    return in_array($source, $this->supportedImageSourcePluginIds, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  public function isModuleIntegrationEnabled(string $module): bool {
    return in_array($module, $this->moduleIntegrationsEnabled, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  public function getResponsiveImgTagAttributeNames(): array {
    $tags = &drupal_static(__METHOD__);
    if (!isset($tags)) {
      $configured_names = $this->configFactory
        ->get('media_helper.settings')
        ->get('integrations.responsive_image.img_tag_attributes')
      ?: [];
      $tags = array_merge(MediaHelperInterface::ALWAYS_IMG_TAG_ATTRS, $configured_names);
    }
    return $tags;
  }

  /**
   * {@inheritdoc}
   */
  public function mediaBundle(MediaInterface|array $media): ?string {
    if (!is_array($media)) {
      return $media->bundle();
    }

    $type = NULL;
    foreach ($media as $single_media) {
      if (!$single_media instanceof MediaInterface) {
        throw new \InvalidArgumentException('You may only pass objects of the MediaInterface type.');
      }

      if (!$type) {
        $type = $single_media->bundle();
      }
      elseif ($type !== $single_media->bundle()) {
        return 'mixed';
      }
    }
    return $type;
  }

  /**
   * {@inheritdoc}
   */
  public function mediaSource(MediaInterface|array $media): ?string {
    if (!is_array($media)) {
      return $media->getSource()->getPluginId();
    }

    $source_id = NULL;
    foreach ($media as $single_media) {
      if (!$single_media instanceof MediaInterface) {
        throw new \InvalidArgumentException('You may only pass objects of the MediaInterface type.');
      }

      if (!$source_id) {
        $source_id = $single_media->getSource()->getPluginId();
      }
      elseif ($source_id !== $single_media->getSource()->getPluginId()) {
        return 'mixed';
      }
    }
    return $source_id;
  }

  /**
   * {@inheritdoc}
   */
  public function mediaImage(MediaInterface|array|null $media, string $style = '', array|string $classes = [], array $attributes = []): ?array {
    if ($media instanceof MediaInterface) {
      $media = [$media];
    }
    elseif (!$media) {
      return NULL;
    }

    $render = [];
    foreach ($media as $image) {
      $render[] = $this->renderMediaImage($image, [$this, 'mediaImageRender'], $style, $classes, $attributes);
    }
    return $render ?: NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function mediaImageUrl(MediaInterface|array|null $media, string $style = ''): ?array {
    if ($media instanceof MediaInterface) {
      $media = [$media];
    }
    elseif (!$media) {
      return NULL;
    }

    $render = [];
    foreach ($media as $image) {
      if (!$image instanceof MediaInterface) {
        throw new \UnexpectedValueException('Non-media item passed in array!');
      }
      $render[] = $this->renderMediaImage($image, [$this, 'mediaImageUrlRender'], $style);
    }
    return $render ?: NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function mediaVideo(MediaInterface|array $media, array|string $classes = [], array $settings = [], array $attributes = []): ?array {
    if ($media instanceof MediaInterface) {
      $media = [$media];
    }
    elseif (!$media) {
      return NULL;
    }

    $render = [];
    foreach ($media as $video) {
      $render[] = $this->renderVideo($video, $classes, $settings, $attributes);
    }
    return $render ?: NULL;
  }

  /**
   * Get the name of the source field on a media entity.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   *
   * @return string|null
   *   The name of the source field, or NULL if no field is configured.
   */
  protected function getSourceFieldName(MediaInterface $media): ?string {
    return $media->getSource()->getSourceFieldDefinition($media->bundle->entity)?->getName();
  }

  /**
   * Handles rendering a media image.
   *
   * @param \Drupal\media\MediaInterface|null $media
   *   A media entity object.
   * @param callable $widget_callback
   *   A callable to finish constructing widget settings with any logic specific to the widget type. Signature:
   *   (array &$widget_settings, MediaInterface $media, string $image_style, string $image_field_name)
   *   Callable should return a boolean indicating if it was successful.
   * @param string $style
   *   (optional) The machine name of the image style to use.
   * @param string[]|string $classes
   *   (optional) Class(es) to add to the element, as a string or array of strings.
   * @param array $attributes
   *   (optional) Attributes to add to the element, name => value.
   *
   * @return array|null
   *   A render array, or NULL if there is no meaningful output (e.g. the media source cannot be handled).
   */
  protected function renderMediaImage(MediaInterface $media, callable $widget_callback, string $style = '', array|string $classes = [], array $attributes = []): ?array {
    $media_source_plugin_id = $media->getSource()->getPluginId();
    if (!in_array($media_source_plugin_id, $this->supportedImageSourcePluginIds, TRUE)) {
      return NULL;
    }

    $image_field_name = $this->getSourceFieldName($media);
    if (!$image_field_name) {
      return NULL;
    }

    $render = [];
    $access_result = $media->access('view', NULL, TRUE);
    if ($access_result->isAllowed()) {
      $current_image_style = $style ?: $this->getDefaultImageStyle($media);

      $image_render_widget_settings = [
        'label' => 'hidden',
      ];
      switch ($media_source_plugin_id) {
        case 'image':
          $image_render_widget_settings['settings'] = [
            'image_link' => '',
            'image_style' => $current_image_style,
          ];
          break;

        case 'svg':
          // Formatter from the svg_image_field module.
          // No common settings between svg_image_field_formatter and
          // svg_image_url_formatter.
          break;

        default:
          throw new \LogicException("Media source plugin {$media_source_plugin_id} has no display widget settings handling!");
      }

      $settings_success = call_user_func_array(
        $widget_callback,
        [&$image_render_widget_settings, $media, $current_image_style, $image_field_name],
      );
      if ($settings_success) {
        $render = $media->get($image_field_name)->view($image_render_widget_settings);

        if (
          !empty($render[0])
          && ($attributes || $classes)
        ) {
          $is_responsive_image = isset($render[0]['#theme']) && $render[0]['#theme'] === 'responsive_image_formatter';
          $attributes_key = match(TRUE) {
            $is_responsive_image              => '#media_helper_attributes',
            $media_source_plugin_id === 'svg' => '#attributes',
            default                           => '#item_attributes',
          };

          if ($attributes) {
            foreach ($attributes as $attribute_name => $attribute_value) {
              $render[0][$attributes_key][$attribute_name] = $attribute_value;
            }
          }

          // Lastly, take care of CSS classes.
          if ($classes) {
            if (is_string($classes)) {
              $classes = preg_split('/\s+/', $classes, -1, PREG_SPLIT_NO_EMPTY);
            }
            $existing_classes = ($render[0][$attributes_key]['class'] ?? NULL) ?: [];
            $render[0][$attributes_key]['class'] = array_merge($existing_classes, $classes);
          }
        }
      }
    }

    CacheableMetadata::createFromRenderArray($render)
      ->addCacheableDependency($media)
      ->addCacheableDependency($access_result)
      ->applyTo($render);
    return $render;
  }

  /**
   * Handles image display widget settings specific to mediaImage().
   *
   * @param array $widget_settings
   *   The half-finished widget settings array.
   * @param \Drupal\media\MediaInterface $media
   *   The media entity being rendered.
   * @param string $image_style_name
   *   The image style to use.
   * @param string $image_field_name
   *   The name of the media image field.
   *
   * @return bool
   *   Whether or not the render array was finished successfully.
   */
  protected function mediaImageRender(array &$widget_settings, MediaInterface $media, string $image_style_name, string $image_field_name): bool {
    $media_source_plugin_id = $media->getSource()->getPluginId();

    if ($media_source_plugin_id === 'image') {
      $image_style = $image_style_name
        ? $this->entityTypeManager->getStorage('image_style')->load($image_style_name)
        : NULL;
      if (!$image_style_name || $image_style) {
        $widget_settings['type'] = 'image';

        // Check if this is an SVG file allowed into an image field by the svg_image module.
        // If so, set width/height attributes from image style effects and/or the SVG file.
        if (
          $this->isModuleIntegrationEnabled('svg_image')
          && ($file_field = $media->get($image_field_name))
          && ($file = $file_field->entity)
          && svg_image_is_file_svg($file)
        ) {
          $image_item = $file_field->first();
          $svg_width = $image_item?->width;
          $svg_height = $image_item?->height;
          $dimensions = ($svg_width && $svg_height)
            ? ['width' => $svg_width, 'height' => $svg_height]
            : NULL;

          if ($image_style) {
            $dimensions = $this->applyImageStyleDimensions($image_style, $dimensions);
          }

          if ($dimensions) {
            foreach (['width', 'height'] as $config_attr) {
              $widget_settings['settings']['svg_attributes'][$config_attr] = $dimensions[$config_attr];
            }
          }
        }
      }
      elseif (
        $this->isModuleIntegrationEnabled('responsive_image')
        && $this->entityTypeManager->getStorage('responsive_image_style')->load($image_style_name)
      ) {
        $widget_settings['type'] = 'responsive_image';
        $widget_settings['settings']['responsive_image_style'] = $image_style_name;
        unset($widget_settings['settings']['image_style']);

        // @todo support svg_image module dimensions when using responsive image styles? See module svg_image_responsive for possible integration.
        if (
          $this->isModuleIntegrationEnabled('svg_image')
          && ($file = $media->get($image_field_name)->entity)
          && svg_image_is_file_svg($file)
        ) {
          trigger_error('svg_image module not yet supported together with responsive_image module\'s responsive image styles.', E_USER_WARNING);
        }
      }
      else {
        trigger_error('media_image Twig filter could not find an image style with machine name "' . $image_style_name . '"! (media entity ' . $media->id() . ')', E_USER_WARNING);
        return FALSE;
      };

      return TRUE;
    }

    if ($media_source_plugin_id === 'svg') {
      $widget_settings['type'] = 'svg_image_field_formatter';
      $widget_settings['settings'] = [
        'enable_alt' => TRUE,
        'inline' => FALSE,
        'link_url' => FALSE,
        'apply_dimensions' => FALSE,
      ];

      $file = $media->get($image_field_name)->entity;
      if ($file) {
        $dimensions = $this->getSvgDimensions($file);

        // We do not directly support the responsive_image module here, but we also ensure we do not fail SVG output due
        // to the use of a responsive image style name. The responsive_image module is "supported" insofar as passing a
        // responsive image style will not cause errors, but neither will it affect image dimension attributes.
        if (
          $image_style_name
          && $this->svgImageFieldModuleOverrideDimensions
          && ($image_style = $this->entityTypeManager->getStorage('image_style')->load($image_style_name))
        ) {
          $dimensions = $this->applyImageStyleDimensions($image_style, $dimensions);
        }

        if ($dimensions) {
          $widget_settings['settings']['apply_dimensions'] = TRUE;
          $widget_settings['settings']['width'] = $dimensions['width'];
          $widget_settings['settings']['height'] = $dimensions['height'];
        }
      }

      return TRUE;
    }

    throw new \LogicException("Media source plugin {$media_source_plugin_id} has no handling in mediaImageRender!");
  }

  /**
   * Manually applies dimensional settings from an image style.
   *
   * Useful for SVG handling.
   *
   * If initial dimensions are provided, every effect's transformDimensions() method will be used in order, to further
   * process dimensions.
   *
   * If initial dimensions are not provided, they will be taken from the first plugin with width and height settings.
   * Only effects after this point will have their transformDimensions() method applied.
   *
   * @param \Drupal\image\ImageStyleInterface $image_style
   *   The image style to apply effects from.
   * @param int[]|null $dimensions
   *   (optional) The dimensions to start from. If provided, should be an array with 'width' and 'height' keys.
   *
   * @return array|null
   *   The Image Style-influenced dimensions, or NULL if no dimensions were passed and no image effects established any.
   */
  protected function applyImageStyleDimensions(ImageStyleInterface $image_style, ?array $dimensions = NULL): ?array {
    foreach ($image_style->getEffects() as $effect) {
      if ($dimensions) {
        $effect->transformDimensions($dimensions, NULL);
      }
      elseif (
        ($config = $effect->getConfiguration())
        && !empty($config['data']['width'])
        && !empty($config['data']['height'])
      ) {
        $dimensions = [
          'width'  => $config['data']['width'],
          'height' => $config['data']['height'],
        ];
      }
    }

    return $dimensions;
  }

  /**
   * Gets the dimensions of an SVG.
   *
   * @param \Drupal\file\FileInterface $file
   *   The SVG to get dimensions for.
   *
   * @return int[]|null
   *   An array with 'width' and 'height' attributes, or NULL if the file could not be loaded or dimensions could not be
   *   determined.
   */
  protected function getSvgDimensions(FileInterface $file): ?array {
    // @todo cache results. At least in drupal_static(), maybe even in cache backend.
    if (
      ($svg = file_get_contents($file->getFileUri()))
      && ($svg = simplexml_load_string(data: $svg, options: LIBXML_NOWARNING))
    ) {
      $attributes = $svg->attributes();
      $width = (int) $attributes->width;
      $height = (int) $attributes->height;
      if ($width && $height) {
        return [
          'width' => $width,
          'height' => $height,
        ];
      }
    }

    return NULL;
  }

  /**
   * Handles image display widget settings specific to mediaImageUrl().
   *
   * @param array $widget_settings
   *   The half-finished widget settings array.
   * @param \Drupal\media\MediaInterface $media
   *   The media entity being rendered.
   * @param string $image_style_name
   *   The image style to use.
   *
   * @return bool
   *   Whether or not the render array was finished successfully.
   */
  protected function mediaImageUrlRender(array &$widget_settings, MediaInterface $media, string $image_style_name): bool {
    $media_source_plugin_id = $media->getSource()->getPluginId();

    if ($media_source_plugin_id === 'image') {
      if (!$image_style_name || $this->entityTypeManager->getStorage('image_style')->load($image_style_name)) {
        $widget_settings['type'] = 'image_url';
        return TRUE;
      }
      trigger_error('media_image_url Twig filter could not find an image style with machine name "' . $image_style_name . '"! (media entity ' . $media->id() . ')', E_USER_WARNING);
      return FALSE;
    }

    if ($media_source_plugin_id === 'svg') {
      $widget_settings['type'] = 'svg_image_url_formatter';
      return TRUE;
    }

    throw new \LogicException("Media source plugin {$media_source_plugin_id} has no handling in mediaImageUrlRender!");
  }

  /**
   * Finds the image style for the source field on media's default display mode.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $source_field_name
   *   (optional) The name of the source image field for this media.
   *
   * @return string
   *   The image style from the default view mode. An empty string if it cannot be found.
   */
  protected function getDefaultImageStyle(MediaInterface $media, string $source_field_name = ''): string {
    $default_image_style = '';

    if (!$source_field_name) {
      $source_field_name = $this->getSourceFieldName($media);
    }

    if ($source_field_name) {
      $view_mode_id = $media->getEntityTypeId() . '.' . $media->bundle() . '.default';
      $default_view_mode = $this->entityTypeManager
        ->getStorage('entity_view_display')
        ->load($view_mode_id);

      if ($default_view_mode) {
        assert($default_view_mode instanceof EntityDisplayInterface);
        $image_field_config = $default_view_mode->getComponent($source_field_name);
        if (!empty($image_field_config['type'])) {
          if (
            $image_field_config['type'] === 'image'
            && !empty($image_field_config['settings']['image_style'])
          ) {
            $default_image_style = $image_field_config['settings']['image_style'];
          }
          elseif (
            $image_field_config['type'] === 'responsive_image'
            && !empty($image_field_config['settings']['responsive_image_style'])
          ) {
            $default_image_style = $image_field_config['settings']['responsive_image_style'];
          }
        }
      }
    }

    return $default_image_style;
  }

  /**
   * Helper function for processing individual video media.
   *
   * @return array|null
   *   A render array, or NULL if there is no meaningful output (e.g. the media source cannot be handled).
   */
  protected function renderVideo(MediaInterface $media, array|string $classes, array $settings, array $attributes): ?array {
    if ($media->getSource()->getPluginId() !== 'video_file') {
      return NULL;
    }

    $video_field_name = $this->getSourceFieldName($media);
    if (!$video_field_name) {
      return NULL;
    }

    $render = [];
    $access_result = $media->access('view', NULL, TRUE);
    if ($access_result->isAllowed()) {
      $video_widget_settings = array_merge(
        [
          'autoplay' => TRUE,
          'controls' => FALSE,
          'loop' => TRUE,
          'muted' => TRUE,
          'multiple_file_display_type' => 'sources',
          'width' => NULL,
          'height' => NULL,
        ],
        $settings,
      );

      $render = $media->get($video_field_name)->view([
        'type' => 'file_video',
        'label' => 'hidden',
        'settings' => $video_widget_settings,
      ]);

      // If the video is set to autoplay, add some sensible defaults for autoplay videos on mobile devices.
      if ($video_widget_settings['autoplay']) {
        $attributes = array_merge(
          [
            'disablePictureInPicture' => 'true',
            'playsinline' => TRUE,
          ],
          $attributes,
        );
      }

      if ($attributes && !empty($render[0])) {
        foreach ($attributes as $attribute_name => $attribute_value) {
          $render[0]['#attributes'][$attribute_name] = $attribute_value;
        }
      }

      // Lastly, take care of CSS classes.
      if ($classes && !empty($render[0])) {
        if (is_string($classes)) {
          $classes = preg_split('/\s+/', $classes, -1, PREG_SPLIT_NO_EMPTY);
        }
        $existing_classes = ($render[0]['#attributes']['class'] ?? NULL) ?: [];
        $render[0]['#attributes']['class'] = array_merge($existing_classes, $classes);
      }
    }

    CacheableMetadata::createFromRenderArray($render)
      ->addCacheableDependency($media)
      ->addCacheableDependency($access_result)
      ->applyTo($render);
    return $render;
  }

}

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

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