EntityAdmin.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <?php
  2. /*
  3. * This file is part of the Sonata 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\BaseApplicationBundle\Admin;
  11. use Symfony\Component\DependencyInjection\ContainerAware;
  12. use Symfony\Component\Form\Form;
  13. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  14. use Sonata\BaseApplicationBundle\Tool\Datagrid;
  15. abstract class EntityAdmin extends Admin
  16. {
  17. /**
  18. * make sure the base fields are set in the correct format
  19. *
  20. * @param $selected_fields
  21. * @return array
  22. */
  23. static public function getBaseFields(ClassMetadataInfo $metadata, $selectedFields)
  24. {
  25. // if nothing is defined we display all fields
  26. if (!$selectedFields) {
  27. $selectedFields = array_keys($metadata->reflFields) + array_keys($metadata->associationMappings);
  28. }
  29. $fields = array();
  30. // make sure we works with array
  31. foreach ($selectedFields as $name => $options) {
  32. $description = new FieldDescription;
  33. if (is_array($options)) {
  34. // remove property value
  35. if (isset($options['type'])) {
  36. $description->setType($options['type']);
  37. unset($options['type']);
  38. }
  39. // remove property value
  40. if (isset($options['template'])) {
  41. $description->setTemplate($options['template']);
  42. unset($options['template']);
  43. }
  44. $description->setOptions($options);
  45. } else {
  46. $name = $options;
  47. }
  48. $description->setName($name);
  49. if (isset($metadata->fieldMappings[$name])) {
  50. $description->setFieldMapping($metadata->fieldMappings[$name]);
  51. }
  52. if (isset($metadata->associationMappings[$name])) {
  53. $description->setAssociationMapping($metadata->associationMappings[$name]);
  54. }
  55. $fields[$name] = $description;
  56. }
  57. return $fields;
  58. }
  59. /**
  60. * return the entity manager
  61. *
  62. * @return EntityManager
  63. */
  64. public function getEntityManager()
  65. {
  66. return $this->container->get('doctrine.orm.default_entity_manager');
  67. }
  68. /**
  69. * build the fields to use in the form
  70. *
  71. * @throws RuntimeException
  72. * @return
  73. */
  74. protected function buildFormFields()
  75. {
  76. if ($this->loaded['form_fields']) {
  77. return;
  78. }
  79. $this->loaded['form_fields'] = true;
  80. $this->formFields = self::getBaseFields($this->getClassMetaData(), $this->formFields);
  81. foreach ($this->formFields as $name => $fieldDescription) {
  82. if (!$fieldDescription->getType()) {
  83. throw new \RuntimeException(sprintf('You must declare a type for the field `%s`', $name));
  84. }
  85. $fieldDescription->setAdmin($this);
  86. $fieldDescription->setOption('edit', $fieldDescription->getOption('edit', 'standard'));
  87. // fix template value for doctrine association fields
  88. if (!$fieldDescription->getTemplate()) {
  89. $fieldDescription->setTemplate(sprintf('SonataBaseApplicationBundle:CRUD:edit_%s.twig.html', $fieldDescription->getType()));
  90. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_ONE) {
  91. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:edit_one_to_one.twig.html');
  92. $this->attachAdminClass($fieldDescription);
  93. }
  94. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_ONE) {
  95. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:edit_many_to_one.twig.html');
  96. $this->attachAdminClass($fieldDescription);
  97. }
  98. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_MANY) {
  99. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:edit_many_to_many.twig.html');
  100. $this->attachAdminClass($fieldDescription);
  101. }
  102. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  103. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:edit_one_to_many.twig.html');
  104. if($fieldDescription->getOption('edit') == 'inline' && !$fieldDescription->getOption('widget')) {
  105. $fieldDescription->setOption('widget', 'Bundle\\Sonata\\BaseApplicationBundle\\Form\\EditableGroupField');
  106. }
  107. $this->attachAdminClass($fieldDescription);
  108. }
  109. }
  110. // set correct default value
  111. if ($fieldDescription->getType() == 'datetime') {
  112. $options = $fieldDescription->getOption('form_fields', array());
  113. if (!isset($options['years'])) {
  114. $options['years'] = range(1900, 2100);
  115. }
  116. $fieldDescription->setOption('form_field', $options);
  117. }
  118. // unset the identifier field as it is not required to update an object
  119. if ($fieldDescription->isIdentifier()) {
  120. unset($this->formFields[$name]);
  121. }
  122. }
  123. $this->configureFormFields();
  124. return $this->formFields;
  125. }
  126. /**
  127. * build the field to use in the list view
  128. *
  129. * @return void
  130. */
  131. protected function buildListFields()
  132. {
  133. if ($this->loaded['list_fields']) {
  134. return;
  135. }
  136. $this->loaded['list_fields'] = true;
  137. $this->listFields = self::getBaseFields($this->getClassMetaData(), $this->listFields);
  138. // normalize field
  139. foreach ($this->listFields as $name => $fieldDescription) {
  140. $fieldDescription->setOption('code', $fieldDescription->getOption('code', $name));
  141. $fieldDescription->setOption('label', $fieldDescription->getOption('label', $name));
  142. // set the default type if none is set
  143. if (!$fieldDescription->getType()) {
  144. $fieldDescription->setType('string');
  145. }
  146. $fieldDescription->setAdmin($this);
  147. if (!$fieldDescription->getTemplate()) {
  148. $fieldDescription->setTemplate(sprintf('SonataBaseApplicationBundle:CRUD:list_%s.twig.html', $fieldDescription->getType()));
  149. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_ONE) {
  150. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:list_many_to_one.twig.html');
  151. $this->attachAdminClass($fieldDescription);
  152. }
  153. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_ONE) {
  154. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:list_one_to_one.twig.html');
  155. $this->attachAdminClass($fieldDescription);
  156. }
  157. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  158. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:list_one_to_many.twig.html');
  159. $this->attachAdminClass($fieldDescription);
  160. }
  161. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_MANY) {
  162. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:list_many_to_many.twig.html');
  163. $this->attachAdminClass($fieldDescription);
  164. }
  165. }
  166. }
  167. $this->configureListFields();
  168. if (!isset($this->listFields['_batch'])) {
  169. $fieldDescription = new FieldDescription();
  170. $fieldDescription->setOptions(array(
  171. 'label' => 'batch',
  172. 'code' => '_batch'
  173. ));
  174. $fieldDescription->setTemplate('SonataBaseApplicationBundle:CRUD:list__batch.twig.html');
  175. $this->listFields = array( '_batch' => $fieldDescription ) + $this->listFields;
  176. }
  177. return $this->listFields;
  178. }
  179. /**
  180. * return the list of choices for one entity
  181. *
  182. * @param FieldDescription $description
  183. * @return array
  184. */
  185. protected function getChoices(FieldDescription $description)
  186. {
  187. if (!isset($this->choicesCache[$description->getTargetEntity()])) {
  188. $targets = $this->getEntityManager()
  189. ->createQueryBuilder()
  190. ->select('t')
  191. ->from($description->getTargetEntity(), 't')
  192. ->getQuery()
  193. ->execute();
  194. $choices = array();
  195. foreach ($targets as $target) {
  196. // todo : puts this into a configuration option and use reflection
  197. foreach (array('getTitle', 'getName', '__toString') as $getter) {
  198. if (method_exists($target, $getter)) {
  199. $choices[$target->getId()] = $target->$getter();
  200. break;
  201. }
  202. }
  203. }
  204. $this->choicesCache[$description->getTargetEntity()] = $choices;
  205. }
  206. return $this->choicesCache[$description->getTargetEntity()];
  207. }
  208. /**
  209. * return the field associated to a FieldDescription
  210. * ie : build the embedded form from the related Admin instance
  211. *
  212. * @throws RuntimeException
  213. * @param $object
  214. * @param FieldDescription $fieldDescription
  215. * @param null $fieldName
  216. * @return FieldGroup
  217. */
  218. protected function getRelatedAssociatedField($object, FieldDescription $fieldDescription, $fieldName = null)
  219. {
  220. $fieldName = $fieldName ?: $fieldDescription->getFieldName();
  221. $associatedAdmin = $fieldDescription->getAssociationAdmin();
  222. if (!$associatedAdmin) {
  223. throw new \RuntimeException(sprintf('inline mode for field `%s` required an Admin definition', $fieldName));
  224. }
  225. // retrieve the related object
  226. $targetObject = $associatedAdmin->getNewInstance();
  227. // retrieve the related form
  228. $targetFields = $associatedAdmin->getFormFields();
  229. $targetForm = $associatedAdmin->getForm($targetObject, $targetFields);
  230. // create the transformer
  231. $transformer = new \Sonata\BaseApplicationBundle\Form\ValueTransformer\ArrayToObjectTransformer(array(
  232. 'em' => $this->getEntityManager(),
  233. 'className' => $fieldDescription->getTargetEntity()
  234. ));
  235. // create the "embedded" field
  236. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  237. $field = new \Sonata\BaseApplicationBundle\Form\EditableFieldGroup($fieldName, array(
  238. 'value_transformer' => $transformer,
  239. ));
  240. } else {
  241. $field = new \Symfony\Component\Form\FieldGroup($fieldName, array(
  242. 'value_transformer' => $transformer,
  243. ));
  244. }
  245. foreach ($targetForm->getFields() as $name => $formField) {
  246. if ($name == '_token') {
  247. continue;
  248. }
  249. $field->add($formField);
  250. }
  251. return $field;
  252. }
  253. /**
  254. * return the class associated to a FieldDescription
  255. *
  256. * @throws RuntimeException
  257. * @param FieldDescription $fieldDescription
  258. * @return bool
  259. */
  260. public function getFormFieldClass(FieldDescription $fieldDescription)
  261. {
  262. $class = isset($this->formFieldClasses[$fieldDescription->getType()]) ? $this->formFieldClasses[$fieldDescription->getType()] : false;
  263. $class = $fieldDescription->getOption('form_field_widget', $class);
  264. if(!$class) {
  265. throw new \RuntimeException(sprintf('unknow type `%s`', $fieldDescription->getType()));
  266. }
  267. if(!class_exists($class)) {
  268. throw new \RuntimeException(sprintf('The class `%s` does not exist for field `%s`', $class, $fieldDescription->getType()));
  269. }
  270. return $class;
  271. }
  272. /**
  273. * Add a new instance to the related FieldDescription value
  274. *
  275. * @param $object
  276. * @param FieldDescription $fieldDescription
  277. * @return void
  278. */
  279. public function addNewInstance($object, FieldDescription $fieldDescription)
  280. {
  281. $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
  282. $mapping = $fieldDescription->getAssociationMapping();
  283. $method = sprintf('add%s', $mapping['fieldName']);
  284. $object->$method($instance);
  285. }
  286. /**
  287. * return an OneToOne associated field
  288. *
  289. * @param $object
  290. * @param FieldDescription $fieldDescription
  291. * @return ChoiceField
  292. */
  293. protected function getOneToOneField($object, FieldDescription $fieldDescription)
  294. {
  295. // tweak the widget depend on the edit mode
  296. if ($fieldDescription->getOption('edit') == 'inline') {
  297. return $this->getRelatedAssociatedField($object, $fieldDescription);
  298. }
  299. $options = array(
  300. 'value_transformer' => new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\EntityToIDTransformer(array(
  301. 'em' => $this->getEntityManager(),
  302. 'className' => $fieldDescription->getTargetEntity()
  303. ))
  304. );
  305. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  306. if ($fieldDescription->getOption('edit') == 'list') {
  307. return new \Symfony\Component\Form\TextField($fieldDescription->getFieldName(), $options);
  308. }
  309. $class = $fieldDescription->getOption('form_field_widget', 'Symfony\\Component\\Form\\ChoiceField');
  310. // set valid default value
  311. if ($class == 'Symfony\\Component\\Form\\ChoiceField') {
  312. $options = array_merge(array(
  313. 'expanded' => false,
  314. 'choices' => $this->getChoices($fieldDescription),
  315. ), $options);
  316. }
  317. return new $class($fieldDescription->getFieldName(), $options);
  318. }
  319. /**
  320. * return the OneToMany associated field
  321. *
  322. * @param $object
  323. * @param FieldDescription $fieldDescription
  324. * @return ChoiceField|CollectionField
  325. */
  326. protected function getOneToManyField($object, FieldDescription $fieldDescription)
  327. {
  328. if ($fieldDescription->getOption('edit') == 'inline') {
  329. $prototype = $this->getRelatedAssociatedField($object, $fieldDescription);
  330. $value = $fieldDescription->getValue($object);
  331. // add new instances if the min number is not matched
  332. if ($fieldDescription->getOption('min', 0) > count($value)) {
  333. $diff = $fieldDescription->getOption('min', 0) - count($value);
  334. foreach (range(1, $diff) as $i) {
  335. $this->addNewInstance($object, $fieldDescription);
  336. }
  337. }
  338. // use custom one to expose the newfield method
  339. return new \Sonata\BaseApplicationBundle\Form\EditableCollectionField($prototype);
  340. }
  341. return $this->getManyToManyField($object, $fieldDescription);
  342. }
  343. protected function getManyToManyField($object, FieldDescription $fieldDescription)
  344. {
  345. $options = array(
  346. 'value_transformer' => new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\CollectionToChoiceTransformer(array(
  347. 'em' => $this->getEntityManager(),
  348. 'className' => $fieldDescription->getTargetEntity()
  349. ))
  350. );
  351. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  352. $class = $fieldDescription->getOption('form_field_widget', 'Symfony\\Component\\Form\\ChoiceField');
  353. // set valid default value
  354. if ($class == 'Symfony\\Component\\Form\\ChoiceField') {
  355. $options = array_merge(array(
  356. 'expanded' => true,
  357. 'multiple' => true,
  358. 'choices' => $this->getChoices($fieldDescription),
  359. ), $options);
  360. }
  361. return new $class($fieldDescription->getFieldName(), $options);
  362. }
  363. protected function getFormFieldInstance($object, FieldDescription $fieldDescription)
  364. {
  365. switch ($fieldDescription->getType()) {
  366. case ClassMetadataInfo::ONE_TO_MANY:
  367. return $this->getOneToManyField($object, $fieldDescription);
  368. case ClassMetadataInfo::MANY_TO_MANY:
  369. return $this->getManyToManyField($object, $fieldDescription);
  370. case ClassMetadataInfo::MANY_TO_ONE:
  371. case ClassMetadataInfo::ONE_TO_ONE:
  372. return $this->getOneToOneField($object, $fieldDescription);
  373. default:
  374. $options = $fieldDescription->getOption('form_field_options', array());
  375. $class = $this->getFormFieldClass($fieldDescription);
  376. return new $class($fieldDescription->getFieldName(), $options);
  377. }
  378. }
  379. /**
  380. * return a form depend on the given $object and FieldDescription $fields array
  381. *
  382. * @throws RuntimeException
  383. * @param $object
  384. * @param $fields
  385. * @return Symfony\Component\Form\Form
  386. */
  387. public function getForm($object, $fields)
  388. {
  389. $this->container->get('session')->start();
  390. $form = new Form('data', $object, $this->container->get('validator'));
  391. foreach ($fields as $fieldDescription) {
  392. if (!$fieldDescription->getType()) {
  393. continue;
  394. }
  395. $form->add($this->getFormFieldInstance($object, $fieldDescription));
  396. }
  397. return $form;
  398. }
  399. }