diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php
new file mode 100644
index 0000000..dd0ec23
--- /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..7e57c79 100644
--- a/Command/FormGenerateCommand.php
+++ b/Command/FormGenerateCommand.php
@@ -10,12 +10,11 @@
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 Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -30,6 +29,8 @@ use Symfony\Component\HttpKernel\Bundle\BundleInterface;
class FormGenerateCommand extends AbstractCommand
{
const DEFAULT_FORM_TYPE_DIRECTORY = '/Form/Type';
+
+ use BundleTrait;
/**
* {@inheritdoc}
@@ -42,7 +43,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;
}
@@ -80,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);
}
}
}
@@ -138,45 +136,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/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/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/FormBuilder.php b/Form/FormBuilder.php
new file mode 100644
index 0000000..cee9547
--- /dev/null
+++ b/Form/FormBuilder.php
@@ -0,0 +1,72 @@
+
+ */
+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()) {
+ 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;
+ }
+}
\ 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..d28970c 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,188 @@ 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;
+ };
+
+ $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' => null,
+ 'choice_name' => $choiceName,
+ 'choice_value' => $choiceValue,
+ 'choice_translation_domain' => false,
+ ]);
+
+ $resolver->setRequired(array('class'));
+ $resolver->setNormalizer('query', $queryNormalizer);
+ $resolver->setNormalizer('choice_label', $choiceLabelNormalizer);
+ $resolver->setAllowedTypes('query', ['null', 'Propel\Runtime\ActiveQuery\ModelCriteria']);
}
/**
@@ -118,8 +249,8 @@ class ModelType extends AbstractType
return 'model';
}
- 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;
}
}
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##',
+ ]);
+ }
}