diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d09b0afb9d49f..8220667d1b3da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -78,6 +78,8 @@ use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; +use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper; use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; use Symfony\Component\Routing\Loader\AnnotationFileLoader; use Symfony\Component\Security\Core\Security; @@ -724,6 +726,10 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co if (isset($config['type'])) { $argument['resource_type'] = $config['type']; } + if (!class_exists(CompiledUrlGenerator::class)) { + $argument['generator_class'] = $argument['generator_base_class']; + $argument['generator_dumper_class'] = PhpGeneratorDumper::class; + } $router->replaceArgument(2, $argument); $container->setParameter('request_listener.http_port', $config['http_port']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 03bac811b2553..0e3fe89ce9944 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -59,9 +59,9 @@ %kernel.cache_dir% %kernel.debug% - Symfony\Component\Routing\Generator\UrlGenerator + Symfony\Component\Routing\Generator\CompiledUrlGenerator Symfony\Component\Routing\Generator\UrlGenerator - Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper + Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper %router.cache_class_prefix%UrlGenerator Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 8e359a3722597..ad5e9536ae973 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG ----- * added fallback to cultureless locale for internationalized routes + * added `CompiledUrlGenerator` and `CompiledUrlGeneratorDumper` + * deprecated `PhpUrlGeneratorDumped` 4.0.0 ----- diff --git a/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php b/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php new file mode 100644 index 0000000000000..13ab4815e156a --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContext; + +class CompiledUrlGenerator extends UrlGenerator +{ + private $compiledRoutes = array(); + private $defaultLocale; + + public function __construct(array $compiledRoutes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null) + { + $this->compiledRoutes = $compiledRoutes; + $this->context = $context; + $this->logger = $logger; + $this->defaultLocale = $defaultLocale; + } + + public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) + { + $locale = $parameters['_locale'] + ?? $this->context->getParameter('_locale') + ?: $this->defaultLocale; + + if (null !== $locale) { + do { + if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) { + unset($parameters['_locale']); + $name .= '.'.$locale; + break; + } + } while (false !== $locale = strstr($locale, '_', true)); + } + + if (!isset($this->compiledRoutes[$name])) { + throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); + } + + list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = $this->compiledRoutes[$name]; + + return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); + } +} diff --git a/src/Symfony/Component/Routing/Generator/Dumper/CompiledUrlGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/CompiledUrlGeneratorDumper.php new file mode 100644 index 0000000000000..5989a7616960f --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; + +/** + * CompiledUrlGeneratorDumper creates a PHP array to be used with CompiledUrlGenerator. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @author Nicolas Grekas + */ +class CompiledUrlGeneratorDumper extends GeneratorDumper +{ + public function getCompiledRoutes(): array + { + $compiledRoutes = array(); + foreach ($this->getRoutes()->all() as $name => $route) { + $compiledRoute = $route->compile(); + + $compiledRoutes[$name] = array( + $compiledRoute->getVariables(), + $route->getDefaults(), + $route->getRequirements(), + $compiledRoute->getTokens(), + $compiledRoute->getHostTokens(), + $route->getSchemes(), + ); + } + + return $compiledRoutes; + } + + /** + * {@inheritdoc} + */ + public function dump(array $options = array()) + { + return <<generateDeclaredRoutes()} +); + +EOF; + } + + /** + * Generates PHP code representing an array of defined routes + * together with the routes properties (e.g. requirements). + */ + private function generateDeclaredRoutes(): string + { + $routes = ''; + foreach ($this->getCompiledRoutes() as $name => $properties) { + $routes .= sprintf("\n '%s' => %s,", $name, PhpMatcherDumper::export($properties)); + } + + return $routes; + } +} diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index 12dd3f28faf0f..397d241b424bf 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Generator\Dumper; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "CompiledUrlGeneratorDumper" instead.', PhpGeneratorDumper::class), E_USER_DEPRECATED); + use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; /** @@ -18,6 +20,8 @@ * * @author Fabien Potencier * @author Tobias Schultze + * + * @deprecated since Symfony 4.2, use CompiledUrlGeneratorDumper instead. */ class PhpGeneratorDumper extends GeneratorDumper { diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php index 56842a4d3b864..f7d34a64a4723 100644 --- a/src/Symfony/Component/Routing/Router.php +++ b/src/Symfony/Component/Routing/Router.php @@ -18,6 +18,7 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -83,6 +84,8 @@ class Router implements RouterInterface, RequestMatcherInterface */ private $expressionLanguageProviders = array(); + private static $dumpCache = array(); + /** * @param LoaderInterface $loader A LoaderInterface instance * @param mixed $resource The main resource to load @@ -320,8 +323,10 @@ public function getGenerator() return $this->generator; } + $compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true); + if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) { - $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger); + $this->generator = new $this->options[$compiled ? 'generator_base_class' : 'generator_class']($this->getRouteCollection(), $this->context, $this->logger); } else { $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['generator_cache_class'].'.php', function (ConfigCacheInterface $cache) { @@ -336,11 +341,19 @@ function (ConfigCacheInterface $cache) { } ); - if (!class_exists($this->options['generator_cache_class'], false)) { - require_once $cache->getPath(); - } + if ($compiled) { + if (!isset(self::$dumpCache[$path = $cache->getPath()])) { + self::$dumpCache[$path] = require $path; + } - $this->generator = new $this->options['generator_cache_class']($this->context, $this->logger); + $this->generator = new $this->options['generator_class'](self::$dumpCache[$path], $this->context, $this->logger); + } else { + if (!class_exists($this->options['generator_cache_class'], false)) { + require_once $cache->getPath(); + } + + $this->generator = new $this->options['generator_cache_class']($this->context, $this->logger); + } } if ($this->generator instanceof ConfigurableRequirementsInterface) { diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php new file mode 100644 index 0000000000000..f3807f6479c2b --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php @@ -0,0 +1,240 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Generator\Dumper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; +use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +class CompiledUrlGeneratorDumperTest extends TestCase +{ + /** + * @var RouteCollection + */ + private $routeCollection; + + /** + * @var CompiledUrlGeneratorDumper + */ + private $generatorDumper; + + /** + * @var string + */ + private $testTmpFilepath; + + /** + * @var string + */ + private $largeTestTmpFilepath; + + protected function setUp() + { + parent::setUp(); + + $this->routeCollection = new RouteCollection(); + $this->generatorDumper = new CompiledUrlGeneratorDumper($this->routeCollection); + $this->testTmpFilepath = sys_get_temp_dir().'/php_generator.'.$this->getName().'.php'; + $this->largeTestTmpFilepath = sys_get_temp_dir().'/php_generator.'.$this->getName().'.large.php'; + @unlink($this->testTmpFilepath); + @unlink($this->largeTestTmpFilepath); + } + + protected function tearDown() + { + parent::tearDown(); + + @unlink($this->testTmpFilepath); + + $this->routeCollection = null; + $this->generatorDumper = null; + $this->testTmpFilepath = null; + } + + public function testDumpWithRoutes() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}')); + $this->routeCollection->add('Test2', new Route('/testing2')); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_URL); + $absoluteUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_PATH); + $relativeUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('http://localhost/app.php/testing/bar', $absoluteUrlWithParameter); + $this->assertEquals('http://localhost/app.php/testing2', $absoluteUrlWithoutParameter); + $this->assertEquals('/app.php/testing/bar', $relativeUrlWithParameter); + $this->assertEquals('/app.php/testing2', $relativeUrlWithoutParameter); + } + + public function testDumpWithSimpleLocalizedRoutes() + { + $this->routeCollection->add('test', (new Route('/foo'))); + $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_locale', 'nl')->setDefault('_canonical_route', 'test')); + + $code = $this->generatorDumper->dump(); + file_put_contents($this->testTmpFilepath, $code); + + $context = new RequestContext('/app.php'); + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, $context, null, 'en'); + + $urlWithDefaultLocale = $projectUrlGenerator->generate('test'); + $urlWithSpecifiedLocale = $projectUrlGenerator->generate('test', array('_locale' => 'nl')); + $context->setParameter('_locale', 'en'); + $urlWithEnglishContext = $projectUrlGenerator->generate('test'); + $context->setParameter('_locale', 'nl'); + $urlWithDutchContext = $projectUrlGenerator->generate('test'); + + $this->assertEquals('/app.php/testing/is/fun', $urlWithDefaultLocale); + $this->assertEquals('/app.php/testen/is/leuk', $urlWithSpecifiedLocale); + $this->assertEquals('/app.php/testing/is/fun', $urlWithEnglishContext); + $this->assertEquals('/app.php/testen/is/leuk', $urlWithDutchContext); + + // test with full route name + $this->assertEquals('/app.php/testing/is/fun', $projectUrlGenerator->generate('test.en')); + + $context->setParameter('_locale', 'de_DE'); + // test that it fall backs to another route when there is no matching localized route + $this->assertEquals('/app.php/foo', $projectUrlGenerator->generate('test')); + } + + /** + * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException + * @expectedExceptionMessage Unable to generate a URL for the named route "test" as such route does not exist. + */ + public function testDumpWithRouteNotFoundLocalizedRoutes() + { + $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test')); + + $code = $this->generatorDumper->dump(); + file_put_contents($this->testTmpFilepath, $code); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php'), null, 'pl_PL'); + $projectUrlGenerator->generate('test'); + } + + public function testDumpWithFallbackLocaleLocalizedRoutes() + { + $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_canonical_route', 'test')); + + $code = $this->generatorDumper->dump(); + file_put_contents($this->testTmpFilepath, $code); + + $context = new RequestContext('/app.php'); + $context->setParameter('_locale', 'en_GB'); + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, $context, null, null); + + // test with context _locale + $this->assertEquals('/app.php/testing/is/fun', $projectUrlGenerator->generate('test')); + // test with parameters _locale + $this->assertEquals('/app.php/testen/is/leuk', $projectUrlGenerator->generate('test', array('_locale' => 'nl_BE'))); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php'), null, 'fr_CA'); + // test with default locale + $this->assertEquals('/app.php/tester/est/amusant', $projectUrlGenerator->generate('test')); + } + + public function testDumpWithTooManyRoutes() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}')); + for ($i = 0; $i < 32769; ++$i) { + $this->routeCollection->add('route_'.$i, new Route('/route_'.$i)); + } + $this->routeCollection->add('Test2', new Route('/testing2')); + + file_put_contents($this->largeTestTmpFilepath, $this->generatorDumper->dump()); + $this->routeCollection = $this->generatorDumper = null; + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->largeTestTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_URL); + $absoluteUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_PATH); + $relativeUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('http://localhost/app.php/testing/bar', $absoluteUrlWithParameter); + $this->assertEquals('http://localhost/app.php/testing2', $absoluteUrlWithoutParameter); + $this->assertEquals('/app.php/testing/bar', $relativeUrlWithParameter); + $this->assertEquals('/app.php/testing2', $relativeUrlWithoutParameter); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testDumpWithoutRoutes() + { + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $projectUrlGenerator->generate('Test', array()); + } + + /** + * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException + */ + public function testGenerateNonExistingRoute() + { + $this->routeCollection->add('Test', new Route('/test')); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext()); + $url = $projectUrlGenerator->generate('NonExisting', array()); + } + + public function testDumpForRouteWithDefaults() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}', array('foo' => 'bar'))); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext()); + $url = $projectUrlGenerator->generate('Test', array()); + + $this->assertEquals('/testing', $url); + } + + public function testDumpWithSchemeRequirement() + { + $this->routeCollection->add('Test1', new Route('/testing', array(), array(), array(), '', array('ftp', 'https'))); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('ftp://localhost/app.php/testing', $absoluteUrl); + $this->assertEquals('ftp://localhost/app.php/testing', $relativeUrl); + + $projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php', 'GET', 'localhost', 'https')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('https://localhost/app.php/testing', $absoluteUrl); + $this->assertEquals('/app.php/testing', $relativeUrl); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index 45464c3a90d53..43bcc9c34e758 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +/** + * @group legacy + */ class PhpGeneratorDumperTest extends TestCase { /** pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy