CollectionToStringTransformer.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form\ValueTransformer;
  11. use Symfony\Component\Form\Configurable;
  12. use Doctrine\Common\Collections\Collection;
  13. /**
  14. * Transforms an instance of Doctrine\Common\Collections\Collection into a string of unique names.
  15. *
  16. * Use-Cases for this transformer include: List of Tag-Names, List Of Group/User-Names or the like.
  17. *
  18. * This transformer only makes sense if you know the list of related collections to be small and
  19. * that they have a unique identifier field that is of meaning to the user (Tag Names) and is
  20. * enforced to be unique in the storage.
  21. *
  22. * This transformer can cause the following SQL operations to happen in the case of an ORM collection:
  23. * 1. Initialize the whole collection using one SELECT query
  24. * 2. For each removed element issue an UPDATE or DELETE stmt (depending on one-to-many or many-to-many)
  25. * 3. For each inserted element issue an INSERT or UPDATE stmt (depending on one-to-many or many-to-many)
  26. * 4. Extra updates if necessary by the ORM.
  27. *
  28. * @todo Refactor to make 'fieldName' optional (identifier).
  29. *
  30. * @author Benjamin Eberlei <kontakt@beberlei.de>
  31. * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
  32. */
  33. class CollectionToStringTransformer extends Configurable implements ValueTransformerInterface
  34. {
  35. protected function configure()
  36. {
  37. $this->addOption('trim', true);
  38. $this->addOption('separator', ',');
  39. $this->addOption('explode_callback', 'explode');
  40. $this->addOption('implode_callback', 'implode');
  41. $this->addOption('create_instance_callback', null);
  42. $this->addRequiredOption('em');
  43. $this->addRequiredOption('class_name');
  44. $this->addRequiredOption('field_name');
  45. parent::configure();
  46. }
  47. /**
  48. * @param string $value
  49. * @param Collection $collection
  50. */
  51. public function reverseTransform($value, $collection)
  52. {
  53. if (strlen(trim($value)) == 0) {
  54. // don't check for collection count, a straight clear doesnt initialize the collection
  55. $collection->clear();
  56. return $collection;
  57. }
  58. $callback = $this->getOption('explode_callback');
  59. $values = call_user_func($callback, $this->getOption('separator'), $value);
  60. if ($this->getOption('trim') === true) {
  61. $values = array_map('trim', $values);
  62. }
  63. /* @var $em Doctrine\ORM\EntityManager */
  64. $em = $this->getOption('em');
  65. $className = $this->getOption('class_name');
  66. $reflField = $em->getClassMetadata($className)
  67. ->getReflectionProperty($this->getOption('field_name'));
  68. // 1. removing elements that are not yet present anymore
  69. foreach ($collection as $object) {
  70. $uniqueIdent = $reflField->getValue($object);
  71. $key = array_search($uniqueIdent, $values);
  72. if (false === $key) {
  73. $collection->removeElement($object);
  74. } else {
  75. // found in the collection, no need to do anything with it so remove it
  76. unset($values[$key]);
  77. }
  78. }
  79. // 2. add elements that are known to the EntityManager but newly connected, query them from the repository
  80. if (count($values)) {
  81. $dql = sprintf('SELECT o FROM %s o WHERE o.%s IN (', $className, $this->getOption('field_name'));
  82. $query = $em->createQuery();
  83. $needles = array();
  84. $i = 0;
  85. foreach ($values as $val) {
  86. $query->setParameter(++$i, $val);
  87. $needles[] = '?'.$i;
  88. }
  89. $dql .= implode(',', $needles).')';
  90. $query->setDql($dql);
  91. $newElements = $query->getResult();
  92. foreach ($newElements as $object) {
  93. $collection->add($object);
  94. $uniqueIdent = $reflField->getValue($object);
  95. $key = array_search($uniqueIdent, $values);
  96. unset($values[$key]);
  97. }
  98. }
  99. // 3. new elements that are not in the repository have to be created and persisted then attached:
  100. if (count($values)) {
  101. $callback = $this->getOption('create_instance_callback');
  102. if (!$callback || !is_callable($callback)) {
  103. 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.');
  104. }
  105. foreach ($values as $newValue) {
  106. $newInstance = call_user_func($callback, $newValue);
  107. if (!($newInstance instanceof $className)) {
  108. throw new TransformationFailedException(sprintf('Error while trying to create a new instance for the identifier "%s". No new instance was created.', $newValue));
  109. }
  110. $collection->add($newInstance);
  111. $em->persist($newInstance);
  112. }
  113. }
  114. return $collection;
  115. }
  116. /**
  117. * Transform a Doctrine Collection into a string of identifies with a separator.
  118. *
  119. * @param Collection $value
  120. * @return string
  121. */
  122. public function transform($value)
  123. {
  124. if (null === $value) {
  125. return '';
  126. }
  127. $values = array();
  128. $em = $this->getOption('em');
  129. $reflField = $em->getClassMetadata($this->getOption('class_name'))
  130. ->getReflectionProperty($this->getOption('field_name'));
  131. foreach ($value as $object) {
  132. $values[] = $reflField->getValue($object);
  133. }
  134. $callback = $this->getOption('implode_callback');
  135. return call_user_func($callback, $this->getOption('separator'), $values);
  136. }
  137. }