<?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\DBAL\Types\ConversionException; 
use Doctrine\ORM\EntityManagerInterface; 
use Doctrine\ORM\NoResultException; 
use Doctrine\Persistence\ManagerRegistry; 
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(); 
    } 
}