diff --git a/Form/ChoiceList/ModelChoiceList.php b/Form/ChoiceList/ModelChoiceList.php new file mode 100644 index 0000000..d9d70e5 --- /dev/null +++ b/Form/ChoiceList/ModelChoiceList.php @@ -0,0 +1,435 @@ + + * + * 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; + } +} diff --git a/Form/DataTransformer/CollectionToArrayTransformer.php b/Form/DataTransformer/CollectionToArrayTransformer.php new file mode 100644 index 0000000..dfe0e63 --- /dev/null +++ b/Form/DataTransformer/CollectionToArrayTransformer.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\DataTransformer; + +use Propel\Runtime\Collection\ObjectCollection; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * CollectionToArrayTransformer class. + * + * @author William Durand + * @author Pierre-Yves Lebecq + */ +class CollectionToArrayTransformer implements DataTransformerInterface +{ + public function transform($collection) + { + if (null === $collection) { + return array(); + } + + if (!$collection instanceof ObjectCollection) { + throw new TransformationFailedException('Expected a \ObjectCollection.'); + } + + return $collection->getData(); + } + + public function reverseTransform($array) + { + $collection = new ObjectCollection(); + + if ('' === $array || null === $array) { + return $collection; + } + + if (!is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + $collection->setData($array); + + return $collection; + } +} diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php new file mode 100644 index 0000000..1fcdfac --- /dev/null +++ b/Form/Type/ModelType.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\Type; + +use Propel\PropelBundle\Form\ChoiceList\ModelChoiceList; +use Propel\PropelBundle\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * ModelType class. + * + * @author William Durand + * @author Toni Uebernickel + * + * Example using the preferred_choices option. + * + * + * public function buildForm(FormBuilderInterface $builder, array $options) + * { + * $builder + * ->add('product', 'model', array( + * 'class' => 'Model\Product', + * 'query' => ProductQuery::create() + * ->filterIsActive(true) + * ->useI18nQuery($options['locale']) + * ->orderByName() + * ->endUse() + * , + * 'preferred_choices' => ProductQuery::create() + * ->filterByIsTopProduct(true) + * , + * )) + * ; + * } + * + */ +class ModelType extends AbstractType +{ + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['multiple']) { + $builder->addViewTransformer(new CollectionToArrayTransformer(), true); + } + } + + public function setDefaultOptions(OptionsResolverInterface $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 + ); + }; + + $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, + )); + } + + public function getParent() + { + return 'choice'; + } + + public function getName() + { + return 'model'; + } +} diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml index 88c8edb..2af723c 100644 --- a/Resources/config/propel.xml +++ b/Resources/config/propel.xml @@ -13,6 +13,7 @@ Propel\PropelBundle\Logger\PropelLogger Propel\PropelBundle\Twig\Extension\SyntaxExtension Propel\PropelBundle\Form\TypeGuesser + Propel\PropelBundle\Form\Type\ModelType Propel\PropelBundle\DataFixtures\Dumper\YamlDataDumper Propel\PropelBundle\DataFixtures\Loader\YamlDataLoader Propel\PropelBundle\DataFixtures\Loader\XmlDataLoader @@ -44,6 +45,10 @@ + + + + %kernel.root_dir% diff --git a/Tests/Fixtures/ReadOnlyItem.php b/Tests/Fixtures/ReadOnlyItem.php new file mode 100644 index 0000000..c0d32cf --- /dev/null +++ b/Tests/Fixtures/ReadOnlyItem.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +use Propel\Runtime\ActiveRecord\ActiveRecordInterface; + +class ReadOnlyItem implements ActiveRecordInterface +{ + public function getName() + { + return 'Marvin'; + } + + public function getPrimaryKey() + { + return 42; + } +} diff --git a/Tests/Fixtures/ReadOnlyItemQuery.php b/Tests/Fixtures/ReadOnlyItemQuery.php new file mode 100644 index 0000000..cb184e0 --- /dev/null +++ b/Tests/Fixtures/ReadOnlyItemQuery.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +use Propel\Runtime\Map\ColumnMap; +use Propel\Runtime\Map\TableMap; + +class ReadOnlyItemQuery +{ + public function getTableMap() + { + // Allows to define methods in this class + // to avoid a lot of mock classes + return $this; + } + + public function getPrimaryKeys() + { + $cm = new ColumnMap('id', new TableMap()); + $cm->setType('INTEGER'); + + return array('id' => $cm); + } +} diff --git a/Tests/Form/ChoiceList/ModelChoiceListTest.php b/Tests/Form/ChoiceList/ModelChoiceListTest.php new file mode 100644 index 0000000..2892949 --- /dev/null +++ b/Tests/Form/ChoiceList/ModelChoiceListTest.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\ChoiceList; + +use Propel\PropelBundle\Form\ChoiceList\ModelChoiceList; +use Propel\PropelBundle\Tests\Fixtures\Item; +use Propel\PropelBundle\Tests\Fixtures\ReadOnlyItem; +use Propel\PropelBundle\Tests\TestCase; + +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + +class ModelChoiceListTest extends TestCase +{ + const ITEM_CLASS = '\Propel\PropelBundle\Tests\Fixtures\Item'; + + protected function setUp() + { + if (!class_exists('Symfony\Component\Form\Form')) { + $this->markTestSkipped('The "Form" component is not available'); + } + + if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) { + $this->markTestSkipped('The "PropertyAccessor" component is not available'); + } + } + + public function testEmptyChoicesReturnsEmpty() + { + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array() + ); + + $this->assertSame(array(), $choiceList->getChoices()); + } + + public function testReadOnlyIsValidChoice() + { + $item = new ReadOnlyItem(); + $choiceList = new ModelChoiceList( + '\Propel\PropelBundle\Tests\Fixtures\ReadOnlyItem', + 'name', + array( + $item, + ) + ); + + $this->assertSame(array(42 => $item), $choiceList->getChoices()); + } + + public function testFlattenedChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + } + + public function testFlattenedPreferredChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ), + null, + null, + array( + $item1 + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + $this->assertEquals(array(1 => new ChoiceView($item1, '1', 'Foo')), $choiceList->getPreferredViews()); + } + + public function testNestedChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + 'group1' => array($item1), + 'group2' => array($item2), + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + $this->assertEquals(array( + 'group1' => array(1 => new ChoiceView($item1, '1', 'Foo')), + 'group2' => array(2 => new ChoiceView($item2, '2', 'Bar')) + ), $choiceList->getRemainingViews()); + } + + public function testGroupBySupportsString() + { + $item1 = new Item(1, 'Foo', 'Group1'); + $item2 = new Item(2, 'Bar', 'Group1'); + $item3 = new Item(3, 'Baz', 'Group2'); + $item4 = new Item(4, 'Boo!', null); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + $item3, + $item4, + ), + null, + 'groupName' + ); + + $this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices()); + $this->assertEquals(array( + 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), + 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), + 4 => new ChoiceView($item4, '4', 'Boo!') + ), $choiceList->getRemainingViews()); + } + + public function testGroupByInvalidPropertyPathReturnsFlatChoices() + { + $item1 = new Item(1, 'Foo', 'Group1'); + $item2 = new Item(2, 'Bar', 'Group1'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ), + null, + 'child.that.does.not.exist' + ); + + $this->assertEquals(array( + 1 => $item1, + 2 => $item2 + ), $choiceList->getChoices()); + } + + public function testGetValuesForChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + null, + null, + null, + null + ); + + $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); + $this->assertEquals(array(1, 2), $choiceList->getIndicesForChoices(array($item1, $item2))); + } + + public function testDifferentEqualObjectsAreChoosen() + { + $item = new Item(1, 'Foo'); + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $choosenItem = new Item(1, 'Foo'); + + $this->assertEquals(array(1), $choiceList->getIndicesForChoices(array($choosenItem))); + } + + public function testGetIndicesForNullChoices() + { + $item = new Item(1, 'Foo'); + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $this->assertEquals(array(), $choiceList->getIndicesForChoices(array(null))); + } +} diff --git a/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php b/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php new file mode 100644 index 0000000..0fbce14 --- /dev/null +++ b/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\Form\DataTransformer; + +use Propel\Runtime\Collection\ObjectCollection; +use Propel\PropelBundle\Tests\TestCase; +use Propel\PropelBundle\Form\DataTransformer\CollectionToArrayTransformer; + +class CollectionToArrayTransformerTest extends TestCase +{ + private $transformer; + + protected function setUp() + { + if (!class_exists('Symfony\Component\Form\Form')) { + $this->markTestSkipped('The "Form" component is not available'); + } + + parent::setUp(); + + $this->transformer = new CollectionToArrayTransformer(); + } + + public function testTransform() + { + $result = $this->transformer->transform(new ObjectCollection()); + + $this->assertTrue(is_array($result)); + $this->assertEquals(0, count($result)); + } + + public function testTransformWithNull() + { + $result = $this->transformer->transform(null); + + $this->assertTrue(is_array($result)); + $this->assertEquals(0, count($result)); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + */ + public function testTransformThrowsExceptionIfNotObjectCollection() + { + $this->transformer->transform(new DummyObject()); + } + + public function testTransformWithData() + { + $coll = new ObjectCollection(); + $coll->setData(array('foo', 'bar')); + + $result = $this->transformer->transform($coll); + + $this->assertTrue(is_array($result)); + $this->assertEquals(2, count($result)); + $this->assertEquals('foo', $result[0]); + $this->assertEquals('bar', $result[1]); + } + + public function testReverseTransformWithNull() + { + $result = $this->transformer->reverseTransform(null); + + $this->assertInstanceOf('\Propel\Runtime\Collection\ObjectCollection', $result); + $this->assertEquals(0, count($result->getData())); + } + + public function testReverseTransformWithEmptyString() + { + $result = $this->transformer->reverseTransform(''); + + $this->assertInstanceOf('\Propel\Runtime\Collection\ObjectCollection', $result); + $this->assertEquals(0, count($result->getData())); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + */ + public function testReverseTransformThrowsExceptionIfNotArray() + { + $this->transformer->reverseTransform(new DummyObject()); + } + + public function testReverseTransformWithData() + { + $inputData = array('foo', 'bar'); + + $result = $this->transformer->reverseTransform($inputData); + $data = $result->getData(); + + $this->assertInstanceOf('\Propel\Runtime\Collection\ObjectCollection', $result); + + $this->assertTrue(is_array($data)); + $this->assertEquals(2, count($data)); + $this->assertEquals('foo', $data[0]); + $this->assertEquals('bar', $data[1]); + $this->assertsame($inputData, $data); + } +} + +class DummyObject {}