vendor/contao/core-bundle/src/Routing/AbstractPageRouteProvider.php line 42

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\Routing;
  11. use Contao\CoreBundle\Framework\ContaoFramework;
  12. use Contao\CoreBundle\Routing\Page\PageRegistry;
  13. use Contao\CoreBundle\Routing\Page\PageRoute;
  14. use Contao\CoreBundle\Util\LocaleUtil;
  15. use Contao\Model\Collection;
  16. use Contao\PageModel;
  17. use Symfony\Cmf\Component\Routing\Candidates\CandidatesInterface;
  18. use Symfony\Cmf\Component\Routing\RouteProviderInterface;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\Routing\Route;
  21. abstract class AbstractPageRouteProvider implements RouteProviderInterface
  22. {
  23. protected ContaoFramework $framework;
  24. protected CandidatesInterface $candidates;
  25. protected PageRegistry $pageRegistry;
  26. public function __construct(ContaoFramework $framework, CandidatesInterface $candidates, PageRegistry $pageRegistry)
  27. {
  28. $this->framework = $framework;
  29. $this->candidates = $candidates;
  30. $this->pageRegistry = $pageRegistry;
  31. }
  32. /**
  33. * @return array<PageModel>
  34. */
  35. protected function findCandidatePages(Request $request): array
  36. {
  37. $candidates = array_map('strval', $this->candidates->getCandidates($request));
  38. if (empty($candidates)) {
  39. return [];
  40. }
  41. $ids = [];
  42. $aliases = [];
  43. foreach ($candidates as $candidate) {
  44. if (preg_match('/^[1-9]\d*$/', $candidate)) {
  45. $ids[] = (int) $candidate;
  46. } else {
  47. $aliases[] = $candidate;
  48. }
  49. }
  50. $conditions = [];
  51. if (!empty($ids)) {
  52. $conditions[] = 'tl_page.id IN ('.implode(',', $ids).')';
  53. }
  54. if (!empty($aliases)) {
  55. $conditions[] = 'tl_page.alias IN ('.implode(',', array_fill(0, \count($aliases), '?')).')';
  56. }
  57. $pageModel = $this->framework->getAdapter(PageModel::class);
  58. $pages = $pageModel->findBy([implode(' OR ', $conditions)], $aliases);
  59. if (!$pages instanceof Collection) {
  60. return [];
  61. }
  62. /** @var array<PageModel> $models */
  63. $models = $pages->getModels();
  64. return array_filter($models, fn (PageModel $model) => $this->pageRegistry->isRoutable($model));
  65. }
  66. /**
  67. * @return array<int>
  68. */
  69. protected function getPageIdsFromNames(array $names): array
  70. {
  71. $ids = [];
  72. foreach ($names as $name) {
  73. if (0 !== strncmp($name, 'tl_page.', 8)) {
  74. continue;
  75. }
  76. [, $id] = explode('.', $name);
  77. if (!preg_match('/^[1-9]\d*$/', $id)) {
  78. continue;
  79. }
  80. $ids[] = (int) $id;
  81. }
  82. return array_unique($ids);
  83. }
  84. protected function compareRoutes(Route $a, Route $b, ?array $languages = null): int
  85. {
  86. if ('' !== $a->getHost() && '' === $b->getHost()) {
  87. return -1;
  88. }
  89. if ('' === $a->getHost() && '' !== $b->getHost()) {
  90. return 1;
  91. }
  92. /** @var PageModel|null $pageA */
  93. $pageA = $a->getDefault('pageModel');
  94. /** @var PageModel|null $pageB */
  95. $pageB = $b->getDefault('pageModel');
  96. // Check if the page models are valid (should always be the case, as routes are generated from pages)
  97. if (!$pageA instanceof PageModel || !$pageB instanceof PageModel) {
  98. return 0;
  99. }
  100. $langA = null;
  101. $langB = null;
  102. if (null !== $languages && $pageA->rootLanguage !== $pageB->rootLanguage) {
  103. $fallbackA = LocaleUtil::getFallbacks($pageA->rootLanguage);
  104. $fallbackB = LocaleUtil::getFallbacks($pageB->rootLanguage);
  105. $langA = $this->getLocalePriority($fallbackA, $fallbackB, $languages);
  106. $langB = $this->getLocalePriority($fallbackB, $fallbackA, $languages);
  107. if (null === $langA && null === $langB && LocaleUtil::getPrimaryLanguage($pageA->rootLanguage) === LocaleUtil::getPrimaryLanguage($pageB->rootLanguage)) {
  108. // If both pages have the same language without region and neither region has a priority,
  109. // (e.g. user prefers "de" but we have "de-CH" and "de-DE"), sort by their root page order.
  110. $langA = $pageA->rootSorting;
  111. $langB = $pageB->rootSorting;
  112. }
  113. }
  114. if (null === $langA && null === $langB) {
  115. if ($pageA->rootIsFallback && !$pageB->rootIsFallback) {
  116. return -1;
  117. }
  118. if ($pageB->rootIsFallback && !$pageA->rootIsFallback) {
  119. return 1;
  120. }
  121. } else {
  122. if (null === $langA && null !== $langB) {
  123. return 1;
  124. }
  125. if (null !== $langA && null === $langB) {
  126. return -1;
  127. }
  128. if ($langA < $langB) {
  129. return -1;
  130. }
  131. if ($langA > $langB) {
  132. return 1;
  133. }
  134. }
  135. if ('root' !== $pageA->type && 'root' === $pageB->type) {
  136. return -1;
  137. }
  138. if ('root' === $pageA->type && 'root' !== $pageB->type) {
  139. return 1;
  140. }
  141. if ($pageA->routePriority !== $pageB->routePriority) {
  142. return $pageB->routePriority <=> $pageA->routePriority;
  143. }
  144. $pathA = $a instanceof PageRoute && $a->getUrlSuffix() ? substr($a->getPath(), 0, -\strlen($a->getUrlSuffix())) : $a->getPath();
  145. $pathB = $b instanceof PageRoute && $b->getUrlSuffix() ? substr($b->getPath(), 0, -\strlen($b->getUrlSuffix())) : $b->getPath();
  146. // Prioritize the default behaviour when "requireItem" is enabled
  147. if ($pathA === $pathB && '{!parameters}' === substr($pathA, -13)) {
  148. $paramA = $a->getRequirement('parameters');
  149. $paramB = $b->getRequirement('parameters');
  150. if ('/.+?' === $paramA && '(/.+?)?' === $paramB) {
  151. return -1;
  152. }
  153. if ('(/.+?)?' === $paramA && '/.+?' === $paramB) {
  154. return 1;
  155. }
  156. }
  157. $countA = \count(explode('/', $pathA));
  158. $countB = \count(explode('/', $pathB));
  159. if ($countA > $countB) {
  160. return -1;
  161. }
  162. if ($countB > $countA) {
  163. return 1;
  164. }
  165. return strnatcasecmp($pathA, $pathB);
  166. }
  167. protected function convertLanguagesForSorting(array $languages): array
  168. {
  169. $result = [];
  170. foreach ($languages as $language) {
  171. if (!$locales = LocaleUtil::getFallbacks($language)) {
  172. continue;
  173. }
  174. $language = array_pop($locales);
  175. $result[] = $language;
  176. foreach (array_reverse($locales) as $locale) {
  177. if (!\in_array($locale, $result, true)) {
  178. $result[] = $locale;
  179. }
  180. }
  181. }
  182. return array_flip($result);
  183. }
  184. private function getLocalePriority(array $locales, array $notIn, array $languagePriority): ?int
  185. {
  186. foreach (array_reverse($locales) as $locale) {
  187. if (isset($languagePriority[$locale]) && !\in_array($locale, $notIn, true)) {
  188. return $languagePriority[$locale];
  189. }
  190. }
  191. return null;
  192. }
  193. }