vendor/sensio/framework-extra-bundle/src/Request/ParamConverter/DoctrineParamConverter.php line 309

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  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 Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter;
  11. use Doctrine\DBAL\Types\ConversionException;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Doctrine\ORM\NoResultException;
  14. use Doctrine\Persistence\ManagerRegistry;
  15. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  16. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  17. use Symfony\Component\ExpressionLanguage\SyntaxError;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  20. /**
  21. * DoctrineParamConverter.
  22. *
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. */
  25. class DoctrineParamConverter implements ParamConverterInterface
  26. {
  27. /**
  28. * @var ManagerRegistry
  29. */
  30. private $registry;
  31. /**
  32. * @var ExpressionLanguage
  33. */
  34. private $language;
  35. /**
  36. * @var array
  37. */
  38. private $defaultOptions;
  39. public function __construct(ManagerRegistry $registry = null, ExpressionLanguage $expressionLanguage = null, array $options = [])
  40. {
  41. $this->registry = $registry;
  42. $this->language = $expressionLanguage;
  43. $defaultValues = [
  44. 'entity_manager' => null,
  45. 'exclude' => [],
  46. 'mapping' => [],
  47. 'strip_null' => false,
  48. 'expr' => null,
  49. 'id' => null,
  50. 'repository_method' => null,
  51. 'map_method_signature' => false,
  52. 'evict_cache' => false,
  53. ];
  54. $this->defaultOptions = array_merge($defaultValues, $options);
  55. }
  56. /**
  57. * {@inheritdoc}
  58. *
  59. * @throws \LogicException When unable to guess how to get a Doctrine instance from the request information
  60. * @throws NotFoundHttpException When object not found
  61. */
  62. public function apply(Request $request, ParamConverter $configuration)
  63. {
  64. $name = $configuration->getName();
  65. $class = $configuration->getClass();
  66. $options = $this->getOptions($configuration);
  67. if (null === $request->attributes->get($name, false)) {
  68. $configuration->setIsOptional(true);
  69. }
  70. $errorMessage = null;
  71. if ($expr = $options['expr']) {
  72. $object = $this->findViaExpression($class, $request, $expr, $options, $configuration);
  73. if (null === $object) {
  74. $errorMessage = sprintf('The expression "%s" returned null', $expr);
  75. }
  76. // find by identifier?
  77. } elseif (false === $object = $this->find($class, $request, $options, $name)) {
  78. // find by criteria
  79. if (false === $object = $this->findOneBy($class, $request, $options)) {
  80. if ($configuration->isOptional()) {
  81. $object = null;
  82. } else {
  83. throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name));
  84. }
  85. }
  86. }
  87. if (null === $object && false === $configuration->isOptional()) {
  88. $message = sprintf('%s object not found by the @%s annotation.', $class, $this->getAnnotationName($configuration));
  89. if ($errorMessage) {
  90. $message .= ' '.$errorMessage;
  91. }
  92. throw new NotFoundHttpException($message);
  93. }
  94. $request->attributes->set($name, $object);
  95. return true;
  96. }
  97. private function find($class, Request $request, $options, $name)
  98. {
  99. if ($options['mapping'] || $options['exclude']) {
  100. return false;
  101. }
  102. $id = $this->getIdentifier($request, $options, $name);
  103. if (false === $id || null === $id) {
  104. return false;
  105. }
  106. if ($options['repository_method']) {
  107. $method = $options['repository_method'];
  108. } else {
  109. $method = 'find';
  110. }
  111. $om = $this->getManager($options['entity_manager'], $class);
  112. if ($options['evict_cache'] && $om instanceof EntityManagerInterface) {
  113. $cacheProvider = $om->getCache();
  114. if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) {
  115. $cacheProvider->evictEntity($class, $id);
  116. }
  117. }
  118. try {
  119. return $om->getRepository($class)->$method($id);
  120. } catch (NoResultException $e) {
  121. return;
  122. } catch (ConversionException $e) {
  123. return;
  124. }
  125. }
  126. private function getIdentifier(Request $request, $options, $name)
  127. {
  128. if (null !== $options['id']) {
  129. if (!\is_array($options['id'])) {
  130. $name = $options['id'];
  131. } elseif (\is_array($options['id'])) {
  132. $id = [];
  133. foreach ($options['id'] as $field) {
  134. if (false !== strstr($field, '%s')) {
  135. // Convert "%s_uuid" to "foobar_uuid"
  136. $field = sprintf($field, $name);
  137. }
  138. $id[$field] = $request->attributes->get($field);
  139. }
  140. return $id;
  141. }
  142. }
  143. if ($request->attributes->has($name)) {
  144. return $request->attributes->get($name);
  145. }
  146. if ($request->attributes->has('id') && !$options['id']) {
  147. return $request->attributes->get('id');
  148. }
  149. return false;
  150. }
  151. private function findOneBy($class, Request $request, $options)
  152. {
  153. if (!$options['mapping']) {
  154. $keys = $request->attributes->keys();
  155. $options['mapping'] = $keys ? array_combine($keys, $keys) : [];
  156. }
  157. foreach ($options['exclude'] as $exclude) {
  158. unset($options['mapping'][$exclude]);
  159. }
  160. if (!$options['mapping']) {
  161. return false;
  162. }
  163. // if a specific id has been defined in the options and there is no corresponding attribute
  164. // return false in order to avoid a fallback to the id which might be of another object
  165. if ($options['id'] && null === $request->attributes->get($options['id'])) {
  166. return false;
  167. }
  168. $criteria = [];
  169. $em = $this->getManager($options['entity_manager'], $class);
  170. $metadata = $em->getClassMetadata($class);
  171. $mapMethodSignature = $options['repository_method']
  172. && $options['map_method_signature']
  173. && true === $options['map_method_signature'];
  174. foreach ($options['mapping'] as $attribute => $field) {
  175. if ($metadata->hasField($field)
  176. || ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field))
  177. || $mapMethodSignature) {
  178. $criteria[$field] = $request->attributes->get($attribute);
  179. }
  180. }
  181. if ($options['strip_null']) {
  182. $criteria = array_filter($criteria, function ($value) {
  183. return null !== $value;
  184. });
  185. }
  186. if (!$criteria) {
  187. return false;
  188. }
  189. if ($options['repository_method']) {
  190. $repositoryMethod = $options['repository_method'];
  191. } else {
  192. $repositoryMethod = 'findOneBy';
  193. }
  194. try {
  195. if ($mapMethodSignature) {
  196. return $this->findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria);
  197. }
  198. return $em->getRepository($class)->$repositoryMethod($criteria);
  199. } catch (NoResultException $e) {
  200. return;
  201. } catch (ConversionException $e) {
  202. return;
  203. }
  204. }
  205. private function findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria)
  206. {
  207. $arguments = [];
  208. $repository = $em->getRepository($class);
  209. $ref = new \ReflectionMethod($repository, $repositoryMethod);
  210. foreach ($ref->getParameters() as $parameter) {
  211. if (\array_key_exists($parameter->name, $criteria)) {
  212. $arguments[] = $criteria[$parameter->name];
  213. } elseif ($parameter->isDefaultValueAvailable()) {
  214. $arguments[] = $parameter->getDefaultValue();
  215. } else {
  216. throw new \InvalidArgumentException(sprintf('Repository method "%s::%s" requires that you provide a value for the "$%s" argument.', \get_class($repository), $repositoryMethod, $parameter->name));
  217. }
  218. }
  219. return $ref->invokeArgs($repository, $arguments);
  220. }
  221. private function findViaExpression($class, Request $request, $expression, $options, ParamConverter $configuration)
  222. {
  223. if (null === $this->language) {
  224. throw new \LogicException(sprintf('To use the @%s tag with the "expr" option, you need to install the ExpressionLanguage component.', $this->getAnnotationName($configuration)));
  225. }
  226. $repository = $this->getManager($options['entity_manager'], $class)->getRepository($class);
  227. $variables = array_merge($request->attributes->all(), ['repository' => $repository]);
  228. try {
  229. return $this->language->evaluate($expression, $variables);
  230. } catch (NoResultException $e) {
  231. return;
  232. } catch (ConversionException $e) {
  233. return;
  234. } catch (SyntaxError $e) {
  235. throw new \LogicException(sprintf('Error parsing expression -- "%s" -- (%s).', $expression, $e->getMessage()), 0, $e);
  236. }
  237. }
  238. /**
  239. * {@inheritdoc}
  240. */
  241. public function supports(ParamConverter $configuration)
  242. {
  243. // if there is no manager, this means that only Doctrine DBAL is configured
  244. if (null === $this->registry || !\count($this->registry->getManagerNames())) {
  245. return false;
  246. }
  247. if (null === $configuration->getClass()) {
  248. return false;
  249. }
  250. $options = $this->getOptions($configuration, false);
  251. // Doctrine Entity?
  252. $em = $this->getManager($options['entity_manager'], $configuration->getClass());
  253. if (null === $em) {
  254. return false;
  255. }
  256. return !$em->getMetadataFactory()->isTransient($configuration->getClass());
  257. }
  258. private function getOptions(ParamConverter $configuration, $strict = true)
  259. {
  260. $passedOptions = $configuration->getOptions();
  261. if (isset($passedOptions['repository_method'])) {
  262. @trigger_error('The repository_method option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', E_USER_DEPRECATED);
  263. }
  264. if (isset($passedOptions['map_method_signature'])) {
  265. @trigger_error('The map_method_signature option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', E_USER_DEPRECATED);
  266. }
  267. $extraKeys = array_diff(array_keys($passedOptions), array_keys($this->defaultOptions));
  268. if ($extraKeys && $strict) {
  269. throw new \InvalidArgumentException(sprintf('Invalid option(s) passed to @%s: "%s".', $this->getAnnotationName($configuration), implode(', ', $extraKeys)));
  270. }
  271. return array_replace($this->defaultOptions, $passedOptions);
  272. }
  273. private function getManager($name, $class)
  274. {
  275. if (null === $name) {
  276. return $this->registry->getManagerForClass($class);
  277. }
  278. return $this->registry->getManager($name);
  279. }
  280. private function getAnnotationName(ParamConverter $configuration)
  281. {
  282. $r = new \ReflectionClass($configuration);
  283. return $r->getShortName();
  284. }
  285. }