[TASK] Add facades for symfony/routing components 01/58401/4
authorBenni Mack <benni@typo3.org>
Wed, 26 Sep 2018 17:42:21 +0000 (19:42 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Wed, 26 Sep 2018 18:41:09 +0000 (20:41 +0200)
We heavily rely on Symfony Routing for TYPO3 routing,
however we want to encapsulate this as much as possible
to allow us to use a different routing system if a better one
comes up.

In order to proceed with Route Enhancers we introduce
our own objects to extend them, and to typehint against
these objects in the next patches.

Some minor code preparations have been made as well.

Resolves: #86393
Releases: master
Change-Id: Id50186aa1a5ad57e097e6ff85e15d0f925bd63ef
Reviewed-on: https://review.typo3.org/58401
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/core/Classes/Routing/PageRouter.php
typo3/sysext/core/Classes/Routing/Route.php [new file with mode: 0644]
typo3/sysext/core/Classes/Routing/RouteCollection.php [new file with mode: 0644]
typo3/sysext/core/Classes/Routing/SiteMatcher.php
typo3/sysext/core/Classes/Utility/ArrayUtility.php
typo3/sysext/core/Tests/Unit/Routing/PageRouterTest.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Middleware/PageResolver.php

index 1647195..305a1d8 100644 (file)
@@ -22,9 +22,8 @@ use Psr\Http\Message\UriInterface;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 use Symfony\Component\Routing\Matcher\UrlMatcher;
 use Symfony\Component\Routing\RequestContext;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\LanguageAspect;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
@@ -32,6 +31,7 @@ use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Page\PageRepository;
 
 /**
  * Page Router - responsible for a page based on a request, by looking up the slug of the page path.
@@ -91,40 +91,41 @@ class PageRouter implements RouterInterface
      */
     public function matchRequest(ServerRequestInterface $request, RouteResultInterface $previousResult = null): ?RouteResultInterface
     {
-        $routePathTail = $previousResult ? $previousResult->getTail() : '';
-        $language = $previousResult ? $previousResult->getLanguage() : null;
-        $slugCandidates = $this->getCandidateSlugsFromRoutePath($routePathTail);
+        $slugCandidates = $this->getCandidateSlugsFromRoutePath($previousResult->getTail());
         if (empty($slugCandidates)) {
             return null;
         }
+        $language = $previousResult->getLanguage();
         $pageCandidates = $this->getPagesFromDatabaseForCandidates($slugCandidates, $language->getLanguageId());
         // Stop if there are no candidates
         if (empty($pageCandidates)) {
             return null;
         }
 
-        $collection = new RouteCollection();
+        $fullCollection = new RouteCollection();
         foreach ($pageCandidates ?? [] as $page) {
-            $path = $page['slug'];
-            $route = new Route(
-                $path . '{tail}',
-                ['page' => $page, 'tail' => ''],
+            $pagePath = $page['slug'];
+            $defaultRouteForPage = new Route(
+                $pagePath . '{tail}',
+                ['tail' => ''],
                 ['tail' => '.*'],
-                ['utf8' => true]
+                ['utf8' => true, '_page' => $page]
             );
-            $collection->add('page_' . $page['uid'], $route);
+            $fullCollection->add('page_' . $page['uid'], $defaultRouteForPage);
         }
 
         $context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost());
-        $matcher = new UrlMatcher($collection, $context);
+        $matcher = new UrlMatcher($fullCollection, $context);
         try {
-            $result = $matcher->match('/' . ltrim($routePathTail, '/'));
+            $result = $matcher->match('/' . ltrim($previousResult->getTail(), '/'));
+            /** @var Route $matchedRoute */
+            $matchedRoute = $fullCollection->get($result['_route']);
             unset($result['_route']);
-            return new RouteResult($request->getUri(), $this->site, $language, $result['tail'], $result);
+            return $this->buildRouteResult($request, $language, $matchedRoute, $result);
         } catch (ResourceNotFoundException $e) {
-            // do nothing
+            // return nothing
         }
-        return new RouteResult($request->getUri(), $this->site, $language);
+        return null;
     }
 
     /**
@@ -138,15 +139,15 @@ class PageRouter implements RouterInterface
      */
     public function generateUri($route, array $parameters = [], string $fragment = '', string $type = ''): UriInterface
     {
-        // Resolve site
-        $siteLanguage = null;
+        // Resolve language
+        $language = null;
         $languageOption = $parameters['_language'] ?? null;
         if ($languageOption instanceof SiteLanguage) {
-            $siteLanguage = $languageOption;
+            $language = $languageOption;
             unset($parameters['_language']);
         }
-        if ($siteLanguage === null) {
-            $siteLanguage = $this->site->getDefaultLanguage();
+        if ($language === null) {
+            $language = $this->site->getDefaultLanguage();
         }
 
         $pageId = 0;
@@ -155,13 +156,15 @@ class PageRouter implements RouterInterface
         } elseif (is_scalar($route)) {
             $pageId = (int)$route;
         }
-        $pageRecord = BackendUtility::getRecord('pages', $pageId);
-        if ($siteLanguage->getLanguageId() > 0) {
-            $pageLocalizations = BackendUtility::getRecordLocalization('pages', $pageId, $siteLanguage->getLanguageId());
-            $pageRecord = $pageLocalizations[0] ?? $pageRecord;
-        }
-        $prefix = (string)$siteLanguage->getBase();
-        $prefix = rtrim($prefix, '/') . '/' . ltrim($pageRecord['slug'] ?? '', '/');
+
+        $context = clone GeneralUtility::makeInstance(Context::class);
+        $context->setAspect('language', new LanguageAspect($language->getLanguageId()));
+        $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
+        $page = $pageRepository->getPage($pageId, true);
+        $pagePath = ltrim($page['slug'] ?? '', '/');
+
+        $prefix = (string)$language->getBase();
+        $prefix = rtrim($prefix, '/') . '/' . $pagePath;
 
         // Add the query parameters as string
         $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
@@ -177,7 +180,7 @@ class PageRouter implements RouterInterface
         if ($fragment) {
             $uri = $uri->withFragment($fragment);
         }
-        if ($type === self::ABSOLUTE_PATH) {
+        if ($type === RouterInterface::ABSOLUTE_PATH) {
             $uri = $uri->withScheme('')->withHost('')->withPort(null);
         }
         return $uri;
@@ -257,4 +260,22 @@ class PageRouter implements RouterInterface
         }
         return $candidatePathParts;
     }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @param SiteLanguage|null $language
+     * @param Route|null $route
+     * @param array $results
+     * @return RouteResult
+     */
+    protected function buildRouteResult(ServerRequestInterface $request, SiteLanguage $language, Route $route, array $results = []): RouteResult
+    {
+        $data = [];
+        // page record the route has been applied for
+        if ($route->hasOption('_page')) {
+            $data['page'] = $route->getOption('_page');
+        }
+        $tail = $results['tail'] ?? '';
+        return new RouteResult($request->getUri(), $this->site, $language, $tail, $data);
+    }
 }
diff --git a/typo3/sysext/core/Classes/Routing/Route.php b/typo3/sysext/core/Classes/Routing/Route.php
new file mode 100644 (file)
index 0000000..554dd9a
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Routing;
+
+/*
+ * 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!
+ */
+
+use Symfony\Component\Routing\Route as SymfonyRoute;
+
+/**
+ * TYPO3's route is built on top of Symfony's route with some special handling
+ *
+ * @internal as this is tightly coupled to Symfony's Routing and we try to encapsulate this, please note that this might change
+ */
+class Route extends SymfonyRoute
+{
+    /**
+     * @return array
+     */
+    public function getArguments(): array
+    {
+        return $this->getOption('_arguments') ?? [];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Routing/RouteCollection.php b/typo3/sysext/core/Classes/Routing/RouteCollection.php
new file mode 100644 (file)
index 0000000..6320ef0
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Routing;
+
+/*
+ * 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!
+ */
+
+use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
+
+/**
+ * Extensible container based on Symfony's Route Collection
+ *
+ * @internal as this is tightly coupled to Symfony's Routing and we try to encapsulate this, please note that this might change
+ */
+class RouteCollection extends SymfonyRouteCollection
+{
+}
index f76611a..bed9bf8 100644 (file)
@@ -21,8 +21,6 @@ use Symfony\Component\Routing\Exception\NoConfigurationException;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 use Symfony\Component\Routing\Matcher\UrlMatcher;
 use Symfony\Component\Routing\RequestContext;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Site\Entity\PseudoSite;
index 9f23f82..1f28fb1 100644 (file)
@@ -718,7 +718,7 @@ class ArrayUtility
 
     /**
      * Filters keys off from first array that also exist in second array. Comparison is done by keys.
-     * This method is a recursive version of php array_diff_assoc()
+     * This method is a recursive version of php array_diff_key()
      *
      * @param array $array1 Source array
      * @param array $array2 Reduce source array by this array
index edcd537..edb6da2 100644 (file)
@@ -51,7 +51,7 @@ class PageRouterTest extends UnitTestCase
         $subject->expects($this->once())->method('getPagesFromDatabaseForCandidates')->willReturn([$pageRecord]);
         $routeResult = $subject->matchRequest($request, $previousResult);
 
-        $expectedRouteResult = new RouteResult($request->getUri(), $site, $language, '', ['page' => $pageRecord, 'tail' => '']);
+        $expectedRouteResult = new RouteResult($request->getUri(), $site, $language, '', ['page' => $pageRecord]);
         $this->assertEquals($expectedRouteResult, $routeResult);
     }
 
@@ -82,7 +82,7 @@ class PageRouterTest extends UnitTestCase
         $subject->expects($this->once())->method('getPagesFromDatabaseForCandidates')->willReturn([$pageRecord]);
         $routeResult = $subject->matchRequest($request, $previousResult);
 
-        $expectedRouteResult = new RouteResult($request->getUri(), $site, $language, 'unknown-code/', ['page' => $pageRecord, 'tail' => 'unknown-code/']);
+        $expectedRouteResult = new RouteResult($request->getUri(), $site, $language, 'unknown-code/', ['page' => $pageRecord]);
         $this->assertEquals($expectedRouteResult, $routeResult);
     }
 }
index 6618114..c11e0e4 100644 (file)
@@ -2272,21 +2272,22 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     public function reqCHash()
     {
-        if (!$this->cHash) {
-            if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
-                if ($this->tempContent) {
-                    $this->clearPageCacheContent();
-                }
-                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                    $GLOBALS['TYPO3_REQUEST'],
-                    'Request parameters could not be validated (&cHash empty)',
-                    ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
-                );
-                throw new ImmediateResponseException($response, 1533931354);
+        if ($this->cHash) {
+            return;
+        }
+        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
+            if ($this->tempContent) {
+                $this->clearPageCacheContent();
             }
-            $this->disableCache();
-            $this->getTimeTracker()->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', 2);
+            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                $GLOBALS['TYPO3_REQUEST'],
+                'Request parameters could not be validated (&cHash empty)',
+                ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
+            );
+            throw new ImmediateResponseException($response, 1533931354);
         }
+        $this->disableCache();
+        $this->getTimeTracker()->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', 2);
     }
 
     /**
index e191a29..f2ec184 100644 (file)
@@ -24,7 +24,6 @@ use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
 use TYPO3\CMS\Core\Http\RedirectResponse;
-use TYPO3\CMS\Core\Routing\PageRouter;
 use TYPO3\CMS\Core\Routing\RouteResult;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
@@ -73,13 +72,18 @@ class PageResolver implements MiddlewareInterface
 
         // Resolve the page ID based on TYPO3's native routing functionality
         if ($hasSiteConfiguration) {
-            /** @var PageRouter $router */
-            $router = $site->getRouter();
             /** @var RouteResult $previousResult */
-            $previousResult = $request->getAttribute('routing', new RouteResult($request->getUri(), $site, $language));
-            if (!empty($previousResult->getTail())) {
+            $previousResult = $request->getAttribute('routing', null);
+            if ($previousResult && $previousResult->getTail()) {
                 // Check for the route
-                $routeResult = $router->matchRequest($request, $previousResult);
+                $routeResult = $site->getRouter()->matchRequest($request, $previousResult);
+                if ($routeResult === null) {
+                    return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'The requested page does not exist',
+                        ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
+                    );
+                }
                 $request = $request->withAttribute('routing', $routeResult);
                 if (is_array($routeResult['page'])) {
                     $page = $routeResult['page'];