AdminHelper.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. /*
  3. * This file is part of the Sonata Project package.
  4. *
  5. * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  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 Sonata\AdminBundle\Admin;
  11. use Doctrine\Common\Inflector\Inflector;
  12. use Doctrine\Common\Util\ClassUtils;
  13. use Sonata\AdminBundle\Exception\NoValueException;
  14. use Sonata\AdminBundle\Util\FormBuilderIterator;
  15. use Sonata\AdminBundle\Util\FormViewIterator;
  16. use Symfony\Component\Form\FormBuilderInterface;
  17. use Symfony\Component\Form\FormView;
  18. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  19. use Symfony\Component\PropertyAccess\PropertyAccessor;
  20. /**
  21. * Class AdminHelper.
  22. *
  23. * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  24. */
  25. class AdminHelper
  26. {
  27. /**
  28. * @var Pool
  29. */
  30. protected $pool;
  31. /**
  32. * @param Pool $pool
  33. */
  34. public function __construct(Pool $pool)
  35. {
  36. $this->pool = $pool;
  37. }
  38. /**
  39. * @throws \RuntimeException
  40. *
  41. * @param FormBuilderInterface $formBuilder
  42. * @param string $elementId
  43. *
  44. * @return FormBuilderInterface
  45. */
  46. public function getChildFormBuilder(FormBuilderInterface $formBuilder, $elementId)
  47. {
  48. foreach (new FormBuilderIterator($formBuilder) as $name => $formBuilder) {
  49. if ($name == $elementId) {
  50. return $formBuilder;
  51. }
  52. }
  53. return;
  54. }
  55. /**
  56. * @param FormView $formView
  57. * @param string $elementId
  58. *
  59. * @return null|FormView
  60. */
  61. public function getChildFormView(FormView $formView, $elementId)
  62. {
  63. foreach (new \RecursiveIteratorIterator(new FormViewIterator($formView), \RecursiveIteratorIterator::SELF_FIRST) as $name => $formView) {
  64. if ($name === $elementId) {
  65. return $formView;
  66. }
  67. }
  68. return;
  69. }
  70. /**
  71. * @deprecated
  72. *
  73. * @param string $code
  74. *
  75. * @return AdminInterface
  76. */
  77. public function getAdmin($code)
  78. {
  79. return $this->pool->getInstance($code);
  80. }
  81. /**
  82. * Note:
  83. * This code is ugly, but there is no better way of doing it.
  84. * For now the append form element action used to add a new row works
  85. * only for direct FieldDescription (not nested one).
  86. *
  87. * @throws \RuntimeException
  88. *
  89. * @param AdminInterface $admin
  90. * @param object $subject
  91. * @param string $elementId
  92. *
  93. * @return array
  94. */
  95. public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
  96. {
  97. // retrieve the subject
  98. $formBuilder = $admin->getFormBuilder();
  99. $form = $formBuilder->getForm();
  100. $form->setData($subject);
  101. $form->handleRequest($admin->getRequest());
  102. // get the field element
  103. $childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
  104. //Child form not found (probably nested one)
  105. //if childFormBuilder was not found resulted in fatal error getName() method call on non object
  106. if (!$childFormBuilder) {
  107. $propertyAccessor = new PropertyAccessor();
  108. $entity = $admin->getSubject();
  109. $path = $this->getElementAccessPath($elementId, $entity);
  110. $collection = $propertyAccessor->getValue($entity, $path);
  111. if ($collection instanceof \Doctrine\ORM\PersistentCollection || $collection instanceof \Doctrine\ODM\MongoDB\PersistentCollection) {
  112. //since doctrine 2.4
  113. $entityClassName = $collection->getTypeClass()->getName();
  114. } elseif ($collection instanceof \Doctrine\Common\Collections\Collection) {
  115. $entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
  116. } else {
  117. throw new \Exception('unknown collection class');
  118. }
  119. $collection->add(new $entityClassName());
  120. $propertyAccessor->setValue($entity, $path, $collection);
  121. $fieldDescription = null;
  122. } else {
  123. // retrieve the FieldDescription
  124. $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
  125. try {
  126. $value = $fieldDescription->getValue($form->getData());
  127. } catch (NoValueException $e) {
  128. $value = null;
  129. }
  130. // retrieve the posted data
  131. $data = $admin->getRequest()->get($formBuilder->getName());
  132. if (!isset($data[$childFormBuilder->getName()])) {
  133. $data[$childFormBuilder->getName()] = array();
  134. }
  135. $objectCount = count($value);
  136. $postCount = count($data[$childFormBuilder->getName()]);
  137. $fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
  138. // for now, not sure how to do that
  139. $value = array();
  140. foreach ($fields as $name) {
  141. $value[$name] = '';
  142. }
  143. // add new elements to the subject
  144. while ($objectCount < $postCount) {
  145. // append a new instance into the object
  146. $this->addNewInstance($form->getData(), $fieldDescription);
  147. ++$objectCount;
  148. }
  149. $this->addNewInstance($form->getData(), $fieldDescription);
  150. }
  151. $finalForm = $admin->getFormBuilder()->getForm();
  152. $finalForm->setData($subject);
  153. // bind the data
  154. $finalForm->setData($form->getData());
  155. return array($fieldDescription, $finalForm);
  156. }
  157. /**
  158. * Add a new instance to the related FieldDescriptionInterface value.
  159. *
  160. * @param object $object
  161. * @param FieldDescriptionInterface $fieldDescription
  162. *
  163. * @throws \RuntimeException
  164. */
  165. public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
  166. {
  167. $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
  168. $mapping = $fieldDescription->getAssociationMapping();
  169. $method = sprintf('add%s', $this->camelize($mapping['fieldName']));
  170. if (!method_exists($object, $method)) {
  171. $method = rtrim($method, 's');
  172. if (!method_exists($object, $method)) {
  173. $method = sprintf('add%s', $this->camelize(Inflector::singularize($mapping['fieldName'])));
  174. if (!method_exists($object, $method)) {
  175. throw new \RuntimeException(sprintf('Please add a method %s in the %s class!', $method, ClassUtils::getClass($object)));
  176. }
  177. }
  178. }
  179. $object->$method($instance);
  180. }
  181. /**
  182. * Camelize a string.
  183. *
  184. * @static
  185. *
  186. * @param string $property
  187. *
  188. * @return string
  189. */
  190. public function camelize($property)
  191. {
  192. return BaseFieldDescription::camelize($property);
  193. }
  194. /**
  195. * Recursively find the class name of the admin responsible for the element at the end of an association chain.
  196. *
  197. * @param AdminInterface $admin
  198. * @param array $elements
  199. *
  200. * @return string
  201. */
  202. protected function getEntityClassName(AdminInterface $admin, $elements)
  203. {
  204. $element = array_shift($elements);
  205. $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
  206. if (count($elements) == 0) {
  207. return $associationAdmin->getClass();
  208. } else {
  209. return $this->getEntityClassName($associationAdmin, $elements);
  210. }
  211. }
  212. /**
  213. * get access path to element which works with PropertyAccessor.
  214. *
  215. * @param string $elementId expects string in format used in form id field. (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
  216. * @param mixed $entity
  217. *
  218. * @return string
  219. *
  220. * @throws \Exception
  221. */
  222. public function getElementAccessPath($elementId, $entity)
  223. {
  224. $propertyAccessor = new PropertyAccessor();
  225. $idWithoutUniqueIdentifier = implode('_', explode('_', substr($elementId, strpos($elementId, '_') + 1)));
  226. //array access of id converted to format which PropertyAccessor understands
  227. $initialPath = preg_replace('#(_(\d+)_)#', '[$2]', $idWithoutUniqueIdentifier);
  228. $parts = preg_split('#\[\d+\]#', $initialPath);
  229. $partReturnValue = $returnValue = '';
  230. $currentEntity = $entity;
  231. foreach ($parts as $key => $value) {
  232. $subParts = explode('_', $value);
  233. $id = '';
  234. $dot = '';
  235. foreach ($subParts as $subValue) {
  236. $id .= ($id) ? '_'.$subValue : $subValue;
  237. if ($this->pathExists($propertyAccessor, $currentEntity, $partReturnValue.$dot.$id)) {
  238. $partReturnValue .= $dot.$id;
  239. $dot = '.';
  240. $id = '';
  241. } else {
  242. $dot = '';
  243. }
  244. }
  245. if ($dot !== '.') {
  246. throw new \Exception(sprintf('Could not get element id from %s Failing part: %s', $elementId, $subValue));
  247. }
  248. //check if array access was in this location originally
  249. preg_match("#$value\[(\d+)#", $initialPath, $matches);
  250. if (isset($matches[1])) {
  251. $partReturnValue .= '['.$matches[1].']';
  252. }
  253. $returnValue .= $returnValue ? '.'.$partReturnValue : $partReturnValue;
  254. $partReturnValue = '';
  255. if (isset($parts[$key + 1])) {
  256. $currentEntity = $propertyAccessor->getValue($entity, $returnValue);
  257. }
  258. }
  259. return $returnValue;
  260. }
  261. /**
  262. * check if given path exists in $entity.
  263. *
  264. * @param PropertyAccessor $propertyAccessor
  265. * @param mixed $entity
  266. * @param string $path
  267. *
  268. * @return bool
  269. *
  270. * @throws \RuntimeException
  271. */
  272. private function pathExists(PropertyAccessor $propertyAccessor, $entity, $path)
  273. {
  274. //sf2 <= 2.3 did not have isReadable method for PropertyAccessor
  275. if (method_exists($propertyAccessor, 'isReadable')) {
  276. return $propertyAccessor->isReadable($entity, $path);
  277. } else {
  278. try {
  279. $propertyAccessor->getValue($entity, $path);
  280. return true;
  281. } catch (NoSuchPropertyException $e) {
  282. return false;
  283. }
  284. }
  285. }
  286. }