.+)-(?P\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[^}]+)\}#'; /** * @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 ); } }