vendor/contao/core-bundle/src/Image/Studio/FigureBuilder.php line 657

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\CoreBundle\Event\FileMetadataEvent;
  12. use Contao\CoreBundle\Exception\InvalidResourceException;
  13. use Contao\CoreBundle\File\Metadata;
  14. use Contao\CoreBundle\Framework\Adapter;
  15. use Contao\CoreBundle\Util\LocaleUtil;
  16. use Contao\FilesModel;
  17. use Contao\Image\ImageInterface;
  18. use Contao\Image\PictureConfiguration;
  19. use Contao\Image\ResizeOptions;
  20. use Contao\PageModel;
  21. use Contao\StringUtil;
  22. use Contao\Validator;
  23. use Nyholm\Psr7\Uri;
  24. use Psr\Container\ContainerInterface;
  25. use Symfony\Component\Filesystem\Filesystem;
  26. use Symfony\Component\Filesystem\Path;
  27. /**
  28.  * Use the FigureBuilder class to create Figure result objects. The class
  29.  * has a fluent interface to configure the desired output. When you are ready,
  30.  * call build() to get a Figure. If you need another instance with similar
  31.  * settings, you can alter values and call build() again - it will not affect
  32.  * your first instance.
  33.  */
  34. class FigureBuilder
  35. {
  36.     private ContainerInterface $locator;
  37.     private string $projectDir;
  38.     private string $uploadPath;
  39.     private string $webDir;
  40.     private Filesystem $filesystem;
  41.     private ?InvalidResourceException $lastException null;
  42.     /**
  43.      * @var array<string>
  44.      */
  45.     private array $validExtensions;
  46.     /**
  47.      * The resource's absolute file path.
  48.      */
  49.     private ?string $filePath null;
  50.     /**
  51.      * The resource's file model if applicable.
  52.      */
  53.     private ?FilesModel $filesModel null;
  54.     /**
  55.      * User defined size configuration.
  56.      *
  57.      * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
  58.      *
  59.      * @var int|string|array|PictureConfiguration|null
  60.      */
  61.     private $sizeConfiguration;
  62.     /**
  63.      * User defined resize options.
  64.      *
  65.      * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
  66.      */
  67.     private ?ResizeOptions $resizeOptions null;
  68.     /**
  69.      * User defined custom locale. This will overwrite the default if set.
  70.      */
  71.     private ?string $locale null;
  72.     /**
  73.      * User defined metadata. This will overwrite the default if set.
  74.      */
  75.     private ?Metadata $metadata null;
  76.     /**
  77.      * User defined metadata. This will be added to the default if set.
  78.      */
  79.     private ?Metadata $overwriteMetadata null;
  80.     /**
  81.      * Determines if a metadata should never be present in the output.
  82.      */
  83.     private ?bool $disableMetadata null;
  84.     /**
  85.      * User defined link attributes. These will add to or overwrite the default values.
  86.      *
  87.      * @var array<string, string|null>
  88.      */
  89.     private array $additionalLinkAttributes = [];
  90.     /**
  91.      * User defined lightbox resource or url. This will overwrite the default if set.
  92.      *
  93.      * @var string|ImageInterface|null
  94.      */
  95.     private $lightboxResourceOrUrl;
  96.     /**
  97.      * User defined lightbox size configuration. This will overwrite the default if set.
  98.      *
  99.      * @var int|string|array|PictureConfiguration|null
  100.      */
  101.     private $lightboxSizeConfiguration;
  102.     /**
  103.      * User defined lightbox resize options.
  104.      */
  105.     private ?ResizeOptions $lightboxResizeOptions null;
  106.     /**
  107.      * User defined lightbox group identifier. This will overwrite the default if set.
  108.      */
  109.     private ?string $lightboxGroupIdentifier null;
  110.     /**
  111.      * Determines if a lightbox (or "fullsize") image should be created.
  112.      */
  113.     private ?bool $enableLightbox null;
  114.     /**
  115.      * User defined template options.
  116.      *
  117.      * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
  118.      *
  119.      * @var array<string, mixed>
  120.      */
  121.     private array $options = [];
  122.     /**
  123.      * @internal Use the Contao\CoreBundle\Image\Studio\Studio factory to get an instance of this class
  124.      */
  125.     public function __construct(ContainerInterface $locatorstring $projectDirstring $uploadPathstring $webDir, array $validExtensions)
  126.     {
  127.         $this->locator $locator;
  128.         $this->projectDir $projectDir;
  129.         $this->uploadPath $uploadPath;
  130.         $this->webDir $webDir;
  131.         $this->validExtensions $validExtensions;
  132.         $this->filesystem = new Filesystem();
  133.     }
  134.     /**
  135.      * Sets the image resource from a FilesModel.
  136.      */
  137.     public function fromFilesModel(FilesModel $filesModel): self
  138.     {
  139.         $this->lastException null;
  140.         if ('file' !== $filesModel->type) {
  141.             $this->lastException = new InvalidResourceException(sprintf('DBAFS item "%s" is not a file.'$filesModel->path));
  142.             return $this;
  143.         }
  144.         $this->filePath Path::makeAbsolute($filesModel->path$this->projectDir);
  145.         $this->filesModel $filesModel;
  146.         if (!$this->filesystem->exists($this->filePath)) {
  147.             $this->lastException = new InvalidResourceException(sprintf('No resource could be located at path "%s".'$this->filePath));
  148.         }
  149.         return $this;
  150.     }
  151.     /**
  152.      * Sets the image resource from a tl_files UUID.
  153.      */
  154.     public function fromUuid(string $uuid): self
  155.     {
  156.         $this->lastException null;
  157.         $filesModel $this->getFilesModelAdapter()->findByUuid($uuid);
  158.         if (null === $filesModel) {
  159.             $this->lastException = new InvalidResourceException(sprintf('DBAFS item with UUID "%s" could not be found.'$uuid));
  160.             return $this;
  161.         }
  162.         return $this->fromFilesModel($filesModel);
  163.     }
  164.     /**
  165.      * Sets the image resource from a tl_files ID.
  166.      */
  167.     public function fromId(int $id): self
  168.     {
  169.         $this->lastException null;
  170.         /** @var FilesModel|null $filesModel */
  171.         $filesModel $this->getFilesModelAdapter()->findByPk($id);
  172.         if (null === $filesModel) {
  173.             $this->lastException = new InvalidResourceException(sprintf('DBAFS item with ID "%s" could not be found.'$id));
  174.             return $this;
  175.         }
  176.         return $this->fromFilesModel($filesModel);
  177.     }
  178.     /**
  179.      * Sets the image resource from an absolute or relative path.
  180.      *
  181.      * @param bool $autoDetectDbafsPaths Set to false to skip searching for a FilesModel
  182.      */
  183.     public function fromPath(string $pathbool $autoDetectDbafsPaths true): self
  184.     {
  185.         $this->lastException null;
  186.         // Make sure path is absolute and in a canonical form
  187.         $path Path::isAbsolute($path) ? Path::canonicalize($path) : Path::makeAbsolute($path$this->projectDir);
  188.         // Only check for a FilesModel if the resource is inside the upload path
  189.         $getDbafsPath = function (string $path): ?string {
  190.             if (Path::isBasePath(Path::join($this->webDir$this->uploadPath), $path)) {
  191.                 return Path::makeRelative($path$this->webDir);
  192.             }
  193.             if (Path::isBasePath(Path::join($this->projectDir$this->uploadPath), $path)) {
  194.                 return $path;
  195.             }
  196.             return null;
  197.         };
  198.         if ($autoDetectDbafsPaths && null !== ($dbafsPath $getDbafsPath($path))) {
  199.             $filesModel $this->getFilesModelAdapter()->findByPath($dbafsPath);
  200.             if (null !== $filesModel) {
  201.                 return $this->fromFilesModel($filesModel);
  202.             }
  203.         }
  204.         $this->filePath $path;
  205.         $this->filesModel null;
  206.         if (!$this->filesystem->exists($this->filePath)) {
  207.             $this->lastException = new InvalidResourceException(sprintf('No resource could be located at path "%s".'$this->filePath));
  208.         }
  209.         return $this;
  210.     }
  211.     /**
  212.      * Sets the image resource from an absolute or relative URL.
  213.      *
  214.      * @param list<string> $baseUrls a list of allowed base URLs, the first match gets stripped from the resource URL
  215.      */
  216.     public function fromUrl(string $url, array $baseUrls = []): self
  217.     {
  218.         $this->lastException null;
  219.         $uri = new Uri($url);
  220.         $path null;
  221.         foreach ($baseUrls as $baseUrl) {
  222.             $baseUri = new Uri($baseUrl);
  223.             if ($baseUri->getHost() === $uri->getHost() && Path::isBasePath($baseUri->getPath(), $uri->getPath())) {
  224.                 $path Path::makeRelative($uri->getPath(), $baseUri->getPath().'/');
  225.                 break;
  226.             }
  227.         }
  228.         if (null === $path) {
  229.             if ('' !== $uri->getHost()) {
  230.                 $this->lastException = new InvalidResourceException(sprintf('Resource URL "%s" outside of base URLs "%s".'$urlimplode('", "'$baseUrls)));
  231.                 return $this;
  232.             }
  233.             $path $uri->getPath();
  234.         }
  235.         if (preg_match('/%2f|%5c/i'$path)) {
  236.             $this->lastException = new InvalidResourceException(sprintf('Resource URL path "%s" contains invalid percent encoding.'$path));
  237.             return $this;
  238.         }
  239.         // Prepend the web_dir (see #6123)
  240.         return $this->fromPath(Path::join($this->webDirurldecode($path)));
  241.     }
  242.     /**
  243.      * Sets the image resource from an ImageInterface.
  244.      */
  245.     public function fromImage(ImageInterface $image): self
  246.     {
  247.         return $this->fromPath($image->getPath());
  248.     }
  249.     /**
  250.      * Sets the image resource by guessing the identifier type.
  251.      *
  252.      * @param int|string|FilesModel|ImageInterface|null $identifier Can be a FilesModel, an ImageInterface, a tl_files UUID/ID/path or a file system path
  253.      */
  254.     public function from($identifier): self
  255.     {
  256.         if (null === $identifier) {
  257.             $this->lastException = new InvalidResourceException('The defined resource is "null".');
  258.             return $this;
  259.         }
  260.         if ($identifier instanceof FilesModel) {
  261.             return $this->fromFilesModel($identifier);
  262.         }
  263.         if ($identifier instanceof ImageInterface) {
  264.             return $this->fromImage($identifier);
  265.         }
  266.         $isString \is_string($identifier);
  267.         if ($isString && $this->getValidatorAdapter()->isUuid($identifier)) {
  268.             return $this->fromUuid($identifier);
  269.         }
  270.         if (is_numeric($identifier)) {
  271.             return $this->fromId((int) $identifier);
  272.         }
  273.         if ($isString) {
  274.             return $this->fromPath($identifier);
  275.         }
  276.         $type \is_object($identifier) ? \get_class($identifier) : \gettype($identifier);
  277.         throw new \TypeError(sprintf('%s(): Argument #1 ($identifier) must be of type FilesModel|ImageInterface|string|int|null, %s given'__METHOD__$type));
  278.     }
  279.     /**
  280.      * Sets a size configuration that will be applied to the resource.
  281.      *
  282.      * @param int|string|array|PictureConfiguration|null $size A picture size configuration or reference
  283.      */
  284.     public function setSize($size): self
  285.     {
  286.         $this->sizeConfiguration $size;
  287.         return $this;
  288.     }
  289.     /**
  290.      * Sets resize options.
  291.      *
  292.      * By default, or if the argument is set to null, resize options are derived
  293.      * from predefined image sizes.
  294.      */
  295.     public function setResizeOptions(?ResizeOptions $resizeOptions): self
  296.     {
  297.         $this->resizeOptions $resizeOptions;
  298.         return $this;
  299.     }
  300.     /**
  301.      * Sets custom metadata.
  302.      *
  303.      * By default, or if the argument is set to null, metadata is trying to be
  304.      * pulled from the FilesModel.
  305.      */
  306.     public function setMetadata(?Metadata $metadata): self
  307.     {
  308.         $this->metadata $metadata;
  309.         return $this;
  310.     }
  311.     /**
  312.      * Sets custom overwrite metadata.
  313.      *
  314.      * The metadata will be merged with the default metadata from the FilesModel.
  315.      */
  316.     public function setOverwriteMetadata(?Metadata $metadata): self
  317.     {
  318.         $this->overwriteMetadata $metadata;
  319.         return $this;
  320.     }
  321.     /**
  322.      * Disables creating/using metadata in the output even if it is present.
  323.      */
  324.     public function disableMetadata(bool $disable true): self
  325.     {
  326.         $this->disableMetadata $disable;
  327.         return $this;
  328.     }
  329.     /**
  330.      * Sets a custom locale.
  331.      *
  332.      * By default, or if the argument is set to null, the locale is determined
  333.      * from the request context and/or system settings.
  334.      */
  335.     public function setLocale(?string $locale): self
  336.     {
  337.         $this->locale $locale;
  338.         return $this;
  339.     }
  340.     /**
  341.      * Adds a custom link attribute.
  342.      *
  343.      * Set the value to null to remove it. If you want to explicitly remove an
  344.      * auto-generated value from the results, set the $forceRemove flag to true.
  345.      */
  346.     public function setLinkAttribute(string $attribute, ?string $valuebool $forceRemove false): self
  347.     {
  348.         if (null !== $value || $forceRemove) {
  349.             $this->additionalLinkAttributes[$attribute] = $value;
  350.         } else {
  351.             unset($this->additionalLinkAttributes[$attribute]);
  352.         }
  353.         return $this;
  354.     }
  355.     /**
  356.      * Sets all custom link attributes as an associative array.
  357.      *
  358.      * This will overwrite previously set attributes. If you want to explicitly
  359.      * remove an auto-generated value from the results, set the respective
  360.      * attribute to null.
  361.      */
  362.     public function setLinkAttributes(array $attributes): self
  363.     {
  364.         foreach ($attributes as $key => $value) {
  365.             if (!\is_string($key) || !\is_string($value)) {
  366.                 throw new \InvalidArgumentException('Link attributes must be an array of type <string, string>.');
  367.             }
  368.         }
  369.         $this->additionalLinkAttributes $attributes;
  370.         return $this;
  371.     }
  372.     /**
  373.      * Sets the link href attribute.
  374.      *
  375.      * Set the value to null to use the auto-generated default.
  376.      */
  377.     public function setLinkHref(?string $url): self
  378.     {
  379.         $this->setLinkAttribute('href'$url);
  380.         return $this;
  381.     }
  382.     /**
  383.      * Sets a custom lightbox resource (file path or ImageInterface) or URL.
  384.      *
  385.      * By default, or if the argument is set to null, the image/target will be
  386.      * automatically determined from the metadata or base resource. For this
  387.      * setting to take effect, make sure you have enabled the creation of a
  388.      * lightbox by calling enableLightbox().
  389.      *
  390.      * @param string|ImageInterface|null $resourceOrUrl
  391.      */
  392.     public function setLightboxResourceOrUrl($resourceOrUrl): self
  393.     {
  394.         $this->lightboxResourceOrUrl $resourceOrUrl;
  395.         return $this;
  396.     }
  397.     /**
  398.      * Sets a size configuration that will be applied to the lightbox image.
  399.      *
  400.      * For this setting to take effect, make sure you have enabled the creation
  401.      * of a lightbox by calling enableLightbox().
  402.      *
  403.      * @param int|string|array|PictureConfiguration|null $size A picture size configuration or reference
  404.      */
  405.     public function setLightboxSize($size): self
  406.     {
  407.         $this->lightboxSizeConfiguration $size;
  408.         return $this;
  409.     }
  410.     /**
  411.      * Sets resize options for the lightbox image.
  412.      *
  413.      * By default, or if the argument is set to null, resize options are derived
  414.      * from predefined image sizes.
  415.      */
  416.     public function setLightboxResizeOptions(?ResizeOptions $resizeOptions): self
  417.     {
  418.         $this->lightboxResizeOptions $resizeOptions;
  419.         return $this;
  420.     }
  421.     /**
  422.      * Sets a custom lightbox group ID.
  423.      *
  424.      * By default, or if the argument is set to null, the ID will be empty. For
  425.      * this setting to take effect, make sure you have enabled the creation of
  426.      * a lightbox by calling enableLightbox().
  427.      */
  428.     public function setLightboxGroupIdentifier(?string $identifier): self
  429.     {
  430.         $this->lightboxGroupIdentifier $identifier;
  431.         return $this;
  432.     }
  433.     /**
  434.      * Enables the creation of a lightbox image (if possible) and/or
  435.      * outputting the respective link attributes.
  436.      *
  437.      * This setting is disabled by default.
  438.      */
  439.     public function enableLightbox(bool $enable true): self
  440.     {
  441.         $this->enableLightbox $enable;
  442.         return $this;
  443.     }
  444.     /**
  445.      * Sets all template options as an associative array.
  446.      */
  447.     public function setOptions(array $options): self
  448.     {
  449.         $this->options $options;
  450.         return $this;
  451.     }
  452.     /**
  453.      * Returns the last InvalidResourceException that was captured when setting
  454.      * resources or null if there was none.
  455.      */
  456.     public function getLastException(): ?InvalidResourceException
  457.     {
  458.         return $this->lastException;
  459.     }
  460.     /**
  461.      * @internal
  462.      */
  463.     public function setLastException(InvalidResourceException $exception): self
  464.     {
  465.         $this->lastException $exception;
  466.         return $this;
  467.     }
  468.     /**
  469.      * Creates a result object with the current settings, throws an exception
  470.      * if the currently defined resource is invalid.
  471.      *
  472.      * @throws InvalidResourceException
  473.      */
  474.     public function build(): Figure
  475.     {
  476.         if (null !== $this->lastException) {
  477.             throw $this->lastException;
  478.         }
  479.         return $this->doBuild();
  480.     }
  481.     /**
  482.      * Creates a result object with the current settings, returns null if the
  483.      * currently defined resource is invalid.
  484.      */
  485.     public function buildIfResourceExists(): ?Figure
  486.     {
  487.         if (null !== $this->lastException) {
  488.             return null;
  489.         }
  490.         $figure $this->doBuild();
  491.         try {
  492.             // Make sure the resource can be processed
  493.             $figure->getImage()->getOriginalDimensions();
  494.         } catch (\Throwable $e) {
  495.             $this->lastException = new InvalidResourceException(sprintf('The file "%s" could not be opened as an image.'$this->filePath), 0$e);
  496.             return null;
  497.         }
  498.         return $figure;
  499.     }
  500.     /**
  501.      * Creates a result object with the current settings.
  502.      */
  503.     private function doBuild(): Figure
  504.     {
  505.         if (null === $this->filePath) {
  506.             throw new \LogicException('You need to set a resource before building the result.');
  507.         }
  508.         // Freeze settings to allow reusing this builder object
  509.         $settings = clone $this;
  510.         $imageResult $this->locator
  511.             ->get('contao.image.studio')
  512.             ->createImage($settings->filePath$settings->sizeConfiguration$settings->resizeOptions)
  513.         ;
  514.         // Define the values via closure to make their evaluation lazy
  515.         return new Figure(
  516.             $imageResult,
  517.             \Closure::bind(
  518.                 function (Figure $figure): ?Metadata {
  519.                     $event = new FileMetadataEvent($this->onDefineMetadata());
  520.                     $this->locator->get('event_dispatcher')->dispatch($event);
  521.                     return $event->getMetadata();
  522.                 },
  523.                 $settings
  524.             ),
  525.             \Closure::bind(
  526.                 fn (Figure $figure): array => $this->onDefineLinkAttributes($figure),
  527.                 $settings
  528.             ),
  529.             \Closure::bind(
  530.                 fn (Figure $figure): ?LightboxResult => $this->onDefineLightboxResult($figure),
  531.                 $settings
  532.             ),
  533.             $settings->options
  534.         );
  535.     }
  536.     /**
  537.      * Defines metadata on demand.
  538.      */
  539.     private function onDefineMetadata(): ?Metadata
  540.     {
  541.         if ($this->disableMetadata) {
  542.             return null;
  543.         }
  544.         $getUuid = static function (?FilesModel $filesModel): ?string {
  545.             if (null === $filesModel || null === $filesModel->uuid) {
  546.                 return null;
  547.             }
  548.             // Normalize UUID to ASCII format
  549.             return Validator::isBinaryUuid($filesModel->uuid)
  550.                 ? StringUtil::binToUuid($filesModel->uuid)
  551.                 : $filesModel->uuid;
  552.         };
  553.         $fileReferenceData array_filter([Metadata::VALUE_UUID => $getUuid($this->filesModel)]);
  554.         if (null !== $this->metadata) {
  555.             return $this->metadata->with($fileReferenceData);
  556.         }
  557.         if (null === $this->filesModel) {
  558.             return null;
  559.         }
  560.         // Get fallback locale list or use without fallbacks if explicitly set
  561.         $locales null !== $this->locale ? [$this->locale] : $this->getFallbackLocaleList();
  562.         $metadata $this->filesModel->getMetadata(...$locales);
  563.         $overwriteMetadata $this->overwriteMetadata $this->overwriteMetadata->all() : [];
  564.         if (null !== $metadata) {
  565.             return $metadata
  566.                 ->with($fileReferenceData)
  567.                 ->with($overwriteMetadata)
  568.             ;
  569.         }
  570.         // If no metadata can be obtained from the model, we create a container
  571.         // from the default meta fields with empty values instead
  572.         $metaFields $this->getFilesModelAdapter()->getMetaFields();
  573.         $data array_merge(
  574.             array_combine($metaFieldsarray_fill(0\count($metaFields), '')),
  575.             $fileReferenceData
  576.         );
  577.         return (new Metadata($data))->with($overwriteMetadata);
  578.     }
  579.     /**
  580.      * Defines link attributes on demand.
  581.      */
  582.     private function onDefineLinkAttributes(Figure $result): array
  583.     {
  584.         $linkAttributes = [];
  585.         // Open in a new window if lightbox was requested but is invalid (fullsize)
  586.         if ($this->enableLightbox && !$result->hasLightbox()) {
  587.             $linkAttributes['target'] = '_blank';
  588.         }
  589.         return array_merge($linkAttributes$this->additionalLinkAttributes);
  590.     }
  591.     /**
  592.      * Defines the lightbox result (if enabled) on demand.
  593.      */
  594.     private function onDefineLightboxResult(Figure $result): ?LightboxResult
  595.     {
  596.         if (!$this->enableLightbox) {
  597.             return null;
  598.         }
  599.         $getMetadataUrl = static function () use ($result): ?string {
  600.             if (!$result->hasMetadata()) {
  601.                 return null;
  602.             }
  603.             return $result->getMetadata()->getUrl() ?: null;
  604.         };
  605.         /**
  606.          * @param ImageInterface|string $target Image object, URL or absolute file path
  607.          */
  608.         $getResourceOrUrl = function ($target): array {
  609.             if ($target instanceof ImageInterface) {
  610.                 return [$targetnull];
  611.             }
  612.             $validExtension \in_array(Path::getExtension($targettrue), $this->validExtensionstrue);
  613.             $externalUrl === preg_match('#^https?://#'$target);
  614.             if (!$validExtension) {
  615.                 return [nullnull];
  616.             }
  617.             if ($externalUrl) {
  618.                 return [null$target];
  619.             }
  620.             if (Path::isAbsolute($target)) {
  621.                 $filePath Path::canonicalize($target);
  622.             } else {
  623.                 // URL relative to the project directory
  624.                 $filePath Path::makeAbsolute(urldecode($target), $this->projectDir);
  625.             }
  626.             if (!is_file($filePath)) {
  627.                 $filePath null;
  628.             }
  629.             return [$filePathnull];
  630.         };
  631.         // Use explicitly set href (1) or lightbox resource (2), fall back to using metadata (3) or use the base resource (4) if empty
  632.         $lightboxResourceOrUrl $this->additionalLinkAttributes['href'] ?? $this->lightboxResourceOrUrl ?? $getMetadataUrl() ?? $this->filePath;
  633.         [$filePathOrImage$url] = $getResourceOrUrl($lightboxResourceOrUrl);
  634.         if (null === $filePathOrImage && null === $url) {
  635.             return null;
  636.         }
  637.         return $this->locator
  638.             ->get('contao.image.studio')
  639.             ->createLightboxImage(
  640.                 $filePathOrImage,
  641.                 $url,
  642.                 $this->lightboxSizeConfiguration,
  643.                 $this->lightboxGroupIdentifier,
  644.                 $this->lightboxResizeOptions
  645.             )
  646.         ;
  647.     }
  648.     /**
  649.      * @return FilesModel
  650.      *
  651.      * @phpstan-return Adapter<FilesModel>
  652.      */
  653.     private function getFilesModelAdapter(): Adapter
  654.     {
  655.         $framework $this->locator->get('contao.framework');
  656.         $framework->initialize();
  657.         return $framework->getAdapter(FilesModel::class);
  658.     }
  659.     /**
  660.      * @return Validator
  661.      *
  662.      * @phpstan-return Adapter<Validator>
  663.      */
  664.     private function getValidatorAdapter(): Adapter
  665.     {
  666.         $framework $this->locator->get('contao.framework');
  667.         $framework->initialize();
  668.         return $framework->getAdapter(Validator::class);
  669.     }
  670.     /**
  671.      * Returns a list of locales (if available) in the following order:
  672.      *  1. language of current page,
  673.      *  2. root page fallback language.
  674.      */
  675.     private function getFallbackLocaleList(): array
  676.     {
  677.         $page $GLOBALS['objPage'] ?? null;
  678.         if (!$page instanceof PageModel) {
  679.             return [];
  680.         }
  681.         $locales = [LocaleUtil::formatAsLocale($page->language)];
  682.         if (null !== $page->rootFallbackLanguage) {
  683.             $locales[] = LocaleUtil::formatAsLocale($page->rootFallbackLanguage);
  684.         }
  685.         return array_unique(array_filter($locales));
  686.     }
  687. }