diff --git a/.gitignore b/.gitignore index 747b70c..343d80a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ -# OS -.DS_Store -Thumbs.db - -# IDEs -.buildpath -.project -.settings/ -.build/ -.external*/ -.idea/ -nbproject/ - -# composer related -vendor/ -composer.lock +# OS +.DS_Store +Thumbs.db + +# IDEs +.buildpath +.project +.settings/ +.build/ +.external*/ +.idea/ +nbproject/ + +# composer related +vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml index 8eb9daa..ed27399 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,18 +11,17 @@ php: - "7.4" - "7.3" - "7.2" - - "7.1" env: - - CONTAO_VERSION=~4.8.0 - - CONTAO_VERSION=~4.4.0 + - CONTAO_VERSION='contao/core-bundle ~4.9.0' + - CONTAO_VERSION='contao/core-bundle ~4.10.0@dev' # Exclude impossible Contao Version combinations. matrix: fast_finish: true allow_failures: - - php: "7.4" - - env: CONTAO_VERSION=~4.8.0 + - php: "7.4" + - env: CONTAO_VERSION='contao/core-bundle ~4.10.0@dev' before_script: - echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini diff --git a/composer.json b/composer.json index 9a67b81..8bef536 100644 --- a/composer.json +++ b/composer.json @@ -26,16 +26,16 @@ "source":"https://github.com/contao-community-alliance/dc-general-contao-frontend" }, "require": { - "php":"^7.1", - "contao-community-alliance/dc-general": "^2.1.3", + "php":"^7.2", + "contao-community-alliance/dc-general": "^2.2", "contao-community-alliance/url-builder": "~1.1", - "contao-community-alliance/translator": "^2.1", - "contao/core-bundle": "^4.4", - "symfony/event-dispatcher": "^3.3 || ^4.0" + "contao-community-alliance/translator": "^2.2", + "contao/core-bundle": "^4.9", + "symfony/event-dispatcher": "4.4.*" }, "require-dev": { - "contao/manager-plugin": "^2.1", - "phpcq/all-tasks": "~1.1", + "contao/manager-plugin": "^2.8", + "phpcq/all-tasks": "~1.2", "phpmd/phpmd": "~2.8.2" }, "autoload": { @@ -46,8 +46,8 @@ "extra":{ "contao-manager-plugin": "ContaoCommunityAlliance\\DcGeneral\\ContaoFrontend\\ContaoManager\\Plugin", "branch-alias": { - "dev-master": "2.1.x-dev", - "dev-support/2.0": "2.0.x-dev" + "dev-support/2.1.x": "2.1.x-dev", + "dev-master": "2.2.x-dev" } }, "config": { diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php new file mode 100644 index 0000000..a0d22c0 --- /dev/null +++ b/src/Resources/contao/config/config.php @@ -0,0 +1,23 @@ + + * @copyright 2016-2019 Contao Community Alliance. + * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +use ContaoCommunityAlliance\DcGeneral\ContaoFrontend\Widgets\UploadOnSteroids; + +// Front end form widgets +$GLOBALS['TL_FFL']['uploadOnSteroids'] = UploadOnSteroids::class; diff --git a/src/Resources/contao/templates/widgets/form_upload-on-steroids.html5 b/src/Resources/contao/templates/widgets/form_upload-on-steroids.html5 new file mode 100644 index 0000000..9d13cca --- /dev/null +++ b/src/Resources/contao/templates/widgets/form_upload-on-steroids.html5 @@ -0,0 +1,55 @@ + +extend('form_row'); ?> + +block('label'); ?> +label): ?> + + + +files): ?> +
+ +
+ +endblock(); ?> + +block('field'); ?> + hasErrors()): ?> +

getErrorAsString() ?>

+ + + getAttributes() ?>> +endblock(); ?> diff --git a/src/View/ActionHandler/CopyHandler.php b/src/View/ActionHandler/CopyHandler.php index cef71a3..ee138f9 100644 --- a/src/View/ActionHandler/CopyHandler.php +++ b/src/View/ActionHandler/CopyHandler.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general-contao-frontend. * - * (c) 2015-2018 Contao Community Alliance. + * (c) 2015-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,13 +12,15 @@ * * @package contao-community-alliance/dc-general-contao-frontend * @author Richard Henkenjohann - * @copyright 2015-2018 Contao Community Alliance. + * @author Ingolf Steinhardt + * @copyright 2015-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE LGPL-3.0 * @filesource */ namespace ContaoCommunityAlliance\DcGeneral\ContaoFrontend\View\ActionHandler; +use Contao\CoreBundle\Exception\PageNotFoundException; use Contao\CoreBundle\Exception\RedirectResponseException; use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminator; use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminatorAwareTrait; @@ -122,7 +124,12 @@ public function process(EnvironmentInterface $environment): void $dataProvider = $environment->getDataProvider(); $model = $dataProvider->fetch($dataProvider->getEmptyConfig()->setId($modelId->getId())); - $copyModel = $environment->getController()->createClonedModel($model); + + if (null === $model) { + throw new PageNotFoundException('Model not found: ' . $modelId->getSerialized()); + } + + $copyModel = $environment->getController()->createClonedModel($model); // Dispatch pre duplicate event. $copyEvent = new PreDuplicateModelEvent($environment, $copyModel, $model); diff --git a/src/View/ActionHandler/DeleteHandler.php b/src/View/ActionHandler/DeleteHandler.php index a26cce3..79e214e 100644 --- a/src/View/ActionHandler/DeleteHandler.php +++ b/src/View/ActionHandler/DeleteHandler.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general-contao-frontend. * - * (c) 2015-2018 Contao Community Alliance. + * (c) 2015-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,13 +12,15 @@ * * @package contao-community-alliance/dc-general-contao-frontend * @author Richard Henkenjohann - * @copyright 2015-2018 Contao Community Alliance. + * @author Ingolf Steinhardt + * @copyright 2015-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE LGPL-3.0 * @filesource */ namespace ContaoCommunityAlliance\DcGeneral\ContaoFrontend\View\ActionHandler; +use Contao\CoreBundle\Exception\PageNotFoundException; use Contao\CoreBundle\Exception\RedirectResponseException; use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminator; use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminatorAwareTrait; @@ -115,6 +117,10 @@ public function process(EnvironmentInterface $environment): void $dataProvider = $environment->getDataProvider(); $model = $dataProvider->fetch($dataProvider->getEmptyConfig()->setId($modelId->getId())); + if (null === $model) { + throw new PageNotFoundException('Model not found: ' . $modelId->getSerialized()); + } + // Trigger event before the model will be deleted. $event = new PreDeleteModelEvent($environment, $model); $environment->getEventDispatcher()->dispatch($event::NAME, $event); diff --git a/src/View/ActionHandler/EditHandler.php b/src/View/ActionHandler/EditHandler.php index 0c86e26..0527acc 100644 --- a/src/View/ActionHandler/EditHandler.php +++ b/src/View/ActionHandler/EditHandler.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general-contao-frontend. * - * (c) 2015-2020 Contao Community Alliance. + * (c) 2015-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,8 @@ * @author Christian Schiffler * @author Richard Henkenjohann * @author Sven Baumann - * @copyright 2015-2020 Contao Community Alliance. + * @author Ingolf Steinhardt + * @copyright 2015-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE LGPL-3.0 * @filesource */ @@ -103,20 +104,23 @@ public function process(EnvironmentInterface $environment) if (!$basicDefinition->isEditable()) { throw new NotEditableException('DataContainer ' . $definition->getName() . ' is not editable'); } + // We only support flat tables, sorry. if (BasicDefinitionInterface::MODE_HIERARCHICAL === $basicDefinition->getMode()) { return false; } + $modelId = ModelId::fromSerialized($environment->getInputProvider()->getParameter('id')); $dataProvider = $environment->getDataProvider(); $model = $dataProvider->fetch($dataProvider->getEmptyConfig()->setId($modelId->getId())); - $clone = clone $model; if (null === $model) { throw new PageNotFoundException('Model not found: ' . $modelId->getSerialized()); } + $clone = clone $model; + return (new EditMask($environment, $model, $clone, null, null))->execute(); } } diff --git a/src/View/EditMask.php b/src/View/EditMask.php index d9863c2..f713d99 100644 --- a/src/View/EditMask.php +++ b/src/View/EditMask.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general-contao-frontend. * - * (c) 2015-2018 Contao Community Alliance. + * (c) 2015-2020 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,7 +13,9 @@ * @package contao-community-alliance/dc-general-contao-frontend * @author Christian Schiffler * @author Richard Henkenjohann - * @copyright 2015-2018 Contao Community Alliance. + * @author Sven Baumann + * @author Ingolf Steinhardt + * @copyright 2015-2020 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE LGPL-3.0 * @filesource */ @@ -194,7 +196,8 @@ public function execute() 'table' => $this->definition->getName(), 'enctype' => 'multipart/form-data', 'error' => $this->errors, - 'editButtons' => $buttons + 'editButtons' => $buttons, + 'model' => $this->model ] ); @@ -315,17 +318,25 @@ private function translateLabel($transString, $parameters = []) */ private function getEditButtons() { + $button = ''; $buttons = []; $buttons['save'] = sprintf( - '', + $button, + 'save', + 'save', + 'save', + 's', $this->translateLabel('save') ); if ($this->definition->getBasicDefinition()->isCreatable()) { $buttons['saveNcreate'] = sprintf( - '', + $button, + 'saveNcreate', + 'saveNcreate', + 'saveNcreate', + 'n', $this->translateLabel('saveNcreate') ); } diff --git a/src/Widgets/UploadOnSteroids.php b/src/Widgets/UploadOnSteroids.php new file mode 100644 index 0000000..36f9509 --- /dev/null +++ b/src/Widgets/UploadOnSteroids.php @@ -0,0 +1,623 @@ + + * @copyright 2016-2019 Contao Community Alliance. + * @license https://github.com/contao-community-alliance/dc-general-contao-frontend/blob/master/LICENSE + * LGPL-3.0-or-later + * @filesource + */ + +namespace ContaoCommunityAlliance\DcGeneral\ContaoFrontend\Widgets; + +use Contao\CoreBundle\Framework\Adapter; +use Contao\Dbafs; +use Contao\FilesModel; +use Contao\FormFileUpload; +use Contao\Input; +use Contao\StringUtil; +use Doctrine\DBAL\Connection; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This is the widget is for upload a file in the frontend editing scope. + * It has the following functions: + * - Saves the uploaded file in the configured directory + * - Can be reset from the model + * - Can delete the file from disk space + * - Can add a default image + * - Can add a default image + * - Output the Image as Thumbnail + * - Normalize the extend folder (StringUtil::generateAlias) + * - Can prefix and postfix the filename. + * + * @property boolean deselect + * @property boolean delete + * @property string extendFolder + * @property boolean normalizeExtendFolder + * @property boolean normalizeFilename + * @property string prefixFilename + * @property string postfixFilename + * @property array files + * @property boolean showThumbnail + * @property boolean multiple + */ +class UploadOnSteroids extends FormFileUpload +{ + /** + * Submit indicator + * + * @var boolean + */ + protected $blnSubmitInput = true; + + /** + * Template + * + * @var string + */ + protected $strTemplate = 'form_upload-on-steroids'; + + /** + * Template + * + * @var string + */ + protected $strPrefix = 'widget widget-upload widget-upload-on-steroids'; + + /** + * The translator + * + * @var TranslatorInterface + */ + protected $translator; + + /** + * The input provider; + * + * @var Adapter|Input + */ + protected $inputProvider; + + /** + * The file model. + * + * @var Adapter|FilesModel + */ + private $filesModel; + + /** + * The filesystem. + * + * @var Filesystem + */ + private $filesystem; + /** + * The string util. + * + * @var StringUtil + */ + private $stringUtil; + + public function __construct($attributes = null) + { + parent::__construct($attributes); + } + + /** + * {@inheritDoc} + */ + public function __set($key, $value) + { + if (\in_array( + $key, + [ + 'deselect', + 'delete', + 'extendFolder', + 'normalizeExtendFolder', + 'normalizeFilename', + 'prefixFilename', + 'postfixFilename', + 'files', + 'showThumbnail', + 'multiple' + ] + )) { + $this->arrConfiguration[$key] = $value; + + return; + } + + parent::__set($key, $value); + } + + /** + * {@inheritDoc} + */ + public function parse($attributes = null) + { + $this->addIsDeletable(); + $this->addIsDeselectable(); + $this->addIsMultiple(); + $this->addShowThumbnail(); + $this->addFiles(); + + $this->value = \implode(',', \array_map('\Contao\StringUtil::binToUuid', (array) $this->value)); + + return parent::parse($attributes); + } + + /** + * Parse the filename. + * + * @param string $filename The filename. + * + * @return string + */ + public function parseFilename(string $filename): string + { + if (empty($filename) || !\is_string($filename)) { + return $filename; + } + + return $this->normalizeFilename($this->preOrPostFixFilename($filename)); + } + + /** + * {@inheritDoc} + */ + public function validate() + { + $inputName = $this->name; + + if ($this->normalizeExtendFolder) { + $this->extendFolder = $this->str()->generateAlias($this->extendFolder); + } + + if ($this->extendFolder) { + $uploadFolder = $this->filesModel()->findByUuid($this->uploadFolder); + $uploadFolderPath = $uploadFolder->path . DIRECTORY_SEPARATOR . $this->extendFolder; + + + $newUploadFolder = null; + if (!$this->filesystem()->exists($uploadFolderPath)) { + $this->filesystem()->mkdir($uploadFolderPath); + $newUploadFolder = Dbafs::addResource($uploadFolderPath); + } + + if (!$newUploadFolder) { + $newUploadFolder = $this->filesModel()->findByPath($uploadFolderPath); + } + + $this->uploadFolder = $newUploadFolder->uuid; + } + + $this->validateSingleUpload(); + $this->validateMultipleUpload(); + $this->deselectFile($inputName); + $this->deleteFile($inputName); + } + + /** + * Validate single upload widget. + * + * @return void + */ + private function validateSingleUpload(): void + { + if ($this->multiple || $this->hasErrors()) { + return; + } + + $inputName = $this->name; + $_FILES[$inputName]['name'] = $this->parseFilename($_FILES[$inputName]['name']); + + parent::validate(); + + if (!isset($_SESSION['FILES'][$inputName]) || $this->hasErrors()) { + return; + } + + $file = $_SESSION['FILES'][$inputName]; + if (!isset($file['uuid'])) { + return; + } + + $this->value = StringUtil::uuidToBin($file['uuid']); + } + + /** + * Validate multiple upload widget. + * + * @return void + */ + private function validateMultipleUpload(): void + { + if (!$this->multiple || $this->hasErrors()) { + return; + } + + $inputName = $this->name; + $values = \array_map('\Contao\StringUtil::binToUuid', $this->value); + + $files = []; + $inputFiles = $this->getMultipleUploadedFiles(); + foreach ($inputFiles as $inputFile) { + $_FILES[$inputName] = $inputFile; + + $_FILES[$inputName]['name'] = $this->parseFilename($_FILES[$inputName]['name']); + + parent::validate(); + + if (!isset($_SESSION['FILES'][$inputName]) || $this->hasErrors()) { + return; + } + + $file = $_SESSION['FILES'][$inputName]; + if (!isset($file['uuid'])) { + return; + } + + $files[] = $file; + + unset($_SESSION['FILES']); + } + + if (!\count($files)) { + return; + } + + $setValues = \array_values(\array_unique(\array_merge($values, \array_column($files, 'uuid')))); + + $this->value = \array_map('\Contao\StringUtil::uuidToBin', $setValues); + } + + /** + * Get the multiple uploaded files. + * + * @return array + */ + private function getMultipleUploadedFiles(): array + { + if (!isset($_FILES[$this->name])) { + return []; + } + + $files = []; + foreach ($_FILES[$this->name] as $propertyName => $values) { + foreach ($values as $key => $value) { + $files[$key][$propertyName] = $value; + } + } + + return $files; + } + + /** + * Deselect the file, if is mark for deselect. + * + * @param string $inputName The input nanme. + * + * @return void + */ + private function deselectFile(string $inputName) + { + if (!$this->deselect + || $this->hasErrors() + || !($post = $this->inputProvider()->post($inputName)) + || !isset($post['reset'][0]) + ) { + return; + } + + if (!$this->multiple && (StringUtil::binToUuid($this->value) === $post['reset'][0])) { + $this->value = ''; + + return; + } + + $values = \array_map('\Contao\StringUtil::binToUuid', $this->value); + $diffValues = \array_values(\array_diff($values, $post['reset'])); + + $this->value = \array_map('\Contao\StringUtil::uuidToBin', $diffValues); + } + + /** + * Delete the file, if is mark for delete. + * + * @param string $inputName The input nanme. + * + * @return void + */ + private function deleteFile(string $inputName) + { + if (!$this->delete + || $this->hasErrors() + || !($post = $this->inputProvider()->post($inputName)) + || !isset($post['delete'][0]) + ) { + return; + } + + if (!$this->multiple && (StringUtil::binToUuid($this->value) === $post['delete'][0])) { + $this->value = ''; + + $file = $this->filesModel()->findByUuid($this->value); + if ($file) { + $this->filesystem->remove($file->path); + $file->delete(); + } + + return; + } + + $values = \array_map('\Contao\StringUtil::binToUuid', $this->value); + $diffValues = \array_values(\array_diff($values, $post['delete'])); + + foreach ($post['delete'] as $delete) { + $file = $this->filesModel()->findByUuid(StringUtil::uuidToBin($delete)); + if (!$file) { + continue; + } + + $this->filesystem()->remove($file->path); + $file->delete(); + } + + $this->value = \array_map('\Contao\StringUtil::uuidToBin', $diffValues); + } + + /** + * Normalize the filename. + * + * @param array $file The file information. + * + * @return void + */ + /** + * Normalize the filename. + * + * @param string $filename The filename. + * + * @return string + */ + private function normalizeFilename(string $filename): string + { + if (!$this->normalizeFilename) { + return $filename; + } + + $fileInfo = \pathinfo($filename); + + $currentExtension = $fileInfo['extension']; + $normalizeExtension = $this->stringUtil()->generateAlias($currentExtension); + + $currentFilename = $fileInfo['filename']; + $normalizeFilename = $this->stringUtil()->generateAlias($currentFilename); + + return $normalizeFilename . '.' . $normalizeExtension; + } + + /** + * Prefix or postfix the filename. + * + * @param string $filename The filename + * + * @return string + */ + private function preOrPostFixFilename(string $filename): string + { + if (!($this->prefixFilename || $this->postfixFilename)) { + return $filename; + } + + $fileInfo = \pathinfo($filename); + + $extension = $fileInfo['extension']; + + $currentFilename = $fileInfo['filename']; + $extendFilename = ($this->prefixFilename ?: '') . + 'place-holder-extend-filename' . + ($this->postfixFilename ?: ''); + if ($this->normalizeFilename) { + $extendFilename = $this->stringUtil()->generateAlias($extendFilename); + } + $extendFilename = \str_replace('place-holder-extend-filename', $currentFilename, $extendFilename); + + return $extendFilename . '.' . $extension; + } + + /** + * Add the files from the value. + * + * @return void + */ + private function addFiles(): void + { + if (empty($this->value)) { + $this->files = null; + return; + } + + /** @var Connection $connection */ + $connection = self::getContainer()->get('database_connection'); + + $platform = $connection->getDatabasePlatform(); + + $builder = $connection->createQueryBuilder(); + $builder + ->select( + $platform->quoteIdentifier('id'), + $platform->quoteIdentifier('pid'), + $platform->quoteIdentifier('uuid'), + $platform->quoteIdentifier('type'), + $platform->quoteIdentifier('path'), + $platform->quoteIdentifier('extension'), + $platform->quoteIdentifier('hash'), + $platform->quoteIdentifier('found'), + $platform->quoteIdentifier('name'), + $platform->quoteIdentifier('importantPartX'), + $platform->quoteIdentifier('importantPartY'), + $platform->quoteIdentifier('importantPartWidth'), + $platform->quoteIdentifier('importantPartHeight'), + $platform->quoteIdentifier('meta') + ) + ->from($platform->quoteIdentifier('tl_files')) + ->where($builder->expr()->in($platform->quoteIdentifier('uuid'), ':uuids')) + ->setParameter(':uuids', (array) $this->value, Connection::PARAM_STR_ARRAY); + + $statement = $builder->execute(); + if (!$statement->rowCount()) { + return; + } + + $this->files = $statement->fetchAll(\PDO::FETCH_OBJ); + } + + /** + * Translate. + * + * @param string $transId The message id (may also be an object that can be cast to string) + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default + * + * @return string + */ + public function trans( + string $transId, + array $parameters = [], + ?string $domain = 'contao_default', + ?string $locale = null + ) { + return $this->translator()->trans($transId, $parameters, $domain, $locale); + } + + /** + * Add file is deletable. + * + * @return void + */ + private function addIsDeletable(): void + { + $this->prefix .= $this->delete ? ' is-deletable' : ''; + } + + /** + * Add file is deselectable. + * + * @return void + */ + private function addIsDeselectable(): void + { + $this->prefix .= $this->deselect ? ' is-deselectable' : ''; + } + + /** + * Add file show thumbnail. + * + * @return void + */ + private function addShowThumbnail(): void + { + $this->prefix .= $this->showThumbnail ? ' show-thumbnail' : ''; + } + + /** + * Add the upload file is multiple. + * + * @return void + */ + private function addIsMultiple(): void + { + if (!$this->multiple) { + return; + } + + $this->prefix .= $this->multiple ? ' is-multiple' : ''; + + $this->addAttribute('multiple', 'multiple'); + } + + /** + * Get the input provider. + * + * @return Adapter|Input + */ + private function inputProvider(): Adapter + { + if (!$this->inputProvider) { + $this->inputProvider = self::getContainer()->get('contao.framework')->getAdapter(Input::class); + } + + return $this->inputProvider; + } + + /** + * Get the files model. + * + * @return Adapter|FilesModel + */ + private function filesModel(): Adapter + { + if (!$this->filesModel) { + $this->filesModel = self::getContainer()->get('contao.framework')->getAdapter(FilesModel::class); + } + + return $this->filesModel; + } + + /** + * Get the filesystem. + * + * @return Filesystem + */ + private function filesystem(): Filesystem + { + if (!$this->filesystem) { + $this->filesystem = self::getContainer()->get('filesystem'); + } + + return $this->filesystem; + } + + /** + * Get the string util. + * + * @return Adapter|StringUtil + */ + private function stringUtil(): Adapter + { + if (!$this->stringUtil) { + $this->stringUtil = self::getContainer()->get('contao.framework')->getAdapter(StringUtil::class); + } + + return $this->stringUtil; + } + + /** + * Get the translator. + * + * @return TranslatorInterface + */ + private function translator(): TranslatorInterface + { + if (!$this->filesystem) { + $this->filesystem = self::getContainer()->get('translator'); + } + + return $this->filesystem; + } +}