vendor/contao/core-bundle/src/Image/Studio/Figure.php line 383

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\CoreBundle\Image\Studio;
  11. use Contao\Controller;
  12. use Contao\CoreBundle\File\Metadata;
  13. use Contao\File;
  14. use Contao\StringUtil;
  15. use Contao\Template;
  16. /**
  17.  * A Figure object holds image and metadata ready to be applied to a
  18.  * template's context. If you are using the legacy PHP templates, you can still
  19.  * use the provided legacy helper methods to manually apply the data to them.
  20.  *
  21.  * Wherever possible, the actual data is only requested/built on demand.
  22.  *
  23.  * @final This class will be made final in Contao 5.
  24.  */
  25. class Figure
  26. {
  27.     /**
  28.      * @var ImageResult
  29.      */
  30.     private $image;
  31.     /**
  32.      * @var Metadata|(\Closure(self):Metadata|null)|null
  33.      */
  34.     private $metadata;
  35.     /**
  36.      * @var array<string, string|null>|(\Closure(self):array<string, string|null>)|null
  37.      */
  38.     private $linkAttributes;
  39.     /**
  40.      * @var LightboxResult|(\Closure(self):LightboxResult|null)|null
  41.      */
  42.     private $lightbox;
  43.     /**
  44.      * @var array<string, mixed>|(\Closure(self):array<string, mixed>)|null
  45.      */
  46.     private $options;
  47.     /**
  48.      * Creates a figure container.
  49.      *
  50.      * All arguments but the main image result can also be set via a Closure
  51.      * that only returns the value on demand.
  52.      *
  53.      * @param Metadata|(\Closure(self):Metadata|null)|null                                $metadata       Metadata container
  54.      * @param array<string, string|null>|(\Closure(self):array<string, string|null>)|null $linkAttributes Link attributes
  55.      * @param LightboxResult|(\Closure(self):LightboxResult|null)|null                    $lightbox       Lightbox
  56.      * @param array<string, mixed>|(\Closure(self):array<string, mixed>)|null             $options        Template options
  57.      */
  58.     public function __construct(ImageResult $image$metadata null$linkAttributes null$lightbox null$options null)
  59.     {
  60.         $this->image $image;
  61.         $this->metadata $metadata;
  62.         $this->linkAttributes $linkAttributes;
  63.         $this->lightbox $lightbox;
  64.         $this->options $options;
  65.     }
  66.     /**
  67.      * Returns the image result of the main resource.
  68.      */
  69.     public function getImage(): ImageResult
  70.     {
  71.         return $this->image;
  72.     }
  73.     /**
  74.      * Returns true if a lightbox result can be obtained.
  75.      */
  76.     public function hasLightbox(): bool
  77.     {
  78.         $this->resolveIfClosure($this->lightbox);
  79.         return $this->lightbox instanceof LightboxResult;
  80.     }
  81.     /**
  82.      * Returns the lightbox result (if available).
  83.      */
  84.     public function getLightbox(): LightboxResult
  85.     {
  86.         if (!$this->hasLightbox()) {
  87.             throw new \LogicException('This result container does not include a lightbox.');
  88.         }
  89.         /** @var LightboxResult */
  90.         return $this->lightbox;
  91.     }
  92.     public function hasMetadata(): bool
  93.     {
  94.         $this->resolveIfClosure($this->metadata);
  95.         return $this->metadata instanceof Metadata;
  96.     }
  97.     /**
  98.      * Returns the main resource's metadata.
  99.      */
  100.     public function getMetadata(): Metadata
  101.     {
  102.         if (!$this->hasMetadata()) {
  103.             throw new \LogicException('This result container does not include metadata.');
  104.         }
  105.         /** @var Metadata */
  106.         return $this->metadata;
  107.     }
  108.     public function getSchemaOrgData(): array
  109.     {
  110.         $imageIdentifier $this->getImage()->getImageSrc();
  111.         if ($this->hasMetadata() && $this->getMetadata()->has(Metadata::VALUE_UUID)) {
  112.             $imageIdentifier '#/schema/image/'.$this->getMetadata()->getUuid();
  113.         }
  114.         $jsonLd = [
  115.             '@type' => 'ImageObject',
  116.             'identifier' => $imageIdentifier,
  117.             'contentUrl' => $this->getImage()->getImageSrc(),
  118.         ];
  119.         if (!$this->hasMetadata()) {
  120.             ksort($jsonLd);
  121.             return $jsonLd;
  122.         }
  123.         $jsonLd array_merge($this->getMetadata()->getSchemaOrgData('ImageObject'), $jsonLd);
  124.         ksort($jsonLd);
  125.         return $jsonLd;
  126.     }
  127.     /**
  128.      * Returns a key-value list of all link attributes. This excludes "href" by
  129.      * default.
  130.      */
  131.     public function getLinkAttributes(bool $includeHref false): array
  132.     {
  133.         $this->resolveIfClosure($this->linkAttributes);
  134.         if (null === $this->linkAttributes) {
  135.             $this->linkAttributes = [];
  136.         }
  137.         // Generate the href attribute
  138.         if (!\array_key_exists('href'$this->linkAttributes)) {
  139.             $this->linkAttributes['href'] = (
  140.                 function () {
  141.                     if ($this->hasLightbox()) {
  142.                         return $this->getLightbox()->getLinkHref();
  143.                     }
  144.                     if ($this->hasMetadata()) {
  145.                         return $this->getMetadata()->getUrl();
  146.                     }
  147.                     return '';
  148.                 }
  149.             )();
  150.         }
  151.         // Add rel attribute "noreferrer noopener" to external links
  152.         if (
  153.             !empty($this->linkAttributes['href'])
  154.             && !\array_key_exists('rel'$this->linkAttributes)
  155.             && preg_match('#^https?://#'$this->linkAttributes['href'])
  156.         ) {
  157.             $this->linkAttributes['rel'] = 'noreferrer noopener';
  158.         }
  159.         // Add lightbox attributes
  160.         if (!\array_key_exists('data-lightbox'$this->linkAttributes) && $this->hasLightbox()) {
  161.             $lightbox $this->getLightbox();
  162.             $this->linkAttributes['data-lightbox'] = $lightbox->getGroupIdentifier();
  163.         }
  164.         // Allow removing attributes by setting them to null
  165.         $linkAttributes array_filter(
  166.             $this->linkAttributes,
  167.             static function ($attribute): bool {
  168.                 return null !== $attribute;
  169.             }
  170.         );
  171.         // Optionally strip the href attribute
  172.         return $includeHref $linkAttributes array_diff_key($linkAttributes, ['href' => null]);
  173.     }
  174.     /**
  175.      * Returns the "href" link attribute.
  176.      */
  177.     public function getLinkHref(): string
  178.     {
  179.         return $this->getLinkAttributes(true)['href'] ?? '';
  180.     }
  181.     /**
  182.      * Returns a key-value list of template options.
  183.      */
  184.     public function getOptions(): array
  185.     {
  186.         $this->resolveIfClosure($this->options);
  187.         return $this->options ?? [];
  188.     }
  189.     /**
  190.      * Compiles an opinionated data set to be applied to a Contao template.
  191.      *
  192.      * Note: Do not use this method when building new templates from scratch or
  193.      *       when using Twig templates! Instead, add this object to your
  194.      *       template's context and directly access the specific data you need.
  195.      *
  196.      * @param string|array|null $margin              Set margins that will compose the inline CSS for the "margin" key
  197.      * @param string|null       $floating            Set/determine values for the "float_class" and "addBefore" keys
  198.      * @param bool              $includeFullMetadata Make all metadata available in the first dimension of the returned data set (key-value pairs)
  199.      */
  200.     public function getLegacyTemplateData($margin nullstring $floating nullbool $includeFullMetadata true): array
  201.     {
  202.         // Create a key-value list of the metadata and apply some renaming and
  203.         // formatting transformations to fit the legacy templates.
  204.         $createLegacyMetadataMapping = static function (Metadata $metadata): array {
  205.             if ($metadata->empty()) {
  206.                 return [];
  207.             }
  208.             $mapping $metadata->all();
  209.             // Handle special chars
  210.             foreach ([Metadata::VALUE_ALTMetadata::VALUE_TITLE] as $key) {
  211.                 if (isset($mapping[$key])) {
  212.                     $mapping[$key] = StringUtil::specialchars($mapping[$key]);
  213.                 }
  214.             }
  215.             // Rename certain keys (as used in the Contao templates)
  216.             if (isset($mapping[Metadata::VALUE_TITLE])) {
  217.                 $mapping['imageTitle'] = $mapping[Metadata::VALUE_TITLE];
  218.             }
  219.             if (isset($mapping[Metadata::VALUE_URL])) {
  220.                 $mapping['imageUrl'] = $mapping[Metadata::VALUE_URL];
  221.             }
  222.             unset($mapping[Metadata::VALUE_TITLE], $mapping[Metadata::VALUE_URL]);
  223.             return $mapping;
  224.         };
  225.         // Create a CSS margin property from an array or serialized string
  226.         $createMargin = static function ($margin): string {
  227.             if (!$margin) {
  228.                 return '';
  229.             }
  230.             $values array_merge(
  231.                 ['top' => '''right' => '''bottom' => '''left' => '''unit' => ''],
  232.                 StringUtil::deserialize($margintrue)
  233.             );
  234.             return Controller::generateMargin($values);
  235.         };
  236.         $image $this->getImage();
  237.         $originalSize $image->getOriginalDimensions()->getSize();
  238.         $fileInfoImageSize = (new File($image->getImageSrc(true)))->imageSize;
  239.         $linkAttributes $this->getLinkAttributes();
  240.         $metadata $this->hasMetadata() ? $this->getMetadata() : new Metadata([]);
  241.         // Primary image and metadata
  242.         $templateData array_merge(
  243.             [
  244.                 'picture' => [
  245.                     'img' => $image->getImg(),
  246.                     'sources' => $image->getSources(),
  247.                     'alt' => StringUtil::specialchars($metadata->getAlt()),
  248.                 ],
  249.                 'width' => $originalSize->getWidth(),
  250.                 'height' => $originalSize->getHeight(),
  251.                 'arrSize' => $fileInfoImageSize,
  252.                 'imgSize' => !empty($fileInfoImageSize) ? sprintf(' width="%d" height="%d"'$fileInfoImageSize[0], $fileInfoImageSize[1]) : '',
  253.                 'singleSRC' => $image->getFilePath(),
  254.                 'src' => $image->getImageSrc(),
  255.                 'fullsize' => ('_blank' === ($linkAttributes['target'] ?? null)) || $this->hasLightbox(),
  256.                 'margin' => $createMargin($margin),
  257.                 'addBefore' => 'below' !== $floating,
  258.                 'addImage' => true,
  259.             ],
  260.             $includeFullMetadata $createLegacyMetadataMapping($metadata) : []
  261.         );
  262.         // Link attributes and title
  263.         if ('' !== ($href $this->getLinkHref())) {
  264.             $templateData['href'] = $href;
  265.             $templateData['attributes'] = ''// always define attributes key if href is set
  266.             // Use link "title" attribute for "linkTitle" as it is already output explicitly in image.html5 (see #3385)
  267.             if (\array_key_exists('title'$linkAttributes)) {
  268.                 $templateData['linkTitle'] = $linkAttributes['title'];
  269.                 unset($linkAttributes['title']);
  270.             } else {
  271.                 // Map "imageTitle" to "linkTitle"
  272.                 $templateData['linkTitle'] = ($templateData['imageTitle'] ?? null) ?? StringUtil::specialchars($metadata->getTitle());
  273.                 unset($templateData['imageTitle']);
  274.             }
  275.         } elseif ($metadata->has(Metadata::VALUE_TITLE)) {
  276.             $templateData['picture']['title'] = StringUtil::specialchars($metadata->getTitle());
  277.         }
  278.         if (!empty($linkAttributes)) {
  279.             $htmlAttributes array_map(
  280.                 static function (string $attributestring $value) {
  281.                     return sprintf('%s="%s"'$attribute$value);
  282.                 },
  283.                 array_keys($linkAttributes),
  284.                 $linkAttributes
  285.             );
  286.             $templateData['attributes'] = ' '.implode(' '$htmlAttributes);
  287.         }
  288.         // Lightbox
  289.         if ($this->hasLightbox()) {
  290.             $lightbox $this->getLightbox();
  291.             if ($lightbox->hasImage()) {
  292.                 $lightboxImage $lightbox->getImage();
  293.                 $templateData['lightboxPicture'] = [
  294.                     'img' => $lightboxImage->getImg(),
  295.                     'sources' => $lightboxImage->getSources(),
  296.                 ];
  297.             }
  298.         }
  299.         // Other
  300.         if ($floating) {
  301.             $templateData['floatClass'] = " float_$floating";
  302.         }
  303.         // Add arbitrary template options
  304.         return array_merge($templateData$this->getOptions());
  305.     }
  306.     /**
  307.      * Applies the legacy template data to an existing template. This will
  308.      * prevent overriding the "href" property if already present and use
  309.      * "imageHref" instead.
  310.      *
  311.      * Note: Do not use this method when building new templates from scratch or
  312.      *       when using Twig templates! Instead, add this object to your
  313.      *       template's context and directly access the specific data you need.
  314.      *
  315.      * @param Template|object   $template            The template to apply the data to
  316.      * @param string|array|null $margin              Set margins that will compose the inline CSS for the template's "margin" property
  317.      * @param string|null       $floating            Set/determine values for the template's "float_class" and "addBefore" properties
  318.      * @param bool              $includeFullMetadata Make all metadata entries directly available in the template
  319.      */
  320.     public function applyLegacyTemplateData(object $template$margin nullstring $floating nullbool $includeFullMetadata true): void
  321.     {
  322.         $new $this->getLegacyTemplateData($margin$floating$includeFullMetadata);
  323.         $existing $template instanceof Template $template->getData() : get_object_vars($template);
  324.         // Do not override the "href" key (see #6468)
  325.         if (isset($new['href'], $existing['href'])) {
  326.             $new['imageHref'] = $new['href'];
  327.             unset($new['href']);
  328.         }
  329.         // Allow accessing Figure methods in a legacy template context
  330.         $new['figure'] = $this;
  331.         // Apply data
  332.         if ($template instanceof Template) {
  333.             $template->setData(array_replace($existing$new));
  334.             return;
  335.         }
  336.         foreach ($new as $key => $value) {
  337.             $template->$key $value;
  338.         }
  339.     }
  340.     /**
  341.      * Evaluates closures to retrieve the value.
  342.      */
  343.     private function resolveIfClosure(&$property): void
  344.     {
  345.         if ($property instanceof \Closure) {
  346.             $property $property($this);
  347.         }
  348.     }
  349. }