[Form] Cleaned up ValueTransformerInterface
This commit removes CollectionToStringTransformer. Transformers should never change the state of the outside world, otherwise hard-to-track bugs might creap in. This functionality needs to be implemented as a custom FieldType (see EntityChoiceField).
This commit is contained in:
parent
bd3e6c6b49
commit
74d0ac82f7
@ -47,7 +47,7 @@ class BooleanToStringTransformer extends Configurable implements ValueTransforme
|
||||
* @param string $value String value.
|
||||
* @return Boolean Boolean value.
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new UnexpectedTypeException($value, 'string');
|
||||
|
@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Form\ValueTransformer;
|
||||
|
||||
use Symfony\Component\Form\Configurable;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
* Transforms an instance of Doctrine\Common\Collections\Collection into a string of unique names.
|
||||
*
|
||||
* Use-Cases for this transformer include: List of Tag-Names, List Of Group/User-Names or the like.
|
||||
*
|
||||
* This transformer only makes sense if you know the list of related collections to be small and
|
||||
* that they have a unique identifier field that is of meaning to the user (Tag Names) and is
|
||||
* enforced to be unique in the storage.
|
||||
*
|
||||
* This transformer can cause the following SQL operations to happen in the case of an ORM collection:
|
||||
* 1. Initialize the whole collection using one SELECT query
|
||||
* 2. For each removed element issue an UPDATE or DELETE stmt (depending on one-to-many or many-to-many)
|
||||
* 3. For each inserted element issue an INSERT or UPDATE stmt (depending on one-to-many or many-to-many)
|
||||
* 4. Extra updates if necessary by the ORM.
|
||||
*
|
||||
* @todo Refactor to make 'fieldName' optional (identifier).
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
|
||||
*/
|
||||
class CollectionToStringTransformer extends Configurable implements ValueTransformerInterface
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->addOption('trim', true);
|
||||
$this->addOption('separator', ',');
|
||||
$this->addOption('explode_callback', 'explode');
|
||||
$this->addOption('implode_callback', 'implode');
|
||||
$this->addOption('create_instance_callback', null);
|
||||
$this->addRequiredOption('em');
|
||||
$this->addRequiredOption('class_name');
|
||||
$this->addRequiredOption('field_name');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @param Collection $collection
|
||||
*/
|
||||
public function reverseTransform($value, $collection)
|
||||
{
|
||||
if (strlen(trim($value)) == 0) {
|
||||
// don't check for collection count, a straight clear doesnt initialize the collection
|
||||
$collection->clear();
|
||||
return $collection;
|
||||
}
|
||||
|
||||
$callback = $this->getOption('explode_callback');
|
||||
$values = call_user_func($callback, $this->getOption('separator'), $value);
|
||||
|
||||
if ($this->getOption('trim') === true) {
|
||||
$values = array_map('trim', $values);
|
||||
}
|
||||
|
||||
/* @var $em Doctrine\ORM\EntityManager */
|
||||
$em = $this->getOption('em');
|
||||
$className = $this->getOption('class_name');
|
||||
$reflField = $em->getClassMetadata($className)
|
||||
->getReflectionProperty($this->getOption('field_name'));
|
||||
|
||||
// 1. removing elements that are not yet present anymore
|
||||
foreach ($collection as $object) {
|
||||
$uniqueIdent = $reflField->getValue($object);
|
||||
$key = array_search($uniqueIdent, $values);
|
||||
if (false === $key) {
|
||||
$collection->removeElement($object);
|
||||
} else {
|
||||
// found in the collection, no need to do anything with it so remove it
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. add elements that are known to the EntityManager but newly connected, query them from the repository
|
||||
if (count($values)) {
|
||||
$dql = sprintf('SELECT o FROM %s o WHERE o.%s IN (', $className, $this->getOption('field_name'));
|
||||
$query = $em->createQuery();
|
||||
$needles = array();
|
||||
$i = 0;
|
||||
foreach ($values as $val) {
|
||||
$query->setParameter(++$i, $val);
|
||||
$needles[] = '?'.$i;
|
||||
}
|
||||
$dql .= implode(',', $needles).')';
|
||||
$query->setDql($dql);
|
||||
$newElements = $query->getResult();
|
||||
|
||||
foreach ($newElements as $object) {
|
||||
$collection->add($object);
|
||||
|
||||
$uniqueIdent = $reflField->getValue($object);
|
||||
$key = array_search($uniqueIdent, $values);
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. new elements that are not in the repository have to be created and persisted then attached:
|
||||
if (count($values)) {
|
||||
$callback = $this->getOption('create_instance_callback');
|
||||
if (!$callback || !is_callable($callback)) {
|
||||
throw new TransformationFailedException('Cannot transform list of identifiers, because a new element was detected and it is unknown how to create an instance of this element.');
|
||||
}
|
||||
|
||||
foreach ($values as $newValue) {
|
||||
$newInstance = call_user_func($callback, $newValue);
|
||||
if (!($newInstance instanceof $className)) {
|
||||
throw new TransformationFailedException(sprintf('Error while trying to create a new instance for the identifier "%s". No new instance was created.', $newValue));
|
||||
}
|
||||
$collection->add($newInstance);
|
||||
$em->persist($newInstance);
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a Doctrine Collection into a string of identifies with a separator.
|
||||
*
|
||||
* @param Collection $value
|
||||
* @return string
|
||||
*/
|
||||
public function transform($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$values = array();
|
||||
$em = $this->getOption('em');
|
||||
$reflField = $em->getClassMetadata($this->getOption('class_name'))
|
||||
->getReflectionProperty($this->getOption('field_name'));
|
||||
|
||||
foreach ($value as $object) {
|
||||
$values[] = $reflField->getValue($object);
|
||||
}
|
||||
$callback = $this->getOption('implode_callback');
|
||||
|
||||
return call_user_func($callback, $this->getOption('separator'), $values);
|
||||
}
|
||||
}
|
@ -95,7 +95,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
|
||||
* @param array $value Localized date string/array
|
||||
* @return DateTime Normalized date
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
|
@ -86,7 +86,7 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
|
||||
* @param string|array $value Localized date string/array
|
||||
* @return DateTime Normalized date
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
$inputTimezone = $this->getOption('input_timezone');
|
||||
|
||||
|
@ -62,7 +62,7 @@ class DateTimeToStringTransformer extends Configurable implements ValueTransform
|
||||
* @param string $value A value as produced by PHP's date() function
|
||||
* @return DateTime A DateTime object
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return null;
|
||||
|
@ -60,7 +60,7 @@ class DateTimeToTimestampTransformer extends Configurable implements ValueTransf
|
||||
* @param string $value A value as produced by PHP's date() function
|
||||
* @return DateTime A DateTime object
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
|
@ -58,9 +58,9 @@ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransform
|
||||
* @param string $value Localized money string
|
||||
* @return number Normalized number
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
$value = parent::reverseTransform($value, $originalValue);
|
||||
$value = parent::reverseTransform($value);
|
||||
|
||||
if (null !== $value) {
|
||||
$value *= $this->getOption('divisor');
|
||||
|
@ -74,7 +74,7 @@ class NumberToLocalizedStringTransformer extends Configurable implements ValueTr
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new UnexpectedTypeException($value, 'string');
|
||||
|
@ -82,7 +82,7 @@ class PercentToLocalizedStringTransformer extends Configurable implements ValueT
|
||||
* @param number $value Percentage value.
|
||||
* @return number Normalized value.
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new UnexpectedTypeException($value, 'string');
|
||||
|
@ -48,7 +48,7 @@ class ReversedTransformer implements ValueTransformerInterface
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
return $this->reversedTransformer->transform($value);
|
||||
}
|
||||
|
@ -66,10 +66,10 @@ class ValueTransformerChain implements ValueTransformerInterface
|
||||
* @param mixed $value The transformed value
|
||||
* @return mixed The reverse-transformed value
|
||||
*/
|
||||
public function reverseTransform($value, $originalValue)
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
for ($i = count($this->transformers) - 1; $i >= 0; --$i) {
|
||||
$value = $this->transformers[$i]->reverseTransform($value, $originalValue);
|
||||
$value = $this->transformers[$i]->reverseTransform($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
@ -65,11 +65,9 @@ interface ValueTransformerInterface
|
||||
* is passed.
|
||||
*
|
||||
* @param mixed $value The value in the transformed representation
|
||||
* @param mixed $originalValue The original value from the datasource that is about to be overwritten by the new value.
|
||||
* @return mixed The value in the original representation
|
||||
* @throws UnexpectedTypeException when the argument is not of the
|
||||
* expected type
|
||||
* @throws ValueTransformerException when the transformation fails
|
||||
*/
|
||||
function reverseTransform($value, $originalValue);
|
||||
function reverseTransform($value);
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Tests\Component\Form\ValueTransformer;
|
||||
|
||||
require_once __DIR__.'/../DoctrineOrmTestCase.php';
|
||||
|
||||
use Symfony\Tests\Component\Form\DoctrineOrmTestCase;
|
||||
use Symfony\Component\Form\ValueTransformer\CollectionToStringTransformer;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
|
||||
class CollectionToStringTransformerTest extends DoctrineOrmTestCase
|
||||
{
|
||||
/**
|
||||
* @var EntityManager
|
||||
*/
|
||||
private $em;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->em = $this->createTestEntityManager();
|
||||
|
||||
$schemaTool = new SchemaTool($this->em);
|
||||
$classes = array($this->em->getClassMetadata(__NAMESPACE__.'\Tag'));
|
||||
try {
|
||||
$schemaTool->dropSchema($classes);
|
||||
} catch(\Exception $e) {
|
||||
|
||||
}
|
||||
try {
|
||||
$schemaTool->createSchema($classes);
|
||||
} catch(\Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function testNoEntityManagerThrowsException()
|
||||
{
|
||||
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
|
||||
$transformer = new CollectionToStringTransformer(array(
|
||||
'class_name' => __NAMESPACE__.'\Tag',
|
||||
'field_name' => 'name',
|
||||
));
|
||||
}
|
||||
|
||||
public function testNoClassNameThrowsException()
|
||||
{
|
||||
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
|
||||
$transformer = new CollectionToStringTransformer(array(
|
||||
'field_name' => 'name',
|
||||
'em' => $this->em,
|
||||
));
|
||||
}
|
||||
|
||||
public function testNoFieldNameThrowsException()
|
||||
{
|
||||
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
|
||||
$transformer = new CollectionToStringTransformer(array(
|
||||
'class_name' => __NAMESPACE__.'\Tag',
|
||||
'em' => $this->em,
|
||||
));
|
||||
}
|
||||
|
||||
public function createTransformer()
|
||||
{
|
||||
$transformer = new CollectionToStringTransformer(array(
|
||||
'class_name' => __NAMESPACE__.'\Tag',
|
||||
'field_name' => 'name',
|
||||
'em' => $this->em,
|
||||
'create_instance_callback' => function($tagName) {
|
||||
return new Tag($tagName);
|
||||
}
|
||||
));
|
||||
return $transformer;
|
||||
}
|
||||
|
||||
public function testTransformEmptyCollection()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
$ret = $transformer->transform(new ArrayCollection());
|
||||
|
||||
$this->assertEquals("", $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testTransformEmptyCollection
|
||||
*/
|
||||
public function testTransformCollection()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$tags = new ArrayCollection();
|
||||
$tags->add(new Tag("foo"));
|
||||
$tags->add(new Tag("bar"));
|
||||
|
||||
$this->assertEquals("foo,bar", $transformer->transform($tags));
|
||||
}
|
||||
|
||||
public function createTagCollection()
|
||||
{
|
||||
$tags = new ArrayCollection();
|
||||
$tags->add(new Tag("foo"));
|
||||
$tags->add(new Tag("bar"));
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testTransformEmptyCollection
|
||||
*/
|
||||
public function testReverseTransformEmptyString()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$col = new ArrayCollection();
|
||||
|
||||
$newCol = $transformer->reverseTransform("", $col);
|
||||
$this->assertSame($col, $newCol, "A collection is an expenive object that is re-used by the transformer!");
|
||||
$this->assertEquals(0, count($newCol));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testReverseTransformEmptyString
|
||||
*/
|
||||
public function testReverseTransformEmptyStringEmptiesCollection()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$col = $this->createTagCollection();
|
||||
|
||||
$newCol = $transformer->reverseTransform("", $col);
|
||||
$this->assertSame($col, $newCol, "A collection is an expenive object that is re-used by the transformer!");
|
||||
$this->assertEquals(0, count($newCol));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testTransformEmptyCollection
|
||||
*/
|
||||
public function testReverseTransformUnchanged()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$tags = $this->createTagCollection();
|
||||
|
||||
$tags = $transformer->reverseTransform("foo,bar", $tags);
|
||||
|
||||
$this->assertEquals(2, count($tags));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testTransformEmptyCollection
|
||||
*/
|
||||
public function testReverseTransformNewKnownEntity()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$newTag = new Tag("baz");
|
||||
$this->em->persist($newTag);
|
||||
$this->em->flush();
|
||||
|
||||
$tags = $this->createTagCollection();
|
||||
$tags = $transformer->reverseTransform("foo, bar, baz", $tags);
|
||||
|
||||
$this->assertEquals(3, count($tags));
|
||||
$this->assertTrue($tags->contains($newTag));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testReverseTransformNewKnownEntity
|
||||
*/
|
||||
public function testReverseTransformNewUnknownEntity()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$tags = $this->createTagCollection();
|
||||
$tags = $transformer->reverseTransform("foo, bar, baz", $tags);
|
||||
|
||||
$this->assertEquals(3, count($tags));
|
||||
$this->em->flush();
|
||||
|
||||
$this->assertSame($this->em, $transformer->getOption('em'));
|
||||
|
||||
$this->assertEquals(1, count($this->em->getRepository(__NAMESPACE__.'\Tag')->findAll()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testReverseTransformNewUnknownEntity
|
||||
*/
|
||||
public function testReverseTransformRemoveEntity()
|
||||
{
|
||||
$transformer = $this->createTransformer();
|
||||
|
||||
$tags = $this->createTagCollection();
|
||||
$tags = $transformer->reverseTransform("foo", $tags);
|
||||
|
||||
$this->assertEquals(1, count($tags));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @Entity */
|
||||
class Tag
|
||||
{
|
||||
/** @Id @GeneratedValue @Column(type="integer") */
|
||||
public $id;
|
||||
|
||||
/** @Column(type="string") */
|
||||
public $name;
|
||||
|
||||
public function __construct($name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user