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

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.      * Creates a result object with the current settings, throws an exception
  462.      * if the currently defined resource is invalid.
  463.      *
  464.      * @throws InvalidResourceException
  465.      */
  466.     public function build(): Figure
  467.     {
  468.         if (null !== $this->lastException) {
  469.             throw $this->lastException;
  470.         }
  471.         return $this->doBuild();
  472.     }
  473.     /**
  474.      * Creates a result object with the current settings, returns null if the
  475.      * currently defined resource is invalid.
  476.      */
  477.     public function buildIfResourceExists(): ?Figure
  478.     {
  479.         if (null !== $this->lastException) {
  480.             return null;
  481.         }
  482.         $figure $this->doBuild();
  483.         try {
  484.             // Make sure the resource can be processed
  485.             $figure->getImage()->getOriginalDimensions();
  486.         } catch (\Throwable $e) {
  487.             $this->lastException = new InvalidResourceException(sprintf('The file "%s" could not be opened as an image.'$this->filePath), 0$e);
  488.             return null;
  489.         }
  490.         return $figure;
  491.     }
  492.     /**
  493.      * Creates a result object with the current settings.
  494.      */
  495.     private function doBuild(): Figure
  496.     {
  497.         if (null === $this->filePath) {
  498.             throw new \LogicException('You need to set a resource before building the result.');
  499.         }
  500.         // Freeze settings to allow reusing this builder object
  501.         $settings = clone $this;
  502.         $imageResult $this->locator
  503.             ->get('contao.image.studio')
  504.             ->createImage($settings->filePath$settings->sizeConfiguration$settings->resizeOptions)
  505.         ;
  506.         // Define the values via closure to make their evaluation lazy
  507.         return new Figure(
  508.             $imageResult,
  509.             \Closure::bind(
  510.                 function (Figure $figure): ?Metadata {
  511.                     $event = new FileMetadataEvent($this->onDefineMetadata());
  512.                     $this->locator->get('event_dispatcher')->dispatch($event);
  513.                     return $event->getMetadata();
  514.                 },
  515.                 $settings
  516.             ),
  517.             \Closure::bind(
  518.                 fn (Figure $figure): array => $this->onDefineLinkAttributes($figure),
  519.                 $settings
  520.             ),
  521.             \Closure::bind(
  522.                 fn (Figure $figure): ?LightboxResult => $this->onDefineLightboxResult($figure),
  523.                 $settings
  524.             ),
  525.             $settings->options
  526.         );
  527.     }
  528.     /**
  529.      * Defines metadata on demand.
  530.      */
  531.     private function onDefineMetadata(): ?Metadata
  532.     {
  533.         if ($this->disableMetadata) {
  534.             return null;
  535.         }
  536.         $getUuid = static function (?FilesModel $filesModel): ?string {
  537.             if (null === $filesModel || null === $filesModel->uuid) {
  538.                 return null;
  539.             }
  540.             // Normalize UUID to ASCII format
  541.             return Validator::isBinaryUuid($filesModel->uuid)
  542.                 ? StringUtil::binToUuid($filesModel->uuid)
  543.                 : $filesModel->uuid;
  544.         };
  545.         $fileReferenceData array_filter([Metadata::VALUE_UUID => $getUuid($this->filesModel)]);
  546.         if (null !== $this->metadata) {
  547.             return $this->metadata->with($fileReferenceData);
  548.         }
  549.         if (null === $this->filesModel) {
  550.             return null;
  551.         }
  552.         // Get fallback locale list or use without fallbacks if explicitly set
  553.         $locales null !== $this->locale ? [$this->locale] : $this->getFallbackLocaleList();
  554.         $metadata $this->filesModel->getMetadata(...$locales);
  555.         $overwriteMetadata $this->overwriteMetadata $this->overwriteMetadata->all() : [];
  556.         if (null !== $metadata) {
  557.             return $metadata
  558.                 ->with($fileReferenceData)
  559.                 ->with($overwriteMetadata)
  560.             ;
  561.         }
  562.         // If no metadata can be obtained from the model, we create a container
  563.         // from the default meta fields with empty values instead
  564.         $metaFields $this->getFilesModelAdapter()->getMetaFields();
  565.         $data array_merge(
  566.             array_combine($metaFieldsarray_fill(0\count($metaFields), '')),
  567.             $fileReferenceData
  568.         );
  569.         return (new Metadata($data))->with($overwriteMetadata);
  570.     }
  571.     /**
  572.      * Defines link attributes on demand.
  573.      */
  574.     private function onDefineLinkAttributes(Figure $result): array
  575.     {
  576.         $linkAttributes = [];
  577.         // Open in a new window if lightbox was requested but is invalid (fullsize)
  578.         if ($this->enableLightbox && !$result->hasLightbox()) {
  579.             $linkAttributes['target'] = '_blank';
  580.         }
  581.         return array_merge($linkAttributes$this->additionalLinkAttributes);
  582.     }
  583.     /**
  584.      * Defines the lightbox result (if enabled) on demand.
  585.      */
  586.     private function onDefineLightboxResult(Figure $result): ?LightboxResult
  587.     {
  588.         if (!$this->enableLightbox) {
  589.             return null;
  590.         }
  591.         $getMetadataUrl = static function () use ($result): ?string {
  592.             if (!$result->hasMetadata()) {
  593.                 return null;
  594.             }
  595.             return $result->getMetadata()->getUrl() ?: null;
  596.         };
  597.         /**
  598.          * @param ImageInterface|string $target Image object, URL or absolute file path
  599.          */
  600.         $getResourceOrUrl = function ($target): array {
  601.             if ($target instanceof ImageInterface) {
  602.                 return [$targetnull];
  603.             }
  604.             $validExtension \in_array(Path::getExtension($targettrue), $this->validExtensionstrue);
  605.             $externalUrl === preg_match('#^https?://#'$target);
  606.             if (!$validExtension) {
  607.                 return [nullnull];
  608.             }
  609.             if ($externalUrl) {
  610.                 return [null$target];
  611.             }
  612.             if (Path::isAbsolute($target)) {
  613.                 $filePath Path::canonicalize($target);
  614.             } else {
  615.                 // URL relative to the project directory
  616.                 $filePath Path::makeAbsolute(urldecode($target), $this->projectDir);
  617.             }
  618.             if (!is_file($filePath)) {
  619.                 $filePath null;
  620.             }
  621.             return [$filePathnull];
  622.         };
  623.         // Use explicitly set data (1), fall back to using metadata (2) or use the base resource (3) if empty
  624.         $lightboxResourceOrUrl $this->lightboxResourceOrUrl ?? $getMetadataUrl() ?? $this->filePath;
  625.         [$filePathOrImage$url] = $getResourceOrUrl($lightboxResourceOrUrl);
  626.         if (null === $filePathOrImage && null === $url) {
  627.             return null;
  628.         }
  629.         return $this->locator
  630.             ->get('contao.image.studio')
  631.             ->createLightboxImage(
  632.                 $filePathOrImage,
  633.                 $url,
  634.                 $this->lightboxSizeConfiguration,
  635.                 $this->lightboxGroupIdentifier,
  636.                 $this->lightboxResizeOptions
  637.             )
  638.         ;
  639.     }
  640.     /**
  641.      * @return FilesModel
  642.      *
  643.      * @phpstan-return Adapter<FilesModel>
  644.      */
  645.     private function getFilesModelAdapter(): Adapter
  646.     {
  647.         $framework $this->locator->get('contao.framework');
  648.         $framework->initialize();
  649.         return $framework->getAdapter(FilesModel::class);
  650.     }
  651.     /**
  652.      * @return Validator
  653.      *
  654.      * @phpstan-return Adapter<Validator>
  655.      */
  656.     private function getValidatorAdapter(): Adapter
  657.     {
  658.         $framework $this->locator->get('contao.framework');
  659.         $framework->initialize();
  660.         return $framework->getAdapter(Validator::class);
  661.     }
  662.     /**
  663.      * Returns a list of locales (if available) in the following order:
  664.      *  1. language of current page,
  665.      *  2. root page fallback language.
  666.      */
  667.     private function getFallbackLocaleList(): array
  668.     {
  669.         $page $GLOBALS['objPage'] ?? null;
  670.         if (!$page instanceof PageModel) {
  671.             return [];
  672.         }
  673.         $locales = [LocaleUtil::formatAsLocale($page->language)];
  674.         if (null !== $page->rootFallbackLanguage) {
  675.             $locales[] = LocaleUtil::formatAsLocale($page->rootFallbackLanguage);
  676.         }
  677.         return array_unique(array_filter($locales));
  678.     }
  679. }