propel-bundle/Form/ChoiceList/ModelChoiceList.php
2013-11-24 11:40:35 +01:00

436 lines
13 KiB
PHP

<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <william.durand1@gmail.com>
* @author Toni Uebernickel <tuebernickel@gmail.com>
*/
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;
}
}