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

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