TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Core\Site\SiteAwareInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Factory for creating aspects
|
||||
*/
|
||||
class AspectFactory
|
||||
{
|
||||
/**
|
||||
* Create aspects from the given settings.
|
||||
*
|
||||
* @return AspectInterface[]
|
||||
*/
|
||||
public function createAspects(array $aspects, SiteLanguage $language, Site $site): array
|
||||
{
|
||||
$aspects = array_map(
|
||||
function ($settings) use ($language, $site) {
|
||||
$type = (string)($settings['type'] ?? '');
|
||||
$aspect = $this->create($type, $settings);
|
||||
return $this->enrich($aspect, $language, $site);
|
||||
},
|
||||
$aspects
|
||||
);
|
||||
uasort($aspects, [$this, 'sortAspects']);
|
||||
return $aspects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an aspect
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
protected function create(string $type, array $settings): AspectInterface
|
||||
{
|
||||
if (empty($type)) {
|
||||
throw new \InvalidArgumentException('Aspect type cannot be empty', 1538079481);
|
||||
}
|
||||
if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects'][$type])) {
|
||||
throw new \OutOfRangeException(sprintf('No aspect found for %s', $type), 1538079482);
|
||||
}
|
||||
unset($settings['type']);
|
||||
$className = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects'][$type];
|
||||
return GeneralUtility::makeInstance($className, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the language aware trait, and adds the site language.
|
||||
*/
|
||||
protected function enrich(AspectInterface $aspect, SiteLanguage $language, Site $site): AspectInterface
|
||||
{
|
||||
if ($aspect instanceof SiteLanguageAwareInterface) {
|
||||
$aspect->setSiteLanguage($language);
|
||||
}
|
||||
if ($aspect instanceof SiteAwareInterface) {
|
||||
$aspect->setSite($site);
|
||||
}
|
||||
return $aspect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts aspects with putting persisted aspects to the end, thus
|
||||
* non-persisted aspects can be executed earlier without invoking database.
|
||||
*/
|
||||
protected function sortAspects(AspectInterface $first, AspectInterface $second): int
|
||||
{
|
||||
// when first is persisted, move it to the end (>0)
|
||||
$first = $first instanceof PersistedMappableAspectInterface ? 1 : 0;
|
||||
// when second is persisted, move it to the beginning (<0)
|
||||
$second = $second instanceof PersistedMappableAspectInterface ? -1 : 0;
|
||||
// 0 + 0 = 0 - both are non-persisted
|
||||
// 1 - 1 = 0 - both are persisted
|
||||
// 1 + 0 = 1 - only first is persisted
|
||||
// 0 - 1 = -1 - only second is persisted
|
||||
return $first + $second;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Base interface for all aspects
|
||||
*/
|
||||
interface AspectInterface {}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
trait AspectTrait
|
||||
{
|
||||
protected function isSlugUniqueInSite(string $tableName, string $fieldName): bool
|
||||
{
|
||||
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($tableName);
|
||||
if (!$schema->hasField($fieldName)) {
|
||||
return false;
|
||||
}
|
||||
$fieldType = $schema->getField($fieldName);
|
||||
return
|
||||
$fieldType->isType(TableColumnType::SLUG)
|
||||
&& GeneralUtility::inList($fieldType->getConfiguration()['eval'] ?? '', 'uniqueInSite');
|
||||
}
|
||||
|
||||
protected function hasSlugUniqueInSite(string $tableName, string ...$fieldNames): bool
|
||||
{
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
if ($this->isSlugUniqueInSite($tableName, $fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
|
||||
|
||||
/**
|
||||
* Locale modifier to be used to modify routePath directly.
|
||||
*
|
||||
* Example:
|
||||
* routeEnhancers:
|
||||
* Blog:
|
||||
* type: Extbase
|
||||
* extension: BlogExample
|
||||
* plugin: Pi1
|
||||
* routes:
|
||||
* - { routePath: '/{list_label}/{paging_widget}', _controller: 'BlogExample::list', _arguments: {'paging_widget': '@widget_0/currentPage'}}
|
||||
* defaultController: 'BlogExample::list'
|
||||
* requirements:
|
||||
* paging_widget: '\d+'
|
||||
* aspects:
|
||||
* list_label:
|
||||
* type: LocaleModifier
|
||||
* default: 'list'
|
||||
* localeMap:
|
||||
* - locale: 'en_US.*|en_GB.*'
|
||||
* value: 'overview'
|
||||
* - locale: 'fr_FR'
|
||||
* value: 'liste'
|
||||
* - locale: 'de_.*'
|
||||
* value: 'übersicht'
|
||||
*/
|
||||
class LocaleModifier implements ModifiableAspectInterface, SiteLanguageAwareInterface
|
||||
{
|
||||
use SiteLanguageAwareTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $localeMap;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $default;
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$localeMap = $settings['localeMap'] ?? null;
|
||||
$default = $settings['default'] ?? null;
|
||||
|
||||
if (!is_array($localeMap)) {
|
||||
throw new \InvalidArgumentException('localeMap must be array', 1537277153);
|
||||
}
|
||||
if (!is_string($default ?? '')) {
|
||||
throw new \InvalidArgumentException('default must be string', 1537277154);
|
||||
}
|
||||
|
||||
$this->settings = $settings;
|
||||
$this->localeMap = $localeMap;
|
||||
$this->default = $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function modify(): ?string
|
||||
{
|
||||
$locale = (string)$this->siteLanguage->getLocale();
|
||||
foreach ($this->localeMap as $item) {
|
||||
$pattern = '#^' . str_replace('_', '-', $item['locale']) . '#i';
|
||||
if (preg_match($pattern, $locale)) {
|
||||
return (string)$item['value'];
|
||||
}
|
||||
}
|
||||
return $this->default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Aspects that have a mapping table (either static, or in the database).
|
||||
*/
|
||||
interface MappableAspectInterface extends AspectInterface
|
||||
{
|
||||
public function generate(string $value): ?string;
|
||||
|
||||
public function resolve(string $value): ?string;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\Route;
|
||||
|
||||
/**
|
||||
* Helper class for resolving all aspects that are mappable.
|
||||
*/
|
||||
class MappableProcessor
|
||||
{
|
||||
public function resolve(Route $route, array &$attributes): bool
|
||||
{
|
||||
$mappers = $this->fetchMappers($route, $attributes);
|
||||
if (empty($mappers)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach ($mappers as $variableName => $mapper) {
|
||||
$value = $mapper->resolve(
|
||||
(string)($attributes[$variableName] ?? '')
|
||||
);
|
||||
if ($value === null) {
|
||||
if (!$mapper instanceof UnresolvedValueInterface || !$mapper->hasFallbackValue()) {
|
||||
return false;
|
||||
}
|
||||
$value = $mapper->getFallbackValue();
|
||||
}
|
||||
$values[$variableName] = $value;
|
||||
}
|
||||
|
||||
$attributes = array_merge($attributes, $values);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function generate(Route $route, array &$attributes): bool
|
||||
{
|
||||
$mappers = $this->fetchMappers($route, $attributes);
|
||||
if (empty($mappers)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach ($mappers as $variableName => $mapper) {
|
||||
$value = $mapper->generate(
|
||||
(string)($attributes[$variableName] ?? '')
|
||||
);
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
$values[$variableName] = $value;
|
||||
}
|
||||
|
||||
$attributes = array_merge($attributes, $values);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MappableAspectInterface[]
|
||||
*/
|
||||
protected function fetchMappers(Route $route, array $attributes, string $type = MappableAspectInterface::class): array
|
||||
{
|
||||
if (empty($attributes)) {
|
||||
return [];
|
||||
}
|
||||
/** @var MappableAspectInterface[] $result */
|
||||
$result = $route->filterAspects([$type], array_keys($attributes));
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Interface that describes modifiers that provide static modifications
|
||||
* to route paths based on a given context (current locale, context, ...).
|
||||
*/
|
||||
interface ModifiableAspectInterface extends AspectInterface
|
||||
{
|
||||
public function modify(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Site\SiteAwareInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Classic usage when using a "URL segment" (e.g. slug) field within a database table.
|
||||
*
|
||||
* Example:
|
||||
* routeEnhancers:
|
||||
* EventsPlugin:
|
||||
* type: Extbase
|
||||
* extension: Events2
|
||||
* plugin: Pi1
|
||||
* routes:
|
||||
* - { routePath: '/events/{event}', _controller: 'Event::detail', _arguments: {'event': 'event_name'}}
|
||||
* defaultController: 'Events2::list'
|
||||
* aspects:
|
||||
* event:
|
||||
* type: PersistedAliasMapper
|
||||
* tableName: 'tx_events2_domain_model_event'
|
||||
* routeFieldName: 'path_segment'
|
||||
* routeValuePrefix: '/'
|
||||
*/
|
||||
class PersistedAliasMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface, SiteAwareInterface, UnresolvedValueInterface
|
||||
{
|
||||
use AspectTrait;
|
||||
use SiteLanguageAccessorTrait;
|
||||
use SiteAccessorTrait;
|
||||
use UnresolvedValueTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $routeFieldName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $routeValuePrefix;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $persistenceFieldNames;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $languageFieldName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $languageParentFieldName;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $slugUniqueInSite;
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$tableName = $settings['tableName'] ?? null;
|
||||
$routeFieldName = $settings['routeFieldName'] ?? null;
|
||||
$routeValuePrefix = $settings['routeValuePrefix'] ?? '';
|
||||
|
||||
if (!is_string($tableName)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'tableName must be string',
|
||||
1537277133
|
||||
);
|
||||
}
|
||||
if (!is_string($routeFieldName)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'routeFieldName name must be string',
|
||||
1537277134
|
||||
);
|
||||
}
|
||||
if (!is_string($routeValuePrefix) || strlen($routeValuePrefix) > 1) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$routeValuePrefix must be string with one character',
|
||||
1537277136
|
||||
);
|
||||
}
|
||||
|
||||
$this->settings = $settings;
|
||||
$this->tableName = $tableName;
|
||||
$this->routeFieldName = $routeFieldName;
|
||||
$this->routeValuePrefix = $routeValuePrefix;
|
||||
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->tableName);
|
||||
if ($schema->isLanguageAware()) {
|
||||
$this->languageFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
$this->languageParentFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
|
||||
} else {
|
||||
$this->languageFieldName = null;
|
||||
$this->languageParentFieldName = null;
|
||||
}
|
||||
$this->persistenceFieldNames = $this->buildPersistenceFieldNames();
|
||||
$this->slugUniqueInSite = $this->isSlugUniqueInSite($this->tableName, $this->routeFieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate(string $value): ?string
|
||||
{
|
||||
$result = $this->findByIdentifier($value);
|
||||
$result = $this->resolveOverlay($result);
|
||||
if (!isset($result[$this->routeFieldName])) {
|
||||
return null;
|
||||
}
|
||||
return $this->purgeRouteValuePrefix(
|
||||
(string)$result[$this->routeFieldName]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(string $value): ?string
|
||||
{
|
||||
$value = $this->routeValuePrefix . $this->purgeRouteValuePrefix($value);
|
||||
$result = $this->findByRouteFieldValue($value);
|
||||
$translatedValue = $this->languageParentFieldName ? ($result[$this->languageParentFieldName] ?? null) : null;
|
||||
if ($translatedValue) {
|
||||
return (string)$translatedValue;
|
||||
}
|
||||
if (isset($result['uid'])) {
|
||||
return (string)$result['uid'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function buildPersistenceFieldNames(): array
|
||||
{
|
||||
return array_filter([
|
||||
'uid',
|
||||
'pid',
|
||||
$this->routeFieldName,
|
||||
$this->languageFieldName,
|
||||
$this->languageParentFieldName,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function purgeRouteValuePrefix(?string $value): ?string
|
||||
{
|
||||
if (empty($this->routeValuePrefix) || $value === null) {
|
||||
return $value;
|
||||
}
|
||||
return ltrim($value, $this->routeValuePrefix);
|
||||
}
|
||||
|
||||
protected function findByIdentifier(string $value): ?array
|
||||
{
|
||||
if (!MathUtility::canBeInterpretedAsInteger($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder();
|
||||
$result = $queryBuilder
|
||||
->select(...$this->persistenceFieldNames)
|
||||
->where($queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($value, Connection::PARAM_INT)
|
||||
))
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
return $result !== false ? $result : null;
|
||||
}
|
||||
|
||||
protected function findByRouteFieldValue(string $value): ?array
|
||||
{
|
||||
$languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null;
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder();
|
||||
$constraints = [
|
||||
$queryBuilder->expr()->eq(
|
||||
$this->routeFieldName,
|
||||
$queryBuilder->createNamedParameter($value)
|
||||
),
|
||||
];
|
||||
|
||||
$languageIds = null;
|
||||
if ($languageAware) {
|
||||
$languageIds = $this->resolveAllRelevantLanguageIds();
|
||||
$constraints[] = $queryBuilder->expr()->in(
|
||||
$this->languageFieldName,
|
||||
$queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$results = $queryBuilder
|
||||
->select(...$this->persistenceFieldNames)
|
||||
->where(...$constraints)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
// limit results to be contained in rootPageId of current Site
|
||||
// (which is defining the route configuration currently being processed)
|
||||
if ($this->slugUniqueInSite) {
|
||||
$results = array_values($this->filterContainedInSite($results));
|
||||
}
|
||||
// return first result record in case table is not language aware
|
||||
if (!$languageAware) {
|
||||
return $results[0] ?? null;
|
||||
}
|
||||
// post-process language fallbacks
|
||||
return $this->resolveLanguageFallback($results, $this->languageFieldName, $languageIds);
|
||||
}
|
||||
|
||||
protected function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->tableName)
|
||||
->from($this->tableName);
|
||||
$queryBuilder->setRestrictions(
|
||||
GeneralUtility::makeInstance(FrontendRestrictionContainer::class, GeneralUtility::makeInstance(Context::class))
|
||||
);
|
||||
// Frontend Groups are not available at this time
|
||||
// So this must be excluded to allow access restricted records
|
||||
$queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
protected function resolveOverlay(?array $record): ?array
|
||||
{
|
||||
$languageId = $this->siteLanguage->getLanguageId();
|
||||
if ($record === null || $languageId === 0) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$pageRepository = $this->createPageRepository();
|
||||
return $pageRepository->getLanguageOverlay($this->tableName, $record) ?: null;
|
||||
}
|
||||
|
||||
protected function createPageRepository(): PageRepository
|
||||
{
|
||||
$context = clone GeneralUtility::makeInstance(Context::class);
|
||||
$context->setAspect(
|
||||
'language',
|
||||
LanguageAspectFactory::createFromSiteLanguage($this->siteLanguage)
|
||||
);
|
||||
return GeneralUtility::makeInstance(
|
||||
PageRepository::class,
|
||||
$context
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Used for anything that invokes (more expensive) persistence invocations.
|
||||
* Basically used to improve performance by deferring their execution.
|
||||
*/
|
||||
interface PersistedMappableAspectInterface extends MappableAspectInterface {}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Site\SiteAwareInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Very useful for building a path segment from a combined value of the database.
|
||||
* Please note: title is not prepared for slugs and used raw.
|
||||
*
|
||||
* Example:
|
||||
* routeEnhancers:
|
||||
* EventsPlugin:
|
||||
* type: Extbase
|
||||
* extension: Events2
|
||||
* plugin: Pi1
|
||||
* routes:
|
||||
* - { routePath: '/events/{event}', _controller: 'Event::detail', _arguments: {'event': 'event_name'}}
|
||||
* defaultController: 'Events2::list'
|
||||
* aspects:
|
||||
* event:
|
||||
* type: PersistedPatternMapper
|
||||
* tableName: 'tx_events2_domain_model_event'
|
||||
* routeFieldPattern: '^(?P<title>.+)-(?P<uid>\d+)$'
|
||||
* routeFieldResult: '{title}-{uid}'
|
||||
*
|
||||
* @internal might change its options in the future, be aware that there might be modifications.
|
||||
*/
|
||||
class PersistedPatternMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface, SiteAwareInterface, UnresolvedValueInterface
|
||||
{
|
||||
use AspectTrait;
|
||||
use SiteLanguageAccessorTrait;
|
||||
use SiteAccessorTrait;
|
||||
use UnresolvedValueTrait;
|
||||
|
||||
protected const PATTERN_RESULT = '#\{(?P<fieldName>[^}]+)\}#';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $routeFieldPattern;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $routeFieldResult;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $routeFieldResultNames;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $languageFieldName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $languageParentFieldName;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $slugUniqueInSite;
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$tableName = $settings['tableName'] ?? null;
|
||||
$routeFieldPattern = $settings['routeFieldPattern'] ?? null;
|
||||
$routeFieldResult = $settings['routeFieldResult'] ?? null;
|
||||
|
||||
if (!is_string($tableName)) {
|
||||
throw new \InvalidArgumentException('tableName must be string', 1537277173);
|
||||
}
|
||||
if (!is_string($routeFieldPattern)) {
|
||||
throw new \InvalidArgumentException('routeFieldPattern must be string', 1537277174);
|
||||
}
|
||||
if (!is_string($routeFieldResult)) {
|
||||
throw new \InvalidArgumentException('routeFieldResult must be string', 1537277175);
|
||||
}
|
||||
if (!preg_match_all(static::PATTERN_RESULT, $routeFieldResult, $routeFieldResultNames)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'routeFieldResult must contain substitutable field names',
|
||||
1537962752
|
||||
);
|
||||
}
|
||||
|
||||
$this->settings = $settings;
|
||||
$this->tableName = $tableName;
|
||||
$this->routeFieldPattern = $routeFieldPattern;
|
||||
$this->routeFieldResult = $routeFieldResult;
|
||||
$this->routeFieldResultNames = $routeFieldResultNames['fieldName'] ?? [];
|
||||
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->tableName);
|
||||
if ($schema->isLanguageAware()) {
|
||||
$this->languageFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
$this->languageParentFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
|
||||
} else {
|
||||
$this->languageFieldName = null;
|
||||
$this->languageParentFieldName = null;
|
||||
}
|
||||
$this->slugUniqueInSite = $this->hasSlugUniqueInSite($this->tableName, ...$this->routeFieldResultNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate(string $value): ?string
|
||||
{
|
||||
$result = $this->findByIdentifier($value);
|
||||
$result = $this->resolveOverlay($result);
|
||||
return $this->createRouteResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(string $value): ?string
|
||||
{
|
||||
if (!preg_match('#' . $this->routeFieldPattern . '#', $value, $matches)) {
|
||||
return null;
|
||||
}
|
||||
$values = $this->filterNamesKeys($matches);
|
||||
$result = $this->findByRouteFieldValues($values);
|
||||
if (($result[$this->languageParentFieldName] ?? null) > 0) {
|
||||
return (string)$result[$this->languageParentFieldName];
|
||||
}
|
||||
if (isset($result['uid'])) {
|
||||
return (string)$result['uid'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function createRouteResult(?array $result): ?string
|
||||
{
|
||||
if ($result === null) {
|
||||
return $result;
|
||||
}
|
||||
$substitutes = [];
|
||||
foreach ($this->routeFieldResultNames as $fieldName) {
|
||||
if (!isset($result[$fieldName])) {
|
||||
return null;
|
||||
}
|
||||
$routeFieldName = '{' . $fieldName . '}';
|
||||
$substitutes[$routeFieldName] = $result[$fieldName];
|
||||
}
|
||||
return str_replace(
|
||||
array_keys($substitutes),
|
||||
array_values($substitutes),
|
||||
$this->routeFieldResult
|
||||
);
|
||||
}
|
||||
|
||||
protected function filterNamesKeys(array $array): array
|
||||
{
|
||||
return array_filter(
|
||||
$array,
|
||||
static function ($key) {
|
||||
return !is_numeric($key);
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
protected function findByIdentifier(string $value): ?array
|
||||
{
|
||||
if (!MathUtility::canBeInterpretedAsInteger($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder();
|
||||
$result = $queryBuilder
|
||||
->select('*')
|
||||
->where($queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($value, Connection::PARAM_INT)
|
||||
))
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
return $result !== false ? $result : null;
|
||||
}
|
||||
|
||||
protected function findByRouteFieldValues(array $values): ?array
|
||||
{
|
||||
$languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null;
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder();
|
||||
$results = $queryBuilder
|
||||
->select('*')
|
||||
->where(...$this->createRouteFieldConstraints($queryBuilder, $values))
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
// limit results to be contained in rootPageId of current Site
|
||||
// (which is defining the route configuration currently being processed)
|
||||
if ($this->slugUniqueInSite) {
|
||||
$results = array_values($this->filterContainedInSite($results));
|
||||
}
|
||||
// return first result record in case table is not language aware
|
||||
if (!$languageAware) {
|
||||
return $results[0] ?? null;
|
||||
}
|
||||
// post-process language fallbacks
|
||||
$languageIds = $this->resolveAllRelevantLanguageIds();
|
||||
return $this->resolveLanguageFallback($results, $this->languageFieldName, $languageIds);
|
||||
}
|
||||
|
||||
protected function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->tableName)
|
||||
->from($this->tableName);
|
||||
$queryBuilder->setRestrictions(
|
||||
GeneralUtility::makeInstance(FrontendRestrictionContainer::class, GeneralUtility::makeInstance(Context::class))
|
||||
);
|
||||
// Frontend Groups are not available at this time
|
||||
// So this must be excluded to allow access restricted records
|
||||
$queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
protected function createRouteFieldConstraints(QueryBuilder $queryBuilder, array $values): array
|
||||
{
|
||||
$languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null;
|
||||
$languageExpansion = $languageAware && isset($values['uid']);
|
||||
|
||||
$constraints = [];
|
||||
foreach ($values as $fieldName => $fieldValue) {
|
||||
if ($languageExpansion && $fieldName === 'uid') {
|
||||
continue;
|
||||
}
|
||||
$constraints[] = $queryBuilder->expr()->eq(
|
||||
$fieldName,
|
||||
$queryBuilder->createNamedParameter(
|
||||
$fieldValue
|
||||
)
|
||||
);
|
||||
}
|
||||
// either match uid or language parent field value (for any language)
|
||||
if ($languageExpansion) {
|
||||
$idParameter = $queryBuilder->createNamedParameter(
|
||||
$values['uid'],
|
||||
Connection::PARAM_INT
|
||||
);
|
||||
$constraints[] = $queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('uid', $idParameter),
|
||||
$queryBuilder->expr()->eq($this->languageParentFieldName, $idParameter)
|
||||
);
|
||||
// otherwise - basically uid is not in pattern - restrict to languages and apply fallbacks
|
||||
} elseif ($languageAware) {
|
||||
$languageIds = $this->resolveAllRelevantLanguageIds();
|
||||
$constraints[] = $queryBuilder->expr()->in(
|
||||
$this->languageFieldName,
|
||||
$queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
return $constraints;
|
||||
}
|
||||
|
||||
protected function resolveOverlay(?array $record): ?array
|
||||
{
|
||||
$languageId = $this->siteLanguage->getLanguageId();
|
||||
if ($record === null || $languageId === 0) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$pageRepository = $this->createPageRepository();
|
||||
return $pageRepository->getLanguageOverlay($this->tableName, $record) ?: null;
|
||||
}
|
||||
|
||||
protected function createPageRepository(): PageRepository
|
||||
{
|
||||
$context = clone GeneralUtility::makeInstance(Context::class);
|
||||
$context->setAspect(
|
||||
'language',
|
||||
LanguageAspectFactory::createFromSiteLanguage($this->siteLanguage)
|
||||
);
|
||||
return GeneralUtility::makeInstance(
|
||||
PageRepository::class,
|
||||
$context
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Routing\SiteMatcher;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Helper trait to use a site within a class.
|
||||
*
|
||||
* @internal this is not public API yet as this might change, and could be changed within TYPO3 Core at any time.
|
||||
*/
|
||||
trait SiteAccessorTrait
|
||||
{
|
||||
/**
|
||||
* @var Site
|
||||
*/
|
||||
protected $site;
|
||||
|
||||
/**
|
||||
* @var SiteMatcher|null
|
||||
*/
|
||||
protected $siteMatcher;
|
||||
|
||||
public function setSite(Site $site): void
|
||||
{
|
||||
$this->site = $site;
|
||||
}
|
||||
|
||||
public function getSite(): Site
|
||||
{
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters records that are contained in current site
|
||||
* (resolved from current SiteLanguage).
|
||||
*
|
||||
* Results keep original indexes and probably needs to
|
||||
* be passed through `array_values` for e.g. using the
|
||||
* first result by `$results[0]`.
|
||||
*
|
||||
* @param array $results
|
||||
*/
|
||||
protected function filterContainedInSite(array $results): array
|
||||
{
|
||||
if (empty($results)) {
|
||||
return $results;
|
||||
}
|
||||
return array_filter(
|
||||
$results,
|
||||
function (array $result) {
|
||||
// default FrontendRestrictionContainer retrieves only live records
|
||||
// (no specific workspace & move-placeholder resolving required here)
|
||||
$pageId = (int)$result['pid'];
|
||||
return $this->isPageIdContainedInSite($pageId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether page is contained in current site
|
||||
* (resolved from current SiteLanguage).
|
||||
*/
|
||||
protected function isPageIdContainedInSite(int $pageId): bool
|
||||
{
|
||||
try {
|
||||
$expectedSite = $this->getSiteMatcher()->matchByPageId($pageId);
|
||||
return $expectedSite->getRootPageId() === $this->site->getRootPageId();
|
||||
} catch (SiteNotFoundException $exception) {
|
||||
// Same as in \TYPO3\CMS\Core\DataHandling\SlugHelper::isUniqueInSite
|
||||
// where it is assumed that a record, that is not in site context,
|
||||
// but still configured uniqueInSite is unique. We therefore must assume
|
||||
// the resolved record to be rightfully part of the current site.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSiteMatcher(): SiteMatcher
|
||||
{
|
||||
if (!isset($this->siteMatcher)) {
|
||||
$this->siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
|
||||
}
|
||||
return $this->siteMatcher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
trait SiteLanguageAccessorTrait
|
||||
{
|
||||
use SiteLanguageAwareTrait;
|
||||
|
||||
/**
|
||||
* @var LanguageAspect
|
||||
*/
|
||||
protected $languageAspect;
|
||||
|
||||
/**
|
||||
* Resolves one record out of given language fallbacks.
|
||||
*/
|
||||
protected function resolveLanguageFallback(array $results, ?string $languageFieldName, ?array $languageIds): ?array
|
||||
{
|
||||
if ($results === []) {
|
||||
return null;
|
||||
}
|
||||
if ($languageFieldName === null || $languageIds === null) {
|
||||
return $results[0];
|
||||
}
|
||||
usort(
|
||||
$results,
|
||||
// orders records by there occurrence in $languageFallbackIds
|
||||
static function (array $a, array $b) use ($languageFieldName, $languageIds): int {
|
||||
$languageA = (int)$a[$languageFieldName];
|
||||
$languageB = (int)$b[$languageFieldName];
|
||||
return array_search($languageA, $languageIds, true)
|
||||
- array_search($languageB, $languageIds, true);
|
||||
}
|
||||
);
|
||||
return $results[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all language ids that are relevant to retrieve the most specific variant of a record.
|
||||
* The order of these ids defines the processing order concerning language fallback - most specific
|
||||
* language comes first in this array.
|
||||
*
|
||||
* + "all language (-1)", most specific if present since there cannot be any localizations
|
||||
* + "current language" most specific for the current given request context
|
||||
* + "language fallbacks" falling back to language alternatives (might include "default language")
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function resolveAllRelevantLanguageIds()
|
||||
{
|
||||
$languageIds = [-1, $this->siteLanguage->getLanguageId()];
|
||||
foreach ($this->getLanguageAspect()->getFallbackChain() as $item) {
|
||||
if (in_array($item, $languageIds, true) || !MathUtility::canBeInterpretedAsInteger($item)) {
|
||||
continue;
|
||||
}
|
||||
$languageIds[] = (int)$item;
|
||||
}
|
||||
return $languageIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides LanguageAspect which contains the logic how fallbacks
|
||||
* for a given context/overlay-mode shall be handled.
|
||||
*
|
||||
* @see LanguageAspectFactory::createFromSiteLanguage
|
||||
*/
|
||||
protected function getLanguageAspect(): LanguageAspect
|
||||
{
|
||||
if ($this->languageAspect === null) {
|
||||
$this->languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->siteLanguage);
|
||||
}
|
||||
return $this->languageAspect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Used for anything that has a fixed list of values mapped against route arguments.
|
||||
*/
|
||||
interface StaticMappableAspectInterface extends MappableAspectInterface {}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Very useful for e.g. pagination or static range like "2011 ... 2030" for years.
|
||||
*
|
||||
* Example:
|
||||
* routeEnhancers:
|
||||
* MyBlogPlugin:
|
||||
* type: Extbase
|
||||
* extension: BlogExample
|
||||
* plugin: Pi1
|
||||
* routes:
|
||||
* - { routePath: '/list/{paging_widget}', _controller: 'BlogExample::list', _arguments: {'paging_widget': '@widget_0/currentPage'}}
|
||||
* - { routePath: '/glossary/{section}', _controller: 'BlogExample::glossary'}
|
||||
* defaultController: 'BlogExample::list'
|
||||
* requirements:
|
||||
* paging_widget: '\d+'
|
||||
* aspects:
|
||||
* paging_widget:
|
||||
* type: StaticRangeMapper
|
||||
* start: '1'
|
||||
* end: '100'
|
||||
* section:
|
||||
* type: StaticRangeMapper
|
||||
* start: 'a'
|
||||
* end: 'z'
|
||||
*/
|
||||
class StaticRangeMapper implements StaticMappableAspectInterface, \Countable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $start;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $end;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $range;
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$start = $settings['start'] ?? null;
|
||||
$end = $settings['end'] ?? null;
|
||||
|
||||
if (!is_string($start)) {
|
||||
throw new \InvalidArgumentException('start must be string', 1537277163);
|
||||
}
|
||||
if (!is_string($end)) {
|
||||
throw new \InvalidArgumentException('end must be string', 1537277164);
|
||||
}
|
||||
|
||||
$this->settings = $settings;
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->range = $this->applyNumericPrefix($this->buildRange());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->range);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate(string $value): ?string
|
||||
{
|
||||
return $this->respondWhenInRange($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(string $value): ?string
|
||||
{
|
||||
return $this->respondWhenInRange($value);
|
||||
}
|
||||
|
||||
protected function respondWhenInRange(string $value): ?string
|
||||
{
|
||||
if (in_array($value, $this->range, true)) {
|
||||
return $value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds range based on given settings and ensures each item is string.
|
||||
* The amount of items is limited to 1000 in order to avoid brute-force
|
||||
* scenarios and the risk of cache-flooding.
|
||||
*
|
||||
* In case that is not enough, creating a custom and more specific mapper
|
||||
* is encouraged. Using high values that are not distinct exposes the site
|
||||
* to the risk of cache-flooding.
|
||||
*
|
||||
* @return string[]
|
||||
* @throws \LengthException
|
||||
*/
|
||||
protected function buildRange(): array
|
||||
{
|
||||
$range = array_map('strval', range($this->start, $this->end));
|
||||
if (count($range) > 1000) {
|
||||
throw new \LengthException(
|
||||
'Range is larger than 1000 items',
|
||||
1537696771
|
||||
);
|
||||
}
|
||||
return $range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function applyNumericPrefix(array $range): array
|
||||
{
|
||||
if (!preg_match('#^\d+$#', $this->start)
|
||||
|| !preg_match('#^\d+$#', $this->end)
|
||||
|| $this->start === '0' || $this->end === '0'
|
||||
|| $this->start[0] !== '0' && $this->end[0] !== '0'
|
||||
) {
|
||||
return $range;
|
||||
}
|
||||
|
||||
$length = strlen(max($this->start, $this->end));
|
||||
$range = array_map(
|
||||
static function ($value) use ($length) {
|
||||
return str_pad($value, $length, '0', STR_PAD_LEFT);
|
||||
},
|
||||
$range
|
||||
);
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
|
||||
|
||||
/**
|
||||
* Mapper for having a static list of mapping them to value properties.
|
||||
*
|
||||
* routeEnhancers:
|
||||
* MyBlogExample:
|
||||
* type: Extbase
|
||||
* extension: BlogExample
|
||||
* plugin: Pi1
|
||||
* routes:
|
||||
* - { routePath: '/archive/{year}', _controller: 'Blog::archive' }
|
||||
* defaultController: 'Blog::list'
|
||||
* aspects:
|
||||
* year:
|
||||
* type: StaticValueMapper
|
||||
* map:
|
||||
* 2k17: '2017'
|
||||
* 2k18: '2018'
|
||||
* next: '2019'
|
||||
* # (optional)
|
||||
* localeMap:
|
||||
* - locale: 'en_US.*|en_GB.*'
|
||||
* map:
|
||||
* twenty-seventeen: '2017'
|
||||
* twenty-eighteen: '2018'
|
||||
* next: '2019'
|
||||
* - locale: 'fr_FR'
|
||||
* map:
|
||||
* vingt-dix-sept: '2017'
|
||||
* vingt-dix-huit: '2018'
|
||||
* prochain: '2019'
|
||||
*/
|
||||
class StaticValueMapper implements StaticMappableAspectInterface, SiteLanguageAwareInterface, UnresolvedValueInterface, \Countable
|
||||
{
|
||||
use SiteLanguageAwareTrait;
|
||||
use UnresolvedValueTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $map;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $localeMap;
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$map = $settings['map'] ?? null;
|
||||
$localeMap = $settings['localeMap'] ?? [];
|
||||
|
||||
if (!is_array($map)) {
|
||||
throw new \InvalidArgumentException('map must be array', 1537277143);
|
||||
}
|
||||
if (!is_array($localeMap)) {
|
||||
throw new \InvalidArgumentException('localeMap must be array', 1537277144);
|
||||
}
|
||||
|
||||
$this->settings = $settings;
|
||||
$this->map = array_map('strval', $map);
|
||||
$this->localeMap = $localeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->retrieveLocaleMap() ?? $this->map);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate(string $value): ?string
|
||||
{
|
||||
$map = $this->retrieveLocaleMap() ?? $this->map;
|
||||
$index = array_search($value, $map, true);
|
||||
return $index !== false ? (string)$index : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(string $value): ?string
|
||||
{
|
||||
$map = $this->retrieveLocaleMap() ?? $this->map;
|
||||
return isset($map[$value]) ? (string)$map[$value] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the map of with the matching locale.
|
||||
*/
|
||||
protected function retrieveLocaleMap(): ?array
|
||||
{
|
||||
$locale = (string)$this->siteLanguage->getLocale();
|
||||
foreach ($this->localeMap as $item) {
|
||||
$pattern = '#^' . str_replace('_', '-', $item['locale']) . '#i';
|
||||
if (preg_match($pattern, $locale)) {
|
||||
return array_map('strval', $item['map']);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Provides fallback values for unresolved values during processing mappers.
|
||||
*/
|
||||
interface UnresolvedValueInterface
|
||||
{
|
||||
public function hasFallbackValue(): bool;
|
||||
public function getFallbackValue(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing\Aspect;
|
||||
|
||||
/**
|
||||
* Provides fallback values for unresolved values during processing mappers.
|
||||
*/
|
||||
trait UnresolvedValueTrait
|
||||
{
|
||||
/**
|
||||
* @var array{fallbackValue?: ?scalar}
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
public function hasFallbackValue(): bool
|
||||
{
|
||||
return array_key_exists('fallbackValue', $this->settings);
|
||||
}
|
||||
|
||||
public function getFallbackValue(): ?string
|
||||
{
|
||||
if (!$this->hasFallbackValue()) {
|
||||
throw new \LogicException('Property fallbackValue must be defined', 1668084601);
|
||||
}
|
||||
/** @var mixed $fallbackValue */
|
||||
$fallbackValue = $this->settings['fallbackValue'];
|
||||
if (is_string($fallbackValue) || is_null($fallbackValue)) {
|
||||
return $fallbackValue;
|
||||
}
|
||||
return (string)$fallbackValue;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user