EntityAdmin.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  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, $prependChoices = array())
  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 $prependChoices + $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', FieldDescription::camelize($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. $choices = array();
  313. if($fieldDescription->getOption('add_empty', false)) {
  314. $choices = array(
  315. $fieldDescription->getOption('add_empty_value', '') => $fieldDescription->getOption('add_empty_value', '')
  316. );
  317. }
  318. $options = array_merge(array(
  319. 'expanded' => false,
  320. 'choices' => $this->getChoices($fieldDescription, $choices),
  321. ), $options);
  322. }
  323. return new $class($fieldDescription->getFieldName(), $options);
  324. }
  325. /**
  326. * return the OneToMany associated field
  327. *
  328. * @param $object
  329. * @param FieldDescription $fieldDescription
  330. * @return ChoiceField|CollectionField
  331. */
  332. protected function getOneToManyField($object, FieldDescription $fieldDescription)
  333. {
  334. if ($fieldDescription->getOption('edit') == 'inline') {
  335. $prototype = $this->getRelatedAssociatedField($object, $fieldDescription);
  336. $value = $fieldDescription->getValue($object);
  337. // add new instances if the min number is not matched
  338. if ($fieldDescription->getOption('min', 0) > count($value)) {
  339. $diff = $fieldDescription->getOption('min', 0) - count($value);
  340. foreach (range(1, $diff) as $i) {
  341. $this->addNewInstance($object, $fieldDescription);
  342. }
  343. }
  344. // use custom one to expose the newfield method
  345. return new \Sonata\BaseApplicationBundle\Form\EditableCollectionField($prototype);
  346. }
  347. return $this->getManyToManyField($object, $fieldDescription);
  348. }
  349. protected function getManyToManyField($object, FieldDescription $fieldDescription)
  350. {
  351. $options = array(
  352. 'value_transformer' => new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\CollectionToChoiceTransformer(array(
  353. 'em' => $this->getEntityManager(),
  354. 'className' => $fieldDescription->getTargetEntity()
  355. ))
  356. );
  357. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  358. $class = $fieldDescription->getOption('form_field_widget', 'Symfony\\Component\\Form\\ChoiceField');
  359. // set valid default value
  360. if ($class == 'Symfony\\Component\\Form\\ChoiceField') {
  361. $choices = array();
  362. if($fieldDescription->getOption('add_empty', false)) {
  363. $choices = array(
  364. $fieldDescription->getOption('add_empty_value', '') => $fieldDescription->getOption('add_empty_value', '')
  365. );
  366. }
  367. $options = array_merge(array(
  368. 'expanded' => true,
  369. 'multiple' => true,
  370. 'choices' => $this->getChoices($fieldDescription, $choices),
  371. ), $options);
  372. }
  373. return new $class($fieldDescription->getFieldName(), $options);
  374. }
  375. protected function getManyToOneField($object, FieldDescription $fieldDescription)
  376. {
  377. // tweak the widget depend on the edit mode
  378. if ($fieldDescription->getOption('edit') == 'inline') {
  379. return $this->getRelatedAssociatedField($object, $fieldDescription);
  380. }
  381. $options = array(
  382. 'value_transformer' => new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\EntityToIDTransformer(array(
  383. 'em' => $this->getEntityManager(),
  384. 'className' => $fieldDescription->getTargetEntity()
  385. ))
  386. );
  387. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  388. if ($fieldDescription->getOption('edit') == 'list') {
  389. return new \Symfony\Component\Form\TextField($fieldDescription->getFieldName(), $options);
  390. }
  391. $class = $fieldDescription->getOption('form_field_widget', 'Symfony\\Component\\Form\\ChoiceField');
  392. // set valid default value
  393. if ($class == 'Symfony\\Component\\Form\\ChoiceField') {
  394. $choices = array();
  395. if($fieldDescription->getOption('add_empty', false)) {
  396. $choices = array(
  397. $fieldDescription->getOption('add_empty_value', '') => $fieldDescription->getOption('add_empty_value', '')
  398. );
  399. }
  400. $options = array_merge(array(
  401. 'expanded' => false,
  402. 'choices' => $this->getChoices($fieldDescription, $choices),
  403. ), $options);
  404. }
  405. return new $class($fieldDescription->getFieldName(), $options);
  406. }
  407. protected function getFormFieldInstance($object, FieldDescription $fieldDescription)
  408. {
  409. switch ($fieldDescription->getType()) {
  410. case ClassMetadataInfo::ONE_TO_MANY:
  411. return $this->getOneToManyField($object, $fieldDescription);
  412. case ClassMetadataInfo::MANY_TO_MANY:
  413. return $this->getManyToManyField($object, $fieldDescription);
  414. case ClassMetadataInfo::MANY_TO_ONE:
  415. return $this->getManyToOneField($object, $fieldDescription);
  416. case ClassMetadataInfo::ONE_TO_ONE:
  417. return $this->getOneToOneField($object, $fieldDescription);
  418. default:
  419. $class = $this->getFormFieldClass($fieldDescription);
  420. $options = $fieldDescription->getOption('form_field_options', array());
  421. return new $class($fieldDescription->getFieldName(), $options);
  422. }
  423. }
  424. /**
  425. * return a form depend on the given $object and FieldDescription $fields array
  426. *
  427. * @throws RuntimeException
  428. * @param $object
  429. * @param $fields
  430. * @return Symfony\Component\Form\Form
  431. */
  432. public function getForm($object, $fields)
  433. {
  434. $form = $this->getBaseForm($object);
  435. foreach ($fields as $fieldDescription) {
  436. if (!$fieldDescription->getType()) {
  437. continue;
  438. }
  439. $form->add($this->getFormFieldInstance($object, $fieldDescription));
  440. }
  441. return $form;
  442. }
  443. }