FormBuilder.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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\AdminBundle\Builder\ORM;
  11. use Sonata\AdminBundle\Form\ValueTransformer\EntityToIDTransformer;
  12. use Sonata\AdminBundle\Form\ValueTransformer\ArrayToObjectTransformer;
  13. use Sonata\AdminBundle\Form\EditableCollectionField;
  14. use Sonata\AdminBundle\Form\EditableFieldGroup;
  15. use Sonata\AdminBundle\Admin\FieldDescription;
  16. use Sonata\AdminBundle\Admin\Admin;
  17. use Sonata\AdminBundle\Builder\FormBuilderInterface;
  18. use Symfony\Component\Form\Form;
  19. use Symfony\Component\Form\FormInterface;
  20. use Symfony\Component\Form\FormContextInterface;
  21. use Symfony\Component\Validator\ValidatorInterface;
  22. use Symfony\Component\Form\FieldFactory\FieldFactoryInterface;
  23. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  24. class FormBuilder implements FormBuilderInterface
  25. {
  26. protected $fieldFactory;
  27. protected $formContext;
  28. protected $validator;
  29. public function __construct(FieldFactoryInterface $fieldFactory, FormContextInterface $formContext, ValidatorInterface $validator)
  30. {
  31. $this->fieldFactory = $fieldFactory;
  32. $this->formContext = $formContext;
  33. $this->validator = $validator;
  34. }
  35. /**
  36. * todo: put this in the DIC
  37. *
  38. * built-in definition
  39. *
  40. * @var array
  41. */
  42. protected $formFieldClasses = array(
  43. 'string' => 'Symfony\\Component\\Form\\TextField',
  44. 'text' => 'Symfony\\Component\\Form\\TextareaField',
  45. 'boolean' => 'Symfony\\Component\\Form\\CheckboxField',
  46. 'integer' => 'Symfony\\Component\\Form\\IntegerField',
  47. 'tinyint' => 'Symfony\\Component\\Form\\IntegerField',
  48. 'smallint' => 'Symfony\\Component\\Form\\IntegerField',
  49. 'mediumint' => 'Symfony\\Component\\Form\\IntegerField',
  50. 'bigint' => 'Symfony\\Component\\Form\\IntegerField',
  51. 'decimal' => 'Symfony\\Component\\Form\\NumberField',
  52. 'datetime' => 'Symfony\\Component\\Form\\DateTimeField',
  53. 'date' => 'Symfony\\Component\\Form\\DateField',
  54. 'choice' => 'Symfony\\Component\\Form\\ChoiceField',
  55. 'array' => 'Symfony\\Component\\Form\\FieldGroup',
  56. 'country' => 'Symfony\\Component\\Form\\CountryField',
  57. );
  58. /**
  59. * return the field associated to a FieldDescription
  60. * ie : build the embedded form from the related Admin instance
  61. *
  62. * @throws RuntimeException
  63. * @param $object
  64. * @param FieldDescription $fieldDescription
  65. * @param null $fieldName
  66. * @return FieldGroup
  67. */
  68. protected function getRelatedAssociatedField($object, FieldDescription $fieldDescription, $fieldName = null)
  69. {
  70. $fieldName = $fieldName ?: $fieldDescription->getFieldName();
  71. $associatedAdmin = $fieldDescription->getAssociationAdmin();
  72. if (!$associatedAdmin) {
  73. throw new \RuntimeException(sprintf('inline mode for field `%s` required an Admin definition', $fieldName));
  74. }
  75. // retrieve the related object
  76. $targetObject = $associatedAdmin->getNewInstance();
  77. // retrieve the related form
  78. $targetForm = $associatedAdmin->getForm($targetObject);
  79. // create the transformer
  80. $transformer = new ArrayToObjectTransformer(array(
  81. 'em' => $fieldDescription->getAdmin()->getModelManager(),
  82. 'className' => $fieldDescription->getTargetEntity()
  83. ));
  84. // create the "embedded" field
  85. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  86. $field = new EditableFieldGroup($fieldName, array(
  87. 'value_transformer' => $transformer,
  88. ));
  89. } else {
  90. $field = new \Symfony\Component\Form\Form($fieldName, array(
  91. 'value_transformer' => $transformer,
  92. ));
  93. }
  94. foreach ($targetForm->getFields() as $name => $formField) {
  95. if ($name == '_token') {
  96. continue;
  97. }
  98. $field->add($formField);
  99. }
  100. return $field;
  101. }
  102. /**
  103. * return the class associated to a FieldDescription if any defined
  104. *
  105. * @throws RuntimeException
  106. * @param FieldDescription $fieldDescription
  107. * @return bool|string
  108. */
  109. public function getFormFieldClass(FieldDescription $fieldDescription)
  110. {
  111. $class = false;
  112. // the user redefined the mapping type, use the default built in definition
  113. if (!$fieldDescription->getFieldMapping() || $fieldDescription->getType() != $fieldDescription->getMappingType()) {
  114. $class = array_key_exists($fieldDescription->getType(), $this->formFieldClasses) ? $this->formFieldClasses[$fieldDescription->getType()] : false;
  115. } else if ($fieldDescription->getOption('form_field_widget', false)) {
  116. $class = $fieldDescription->getOption('form_field_widget', false);
  117. }
  118. if ($class && !class_exists($class)) {
  119. throw new \RuntimeException(sprintf('The class `%s` does not exist for field `%s`', $class, $fieldDescription->getType()));
  120. }
  121. return $class;
  122. }
  123. /**
  124. * Add a new instance to the related FieldDescription value
  125. *
  126. * @param $object
  127. * @param FieldDescription $fieldDescription
  128. * @return void
  129. */
  130. public function addNewInstance($object, FieldDescription $fieldDescription)
  131. {
  132. $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
  133. $mapping = $fieldDescription->getAssociationMapping();
  134. $method = sprintf('add%s', FieldDescription::camelize($mapping['fieldName']));
  135. $object->$method($instance);
  136. }
  137. /**
  138. * return an OneToOne associated field
  139. *
  140. * @param $object
  141. * @param FieldDescription $fieldDescription
  142. * @return ChoiceField
  143. */
  144. protected function getOneToOneField($object, FieldDescription $fieldDescription)
  145. {
  146. // tweak the widget depend on the edit mode
  147. if ($fieldDescription->getOption('edit') == 'inline') {
  148. return $this->getRelatedAssociatedField($object, $fieldDescription);
  149. }
  150. // TODO : remove this once an EntityField will be available
  151. $options = array(
  152. 'value_transformer' => new EntityToIDTransformer(array(
  153. 'em' => $fieldDescription->getAdmin()->getModelManager(),
  154. 'className' => $fieldDescription->getTargetEntity()
  155. ))
  156. );
  157. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  158. if ($fieldDescription->getOption('edit') == 'list') {
  159. return new \Symfony\Component\Form\TextField($fieldDescription->getFieldName(), $options);
  160. }
  161. $class = $fieldDescription->getOption('form_field_widget', false);
  162. // set valid default value
  163. if (!$class) {
  164. $instance = $this->getFieldFactory()->getInstance(
  165. $fieldDescription->getAdmin()->getClass(),
  166. $fieldDescription->getFieldName(),
  167. $fieldDescription->getOption('form_field_options', array())
  168. );
  169. } else {
  170. $instance = new $class($fieldDescription->getFieldName(), $options);
  171. }
  172. return $instance;
  173. }
  174. /**
  175. * return the OneToMany associated field
  176. *
  177. * @param $object
  178. * @param FieldDescription $fieldDescription
  179. * @return ChoiceField|CollectionField
  180. */
  181. protected function getOneToManyField($object, FieldDescription $fieldDescription)
  182. {
  183. if ($fieldDescription->getOption('edit') == 'inline') {
  184. $prototype = $this->getRelatedAssociatedField($object, $fieldDescription);
  185. $value = $fieldDescription->getValue($object);
  186. // add new instances if the min number is not matched
  187. if ($fieldDescription->getOption('min', 0) > count($value)) {
  188. $diff = $fieldDescription->getOption('min', 0) - count($value);
  189. foreach (range(1, $diff) as $i) {
  190. $this->addNewInstance($object, $fieldDescription);
  191. }
  192. }
  193. // use custom one to expose the newfield method
  194. return new \Sonata\AdminBundle\Form\EditableCollectionField($prototype);
  195. }
  196. return $this->getManyToManyField($object, $fieldDescription);
  197. }
  198. protected function getManyToManyField($object, FieldDescription $fieldDescription)
  199. {
  200. $class = $fieldDescription->getOption('form_field_widget', false);
  201. // set valid default value
  202. if (!$class) {
  203. $instance = $this->getFieldFactory()->getInstance(
  204. $fieldDescription->getAdmin()->getClass(),
  205. $fieldDescription->getFieldName(),
  206. $fieldDescription->getOption('form_field_options', array())
  207. );
  208. } else {
  209. $instance = new $class(
  210. $fieldDescription->getFieldName(),
  211. $fieldDescription->getOption('form_field_options', array())
  212. );
  213. }
  214. return $instance;
  215. }
  216. protected function getManyToOneField($object, FieldDescription $fieldDescription)
  217. {
  218. // tweak the widget depend on the edit mode
  219. if ($fieldDescription->getOption('edit') == 'inline') {
  220. return $this->getRelatedAssociatedField($object, $fieldDescription);
  221. }
  222. $options = array(
  223. 'value_transformer' => new EntityToIDTransformer(array(
  224. 'em' => $fieldDescription->getAdmin()->getModelManager(),
  225. 'className' => $fieldDescription->getTargetEntity()
  226. ))
  227. );
  228. $options = array_merge($options, $fieldDescription->getOption('form_field_options', array()));
  229. if ($fieldDescription->getOption('edit') == 'list') {
  230. return new \Symfony\Component\Form\TextField($fieldDescription->getFieldName(), $options);
  231. }
  232. $class = $fieldDescription->getOption('form_field_widget', false);
  233. if (!$class) {
  234. $instance = $this->getFieldFactory()->getInstance(
  235. $fieldDescription->getAdmin()->getClass(),
  236. $fieldDescription->getFieldName(),
  237. $fieldDescription->getOption('form_field_options', array())
  238. );
  239. } else {
  240. $instance = new $class($fieldDescription->getFieldName(), array_merge(array('expanded' => true), $options));
  241. }
  242. return $instance;
  243. }
  244. /**
  245. * The method add a new field to the provided Form, there are 4 ways to add new field :
  246. *
  247. * - if $name is a string with no related FieldDescription, then the form will use the FieldFactory
  248. * to instantiate a new Field
  249. * - if $name is a FormDescription, the method uses information defined in the FormDescription to
  250. * instantiate a new Field
  251. * - if $name is a FieldInterface, then a FieldDescription is created, the FieldInterface is added to
  252. * the form
  253. * - if $name is a string with a related FieldDescription, then the method uses information defined in the
  254. * FormDescription to instantiate a new Field
  255. *
  256. *
  257. * @param Form $form
  258. * @param FieldDescription $name
  259. * @param array $options
  260. * @return void
  261. */
  262. public function addField(Form $form, FieldDescription $fieldDescription)
  263. {
  264. switch ($fieldDescription->getType()) {
  265. case ClassMetadataInfo::ONE_TO_MANY:
  266. $field = $this->getOneToManyField($form->getData(), $fieldDescription);
  267. break;
  268. case ClassMetadataInfo::MANY_TO_MANY:
  269. $field = $this->getManyToManyField($form->getData(), $fieldDescription);
  270. break;
  271. case ClassMetadataInfo::MANY_TO_ONE:
  272. $field = $this->getManyToOneField($form->getData(), $fieldDescription);
  273. break;
  274. case ClassMetadataInfo::ONE_TO_ONE:
  275. $field = $this->getOneToOneField($form->getData(), $fieldDescription);
  276. break;
  277. default:
  278. $class = $this->getFormFieldClass($fieldDescription);
  279. // there is no way to use a custom widget with the FieldFactory
  280. if ($class) {
  281. $field = new $class(
  282. $fieldDescription->getFieldName(),
  283. $fieldDescription->getOption('form_field_options', array())
  284. );
  285. } else {
  286. $field = $this->getFieldFactory()->getInstance(
  287. $fieldDescription->getAdmin()->getClass(),
  288. $fieldDescription->getFieldName(),
  289. $fieldDescription->getOption('form_field_options', array())
  290. );
  291. }
  292. }
  293. return $form->add($field);
  294. }
  295. /**
  296. * The method define the correct default settings for the provided FieldDescription
  297. *
  298. * @param FieldDescription $fieldDescription
  299. * @return void
  300. */
  301. public function fixFieldDescription(Admin $admin, FieldDescription $fieldDescription, array $options = array())
  302. {
  303. $fieldDescription->mergeOptions($options);
  304. // set the default field mapping
  305. if (isset($admin->getClassMetaData()->fieldMappings[$fieldDescription->getName()])) {
  306. $fieldDescription->setFieldMapping($admin->getClassMetaData()->fieldMappings[$fieldDescription->getName()]);
  307. }
  308. // set the default association mapping
  309. if (isset($admin->getClassMetaData()->associationMappings[$fieldDescription->getName()])) {
  310. $fieldDescription->setAssociationMapping($admin->getClassMetaData()->associationMappings[$fieldDescription->getName()]);
  311. }
  312. if (!$fieldDescription->getType()) {
  313. throw new \RuntimeException(sprintf('Please define a type for field `%s` in `%s`', $fieldDescription->getName(), get_class($admin)));
  314. }
  315. $fieldDescription->setAdmin($admin);
  316. $fieldDescription->setOption('edit', $fieldDescription->getOption('edit', 'standard'));
  317. // fix template value for doctrine association fields
  318. if (!$fieldDescription->getTemplate()) {
  319. $fieldDescription->setTemplate(sprintf('SonataAdmin:CRUD:edit_%s.html.twig', $fieldDescription->getType()));
  320. }
  321. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_ONE) {
  322. $fieldDescription->setTemplate('SonataAdmin:CRUD:edit_orm_one_to_one.html.twig');
  323. $admin->attachAdminClass($fieldDescription);
  324. }
  325. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_ONE) {
  326. $fieldDescription->setTemplate('SonataAdmin:CRUD:edit_orm_many_to_one.html.twig');
  327. $admin->attachAdminClass($fieldDescription);
  328. }
  329. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_MANY) {
  330. $fieldDescription->setTemplate('SonataAdmin:CRUD:edit_orm_many_to_many.html.twig');
  331. $admin->attachAdminClass($fieldDescription);
  332. }
  333. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  334. $fieldDescription->setTemplate('SonataAdmin:CRUD:edit_orm_one_to_many.html.twig');
  335. if ($fieldDescription->getOption('edit') == 'inline' && !$fieldDescription->getOption('widget_form_field')) {
  336. $fieldDescription->setOption('widget_form_field', 'Bundle\\Sonata\\AdminBundle\\Form\\EditableFieldGroup');
  337. }
  338. $admin->attachAdminClass($fieldDescription);
  339. }
  340. // set correct default value
  341. if ($fieldDescription->getType() == 'datetime') {
  342. $options = $fieldDescription->getOption('form_field_options', array());
  343. if (!isset($options['years'])) {
  344. $options['years'] = range(1900, 2100);
  345. }
  346. $fieldDescription->setOption('form_field', $options);
  347. }
  348. }
  349. public function setFieldFactory($fieldFactory)
  350. {
  351. $this->fieldFactory = $fieldFactory;
  352. }
  353. public function getFieldFactory()
  354. {
  355. return $this->fieldFactory;
  356. }
  357. public function setFormContext($formContext)
  358. {
  359. $this->formContext = $formContext;
  360. }
  361. public function getFormContext()
  362. {
  363. return $this->formContext;
  364. }
  365. public function setFormFieldClasses(array $formFieldClasses)
  366. {
  367. $this->formFieldClasses = $formFieldClasses;
  368. }
  369. public function getFormFieldClasses()
  370. {
  371. return $this->formFieldClasses;
  372. }
  373. public function getBaseForm($name, $object, array $options = array())
  374. {
  375. return new Form($name, array_merge(array(
  376. 'data' => $object,
  377. 'validator' => $this->getValidator(),
  378. 'context' => $this->getFormContext(),
  379. ), $options));
  380. }
  381. public function setValidator($validator)
  382. {
  383. $this->validator = $validator;
  384. }
  385. public function getValidator()
  386. {
  387. return $this->validator;
  388. }
  389. }