* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Propel\PropelBundle\Form\ChoiceList; use Propel\Runtime\Map\ColumnMap; use Propel\Runtime\ActiveQuery\ModelCriteria; use Propel\Runtime\ActiveRecord\ActiveRecordInterface; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Widely inspired by the EntityChoiceList. * * @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 Boolean */ protected $loaded = false; /** * Whether to use the identifier for index generation * * @var Boolean */ 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. */ public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null) { $this->class = $class; $queryClass = $this->class.'Query'; $query = new $queryClass(); $this->identifier = $query->getTableMap()->getPrimaryKeys(); $this->query = $queryObject ?: $query; $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->isInteger(current($this->identifier))) { $this->identifierAsIndex = true; } parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor); } /** * Returns the class name * * @return string */ public function getClass() { return $this->class; } /** * Returns the list of model objects * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getChoices() { if (!$this->loaded) { $this->load(); } return parent::getChoices(); } /** * Returns the values for the model objects * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getValues() { if (!$this->loaded) { $this->load(); } return parent::getValues(); } /** * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getPreferredViews() { if (!$this->loaded) { $this->load(); } return parent::getPreferredViews(); } /** * Returns the choice views of the choices that are not preferred as nested * array with the choice groups as top-level keys. * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getRemainingViews() { if (!$this->loaded) { $this->load(); } return parent::getRemainingViews(); } /** * Returns the model objects corresponding to the given values. * * @param array $values * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getChoicesForValues(array $values) { if (!$this->loaded) { if (1 === count($this->identifier)) { $filterBy = 'filterBy'.current($this->identifier)->getPhpName(); return $this->query->create() ->$filterBy($values) ->find() ->getData(); } $this->load(); } return parent::getChoicesForValues($values); } /** * Returns the values corresponding to the given model objects. * * @param array $models * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getValuesForChoices(array $models) { if (!$this->loaded) { // Optimize performance for single-field identifiers. We already // know that the IDs are used as values // Attention: This optimization does not check choices for existence if (1 === count($this->identifier)) { $values = array(); foreach ($models as $model) { if ($model instanceof $this->class) { // Make sure to convert to the right format $values[] = $this->fixValue(current($this->getIdentifierValues($model))); } } return $values; } $this->load(); } return parent::getValuesForChoices($models); } /** * Returns the indices corresponding to the given models. * * @param array $models * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getIndicesForChoices(array $models) { $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 ($this->getChoices() as $i => $choice) { foreach ($choices as $j => $givenChoice) { if (null !== $givenChoice && $this->getIdentifierValues($choice) === $this->getIdentifierValues($givenChoice)) { $indices[] = $i; unset($choices[$j]); if (0 === count($choices)) { break 2; } } } } return $indices; } /** * Returns the models corresponding to the given values. * * @param array $values * * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ public function getIndicesForValues(array $values) { if (!$this->loaded) { // Optimize performance for single-field identifiers. We already // know that the IDs are used as indices and values // Attention: This optimization does not check values for existence if ($this->identifierAsIndex) { return $this->fixIndices($values); } $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 integer|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 integer|string A unique value without character limitations. */ protected function createValue($model) { if (1 === count($this->identifier)) { return (string) current($this->getIdentifierValues($model)); } return parent::createValue($model); } /** * Loads the list with model objects. */ private function load() { $models = $this->query->find()->getData(); $preferred = array(); if ($this->preferredQuery instanceof ModelCriteria) { $preferred = $this->preferredQuery->find()->getData(); } try { // The second parameter $labels is ignored by ObjectChoiceList parent::initialize($models, array(), $preferred); } catch (StringCastException $e) { throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e); } $this->loaded = true; } /** * Returns the values of the identifier fields of an 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 ActiveRecordInterface) { return array($model->getPrimaryKey()); } // readonly="true" models do not implement ActiveRecordInterface. if ($model instanceof ActiveRecordInterface && method_exists($model, 'getPrimaryKey')) { return array($model->getPrimaryKey()); } if (null === $model) { return array(); } return $model->getPrimaryKeys(); } /** * Whether this column in an integer * * @param ColumnMap $column * * @return Boolean */ private function isInteger(ColumnMap $column) { return $column->getPdoType() === \PDO::PARAM_INT; } }