FormContractor.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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\Admin\AdminInterface;
  12. use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
  13. use Sonata\AdminBundle\Admin\ORM\FieldDescription;
  14. use Sonata\AdminBundle\Model\ModelManagerInterface;
  15. use Sonata\AdminBundle\Builder\FormContractorInterface;
  16. use Sonata\AdminBundle\Form\Type\AdminType;
  17. use Sonata\AdminBundle\Form\Type\ModelType;
  18. use Sonata\AdminBundle\Admin\NoValueException;
  19. use Symfony\Component\Form\FormBuilder;
  20. use Symfony\Component\Form\FormInterface;
  21. use Symfony\Component\Form\FormFactoryInterface;
  22. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  23. class FormContractor implements FormContractorInterface
  24. {
  25. protected $fieldFactory;
  26. protected $validator;
  27. /**
  28. * built-in definition
  29. *
  30. * @var array
  31. */
  32. protected $formTypes = array(
  33. 'string' => array('text', array()),
  34. 'text' => array('textarea', array()),
  35. 'boolean' => array('checkbox', array()),
  36. 'checkbox' => array('checkbox', array()),
  37. 'integer' => array('integer', array()),
  38. 'tinyint' => array('integer', array()),
  39. 'smallint' => array('integer', array()),
  40. 'mediumint' => array('integer', array()),
  41. 'bigint' => array('integer', array()),
  42. 'decimal' => array('number', array()),
  43. 'datetime' => array('datetime', array()),
  44. 'date' => array('date', array()),
  45. 'choice' => array('choice', array()),
  46. 'array' => array('collection', array()),
  47. 'country' => array('country', array()),
  48. );
  49. public function __construct(FormFactoryInterface $formFactory)
  50. {
  51. $this->formFactory = $formFactory;
  52. }
  53. /**
  54. * Returns the field associated to a FieldDescriptionInterface
  55. * ie : build the embedded form from the related AdminInterface instance
  56. *
  57. * @throws RuntimeException
  58. * @param \Symfony\Component\Form\FormBuilder $formBuilder
  59. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  60. * @param null $fieldName
  61. * @return FieldGroup
  62. */
  63. protected function defineChildFormBuilder(FormBuilder $formBuilder, FieldDescriptionInterface $fieldDescription, $fieldName = null)
  64. {
  65. $fieldName = $fieldName ?: $fieldDescription->getFieldName();
  66. $associatedAdmin = $fieldDescription->getAssociationAdmin();
  67. if (!$associatedAdmin) {
  68. throw new \RuntimeException(sprintf('inline mode for field `%s` required an Admin definition', $fieldName));
  69. }
  70. // retrieve the related object
  71. $childBuilder = $formBuilder->create($fieldName, 'sonata_type_admin', array(
  72. 'field_description' => $fieldDescription
  73. ));
  74. $formBuilder->add($childBuilder);
  75. $associatedAdmin->defineFormBuilder($childBuilder);
  76. }
  77. /**
  78. * Returns the class associated to a FieldDescriptionInterface if any defined
  79. *
  80. * @throws RuntimeException
  81. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  82. * @return bool|string
  83. */
  84. public function getFormTypeName(FieldDescriptionInterface $fieldDescription)
  85. {
  86. $typeName = false;
  87. // the user redefined the mapping type, use the default built in definition
  88. if (!$fieldDescription->getFieldMapping() || $fieldDescription->getType() != $fieldDescription->getMappingType()) {
  89. $typeName = array_key_exists($fieldDescription->getType(), $this->formTypes) ? $this->formTypes[$fieldDescription->getType()] : false;
  90. } else if ($fieldDescription->getOption('form_field_type', false)) {
  91. $typeName = $fieldDescription->getOption('form_field_type', false);
  92. } else if (array_key_exists($fieldDescription->getType(), $this->formTypes)) {
  93. $typeName = $this->formTypes[$fieldDescription->getType()];
  94. }
  95. if (!$typeName) {
  96. throw new \RuntimeException(sprintf('No known form type for field `%s` (`%s`) is implemented.', $fieldDescription->getFieldName(), $fieldDescription->getType()));
  97. }
  98. return $typeName;
  99. }
  100. /**
  101. * Returns an OneToOne associated field
  102. *
  103. * @param \Symfony\Component\Form\FormBuilder $formBuilder
  104. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  105. * @return \Symfony\Component\Form\Type\FormTypeInterface
  106. */
  107. protected function defineOneToOneField(FormBuilder $formBuilder, FieldDescriptionInterface $fieldDescription)
  108. {
  109. if (!$fieldDescription->hasAssociationAdmin()) {
  110. return;
  111. }
  112. // tweak the widget depend on the edit mode
  113. if ($fieldDescription->getOption('edit') == 'inline') {
  114. return $this->defineChildFormBuilder($formBuilder, $fieldDescription);
  115. }
  116. $type = 'sonata_type_model';
  117. $options = $fieldDescription->getOption('form_field_options', array());
  118. $options['class'] = $fieldDescription->getTargetEntity();
  119. $options['model_manager'] = $fieldDescription->getAdmin()->getModelManager();
  120. if ($fieldDescription->getOption('edit') == 'list') {
  121. $options['parent'] = 'text';
  122. }
  123. $formBuilder->add($fieldDescription->getFieldName(), $type, $options);
  124. }
  125. /**
  126. * Returns the OneToMany associated field
  127. *
  128. * @param \Symfony\Component\Form\FormBuilder $formBuilder
  129. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  130. * @return \Symfony\Component\Form\Type\FormTypeInterface
  131. */
  132. protected function getOneToManyField(FormBuilder $formBuilder, FieldDescriptionInterface $fieldDescription)
  133. {
  134. if (!$fieldDescription->hasAssociationAdmin()) {
  135. return;
  136. }
  137. if ($fieldDescription->getOption('edit') == 'inline') {
  138. // create a collection type with the generated prototype
  139. $options = $fieldDescription->getOption('form_field_options', array());
  140. $options['type'] = 'sonata_type_admin';
  141. $options['modifiable'] = true;
  142. $options['type_options'] = array(
  143. 'field_description' => $fieldDescription,
  144. );
  145. $formBuilder->add($fieldDescription->getFieldName(), 'sonata_type_collection', $options);
  146. return;
  147. // $value = $fieldDescription->getValue($formBuilder->getData());
  148. //
  149. // // add new instances if the min number is not matched
  150. // if ($fieldDescription->getOption('min', 0) > count($value)) {
  151. //
  152. // $diff = $fieldDescription->getOption('min', 0) - count($value);
  153. // foreach (range(1, $diff) as $i) {
  154. // $this->addNewInstance($formBuilder->getData(), $fieldDescription);
  155. // }
  156. // }
  157. // use custom one to expose the newfield method
  158. // return new \Sonata\AdminBundle\Form\EditableCollectionField($prototype);
  159. }
  160. return $this->defineManyToManyField($formBuilder, $fieldDescription);
  161. }
  162. /**
  163. * @param \Symfony\Component\Form\FormBuilder $formBuilder
  164. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  165. * @return \Symfony\Component\Form\Type\FormTypeInterface
  166. */
  167. protected function defineManyToManyField(FormBuilder $formBuilder, FieldDescriptionInterface $fieldDescription)
  168. {
  169. if (!$fieldDescription->hasAssociationAdmin()) {
  170. return;
  171. }
  172. $type = $fieldDescription->getOption('form_field_type', 'sonata_type_model');
  173. $options = $fieldDescription->getOption('form_field_options', array());
  174. if ($type == 'sonata_type_model') {
  175. $options['class'] = $fieldDescription->getTargetEntity();
  176. $options['multiple'] = true;
  177. $options['field_description'] = $fieldDescription;
  178. $options['parent'] = 'choice';
  179. $options['model_manager'] = $fieldDescription->getAdmin()->getModelManager();
  180. }
  181. $formBuilder->add($fieldDescription->getName(), $type, $options);
  182. }
  183. /**
  184. * Add a new field type into the provided FormBuilder
  185. *
  186. * @param \Symfony\Component\Form\FormBuilder $formBuilder
  187. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  188. * @return void
  189. */
  190. public function addField(FormBuilder $formBuilder, FieldDescriptionInterface $fieldDescription)
  191. {
  192. // There is a bug in the GraphWalker, so for now we always load related associations
  193. // for more information : https://github.com/symfony/symfony/pull/1056
  194. if ($formBuilder->getData() && in_array($fieldDescription->getType(), array(ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY, ClassMetadataInfo::MANY_TO_ONE, ClassMetadataInfo::ONE_TO_ONE ))) {
  195. try {
  196. $value = $fieldDescription->getValue($formBuilder->getData());
  197. } catch (NoValueException $e) {
  198. $value = null;
  199. }
  200. $infos = $fieldDescription->getAssociationMapping();
  201. if ($value instanceof $infos['targetEntity'] && $value instanceof \Doctrine\ORM\Proxy\Proxy) {
  202. $relatedId = 'get'.current($fieldDescription->getAdmin()->getModelManager()->getIdentifierFieldNames($infos['targetEntity']));
  203. $value->{$relatedId}(); // force to load the lazy loading method __load in the proxy methode
  204. }
  205. }
  206. switch ($fieldDescription->getType()) {
  207. case ClassMetadataInfo::ONE_TO_MANY:
  208. $this->getOneToManyField($formBuilder, $fieldDescription);
  209. break;
  210. case ClassMetadataInfo::MANY_TO_MANY:
  211. $this->defineManyToManyField($formBuilder, $fieldDescription);
  212. break;
  213. case ClassMetadataInfo::MANY_TO_ONE:
  214. case ClassMetadataInfo::ONE_TO_ONE:
  215. $this->defineOneToOneField($formBuilder, $fieldDescription);
  216. break;
  217. default:
  218. list($type, $default_options) = $this->getFormTypeName($fieldDescription);
  219. $formBuilder->add(
  220. $fieldDescription->getFieldName(),
  221. $type,
  222. array_merge($default_options, $fieldDescription->getOption('form_field_options', array()))
  223. );
  224. }
  225. }
  226. /**
  227. * The method defines the correct default settings for the provided FieldDescription
  228. *
  229. * @param \Sonata\AdminBundle\Admin\AdminInterface $admin
  230. * @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
  231. * @param array $options
  232. * @return void
  233. */
  234. public function fixFieldDescription(AdminInterface $admin, FieldDescriptionInterface $fieldDescription, array $options = array())
  235. {
  236. $fieldDescription->mergeOptions($options);
  237. if ($admin->getModelManager()->hasMetadata($admin->getClass())) {
  238. $metadata = $admin->getModelManager()->getMetadata($admin->getClass());
  239. // set the default field mapping
  240. if (isset($metadata->fieldMappings[$fieldDescription->getName()])) {
  241. $fieldDescription->setFieldMapping($metadata->fieldMappings[$fieldDescription->getName()]);
  242. }
  243. // set the default association mapping
  244. if (isset($metadata->associationMappings[$fieldDescription->getName()])) {
  245. $fieldDescription->setAssociationMapping($metadata->associationMappings[$fieldDescription->getName()]);
  246. }
  247. }
  248. if (!$fieldDescription->getType()) {
  249. throw new \RuntimeException(sprintf('Please define a type for field `%s` in `%s`', $fieldDescription->getName(), get_class($admin)));
  250. }
  251. $fieldDescription->setAdmin($admin);
  252. $fieldDescription->setOption('edit', $fieldDescription->getOption('edit', 'standard'));
  253. // fix template value for doctrine association fields
  254. if (!$fieldDescription->getTemplate()) {
  255. $fieldDescription->setTemplate(sprintf('SonataAdminBundle:CRUD:edit_%s.html.twig', $fieldDescription->getType()));
  256. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_ONE) {
  257. $fieldDescription->setTemplate('SonataAdminBundle:CRUD:edit_orm_one_to_one.html.twig');
  258. }
  259. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_ONE) {
  260. $fieldDescription->setTemplate('SonataAdminBundle:CRUD:edit_orm_many_to_one.html.twig');
  261. }
  262. if ($fieldDescription->getType() == ClassMetadataInfo::MANY_TO_MANY) {
  263. $fieldDescription->setTemplate('SonataAdminBundle:CRUD:edit_orm_many_to_many.html.twig');
  264. }
  265. if ($fieldDescription->getType() == ClassMetadataInfo::ONE_TO_MANY) {
  266. $fieldDescription->setTemplate('SonataAdminBundle:CRUD:edit_orm_one_to_many.html.twig');
  267. }
  268. }
  269. if (in_array($fieldDescription->getType(), array(ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY, ClassMetadataInfo::MANY_TO_ONE, ClassMetadataInfo::ONE_TO_ONE ))) {
  270. $admin->attachAdminClass($fieldDescription);
  271. }
  272. // set correct default value
  273. if ($fieldDescription->getType() == 'datetime') {
  274. $options = $fieldDescription->getOption('form_field_options', array());
  275. if (!isset($options['years'])) {
  276. $options['years'] = range(1900, 2100);
  277. }
  278. $fieldDescription->setOption('form_field', $options);
  279. }
  280. }
  281. public function getFormFactory()
  282. {
  283. return $this->formFactory;
  284. }
  285. /**
  286. * @param string $name
  287. * @param array $options
  288. * @return \Symfony\Component\Form\FormBuilder
  289. */
  290. public function getFormBuilder($name, array $options = array())
  291. {
  292. return $this->getFormFactory()->createNamedBuilder('form', $name, $options);
  293. }
  294. }