From 6119fc9806c84143fc0696c1ccb48d7ad0c557a4 Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 18:54:32 +0100 Subject: [PATCH 1/8] Initial commit for new sf3 choiceList --- Form/ChoiceList/ModelChoiceList.php | 514 ------------------------- Form/ChoiceList/PropelChoiceLoader.php | 211 ++++++++++ Form/PropelExtension.php | 29 +- Form/Type/ModelType.php | 218 ++++++++--- Form/TypeGuesser.php | 17 + 5 files changed, 420 insertions(+), 569 deletions(-) delete mode 100644 Form/ChoiceList/ModelChoiceList.php create mode 100644 Form/ChoiceList/PropelChoiceLoader.php diff --git a/Form/ChoiceList/ModelChoiceList.php b/Form/ChoiceList/ModelChoiceList.php deleted file mode 100644 index f488253..0000000 --- a/Form/ChoiceList/ModelChoiceList.php +++ /dev/null @@ -1,514 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Propel\Bundle\PropelBundle\Form\ChoiceList; - -use Propel\Runtime\ActiveQuery\ModelCriteria; -use Propel\Runtime\ActiveRecord\ActiveRecordInterface; -use Propel\Runtime\Map\ColumnMap; -use Symfony\Component\Form\Exception\StringCastException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; -use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * A choice list for object choices based on Propel model. - * - * @author William Durand - * @author Toni Uebernickel - */ -class ModelChoiceList extends ObjectChoiceList -{ - /** - * The fields of which the identifier of the underlying class consists - * - * This property should only be accessed through identifier. - * - * @var array - */ - protected $identifier = array(); - - /** - * The query to retrieve the choices of this list. - * - * @var ModelCriteria - */ - protected $query; - - /** - * The query to retrieve the preferred choices for this list. - * - * @var ModelCriteria - */ - protected $preferredQuery; - - /** - * Whether the model objects have already been loaded. - * - * @var bool - */ - protected $loaded = false; - - /** - * Whether to use the identifier for index generation. - * - * @var bool - */ - private $identifierAsIndex = false; - - /** - * Constructor. - * - * @see \Symfony\Bridge\Propel1\Form\Type\ModelType How to use the preferred choices. - * - * @param string $class The FQCN of the model class to be loaded. - * @param string $labelPath A property path pointing to the property used for the choice - * labels. - * @param array $choices An optional array to use, rather than fetching the models. - * @param ModelCriteria $queryObject The query to use retrieving model data from database. - * @param string $groupPath A property path pointing to the property used to group the - * choices. - * @param array|ModelCriteria $preferred The preferred items of this choice. - * Either an array if $choices is given, - * or a ModelCriteria to be merged with the $queryObject. - * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. - * @param string $useAsIdentifier a custom unique column (eg slug) to use instead of primary - * key. - * - * @throws MissingOptionsException In case the class parameter is empty. - * @throws InvalidOptionsException In case the query class is not found. - */ - public function __construct( - $class, - $labelPath = null, - $choices = null, - $queryObject = null, - $groupPath = null, - $preferred = array(), - PropertyAccessorInterface $propertyAccessor = null, - $useAsIdentifier = null - ) { - $this->class = $class; - $queryClass = $this->class . 'Query'; - if (!class_exists($queryClass)) { - if (empty($this->class)) { - throw new MissingOptionsException('The "class" parameter is empty, you should provide the model class'); - } - throw new InvalidOptionsException( - sprintf( - 'The query class "%s" is not found, you should provide the FQCN of the model class', - $queryClass - ) - ); - } - $query = new $queryClass(); - $this->query = $queryObject ?: $query; - if ($useAsIdentifier) { - $this->identifier = array($this->query->getTableMap()->getColumn($useAsIdentifier)); - } else { - $this->identifier = $this->query->getTableMap()->getPrimaryKeys(); - } - $this->loaded = is_array($choices) || $choices instanceof \Traversable; - if ($preferred instanceof ModelCriteria) { - $this->preferredQuery = $preferred->mergeWith($this->query); - } - if (!$this->loaded) { - // Make sure the constraints of the parent constructor are - // fulfilled - $choices = array(); - $preferred = array(); - } - if (1 === count($this->identifier) && $this->isScalar(current($this->identifier))) { - $this->identifierAsIndex = true; - } - parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor); - } - - /** - * Returns the class name of the model. - * - * @return string - */ - public function getClass() - { - return $this->class; - } - - /** - * {@inheritdoc} - */ - public function getChoices() - { - $this->load(); - - return parent::getChoices(); - } - - /** - * {@inheritdoc} - */ - public function getValues() - { - $this->load(); - - return parent::getValues(); - } - - /** - * {@inheritdoc} - */ - public function getPreferredViews() - { - $this->load(); - - return parent::getPreferredViews(); - } - - /** - * {@inheritdoc} - */ - public function getRemainingViews() - { - $this->load(); - - return parent::getRemainingViews(); - } - - /** - * {@inheritdoc} - */ - public function getChoicesForValues(array $values) - { - if (empty($values)) { - return array(); - } - - /** - * This performance optimization reflects a common scenario: - * * A simple select of a model entry. - * * The choice option "expanded" is set to false. - * * The current request is the submission of the selected value. - * - * @see \Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer::reverseTransform - * @see \Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer::reverseTransform - */ - if (!$this->loaded) { - if (1 === count($this->identifier)) { - $filterBy = 'filterBy' . current($this->identifier)->getPhpName(); - // The initial query is cloned, so all additional filters are applied correctly. - $query = clone $this->query; - $query - ->$filterBy($values) - ; - $result = iterator_to_array($query->find()); - // Preserve the keys as provided by the values. - $models = array(); - foreach ($values as $index => $value) { - foreach ($result as $eachModel) { - if ($value === $this->createValue($eachModel)) { - // Make sure to convert to the right format - $models[$index] = $this->fixChoice($eachModel); - // If all values have been assigned, skip further loops. - unset($values[$index]); - if (0 === count($values)) { - break 2; - } - } - } - } - - return $models; - } - } - $this->load(); - - return parent::getChoicesForValues($values); - } - - /** - * {@inheritdoc} - */ - public function getValuesForChoices(array $models) - { - if (empty($models)) { - return array(); - } - if (!$this->loaded) { - /** - * This performance optimization assumes the validation of the respective values will be done by other means. - * - * It correlates with the performance optimization in {@link ModelChoiceList::getChoicesForValues()} - * as it won't load the actual entries from the database. - * - * @see \Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer::transform - * @see \Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer::transform - */ - if (1 === count($this->identifier)) { - $values = array(); - foreach ($models as $index => $model) { - if ($model instanceof $this->class) { - // Make sure to convert to the right format - $values[$index] = $this->fixValue(current($this->getIdentifierValues($model))); - } - } - - return $values; - } - } - - $this->load(); - $values = array(); - $availableValues = $this->getValues(); - - /* - * Overwriting default implementation. - * - * The two objects may represent the same entry in the database, - * but if they originated from different queries, there are not the same object within the code. - * - * This happens when using m:n relations with either sides model as data_class of the form. - * The choicelist will retrieve the list of available related models with a different query, resulting in different objects. - */ - $choices = $this->fixChoices($models); - foreach ($choices as $i => $givenChoice) { - if (null === $givenChoice) { - continue; - } - foreach ($this->getChoices() as $j => $choice) { - if ($this->isEqual($choice, $givenChoice)) { - $values[$i] = $availableValues[$j]; - // If all choices have been assigned, skip further loops. - unset($choices[$i]); - if (0 === count($choices)) { - break 2; - } - } - } - } - - return $values; - } - - /** - * {@inheritdoc} - * - * @deprecated Deprecated since version 2.4, to be removed in 3.0. - */ - public function getIndicesForChoices(array $models) - { - if (empty($models)) { - return array(); - } - $indices = array(); - - if (!$this->loaded) { - // Optimize performance for single-field identifiers. We already - // know that the IDs are used as indices - - // Attention: This optimization does not check choices for existence - if ($this->identifierAsIndex) { - foreach ($models as $model) { - if ($model instanceof $this->class) { - // Make sure to convert to the right format - $indices[] = $this->fixIndex(current($this->getIdentifierValues($model))); - } - } - - return $indices; - } - - $this->load(); - } - - /* - * Overwriting default implementation. - * - * The two objects may represent the same entry in the database, - * but if they originated from different queries, there are not the same object within the code. - * - * This happens when using m:n relations with either sides model as data_class of the form. - * The choicelist will retrieve the list of available related models with a different query, resulting in different objects. - */ - $choices = $this->fixChoices($models); - foreach ($choices as $i => $givenChoice) { - if (null === $givenChoice) { - continue; - } - foreach ($this->getChoices() as $j => $choice) { - if ($this->isEqual($choice, $givenChoice)) { - $indices[$i] = $j; - // If all choices have been assigned, skip further loops. - unset($choices[$i]); - if (0 === count($choices)) { - break 2; - } - } - } - } - - return $indices; - } - - /** - * {@inheritdoc} - * - * @deprecated Deprecated since version 2.4, to be removed in 3.0. - */ - public function getIndicesForValues(array $values) - { - if (empty($values)) { - return array(); - } - $this->load(); - - return parent::getIndicesForValues($values); - } - - /** - * Creates a new unique index for this model. - * - * If the model has a single-field identifier, this identifier is used. - * - * Otherwise a new integer is generated. - * - * @param mixed $model The choice to create an index for - * - * @return int|string A unique index containing only ASCII letters, - * digits and underscores. - */ - protected function createIndex($model) - { - if ($this->identifierAsIndex) { - return current($this->getIdentifierValues($model)); - } - - return parent::createIndex($model); - } - - /** - * Creates a new unique value for this model. - * - * If the model has a single-field identifier, this identifier is used. - * - * Otherwise a new integer is generated. - * - * @param mixed $model The choice to create a value for - * - * @return int|string A unique value without character limitations. - */ - protected function createValue($model) - { - if ($this->identifierAsIndex) { - return (string)current($this->getIdentifierValues($model)); - } - - return parent::createValue($model); - } - - /** - * Loads the complete choice list entries, once. - * - * If data has been loaded the choice list is initialized with the retrieved data. - */ - private function load() - { - if ($this->loaded) { - return; - } - - $models = iterator_to_array($this->query->find()); - $preferred = array(); - if ($this->preferredQuery instanceof ModelCriteria) { - $preferred = iterator_to_array($this->preferredQuery->find()); - } - - try { - // The second parameter $labels is ignored by ObjectChoiceList - parent::initialize($models, array(), $preferred); - $this->loaded = true; - } catch(StringCastException $e) { - throw new StringCastException( - str_replace('argument $labelPath', 'option "property"', $e->getMessage()), - null, - $e - ); - } - } - - /** - * Returns the values of the identifier fields of a model. - * - * Propel must know about this model, that is, the model must already - * be persisted or added to the idmodel map before. Otherwise an - * exception is thrown. - * - * @param object $model The model for which to get the identifier - * - * @return array - */ - private function getIdentifierValues($model) - { - if (!$model instanceof $this->class) { - return array(); - } - - if (1 === count($this->identifier) && current($this->identifier) instanceof ColumnMap) { - $phpName = current($this->identifier)->getPhpName(); - if (method_exists($model, 'get' . $phpName)) { - return array($model->{'get' . $phpName}()); - } - } - - if ($model instanceof ActiveRecordInterface) { - return array($model->getPrimaryKey()); - } - - return $model->getPrimaryKeys(); - } - - /** - * Whether this column contains scalar values (to be used as indices). - * - * @param ColumnMap $column - * - * @return bool - */ - private function isScalar(ColumnMap $column) - { - return in_array( - $column->getPdoType(), - array( - \PDO::PARAM_BOOL, - \PDO::PARAM_INT, - \PDO::PARAM_STR, - ) - ); - } - - /** - * Check the given choices for equality. - * - * @param mixed $choice - * @param mixed $givenChoice - * - * @return bool - */ - private function isEqual($choice, $givenChoice) - { - if ($choice === $givenChoice) { - return true; - } - if ($this->getIdentifierValues($choice) === $this->getIdentifierValues($givenChoice)) { - return true; - } - - return false; - } -} diff --git a/Form/ChoiceList/PropelChoiceLoader.php b/Form/ChoiceList/PropelChoiceLoader.php new file mode 100644 index 0000000..fc67cb0 --- /dev/null +++ b/Form/ChoiceList/PropelChoiceLoader.php @@ -0,0 +1,211 @@ + + * @author Toni Uebernickel + * @author Moritz Schroeder + */ +class PropelChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + protected $factory; + + /** + * @var string + */ + protected $class; + + /** + * @var ModelCriteria + */ + protected $query; + + /** + * The fields of which the identifier of the underlying class consists + * + * This property should only be accessed through identifier. + * + * @var array + */ + protected $identifier = array(); + + /** + * Whether to use the identifier for index generation. + * + * @var bool + */ + protected $identifierAsIndex = false; + + /** + * @var ChoiceListInterface + */ + protected $choiceList; + + /** + * PropelChoiceListLoader constructor. + * + * @param ChoiceListFactoryInterface $factory + * @param string $class + */ + public function __construct(ChoiceListFactoryInterface $factory, $class, ModelCriteria $queryObject, $useAsIdentifier = null) + { + $this->factory = $factory; + $this->class = $class; + $this->query = $queryObject; + if ($useAsIdentifier) { + $this->identifier = array($this->query->getTableMap()->getColumn($useAsIdentifier)); + } else { + $this->identifier = $this->query->getTableMap()->getPrimaryKeys(); + } + if (1 === count($this->identifier) && $this->isScalar(current($this->identifier))) { + $this->identifierAsIndex = true; + } + } + + /** + * {@inheritdoc} + */ + public function loadChoiceList($value = null) + { + if ($this->choiceList) { + return $this->choiceList; + } + + $models = iterator_to_array($this->query->find()); + + $this->choiceList = $this->factory->createListFromChoices($models, $value); + + return $this->choiceList; + } + + /** + * {@inheritdoc} + */ + public function loadChoicesForValues(array $values, $value = null) + { + // Performance optimization + if (empty($values)) { + return array(); + } + + // Optimize performance in case we have a single-field identifier + if (!$this->choiceList && $this->identifierAsIndex && current($this->identifier) instanceof ColumnMap) { + $phpName = current($this->identifier)->getPhpName(); + $unorderedObjects = $this->query->filterBy($phpName, $values); + $objectsById = array(); + $objects = array(); + + // Maintain order and indices from the given $values + foreach ($unorderedObjects as $object) { + $objectsById[(string) current($this->getIdentifierValues($object))] = $object; + } + + foreach ($values as $i => $id) { + if (isset($objectsById[$id])) { + $objects[$i] = $objectsById[$id]; + } + } + + return $objects; + } + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function loadValuesForChoices(array $choices, $value = null) + { + // Performance optimization + if (empty($choices)) { + return array(); + } + + if (!$this->choiceList && $this->identifierAsIndex) { + $values = array(); + + // Maintain order and indices of the given objects + foreach ($choices as $i => $object) { + if ($object instanceof $this->class) { + // Make sure to convert to the right format + $values[$i] = (string) current($this->getIdentifierValues($object)); + } + } + + return $values; + } + + return $this->loadChoiceList($value)->getValuesForChoices($choices); + } + + /** + * Whether this column contains scalar values (to be used as indices). + * + * @param ColumnMap $column + * + * @return bool + */ + private function isScalar(ColumnMap $column) + { + return in_array( + $column->getPdoType(), + array( + \PDO::PARAM_BOOL, + \PDO::PARAM_INT, + \PDO::PARAM_STR, + ) + ); + } + + /** + * Returns the values of the identifier fields of a model. + * + * Propel must know about this model, that is, the model must already + * be persisted or added to the idmodel map before. Otherwise an + * exception is thrown. + * + * @param object $model The model for which to get the identifier + * + * @return array + */ + private function getIdentifierValues($model) + { + if (!$model instanceof $this->class) { + return array(); + } + + if (1 === count($this->identifier) && current($this->identifier) instanceof ColumnMap) { + $phpName = current($this->identifier)->getPhpName(); + if (method_exists($model, 'get' . $phpName)) { + return array($model->{'get' . $phpName}()); + } + } + + if ($model instanceof ActiveRecordInterface) { + return array($model->getPrimaryKey()); + } + + return $model->getPrimaryKeys(); + } + +} \ No newline at end of file diff --git a/Form/PropelExtension.php b/Form/PropelExtension.php index 9acadea..3298dc5 100644 --- a/Form/PropelExtension.php +++ b/Form/PropelExtension.php @@ -12,7 +12,11 @@ namespace Propel\Bundle\PropelBundle\Form; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Represents the Propel form extension, which loads the Propel functionality. @@ -21,10 +25,33 @@ use Symfony\Component\PropertyAccess\PropertyAccess; */ class PropelExtension extends AbstractExtension { + + /** + * @var PropertyAccessorInterface + */ + protected $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + protected $choiceListFactory; + + /** + * PropelExtension constructor. + * + * @param PropertyAccessorInterface|null $propertyAccessor + * @param ChoiceListFactoryInterface|null $choiceListFactory + */ + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor); + } + protected function loadTypes() { return array( - new Type\ModelType(PropertyAccess::createPropertyAccessor()), + new Type\ModelType($this->propertyAccessor, $this->choiceListFactory), new Type\TranslationCollectionType(), new Type\TranslationType() ); diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php index 22373a5..3a4dd98 100644 --- a/Form/Type/ModelType.php +++ b/Form/Type/ModelType.php @@ -11,16 +11,20 @@ namespace Propel\Bundle\PropelBundle\Form\Type; -use Propel\Bundle\PropelBundle\Form\ChoiceList\ModelChoiceList; +use Propel\Bundle\PropelBundle\Form\ChoiceList\PropelChoiceLoader; use Propel\Bundle\PropelBundle\Form\DataTransformer\CollectionToArrayTransformer; +use Propel\Runtime\ActiveQuery\ModelCriteria; +use Propel\Runtime\Map\ColumnMap; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; /** * ModelType class. @@ -53,61 +57,167 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class ModelType extends AbstractType { /** - * @var PropertyAccessorInterface + * @var ChoiceListFactoryInterface */ - private $propertyAccessor; + private $choiceListFactory; - public function __construct(PropertyAccessorInterface $propertyAccessor = null) + /** + * ModelType constructor. + * + * @param PropertyAccessorInterface|null $propertyAccessor + * @param ChoiceListFactoryInterface|null $choiceListFactory + */ + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - if ($options['multiple']) { - $builder->addViewTransformer(new CollectionToArrayTransformer(), true); - } - } - - public function configureOptions(OptionsResolver $resolver) - { - $propertyAccessor = $this->propertyAccessor; - - $choiceList = function (Options $options) use ($propertyAccessor) { - return new ModelChoiceList( - $options['class'], - $options['property'], - $options['choices'], - $options['query'], - $options['group_by'], - $options['preferred_choices'], - $propertyAccessor, - $options['index_property'] - ); - }; - - $resolver->setDefaults(array( - 'template' => 'choice', - 'multiple' => false, - 'expanded' => false, - 'class' => null, - 'property' => null, - 'query' => null, - 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, - 'by_reference' => false, - 'index_property' => null, - 'choice_translation_domain' => false, - )); + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator( + new DefaultChoiceListFactory(), + $propertyAccessor + ); } /** - * {@inheritdoc} + * Creates the label for a choice. + * + * For backwards compatibility, objects are cast to strings by default. + * + * @param object $choice The object. + * + * @return string The string representation of the object. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. */ - public function getParent() + public static function createChoiceLabel($choice) { - return ChoiceType::class; + return (string) $choice; + } + + /** + * Creates the field name for a choice. + * + * This method is used to generate field names if the underlying object has + * a single-column integer ID. In that case, the value of the field is + * the ID of the object. That ID is also used as field name. + * + * @param object $choice The object. + * @param int|string $key The choice key. + * @param string $value The choice value. Corresponds to the object's + * ID here. + * + * @return string The field name. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceName($choice, $key, $value) + { + return str_replace('-', '_', (string) $value); + } + + /** + * {@inheritDoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['multiple']) { + $builder + # ->addEventSubscriber(new MergeDoctrineCollectionListener()) + ->addViewTransformer(new CollectionToArrayTransformer(), true) + ; + } + } + + /** + * {@inheritDoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $choiceLoader = function (Options $options) { + // Unless the choices are given explicitly, load them on demand + if (null === $options['choices']) { + + $propelChoiceLoader = new PropelChoiceLoader( + $this->choiceListFactory, + $options['class'], + $options['query'], + $options['index_property'] + ); + + return $propelChoiceLoader; + } + + return null; + }; + + $choiceName = function (Options $options) { + + /** @var ModelCriteria $query */ + $query = $options['query']; + if ($options['index_property']) { + $identifier = array($query->getTableMap()->getColumn($options['index_property'])); + } else { + $identifier = $query->getTableMap()->getPrimaryKeys(); + } + /** @var ColumnMap $firstIdentifier */ + $firstIdentifier = current($identifier); + if (count($identifier) === 1 && $firstIdentifier->getPdoType() === \PDO::PARAM_INT) { + return array(__CLASS__, 'createChoiceName'); + } + return null; + }; + + $choiceValue = function (Options $options) { + + /** @var ModelCriteria $query */ + $query = $options['query']; + if ($options['index_property']) { + $identifier = array($query->getTableMap()->getColumn($options['index_property'])); + } else { + $identifier = $query->getTableMap()->getPrimaryKeys(); + } + /** @var ColumnMap $firstIdentifier */ + $firstIdentifier = current($identifier); + if (count($identifier) === 1 && in_array($firstIdentifier->getPdoType(), [\PDO::PARAM_BOOL, \PDO::PARAM_INT, \PDO::PARAM_STR])) { + return function($object) use ($firstIdentifier) { + return call_user_func([$object, 'get' . ucfirst($firstIdentifier->getPhpName())]); + }; + } + return null; + }; + + $queryNormalizer = function (Options $options, $query) { + if ($query === null) { + $queryClass = $options['class'] . 'Query'; + if (!class_exists($queryClass)) { + if (empty($options['class'])) { + throw new MissingOptionsException('The "class" parameter is empty, you should provide the model class'); + } + throw new InvalidOptionsException( + sprintf( + 'The query class "%s" is not found, you should provide the FQCN of the model class', + $queryClass + ) + ); + } + $query = new $queryClass(); + } + return $query; + }; + + $resolver->setDefaults([ + 'query' => null, + 'index_property' => null, + 'choices' => null, + 'choice_loader' => $choiceLoader, + 'choice_label' => array(__CLASS__, 'createChoiceLabel'), + 'choice_name' => $choiceName, + 'choice_value' => $choiceValue, + 'choice_translation_domain' => false, + ]); + + $resolver->setRequired(array('class')); + $resolver->setNormalizer('query', $queryNormalizer); + $resolver->setAllowedTypes('query', ['null', 'Propel\Runtime\ActiveQuery\ModelCriteria']); } /** @@ -115,11 +225,11 @@ class ModelType extends AbstractType */ public function getBlockPrefix() { - return 'model'; + return 'entity'; } - public function getName() + public function getParent() { - return $this->getBlockPrefix(); + return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'; } } diff --git a/Form/TypeGuesser.php b/Form/TypeGuesser.php index ea8d3ce..a53e464 100644 --- a/Form/TypeGuesser.php +++ b/Form/TypeGuesser.php @@ -12,8 +12,10 @@ namespace Propel\Bundle\PropelBundle\Form; use Propel\Bundle\PropelBundle\Form\Type\ModelType; +use Propel\Runtime\Map\ColumnMap; use Propel\Runtime\Map\RelationMap; use Propel\Generator\Model\PropelTypes; +use Propel\Runtime\Map\TableMap; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; @@ -164,6 +166,11 @@ class TypeGuesser implements FormTypeGuesserInterface } } + /** + * @param string $class + * + * @return TableMap|null + */ protected function getTable($class) { if (isset($this->cache[$class])) { @@ -175,8 +182,16 @@ class TypeGuesser implements FormTypeGuesserInterface return $this->cache[$class] = $query->getTableMap(); } + + return null; } + /** + * @param string $class + * @param string $property + * + * @return ColumnMap|null + */ protected function getColumn($class, $property) { if (isset($this->cache[$class.'::'.$property])) { @@ -188,5 +203,7 @@ class TypeGuesser implements FormTypeGuesserInterface if ($table && $table->hasColumn($property)) { return $this->cache[$class.'::'.$property] = $table->getColumn($property); } + + return null; } } From b64f7446e76fe8b722219b06aa11e6b5260b648e Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 18:57:00 +0100 Subject: [PATCH 2/8] Initial commit for new sf3 form creation --- Command/BundleTrait.php | 65 +++++++++++++++++++++++++++++++++ Command/FormGenerateCommand.php | 51 ++++++-------------------- Form/FormBuilder.php | 62 +++++++++++++++++++++++++++++++ Resources/skeleton/FormType.php | 21 +++++++---- 4 files changed, 153 insertions(+), 46 deletions(-) create mode 100644 Command/BundleTrait.php create mode 100644 Form/FormBuilder.php diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php new file mode 100644 index 0000000..c3043fb --- /dev/null +++ b/Command/BundleTrait.php @@ -0,0 +1,65 @@ + + */ +trait BundleTrait +{ + /** + * @return ContainerInterface + */ + protected abstract function getContainer(); + + /** + * Returns the selected bundle. + * If no bundle argument is set, the user will get ask for it. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return BundleInterface + */ + protected function getBundle(InputInterface $input, OutputInterface $output) + { + $kernel = $this + ->getContainer() + ->get('kernel'); + + if ($input->hasArgument('bundle') && '@' === substr($input->getArgument('bundle'), 0, 1)) { + return $kernel->getBundle(substr($input->getArgument('bundle'), 1)); + } + + $bundleNames = array_keys($kernel->getBundles()); + + do { + $question = 'Select the bundle: '; + $question = new Question($question); + $question->setAutocompleterValues($bundleNames); + + $bundleName = $this->getHelperSet()->get('question')->ask($input, $output, $question); + + if (in_array($bundleName, $bundleNames)) { + break; + } + $output->writeln(sprintf('Bundle "%s" does not exist.', $bundleName)); + } while (true); + + return $kernel->getBundle($bundleName); + } +} \ No newline at end of file diff --git a/Command/FormGenerateCommand.php b/Command/FormGenerateCommand.php index 46f68c9..91c6bd7 100644 --- a/Command/FormGenerateCommand.php +++ b/Command/FormGenerateCommand.php @@ -10,12 +10,12 @@ namespace Propel\Bundle\PropelBundle\Command; +use Propel\Bundle\PropelBundle\Form\FormBuilder; use Propel\Generator\Config\GeneratorConfig; -use Propel\Generator\Command\ModelBuildCommand as BaseModelBuildCommand; use Propel\Generator\Model\Database; use Propel\Generator\Model\Table; use Propel\Generator\Manager\ModelManager; -use Propel\Runtime\Propel; +use Propel\PropelBundle\Command\BundleTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -30,6 +30,8 @@ use Symfony\Component\HttpKernel\Bundle\BundleInterface; class FormGenerateCommand extends AbstractCommand { const DEFAULT_FORM_TYPE_DIRECTORY = '/Form/Type'; + + use BundleTrait; /** * {@inheritdoc} @@ -42,7 +44,7 @@ class FormGenerateCommand extends AbstractCommand ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing Form types') ->addOption('platform', null, InputOption::VALUE_REQUIRED, 'The platform') - ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to use to generate Form types (Ex: @AcmeDemoBundle)') + ->addArgument('bundle', InputArgument::OPTIONAL, 'The bundle to use to generate Form types (Ex: @AcmeDemoBundle)') ->addArgument('models', InputArgument::IS_ARRAY, 'Model classes to generate Form Types from') ->setHelp(<<getArgument('models'); $force = $input->getOption('force'); - if (!$this->bundle) { - throw new \InvalidArgumentException('No valid bundle given'); - } + $bundle = $this->getBundle($input, $output); $this->setupBuildTimeFiles(); - - if (!($schemas = $this->getFinalSchemas($kernel, $this->bundle))) { - $output->writeln(sprintf('No *schemas.xml files found in bundle %s.', $this->bundle->getName())); - + $schemas = $this->getFinalSchemas($kernel, $bundle); + if (!$schemas) { + $output->writeln(sprintf('No *schemas.xml files found in bundle %s.', $bundle->getName())); return; } @@ -138,45 +137,19 @@ EOT * * @param BundleInterface $bundle The bundle in which the FormType will be created. * @param Table $table The table for which the FormType will be created. - * @param SplFileInfo $file File representing the FormType. + * @param \SplFileInfo $file File representing the FormType. * @param boolean $force Is the write forced? * @param OutputInterface $output An OutputInterface instance. */ protected function writeFormType(BundleInterface $bundle, Table $table, \SplFileInfo $file, $force, OutputInterface $output) { - $modelName = $table->getPhpName(); - $formTypeContent = file_get_contents(__DIR__ . '/../Resources/skeleton/FormType.php'); - - $formTypeContent = str_replace('##NAMESPACE##', $bundle->getNamespace() . str_replace('/', '\\', self::DEFAULT_FORM_TYPE_DIRECTORY), $formTypeContent); - $formTypeContent = str_replace('##CLASS##', $modelName . 'Type', $formTypeContent); - $formTypeContent = str_replace('##FQCN##', sprintf('%s\%s', $table->getNamespace(), $modelName), $formTypeContent); - $formTypeContent = str_replace('##TYPE_NAME##', strtolower($modelName), $formTypeContent); - $formTypeContent = $this->addFields($table, $formTypeContent); + $formBuilder = new FormBuilder(); + $formTypeContent = $formBuilder->buildFormType($bundle, $table, self::DEFAULT_FORM_TYPE_DIRECTORY); file_put_contents($file->getPathName(), $formTypeContent); $this->writeNewFile($output, $this->getRelativeFileName($file) . ($force ? ' (forced)' : '')); } - /** - * Add the fields in the FormType. - * - * @param Table $table Table from which the fields will be extracted. - * @param string $formTypeContent FormType skeleton. - * - * @return string The FormType code. - */ - protected function addFields(Table $table, $formTypeContent) - { - $buildCode = ''; - foreach ($table->getColumns() as $column) { - if (!$column->isPrimaryKey()) { - $buildCode .= sprintf("\n \$builder->add('%s');", lcfirst($column->getPhpName())); - } - } - - return str_replace('##BUILD_CODE##', $buildCode, $formTypeContent); - } - /** * @param \SplFileInfo $file * @return string diff --git a/Form/FormBuilder.php b/Form/FormBuilder.php new file mode 100644 index 0000000..a6c43eb --- /dev/null +++ b/Form/FormBuilder.php @@ -0,0 +1,62 @@ + + */ +class FormBuilder +{ + /** + * Build a form based on the given table. + * + * @param BundleInterface $bundle + * @param Table $table + * @param string $formTypeNamespace + * + * @return string + */ + public function buildFormType(BundleInterface $bundle, Table $table, $formTypeNamespace) + { + $modelName = $table->getPhpName(); + $formTypeContent = file_get_contents(__DIR__ . '/../Resources/skeleton/FormType.php'); + + $formTypeContent = str_replace('##NAMESPACE##', $bundle->getNamespace() . str_replace('/', '\\', $formTypeNamespace), $formTypeContent); + $formTypeContent = str_replace('##CLASS##', $modelName . 'Type', $formTypeContent); + $formTypeContent = str_replace('##FQCN##', sprintf('%s\%s', $table->getNamespace(), $modelName), $formTypeContent); + $formTypeContent = str_replace('##TYPE_NAME##', strtolower($modelName), $formTypeContent); + $formTypeContent = str_replace('##BUILD_CODE##', $this->buildFormFields($table), $formTypeContent); + + return $formTypeContent; + } + + /** + * Build the fields in the FormType. + * + * @param Table $table Table from which the fields will be extracted. + * + * @return string The FormType code. + */ + protected function buildFormFields(Table $table) + { + $buildCode = ''; + foreach ($table->getColumns() as $column) { + if (!$column->isPrimaryKey()) { + $buildCode .= sprintf("\n \$builder->add('%s');", lcfirst($column->getPhpName())); + } + } + + return $buildCode; + } +} \ No newline at end of file diff --git a/Resources/skeleton/FormType.php b/Resources/skeleton/FormType.php index 566c43c..fd8c2f8 100644 --- a/Resources/skeleton/FormType.php +++ b/Resources/skeleton/FormType.php @@ -2,20 +2,27 @@ namespace ##NAMESPACE##; -use Propel\Bundle\PropelBundle\Form\BaseAbstractType; +use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; -class ##CLASS## extends BaseAbstractType +class ##CLASS## extends AbstractType { - protected $options = array( - 'data_class' => '##FQCN##', - 'name' => '##TYPE_NAME##', - ); - /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) {##BUILD_CODE## } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => '##FQCN##', + 'name' => '##TYPE_NAME##', + ]); + } } From 6250e8d2fe565f85b1559dff8d90e1d0d62bbff5 Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 19:05:17 +0100 Subject: [PATCH 3/8] Used old namespace --- Command/BundleTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php index c3043fb..dd0ec23 100644 --- a/Command/BundleTrait.php +++ b/Command/BundleTrait.php @@ -8,7 +8,7 @@ * @license MIT License */ -namespace Propel\PropelBundle\Command; +namespace Propel\Bundle\PropelBundle\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; From 51fb7fd287fbd653b2243c5ae0d941a0d97ab4b8 Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 19:06:59 +0100 Subject: [PATCH 4/8] Used old namespace --- Command/FormGenerateCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Command/FormGenerateCommand.php b/Command/FormGenerateCommand.php index 91c6bd7..20a0c86 100644 --- a/Command/FormGenerateCommand.php +++ b/Command/FormGenerateCommand.php @@ -15,7 +15,6 @@ use Propel\Generator\Config\GeneratorConfig; use Propel\Generator\Model\Database; use Propel\Generator\Model\Table; use Propel\Generator\Manager\ModelManager; -use Propel\PropelBundle\Command\BundleTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; From edfb13c388848889c1682e612daed1d8f869e4e4 Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 19:51:22 +0100 Subject: [PATCH 5/8] Added special logic for foreignKey columns --- Command/FormGenerateCommand.php | 2 +- Form/FormBuilder.php | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Command/FormGenerateCommand.php b/Command/FormGenerateCommand.php index 20a0c86..7e57c79 100644 --- a/Command/FormGenerateCommand.php +++ b/Command/FormGenerateCommand.php @@ -78,7 +78,7 @@ EOT foreach ($manager->getDataModels() as $dataModel) { foreach ($dataModel->getDatabases() as $database) { - $this->createFormTypeFromDatabase($this->bundle, $database, $models, $output, $force); + $this->createFormTypeFromDatabase($bundle, $database, $models, $output, $force); } } } diff --git a/Form/FormBuilder.php b/Form/FormBuilder.php index a6c43eb..cee9547 100644 --- a/Form/FormBuilder.php +++ b/Form/FormBuilder.php @@ -10,6 +10,7 @@ namespace Propel\Bundle\PropelBundle\Form; +use Propel\Generator\Model\ForeignKey; use Propel\Generator\Model\Table; use Symfony\Component\HttpKernel\Bundle\BundleInterface; @@ -52,9 +53,18 @@ class FormBuilder { $buildCode = ''; foreach ($table->getColumns() as $column) { - if (!$column->isPrimaryKey()) { - $buildCode .= sprintf("\n \$builder->add('%s');", lcfirst($column->getPhpName())); + if ($column->isPrimaryKey()) { + continue; } + $name = $column->getPhpName(); + + // Use foreignKey table name, so the TypeGuesser gets it right + if ($column->isForeignKey()) { + /** @var ForeignKey $foreignKey */ + $foreignKey = current($column->getForeignKeys()); + $name = $foreignKey->getForeignTable()->getPhpName(); + } + $buildCode .= sprintf("\n \$builder->add('%s');", lcfirst($name)); } return $buildCode; From 224add2612df20db597de848c49e8ebf14d4c5fc Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Wed, 17 Feb 2016 20:09:56 +0100 Subject: [PATCH 6/8] Added @deprecated for BaseAbstractType and use correct "blockPrefix" --- Form/BaseAbstractType.php | 1 + Form/Type/ModelType.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Form/BaseAbstractType.php b/Form/BaseAbstractType.php index 233f0f5..c592cb4 100644 --- a/Form/BaseAbstractType.php +++ b/Form/BaseAbstractType.php @@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; /** * @author William DURAND + * @deprecated use AbstractType directly */ abstract class BaseAbstractType extends AbstractType { diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php index 3a4dd98..f97c915 100644 --- a/Form/Type/ModelType.php +++ b/Form/Type/ModelType.php @@ -225,7 +225,7 @@ class ModelType extends AbstractType */ public function getBlockPrefix() { - return 'entity'; + return 'model'; } public function getParent() From 9e745df8fef1eb6a67cd83ac956b67844d557fda Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Fri, 19 Feb 2016 21:40:05 +0100 Subject: [PATCH 7/8] Added missing "property" option --- Form/Type/ModelType.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php index f97c915..6b13f4c 100644 --- a/Form/Type/ModelType.php +++ b/Form/Type/ModelType.php @@ -204,12 +204,32 @@ class ModelType extends AbstractType return $query; }; + $choiceLabelNormalizer = function (Options $options, $choiceLabel) { + if ($choiceLabel === null) { + if ($options['property'] == null) { + $choiceLabel = array(__CLASS__, 'createChoiceLabel'); + } else { + $valueProperty = $options['property']; + /** @var ModelCriteria $query */ + $query = $options['query']; + $getter = 'get' . ucfirst($query->getTableMap()->getColumn($valueProperty)->getPhpName()); + + $choiceLabel = function($choice) use ($getter) { + return call_user_func([$choice, $getter]); + }; + } + } + + return $choiceLabel; + }; + $resolver->setDefaults([ 'query' => null, 'index_property' => null, + 'property' => null, 'choices' => null, 'choice_loader' => $choiceLoader, - 'choice_label' => array(__CLASS__, 'createChoiceLabel'), + 'choice_label' => null, 'choice_name' => $choiceName, 'choice_value' => $choiceValue, 'choice_translation_domain' => false, @@ -217,6 +237,7 @@ class ModelType extends AbstractType $resolver->setRequired(array('class')); $resolver->setNormalizer('query', $queryNormalizer); + $resolver->setNormalizer('choice_value', $choiceLabelNormalizer); $resolver->setAllowedTypes('query', ['null', 'Propel\Runtime\ActiveQuery\ModelCriteria']); } From 6385c47731114735a69d55ca869d047f81d7cff3 Mon Sep 17 00:00:00 2001 From: Moritz Schroeder Date: Fri, 19 Feb 2016 21:50:01 +0100 Subject: [PATCH 8/8] Use wrong options for normalizer --- Form/Type/ModelType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php index 6b13f4c..d28970c 100644 --- a/Form/Type/ModelType.php +++ b/Form/Type/ModelType.php @@ -237,7 +237,7 @@ class ModelType extends AbstractType $resolver->setRequired(array('class')); $resolver->setNormalizer('query', $queryNormalizer); - $resolver->setNormalizer('choice_value', $choiceLabelNormalizer); + $resolver->setNormalizer('choice_label', $choiceLabelNormalizer); $resolver->setAllowedTypes('query', ['null', 'Propel\Runtime\ActiveQuery\ModelCriteria']); }