vendor/wearemarketing/routingbundle/Generator/ContentAndRequirementsAwareGenerator.php line 71

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony CMF package.
  4.  *
  5.  * (c) 2011-2017 Symfony CMF
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace WAM\Bundle\RoutingBundle\Generator;
  11. use Doctrine\Common\Collections\Collection;
  12. use Symfony\Cmf\Component\Routing\ContentRepositoryInterface;
  13. use Symfony\Cmf\Component\Routing\ProviderBasedGenerator;
  14. use Symfony\Cmf\Component\Routing\RouteObjectInterface;
  15. use Symfony\Cmf\Component\Routing\RouteReferrersReadInterface;
  16. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  17. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  18. use Symfony\Component\Routing\Route as SymfonyRoute;
  19. use Symfony\Component\Routing\RouteCollection;
  20. /**
  21.  * A generator that tries to generate routes from object, route names or
  22.  * content objects or names.
  23.  *
  24.  * @author Mauro Casula
  25.  */
  26. class ContentAndRequirementsAwareGenerator extends ProviderBasedGenerator
  27. {
  28.     /**
  29.      * The locale to use when neither the parameters nor the request context
  30.      * indicate the locale to use.
  31.      *
  32.      * @var string
  33.      */
  34.     protected ?string $defaultLocale null;
  35.     /**
  36.      * The content repository used to find content by it's id
  37.      * This can be used to specify a parameter content_id when generating urls.
  38.      *
  39.      * This is optional and might not be initialized.
  40.      *
  41.      * @var ContentRepositoryInterface|null
  42.      */
  43.     protected ?ContentRepositoryInterface $contentRepository null;
  44.     /**
  45.      * Set an optional content repository to find content by ids.
  46.      *
  47.      * @param ContentRepositoryInterface $contentRepository
  48.      */
  49.     public function setContentRepository(ContentRepositoryInterface $contentRepository)
  50.     {
  51.         $this->contentRepository $contentRepository;
  52.     }
  53.     /**
  54.      * {@inheritdoc}
  55.      *
  56.      * @param string $name ignored
  57.      * @param array $parameters must either contain the field 'route' with a
  58.      *                           RouteObjectInterface or the field 'content_id'
  59.      *                           with the id of a document implementing
  60.      *                           RouteReferrersReadInterface
  61.      *
  62.      * @throws RouteNotFoundException If there is no such route in the database
  63.      */
  64.     public function generate($name$parameters = [], $absolute UrlGeneratorInterface::ABSOLUTE_PATH)
  65.     {
  66.         if ($name instanceof SymfonyRoute) {
  67.             $route $this->getBestLocaleRoute($name$parameters);
  68.         } elseif (is_string($name) && $name) {
  69.             $route $this->getRouteByName($name$parameters);
  70.         } else {
  71.             $route $this->getRouteByContent($name$parameters);
  72.         }
  73.         if (!$route instanceof SymfonyRoute) {
  74.             $hint is_object($route) ? get_class($route) : gettype($route);
  75.             throw new RouteNotFoundException('Route of this document is not an instance of Symfony\Component\Routing\Route but: ' $hint);
  76.         }
  77.         $this->unsetLocaleIfNotNeeded($route$parameters);
  78.         return parent::generate($route$parameters$absolute);
  79.     }
  80.     /**
  81.      * Get the route by a string name.
  82.      *
  83.      * @param string $name
  84.      * @param array $parameters
  85.      *
  86.      * @return SymfonyRoute
  87.      *
  88.      * @throws RouteNotFoundException if there is no route found for the provided name
  89.      */
  90.     protected function getRouteByName($name, array $parameters)
  91.     {
  92.         $route $this->provider->getRouteByName($name);
  93.         if (null === $route) {
  94.             throw new RouteNotFoundException('No route found for name: ' $name);
  95.         }
  96.         return $this->getBestRequirementsRoute($route$parameters);
  97.     }
  98.     /**
  99.      * Determine if there is a route with matching locale associated with the
  100.      * given route via associated content.
  101.      *
  102.      * @param SymfonyRoute $route
  103.      * @param array $parameters
  104.      *
  105.      * @return SymfonyRoute either the passed route or an alternative with better locale
  106.      */
  107.     protected function getBestLocaleRoute(SymfonyRoute $route$parameters)
  108.     {
  109.         if (!$route instanceof RouteObjectInterface) {
  110.             // this route has no content, we can't get the alternatives
  111.             return $route;
  112.         }
  113.         $locale $this->getLocale($parameters);
  114.         if (!$this->checkLocaleRequirement($route$locale)) {
  115.             $content $route->getContent();
  116.             if ($content instanceof RouteReferrersReadInterface) {
  117.                 $routes $content->getRoutes();
  118.                 $contentRoute $this->getRouteByLocale($routes$locale);
  119.                 if ($contentRoute) {
  120.                     return $contentRoute;
  121.                 }
  122.             }
  123.         }
  124.         return $route;
  125.     }
  126.     /**
  127.      * Determine if there is a route with matching requirements associated with the
  128.      * given route via associated content.
  129.      *
  130.      * @param SymfonyRoute $route
  131.      * @param array $parameters
  132.      *
  133.      * @return SymfonyRoute either the passed route or an alternative with better locale
  134.      */
  135.     protected function getBestRequirementsRoute(SymfonyRoute $route$parameters)
  136.     {
  137.         if (!$route instanceof RouteObjectInterface) {
  138.             // this route has no content, we can't get the alternatives
  139.             return $route;
  140.         }
  141.         if (!$this->checkRequirements($route$parameters)) {
  142.             $content $route->getContent();
  143.             if ($content instanceof RouteReferrersReadInterface) {
  144.                 $routes $content->getRoutes();
  145.                 $contentRoute $this->getRouteMatchingRequirements($routes$parameters);
  146.                 if ($contentRoute) {
  147.                     return $contentRoute;
  148.                 }
  149.             }
  150.         }
  151.         return $route;
  152.     }
  153.     /**
  154.      * Get the route based on the $name that is an object implementing
  155.      * RouteReferrersReadInterface or a content found in the content repository
  156.      * with the content_id specified in parameters that is an instance of
  157.      * RouteReferrersReadInterface.
  158.      *
  159.      * Called in generate when there is no route given in the parameters.
  160.      *
  161.      * If there is more than one route for the content, tries to find the
  162.      * first one that matches the _locale (provided in $parameters or otherwise
  163.      * defaulting to the request locale).
  164.      *
  165.      * If no route with matching locale is found, falls back to just return the
  166.      * first route.
  167.      *
  168.      * @param mixed $name
  169.      * @param array $parameters which should contain a content field containing
  170.      *                          a RouteReferrersReadInterface object
  171.      *
  172.      * @return SymfonyRoute the route instance
  173.      *
  174.      * @throws RouteNotFoundException if no route can be determined
  175.      */
  176.     protected function getRouteByContent($name, &$parameters)
  177.     {
  178.         if ($name instanceof RouteReferrersReadInterface) {
  179.             $content $name;
  180.         } elseif (array_key_exists('content_id'$parameters)
  181.             && null !== $this->contentRepository
  182.         ) {
  183.             $content $this->contentRepository->findById($parameters['content_id']);
  184.             if (null === $content) {
  185.                 throw new RouteNotFoundException('The content repository found nothing at id ' $parameters['content_id']);
  186.             }
  187.             if (!$content instanceof RouteReferrersReadInterface) {
  188.                 throw new RouteNotFoundException('Content repository did not return a RouteReferrersReadInterface instance for id ' $parameters['content_id']);
  189.             }
  190.         } else {
  191.             $hint is_object($name) ? get_class($name) : gettype($name);
  192.             throw new RouteNotFoundException("The route name argument '$hint' is not RouteReferrersReadInterface instance and there is no 'content_id' parameter");
  193.         }
  194.         $routes $content->getRoutes();
  195.         if (=== count($routes)) {
  196.             $hint = ($this->contentRepository && $this->contentRepository->getContentId($content))
  197.                 ? $this->contentRepository->getContentId($content)
  198.                 : get_class($content);
  199.             throw new RouteNotFoundException('Content document has no route: ' $hint);
  200.         }
  201.         unset($parameters['content_id']);
  202.         $route $this->getRouteMatchingRequirements($routes$parameters);
  203.         if ($route) {
  204.             return $route;
  205.         }
  206.         // if none matched, randomly return the first one
  207.         if ($routes instanceof Collection) {
  208.             return $routes->first();
  209.         }
  210.         return reset($routes);
  211.     }
  212.     /**
  213.      * @param RouteCollection|SymfonyRoute[] $routes
  214.      * @param string $locale
  215.      *
  216.      * @return bool|SymfonyRoute false if no route requirement matches the provided locale
  217.      */
  218.     protected function getRouteByLocale($routes$locale)
  219.     {
  220.         foreach ($routes as $route) {
  221.             if (!$route instanceof SymfonyRoute) {
  222.                 continue;
  223.             }
  224.             if ($this->checkLocaleRequirement($route$locale)) {
  225.                 return $route;
  226.             }
  227.         }
  228.         return false;
  229.     }
  230.     /**
  231.      * @param $routes
  232.      * @param $parameters
  233.      *
  234.      * @return bool
  235.      */
  236.     protected function getRouteMatchingRequirements($routes$parameters)
  237.     {
  238.         $parameters['_locale'] = $this->getLocale($parameters);
  239.         foreach ($routes as $route) {
  240.             if (!$route instanceof SymfonyRoute) {
  241.                 continue;
  242.             }
  243.             if ($this->checkRequirements($route$parameters)) {
  244.                 return $route;
  245.             }
  246.         }
  247.         return false;
  248.     }
  249.     private function checkRequirements($route$parameters)
  250.     {
  251.         foreach ($parameters as $parameterName => $parameterValue) {
  252.             if ($this->routeMatchParameter($route$parameterName$parameterValue)) {
  253.                 unset($parameters[$parameterName]);
  254.             } else {
  255.                 return false;
  256.             }
  257.         }
  258.         if (empty($parameters)) {
  259.             return true;
  260.         }
  261.         return false;
  262.     }
  263.     /**
  264.      * @param SymfonyRoute $route
  265.      * @param string $locale
  266.      *
  267.      * @return bool true if there is either no $locale, no _locale requirement
  268.      *              on the route or if the requirement and the passed $locale
  269.      *              match
  270.      */
  271.     private function checkLocaleRequirement(SymfonyRoute $route$locale)
  272.     {
  273.         return !$locale
  274.             || !$route->getRequirement('_locale')
  275.             || preg_match('/' $route->getRequirement('_locale') . '/'$locale);
  276.     }
  277.     private function routeMatchParameter(SymfonyRoute $route$parameterName$parameterValue)
  278.     {
  279.         return !$route->getRequirement($parameterName)
  280.             || preg_match('/' $route->getRequirement($parameterName) . '/'$parameterValue);
  281.     }
  282.     /**
  283.      * Determine the locale to be used with this request.
  284.      *
  285.      * @param array $parameters the parameters determined by the route
  286.      *
  287.      * @return string the locale following of the parameters or any other
  288.      *                information the router has available. defaultLocale if no
  289.      *                other locale can be determined
  290.      */
  291.     protected function getLocale($parameters)
  292.     {
  293.         if (array_key_exists('_locale'$parameters)) {
  294.             return $parameters['_locale'];
  295.         }
  296.         if ($this->getContext()->hasParameter('_locale')) {
  297.             return $this->getContext()->getParameter('_locale');
  298.         }
  299.         return $this->defaultLocale;
  300.     }
  301.     /**
  302.      * Overwrite the locale to be used by default if there is neither one in
  303.      * the parameters when building the route nor a request available (i.e. CLI).
  304.      *
  305.      * @param string $locale
  306.      */
  307.     public function setDefaultLocale($locale)
  308.     {
  309.         $this->defaultLocale $locale;
  310.     }
  311.     /**
  312.      * We additionally support empty name and data in parameters and RouteAware content.
  313.      *
  314.      * {@inheritdoc}
  315.      */
  316.     public function supports($name)
  317.     {
  318.         return !$name || parent::supports($name) || $name instanceof RouteReferrersReadInterface;
  319.     }
  320.     /**
  321.      * {@inheritdoc}
  322.      */
  323.     public function getRouteDebugMessage($name, array $parameters = [])
  324.     {
  325.         if (!$name && array_key_exists('content_id'$parameters)) {
  326.             return 'Content id ' $parameters['content_id'];
  327.         }
  328.         if ($name instanceof RouteReferrersReadInterface) {
  329.             return 'Route aware content ' parent::getRouteDebugMessage($name$parameters);
  330.         }
  331.         return parent::getRouteDebugMessage($name$parameters);
  332.     }
  333.     /**
  334.      * If the _locale parameter is allowed by the requirements of the route
  335.      * and it is the default locale, remove it from the parameters so that we
  336.      * do not get an unneeded ?_locale= query string.
  337.      *
  338.      * @param SymfonyRoute $route The route being generated
  339.      * @param array $parameters The parameters used, will be modified to
  340.      *                                 remove the _locale field if needed
  341.      */
  342.     protected function unsetLocaleIfNotNeeded(SymfonyRoute $route, array &$parameters)
  343.     {
  344.         $locale $this->getLocale($parameters);
  345.         if (null !== $locale
  346.             && preg_match('/' $route->getRequirement('_locale') . '/'$locale)
  347.             && $locale == $route->getDefault('_locale')
  348.         ) {
  349.             $compiledRoute $route->compile();
  350.             if (!in_array('_locale'$compiledRoute->getVariables())) {
  351.                 unset($parameters['_locale']);
  352.             }
  353.         }
  354.     }
  355. }