You are here

BlazyManager.php in Blazy 8

Same filename and directory in other branches
  1. 8.2 src/BlazyManager.php
  2. 7 src/BlazyManager.php

Namespace

Drupal\blazy

File

src/BlazyManager.php
View source
<?php

namespace Drupal\blazy;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;

/**
 * Implements a public facing blazy manager.
 *
 * A few modules re-use this: GridStack, Mason, Slick...
 */
class BlazyManager extends BlazyManagerBase {

  /**
   * Cleans up empty breakpoints.
   *
   * @param array $settings
   *   The settings being modified.
   */
  public function cleanUpBreakpoints(array &$settings = []) {
    if (!empty($settings['breakpoints'])) {
      $breakpoints = array_filter(array_map('array_filter', $settings['breakpoints']));
      $settings['breakpoints'] = NestedArray::filter($breakpoints, function ($breakpoint) {
        return !(is_array($breakpoint) && (empty($breakpoint['width']) || empty($breakpoint['image_style'])));
      });
    }
  }

  /**
   * Checks if an image style contains crop effect.
   */
  public function isCrop($style = NULL) {
    foreach ($style
      ->getEffects() as $effect) {
      if (strpos($effect
        ->getPluginId(), 'crop') !== FALSE) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Sets dimensions once to reduce method calls, if image style contains crop.
   *
   * The implementor should only call this if not using Responsive image style.
   *
   * @param array $settings
   *   The settings being modified.
   */
  public function setDimensionsOnce(array &$settings = []) {
    $item = isset($settings['item']) ? $settings['item'] : NULL;
    $dimensions['width'] = $settings['original_width'] = isset($item->width) ? $item->width : NULL;
    $dimensions['height'] = $settings['original_height'] = isset($item->height) ? $item->height : NULL;

    // If image style contains crop, sets dimension once, and let all inherit.
    if (!empty($settings['image_style']) && ($style = $this
      ->entityLoad($settings['image_style']))) {
      if ($this
        ->isCrop($style)) {
        $style
          ->transformDimensions($dimensions, $settings['uri']);
        $settings['height'] = $dimensions['height'];
        $settings['width'] = $dimensions['width'];

        // Informs individual images that dimensions are already set once.
        $settings['_dimensions'] = TRUE;
      }
    }

    // Also sets breakpoint dimensions once, if cropped.
    if (!empty($settings['breakpoints'])) {
      $this
        ->buildDataBlazy($settings, $item);
    }

    // Remove these since this method is meant for top-level container.
    unset($settings['uri'], $settings['item']);
  }

  /**
   * Checks for Blazy formatter such as from within a Views style plugin.
   *
   * Ensures the settings traverse up to the container where Blazy is clueless.
   * The supported plugins can add [data-blazy] attribute into its container
   * containing $settings['blazy_data'] converted into [data-blazy] JSON.
   *
   * @param array $settings
   *   The settings being modified.
   * @param array $item
   *   The item containing settings or item keys.
   */
  public function isBlazy(array &$settings, array $item = []) {

    // Retrieves Blazy formatter related settings from within Views style.
    $content = !empty($settings['item_id']) && isset($item[$settings['item_id']]) ? $item[$settings['item_id']] : $item;

    // 1. Blazy formatter within Views fields by supported modules.
    if (isset($item['settings'])) {

      // Prevents edge case with unexpected flattened Views results which is
      // normally triggered by checking "Use field template" option.
      $blazy = is_array($content) && isset($content['#build']['settings']) ? $content['#build']['settings'] : [];

      // Allows breakpoints overrides such as multi-styled images by GridStack.
      if (empty($settings['breakpoints']) && isset($blazy['breakpoints'])) {
        $settings['breakpoints'] = $blazy['breakpoints'];
      }
      $cherries = [
        'blazy',
        'box_style',
        'image_style',
        'lazy',
        'media_switch',
        'ratio',
        'uri',
      ];
      foreach ($cherries as $key) {
        $fallback = isset($settings[$key]) ? $settings[$key] : '';
        $settings[$key] = isset($blazy[$key]) && empty($fallback) ? $blazy[$key] : $fallback;
      }
    }

    // 2. Blazy Views fields by supported modules.
    if (is_array($content) && isset($content['#view']) && ($view = $content['#view'])) {
      if ($blazy_field = BlazyViews::viewsField($view)) {
        $settings = array_merge(array_filter($blazy_field
          ->mergedViewsSettings()), array_filter($settings));
      }
    }

    // Provides data for the [data-blazy] attribute at the containing element.
    $this
      ->cleanUpBreakpoints($settings);
    if (!empty($settings['breakpoints'])) {
      $image = isset($item['item']) ? $item['item'] : NULL;
      $this
        ->buildDataBlazy($settings, $image);
    }
    unset($settings['uri']);
  }

  /**
   * Builds breakpoints suitable for top-level [data-blazy] wrapper attributes.
   *
   * The hustle is because we need to define dimensions once, if applicable, and
   * let all images inherit. Each breakpoint image may be cropped, or scaled
   * without a crop. To set dimensions once requires all breakpoint images
   * uniformly cropped. But that is not always the case.
   *
   * @param array $settings
   *   The settings being modified.
   * @param object|mixed $item
   *   The \Drupal\image\Plugin\Field\FieldType\ImageItem item, or array when
   *   dealing with Video Embed Field.
   *
   * @todo: Refine this like everything else.
   */
  public function buildDataBlazy(array &$settings, $item = NULL) {

    // Early opt-out if blazy_data has already been defined.
    // Blazy doesn't always deal with image directly.
    if (!empty($settings['blazy_data'])) {
      return;
    }
    if (empty($settings['original_width'])) {
      $settings['original_width'] = isset($item->width) ? $item->width : NULL;
      $settings['original_height'] = isset($item->height) ? $item->height : NULL;
    }
    $json = $sources = [];
    $end = end($settings['breakpoints']);
    foreach ($settings['breakpoints'] as $key => $breakpoint) {
      if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
        continue;
      }
      if ($width = Blazy::widthFromDescriptors($breakpoint['width'])) {

        // If contains crop, sets dimension once, and let all images inherit.
        if (!empty($settings['uri']) && !empty($settings['ratio'])) {
          $dimensions['width'] = $settings['original_width'];
          $dimensions['height'] = $settings['original_height'];
          if (!empty($breakpoint['image_style']) && ($style = $this
            ->entityLoad($breakpoint['image_style']))) {
            if ($this
              ->isCrop($style)) {
              $style
                ->transformDimensions($dimensions, $settings['uri']);
              $padding = round($dimensions['height'] / $dimensions['width'] * 100, 2);
              $json['dimensions'][$width] = $padding;

              // Only set padding-bottom for the last breakpoint to avoid FOUC.
              if ($end['width'] == $breakpoint['width']) {
                $settings['padding_bottom'] = $padding;
              }
            }
          }
        }

        // If BG, provide [data-src-BREAKPOINT].
        if (!empty($settings['background'])) {
          $sources[] = [
            'width' => (int) $width,
            'src' => 'data-src-' . $key,
          ];
        }
      }
    }

    // As of Blazy v1.6.0 applied to BG only.
    if ($sources) {
      $json['breakpoints'] = $sources;
    }

    // @todo: A more efficient way not to do this in the first place.
    // ATM, this is okay as this method is run once on the top-level container.
    if (isset($json['dimensions']) && count($settings['breakpoints']) != count($json['dimensions'])) {
      unset($json['dimensions'], $settings['padding_bottom']);
    }

    // Supported modules can add blazy_data as [data-blazy] to the container.
    // This also informs individual images to not work with dimensions any more
    // if the image style contains 'crop'.
    if ($json) {
      $settings['blazy_data'] = $json;
    }

    // Identify that Blazy can be activated only by breakpoints.
    $settings['blazy'] = TRUE;
  }

  /**
   * Returns the enforced content, or image using theme_blazy().
   *
   * @param array $build
   *   The array containing: item, content, settings, or optional captions.
   *
   * @return array
   *   The alterable and renderable array of enforced content, or theme_blazy().
   */
  public function getBlazy(array $build = []) {
    foreach (BlazyDefault::themeProperties() as $key) {
      $build[$key] = isset($build[$key]) ? $build[$key] : [];
    }
    $settings =& $build['settings'];
    $settings += BlazyDefault::itemSettings();

    // Respects content not handled by theme_blazy(), but passed through.
    if (empty($build['content'])) {
      $image = [
        '#theme' => $settings['theme_hook_image'] ?: 'blazy',
        '#delta' => $settings['delta'],
        '#item' => $settings['entity_type_id'] == 'user' ? $build['item'] : [],
        '#image_style' => $settings['image_style'],
        '#build' => $build,
        '#pre_render' => [
          [
            $this,
            'preRenderImage',
          ],
        ],
      ];
    }
    else {
      $image = $build['content'];
    }
    $this
      ->getModuleHandler()
      ->alter('blazy', $image, $settings);
    return $image;
  }

  /**
   * Builds the Blazy image as a structured array ready for ::renderer().
   *
   * @param array $element
   *   The pre-rendered element.
   *
   * @return array
   *   The renderable array of pre-rendered element.
   */
  public function preRenderImage(array $element) {
    $build = $element['#build'];
    unset($element['#build']);
    $item = $build['item'];
    $settings = $build['settings'];
    if (empty($settings['uri']) && is_object($item)) {
      $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity
        ->getFileUri() : $item->uri;
    }

    // Extract field item attributes for the theme function, and unset them
    // from the $item so that the field template does not re-render them.
    $item_attributes = [];
    if ($item && isset($item->_attributes)) {
      $item_attributes = $item->_attributes;
      unset($item->_attributes);
    }

    // Responsive image integration.
    if (!empty($settings['resimage'])) {
      $settings['responsive_image_style_id'] = $settings['resimage']
        ->id();
      $item_attributes['data-b-lazy'] = $settings['one_pixel'];
      $settings['lazy'] = 'responsive';
      $element['#cache']['tags'] = $this
        ->getResponsiveImageCacheTags($settings['resimage']);
    }
    else {
      if (empty($settings['_no_cache'])) {
        $file_tags = isset($settings['file_tags']) ? $settings['file_tags'] : [];
        $settings['cache_tags'] = empty($settings['cache_tags']) ? $file_tags : Cache::mergeTags($settings['cache_tags'], $file_tags);
        $element['#cache']['max-age'] = -1;
        foreach ([
          'contexts',
          'keys',
          'tags',
        ] as $key) {
          if (!empty($settings['cache_' . $key])) {
            $element['#cache'][$key] = $settings['cache_' . $key];
          }
        }
      }
    }
    $element['#item'] = $item;
    $element['#captions'] = empty($build['captions']) ? [] : [
      'inline' => $build['captions'],
    ];
    $element['#item_attributes'] = $item_attributes;
    $element['#settings'] = $settings;
    foreach ([
      'caption',
      'media',
      'wrapper',
    ] as $key) {
      if (!empty($settings[$key . '_attributes'])) {
        $element["#{$key}" . '_attributes'] = $settings[$key . '_attributes'];
      }
    }
    if (!empty($settings['media_switch'])) {
      if ($settings['media_switch'] == 'content' && !empty($settings['content_url'])) {
        $element['#url'] = $settings['content_url'];
      }
      elseif (!empty($settings['lightbox'])) {
        BlazyLightbox::build($element);
      }
    }
    return $element;
  }

  /**
   * Returns the entity view, if available.
   *
   * @param object $entity
   *   The entity being rendered.
   * @param array $settings
   *   The settings containing view_mode.
   * @param string $fallback
   *   The fallback content when all fails, probably just entity label.
   *
   * @return array|bool
   *   The renderable array of the view builder, or false if not applicable.
   */
  public function getEntityView($entity = NULL, array $settings = [], $fallback = '') {
    if ($entity instanceof EntityInterface) {
      $entity_type_id = $entity
        ->getEntityTypeId();
      $view_hook = $entity_type_id . '_view';
      $view_mode = empty($settings['view_mode']) ? 'default' : $settings['view_mode'];
      $langcode = $entity
        ->language()
        ->getId();

      // If module implements own {entity_type}_view.
      if (function_exists($view_hook)) {
        return $view_hook($entity, $view_mode, $langcode);
      }
      elseif ($this
        ->getEntityTypeManager()
        ->hasHandler($entity_type_id, 'view_builder')) {
        return $this
          ->getEntityTypeManager()
          ->getViewBuilder($entity_type_id)
          ->view($entity, $view_mode, $langcode);
      }
      elseif ($fallback) {
        return [
          '#markup' => $fallback,
        ];
      }
    }
    return FALSE;
  }

  /**
   * Returns the Responsive image cache tags.
   *
   * @param object $responsive
   *   The responsive image style entity.
   *
   * @return array
   *   The responsive image cache tags, or empty array.
   */
  public function getResponsiveImageCacheTags($responsive = NULL) {
    $cache_tags = [];
    $image_styles_to_load = [];
    if ($responsive) {
      $cache_tags = Cache::mergeTags($cache_tags, $responsive
        ->getCacheTags());
      $image_styles_to_load = $responsive
        ->getImageStyleIds();
    }
    $image_styles = $this
      ->entityLoadMultiple('image_style', $image_styles_to_load);
    foreach ($image_styles as $image_style) {
      $cache_tags = Cache::mergeTags($cache_tags, $image_style
        ->getCacheTags());
    }
    return $cache_tags;
  }

  /**
   * Backported few cross-module methods to minimize mismatched branch issues.
   */
  public function getImage(array $build = []) {
    return $this
      ->getBlazy($build);
  }

}

Classes

Namesort descending Description
BlazyManager Implements a public facing blazy manager.