<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NoResultException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* DoctrineParamConverter.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class DoctrineParamConverter implements ParamConverterInterface
{
/**
* @var ManagerRegistry
*/
private $registry;
/**
* @var ExpressionLanguage
*/
private $language;
/**
* @var array
*/
private $defaultOptions;
public function __construct(ManagerRegistry $registry = null, ExpressionLanguage $expressionLanguage = null, array $options = [])
{
$this->registry = $registry;
$this->language = $expressionLanguage;
$defaultValues = [
'entity_manager' => null,
'exclude' => [],
'mapping' => [],
'strip_null' => false,
'expr' => null,
'id' => null,
'repository_method' => null,
'map_method_signature' => false,
'evict_cache' => false,
];
$this->defaultOptions = array_merge($defaultValues, $options);
}
/**
* {@inheritdoc}
*
* @throws \LogicException When unable to guess how to get a Doctrine instance from the request information
* @throws NotFoundHttpException When object not found
*/
public function apply(Request $request, ParamConverter $configuration)
{
$name = $configuration->getName();
$class = $configuration->getClass();
$options = $this->getOptions($configuration);
if (null === $request->attributes->get($name, false)) {
$configuration->setIsOptional(true);
}
$errorMessage = null;
if ($expr = $options['expr']) {
$object = $this->findViaExpression($class, $request, $expr, $options, $configuration);
if (null === $object) {
$errorMessage = sprintf('The expression "%s" returned null', $expr);
}
// find by identifier?
} elseif (false === $object = $this->find($class, $request, $options, $name)) {
// find by criteria
if (false === $object = $this->findOneBy($class, $request, $options)) {
if ($configuration->isOptional()) {
$object = null;
} else {
throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name));
}
}
}
if (null === $object && false === $configuration->isOptional()) {
$message = sprintf('%s object not found by the @%s annotation.', $class, $this->getAnnotationName($configuration));
if ($errorMessage) {
$message .= ' '.$errorMessage;
}
throw new NotFoundHttpException($message);
}
$request->attributes->set($name, $object);
return true;
}
private function find($class, Request $request, $options, $name)
{
if ($options['mapping'] || $options['exclude']) {
return false;
}
$id = $this->getIdentifier($request, $options, $name);
if (false === $id || null === $id) {
return false;
}
if ($options['repository_method']) {
$method = $options['repository_method'];
} else {
$method = 'find';
}
$om = $this->getManager($options['entity_manager'], $class);
if ($options['evict_cache'] && $om instanceof EntityManagerInterface) {
$cacheProvider = $om->getCache();
if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) {
$cacheProvider->evictEntity($class, $id);
}
}
try {
return $om->getRepository($class)->$method($id);
} catch (NoResultException $e) {
return;
} catch (ConversionException $e) {
return;
}
}
private function getIdentifier(Request $request, $options, $name)
{
if (null !== $options['id']) {
if (!\is_array($options['id'])) {
$name = $options['id'];
} elseif (\is_array($options['id'])) {
$id = [];
foreach ($options['id'] as $field) {
if (false !== strstr($field, '%s')) {
// Convert "%s_uuid" to "foobar_uuid"
$field = sprintf($field, $name);
}
$id[$field] = $request->attributes->get($field);
}
return $id;
}
}
if ($request->attributes->has($name)) {
return $request->attributes->get($name);
}
if ($request->attributes->has('id') && !$options['id']) {
return $request->attributes->get('id');
}
return false;
}
private function findOneBy($class, Request $request, $options)
{
if (!$options['mapping']) {
$keys = $request->attributes->keys();
$options['mapping'] = $keys ? array_combine($keys, $keys) : [];
}
foreach ($options['exclude'] as $exclude) {
unset($options['mapping'][$exclude]);
}
if (!$options['mapping']) {
return false;
}
// if a specific id has been defined in the options and there is no corresponding attribute
// return false in order to avoid a fallback to the id which might be of another object
if ($options['id'] && null === $request->attributes->get($options['id'])) {
return false;
}
$criteria = [];
$em = $this->getManager($options['entity_manager'], $class);
$metadata = $em->getClassMetadata($class);
$mapMethodSignature = $options['repository_method']
&& $options['map_method_signature']
&& true === $options['map_method_signature'];
foreach ($options['mapping'] as $attribute => $field) {
if ($metadata->hasField($field)
|| ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field))
|| $mapMethodSignature) {
$criteria[$field] = $request->attributes->get($attribute);
}
}
if ($options['strip_null']) {
$criteria = array_filter($criteria, function ($value) {
return null !== $value;
});
}
if (!$criteria) {
return false;
}
if ($options['repository_method']) {
$repositoryMethod = $options['repository_method'];
} else {
$repositoryMethod = 'findOneBy';
}
try {
if ($mapMethodSignature) {
return $this->findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria);
}
return $em->getRepository($class)->$repositoryMethod($criteria);
} catch (NoResultException $e) {
return;
} catch (ConversionException $e) {
return;
}
}
private function findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria)
{
$arguments = [];
$repository = $em->getRepository($class);
$ref = new \ReflectionMethod($repository, $repositoryMethod);
foreach ($ref->getParameters() as $parameter) {
if (\array_key_exists($parameter->name, $criteria)) {
$arguments[] = $criteria[$parameter->name];
} elseif ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
} else {
throw new \InvalidArgumentException(sprintf('Repository method "%s::%s" requires that you provide a value for the "$%s" argument.', \get_class($repository), $repositoryMethod, $parameter->name));
}
}
return $ref->invokeArgs($repository, $arguments);
}
private function findViaExpression($class, Request $request, $expression, $options, ParamConverter $configuration)
{
if (null === $this->language) {
throw new \LogicException(sprintf('To use the @%s tag with the "expr" option, you need to install the ExpressionLanguage component.', $this->getAnnotationName($configuration)));
}
$repository = $this->getManager($options['entity_manager'], $class)->getRepository($class);
$variables = array_merge($request->attributes->all(), ['repository' => $repository]);
try {
return $this->language->evaluate($expression, $variables);
} catch (NoResultException $e) {
return;
} catch (ConversionException $e) {
return;
} catch (SyntaxError $e) {
throw new \LogicException(sprintf('Error parsing expression -- %s -- (%s)', $expression, $e->getMessage()), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function supports(ParamConverter $configuration)
{
// if there is no manager, this means that only Doctrine DBAL is configured
if (null === $this->registry || !\count($this->registry->getManagerNames())) {
return false;
}
if (null === $configuration->getClass()) {
return false;
}
$options = $this->getOptions($configuration, false);
// Doctrine Entity?
$em = $this->getManager($options['entity_manager'], $configuration->getClass());
if (null === $em) {
return false;
}
return !$em->getMetadataFactory()->isTransient($configuration->getClass());
}
private function getOptions(ParamConverter $configuration, $strict = true)
{
$passedOptions = $configuration->getOptions();
if (isset($passedOptions['repository_method'])) {
@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);
}
if (isset($passedOptions['map_method_signature'])) {
@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);
}
$extraKeys = array_diff(array_keys($passedOptions), array_keys($this->defaultOptions));
if ($extraKeys && $strict) {
throw new \InvalidArgumentException(sprintf('Invalid option(s) passed to @%s: %s', $this->getAnnotationName($configuration), implode(', ', $extraKeys)));
}
return array_replace($this->defaultOptions, $passedOptions);
}
private function getManager($name, $class)
{
if (null === $name) {
return $this->registry->getManagerForClass($class);
}
return $this->registry->getManager($name);
}
private function getAnnotationName(ParamConverter $configuration)
{
$r = new \ReflectionClass($configuration);
return $r->getShortName();
}
}