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; } }