diff --git a/DependencyInjection/PropelExtension.php b/DependencyInjection/PropelExtension.php index fa9b83a..001b38c 100644 --- a/DependencyInjection/PropelExtension.php +++ b/DependencyInjection/PropelExtension.php @@ -48,7 +48,7 @@ class PropelExtension extends Extension if (!$container->hasDefinition('propel')) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('propel.xml'); - //$loader->load('converters.xml'); + $loader->load('converters.xml'); } // build properties diff --git a/Request/ParamConverter/PropelParamConverter.php b/Request/ParamConverter/PropelParamConverter.php new file mode 100644 index 0000000..73834ea --- /dev/null +++ b/Request/ParamConverter/PropelParamConverter.php @@ -0,0 +1,281 @@ + column + * exclude : take an array of routeParam to exclude from the conversion process + * + * + * @author Jérémie Augustin + */ +class PropelParamConverter implements ParamConverterInterface +{ + /** + * the pk column (e.g. id) + * @var string + */ + protected $pk; + + /** + * list of column/value to use with filterBy + * @var array + */ + protected $filters = array(); + + /** + * list of route parameters to exclude from the conversion process + * @var array + */ + protected $exclude = array(); + + /** + * list of with option use to hydrate related object + * @var array + */ + protected $withs; + + /** + * @var bool + */ + protected $hasWith = false; + + /** + * @var RouterInterface + */ + protected $router; + + public function setRouter(RouterInterface $router = null) + { + $this->router = $router; + } + + /** + * @param Request $request + * @param ConfigurationInterface $configuration + * + * @return bool + * + * @throws \LogicException + * @throws NotFoundHttpException + * @throws \Exception + */ + public function apply(Request $request, ConfigurationInterface $configuration) + { + $class = $configuration->getClass(); + $classQuery = $class . 'Query'; + $classTableMap = $class::TABLE_MAP; + $this->filters = array(); + $this->exclude = array(); + + if (!class_exists($classQuery)) { + throw new \Exception(sprintf('The %s Query class does not exist', $classQuery)); + } + + $tableMap = new $classTableMap(); + $pkColumns = $tableMap->getPrimaryKeys(); + + if (count($pkColumns) === 1) { + $pk = array_pop($pkColumns); + $this->pk = strtolower($pk->getName()); + } + + $options = $configuration->getOptions(); + + // Check route options for converter options, if there are non provided. + if (empty($options) && $request->attributes->has('_route') && $this->router && $configuration instanceof ParamConverter) { + $converterOption = $this->router->getRouteCollection()->get($request->attributes->get('_route'))->getOption('propel_converter'); + if (!empty($converterOption[$configuration->getName()])) { + $options = $converterOption[$configuration->getName()]; + } + } + + if (isset($options['mapping'])) { + // We use the mapping for calling findPk or filterBy + foreach ($options['mapping'] as $routeParam => $column) { + if ($request->attributes->has($routeParam)) { + if ($this->pk === $column) { + $this->pk = $routeParam; + } else { + $this->filters[$column] = $request->attributes->get($routeParam); + } + } + } + } else { + $this->exclude = isset($options['exclude'])? $options['exclude'] : array(); + $this->filters = $request->attributes->all(); + } + + $this->withs = isset($options['with'])? is_array($options['with'])? $options['with'] : array($options['with']) : array(); + + // find by Pk + if (false === $object = $this->findPk($classQuery, $request)) { + // find by criteria + if (false === $object = $this->findOneBy($classQuery, $request)) { + if ($configuration->isOptional()) { + //we find nothing but the object is optional + $object = null; + } else { + throw new \LogicException('Unable to guess how to get a Propel object from the request information.'); + } + } + } + + if (null === $object && false === $configuration->isOptional()) { + throw new NotFoundHttpException(sprintf('%s object not found.', $configuration->getClass())); + } + + $request->attributes->set($configuration->getName(), $object); + + return true; + } + + /** + * @param ConfigurationInterface $configuration + * + * @return bool + */ + public function supports(ConfigurationInterface $configuration) + { + if (null === ($classname = $configuration->getClass())) { + return false; + } + + if (!class_exists($classname)) { + return false; + } + // Propel Class? + $class = new \ReflectionClass($configuration->getClass()); + if ($class->implementsInterface('\Propel\Runtime\ActiveRecord\ActiveRecordInterface')) { + return true; + } + + return false; + } + + /** + * Try to find the object with the id + * + * @param string $classQuery the query class + * @param Request $request + * + * @return mixed + */ + protected function findPk($classQuery, Request $request) + { + if (in_array($this->pk, $this->exclude) || !$request->attributes->has($this->pk)) { + return false; + } + + $query = $this->getQuery($classQuery); + + if (!$this->hasWith) { + return $query->findPk($request->attributes->get($this->pk)); + } else { + return $query->filterByPrimaryKey($request->attributes->get($this->pk))->find()->getFirst(); + } + } + + /** + * Try to find the object with all params from the $request + * + * @param string $classQuery the query class + * @param Request $request + * + * @return mixed + */ + protected function findOneBy($classQuery, Request $request) + { + $query = $this->getQuery($classQuery); + $hasCriteria = false; + foreach ($this->filters as $column => $value) { + if (!in_array($column, $this->exclude)) { + try { + $query->{'filterBy' . PropelInflector::camelize($column)}($value); + $hasCriteria = true; + } catch (\PropelException $e) { } + } + } + + if (!$hasCriteria) { + return false; + } + + if (!$this->hasWith) { + return $query->findOne(); + } else { + return $query->find()->getFirst(); + } + } + + /** + * Init the query class with optional joinWith + * + * @param string $classQuery + * + * @return \ModelCriteria + * + * @throws \Exception + */ + protected function getQuery($classQuery) + { + $query = $classQuery::create(); + + foreach ($this->withs as $with) { + if (is_array($with)) { + if (2 == count($with)) { + $query->joinWith($with[0], $this->getValidJoin($with)); + $this->hasWith = true; + } else { + throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid, + only string relation name (e.g. "Book") or an array with two keys (e.g. {"Book", "LEFT_JOIN"}) are allowed', + var_export($with, true))); + } + } else { + $query->joinWith($with); + $this->hasWith = true; + } + } + + return $query; + } + + /** + * Return the valid join Criteria base on the with parameter + * + * @param array $with + * + * @return string + * + * @throws \Exception + */ + protected function getValidJoin($with) + { + switch (trim(str_replace(array('_', 'JOIN'), '', strtoupper($with[1])))) { + case 'LEFT': + return \Criteria::LEFT_JOIN; + case 'RIGHT': + return \Criteria::RIGHT_JOIN; + case 'INNER': + return \Criteria::INNER_JOIN; + } + + throw new \Exception(sprintf('ParamConverter : "with" parameter "%s" is invalid, + only "left", "right" or "inner" are allowed for join option', + var_export($with, true))); + } + +} diff --git a/Resources/config/converters.xml b/Resources/config/converters.xml new file mode 100644 index 0000000..40af5d2 --- /dev/null +++ b/Resources/config/converters.xml @@ -0,0 +1,20 @@ + + + + + + Propel\PropelBundle\Request\ParamConverter\PropelParamConverter + + + + + + + + + + + + diff --git a/Util/PropelInflector.php b/Util/PropelInflector.php new file mode 100644 index 0000000..a9765d6 --- /dev/null +++ b/Util/PropelInflector.php @@ -0,0 +1,31 @@ + + */ +class PropelInflector +{ + /** + * Camelize a word. + * Inspirated by https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Util/Inflector.php + * + * @param string $word The word to camelize. + * @return string + */ + public static function camelize($word) + { + return lcfirst(str_replace(" ", "", ucwords(strtr($word, "_-", " ")))); + } +}