AdminHelper.php 10 KB

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