ContentAwareGenerator.php 11 KB

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