Admin.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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 Bundle\Sonata\BaseApplicationBundle\Admin;
  11. use Symfony\Component\DependencyInjection\ContainerAware;
  12. use Symfony\Component\Form\Form;
  13. use Bundle\Sonata\BaseApplicationBundle\Tool\Datagrid;
  14. abstract class Admin extends ContainerAware
  15. {
  16. protected $class;
  17. protected $list_fields = false;
  18. protected $form_fields = false;
  19. protected $filter_fields = array(); // by default there is no filter
  20. protected $filter_datagrid;
  21. protected $max_per_page = 25;
  22. protected $base_route = '';
  23. protected $base_controller_name;
  24. protected $form_groups = false;
  25. // note : don't like this, but havn't find a better way to do it
  26. protected $configuration_pool;
  27. protected $code;
  28. protected $label;
  29. public function configure()
  30. {
  31. $this->buildFormFields();
  32. $this->buildFormGroups();
  33. $this->buildListFields();
  34. }
  35. public function getClass()
  36. {
  37. return $this->class;
  38. }
  39. public function getEntityManager()
  40. {
  41. return $this->container->get('doctrine.orm.default_entity_manager');
  42. }
  43. public function getClassMetaData()
  44. {
  45. return $this->getEntityManager()
  46. ->getClassMetaData($this->getClass());
  47. }
  48. public function getBatchActions()
  49. {
  50. return array(
  51. 'delete' => 'action_delete'
  52. );
  53. }
  54. public function getUrls()
  55. {
  56. return array(
  57. 'list' => array(
  58. 'url' => $this->base_route.'_list',
  59. 'params' => array(),
  60. ),
  61. 'create' => array(
  62. 'url' => $this->base_route.'_create',
  63. 'params' => array(),
  64. ),
  65. 'update' => array(
  66. 'url' => $this->base_route.'_update',
  67. 'params' => array()
  68. ),
  69. 'delete' => array(
  70. 'url' => $this->base_route.'_delete',
  71. 'params' => array()
  72. ),
  73. 'edit' => array(
  74. 'url' => $this->base_route.'_edit',
  75. 'params' => array()
  76. ),
  77. 'batch' => array(
  78. 'url' => $this->base_route.'_batch',
  79. 'params' => array()
  80. )
  81. );
  82. }
  83. public function getUrl($name)
  84. {
  85. $urls = $this->getUrls();
  86. if(!isset($urls[$name])) {
  87. return false;
  88. }
  89. return $urls[$name];
  90. }
  91. public function generateUrl($name, $params = array())
  92. {
  93. $url = $this->getUrl($name);
  94. if(!$url) {
  95. throw new \RuntimeException(sprintf('unable to find the url `%s`', $name));
  96. }
  97. if(!is_array($params)) {
  98. $params = array();
  99. }
  100. return $this->container->get('router')->generate($url['url'], array_merge($url['params'], $params));
  101. }
  102. public function getListTemplate()
  103. {
  104. return 'Sonata\BaseApplicationBundle:CRUD:list.twig';
  105. }
  106. public function getEditTemplate()
  107. {
  108. return 'Sonata\BaseApplicationBundle:CRUD:edit.twig';
  109. }
  110. public function getReflectionFields()
  111. {
  112. return $this->getClassMetaData()->reflFields;
  113. }
  114. /**
  115. * make sure the base field are set in the correct format
  116. *
  117. * @param $selected_fields
  118. * @return array
  119. */
  120. static public function getBaseFields($metadata, $selected_fields)
  121. {
  122. // if nothing is defined we display all fields
  123. if(!$selected_fields) {
  124. $selected_fields = array_keys($metadata->reflFields) + array_keys($metadata->associationMappings);
  125. }
  126. // make sure we works with array
  127. foreach($selected_fields as $name => $options) {
  128. if(is_array($options)) {
  129. $fields[$name] = $options;
  130. } else {
  131. $fields[$options] = array();
  132. $name = $options;
  133. }
  134. if(isset($metadata->fieldMappings[$name])) {
  135. $fields[$name] = array_merge(
  136. $metadata->fieldMappings[$name],
  137. $fields[$name]
  138. );
  139. }
  140. if(isset($metadata->associationMappings[$name])) {
  141. $fields[$name] = array_merge(
  142. $metadata->associationMappings[$name],
  143. $fields[$name]
  144. );
  145. }
  146. if(isset($metadata->reflFields[$name])) {
  147. $fields[$name]['reflection'] =& $metadata->reflFields[$name];
  148. }
  149. }
  150. return $fields;
  151. }
  152. /**
  153. * @return void
  154. */
  155. public function configureFormFields()
  156. {
  157. }
  158. /**
  159. * return the target objet
  160. *
  161. * @param $id
  162. * @return
  163. */
  164. public function getObject($id)
  165. {
  166. return $this->getEntityManager()
  167. ->find($this->getClass(), $id);
  168. }
  169. public function buildFormGroups()
  170. {
  171. if(!$this->form_groups) {
  172. $this->form_groups = array(
  173. false => array('fields' => array_keys($this->form_fields))
  174. );
  175. return;
  176. }
  177. // normalize array
  178. foreach($this->form_groups as $name => &$group) {
  179. if(!isset($group['collapsed'])) {
  180. $group['collapsed'] = false;
  181. }
  182. }
  183. }
  184. /**
  185. * build the fields to use in the form
  186. *
  187. * @throws RuntimeException
  188. * @return
  189. */
  190. public function buildFormFields()
  191. {
  192. $this->form_fields = self::getBaseFields($this->getClassMetaData(), $this->form_fields);
  193. foreach($this->form_fields as $name => $options) {
  194. if(!isset($this->form_fields[$name]['type'])) {
  195. throw new \RuntimeException(sprintf('You must declare a type for the field `%s`', $name));
  196. }
  197. // make sure the options field is set
  198. if(!isset($this->form_fields[$name]['options'])) {
  199. $this->form_fields[$name]['options'] = array();
  200. }
  201. // fix template value for doctrine association fields
  202. if(!isset($this->form_fields[$name]['template']) && isset($this->form_fields[$name]['type'])) {
  203. $this->form_fields[$name]['template'] = sprintf('Sonata\BaseApplicationBundle:CRUD:edit_%s.twig', $this->form_fields[$name]['type']);
  204. if($this->form_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::ONE_TO_ONE)
  205. {
  206. $this->form_fields[$name]['template'] = 'Sonata\BaseApplicationBundle:CRUD:edit_one_to_one.twig';
  207. }
  208. if($this->form_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_ONE)
  209. {
  210. $this->form_fields[$name]['template'] = 'Sonata\BaseApplicationBundle:CRUD:edit_many_to_one.twig';
  211. $this->form_fields[$name]['configuration'] = $this->getConfigurationPool()
  212. ->getConfigurationByClass($this->form_fields[$name]['targetEntity']);
  213. }
  214. if($this->form_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY)
  215. {
  216. $this->form_fields[$name]['template'] = 'Sonata\BaseApplicationBundle:CRUD:edit_many_to_many.twig';
  217. $this->form_fields[$name]['configuration'] = $this->getConfigurationPool()
  218. ->getConfigurationByClass($this->form_fields[$name]['targetEntity']);
  219. }
  220. }
  221. // set correct default value
  222. if($this->form_fields[$name]['type'] == 'datetime') {
  223. if(!isset($this->form_fields[$name]['options']['date_widget'])) {
  224. $this->form_fields[$name]['options']['date_widget'] = \Symfony\Component\Form\DateField::CHOICE;
  225. }
  226. if(!isset($this->form_fields[$name]['options']['years'])) {
  227. $this->form_fields[$name]['options']['years'] = range(1900, 2100);
  228. }
  229. }
  230. // unset the identifier field as it is not required to update an object
  231. if(isset($this->form_fields[$name]['id'])) {
  232. unset($this->form_fields[$name]);
  233. }
  234. }
  235. $this->configureFormFields();
  236. return $this->form_fields;
  237. }
  238. /**
  239. * build the field to use in the list view
  240. *
  241. * @return void
  242. */
  243. public function buildListFields()
  244. {
  245. $this->list_fields = self::getBaseFields($this->getClassMetaData(), $this->list_fields);
  246. $this->configureListFields();
  247. // normalize field
  248. foreach($this->list_fields as $name => $options) {
  249. $this->list_fields[$name]['code'] = $name;
  250. // set the label if none is set
  251. if(!isset($this->list_fields[$name]['label']))
  252. {
  253. $this->list_fields[$name]['label'] = $name;
  254. }
  255. // set the default type if none is set
  256. if(!isset($this->list_fields[$name]['type'])) {
  257. $this->list_fields[$name]['type'] = 'string';
  258. }
  259. // fix template for mapping
  260. if($this->list_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_ONE) {
  261. $this->list_fields[$name]['template'] = 'Sonata/BaseApplicationBundle:CRUD:list_many_to_one.twig';
  262. $this->list_fields[$name]['configuration'] = $this->getConfigurationPool()
  263. ->getConfigurationByClass($this->list_fields[$name]['targetEntity']);
  264. }
  265. if($this->list_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::ONE_TO_MANY) {
  266. $this->list_fields[$name]['template'] = 'Sonata/BaseApplicationBundle:CRUD:list_one_to_many.twig';
  267. }
  268. if($this->list_fields[$name]['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY) {
  269. $this->list_fields[$name]['template'] = 'Sonata/BaseApplicationBundle:CRUD:list_many_to_many.twig';
  270. }
  271. // define the default template
  272. if(!isset($this->list_fields[$name]['template'])) {
  273. $this->list_fields[$name]['template'] = sprintf('Sonata/BaseApplicationBundle:CRUD:list_%s.twig', $this->list_fields[$name]['type']);
  274. }
  275. // define the default template for identifier field
  276. if(isset($this->list_fields[$name]['id'])) {
  277. $this->list_fields[$name]['identifier'] = true;
  278. }
  279. if(!isset($this->list_fields[$name]['identifier'])) {
  280. $this->list_fields[$name]['identifier'] = false;
  281. }
  282. }
  283. if(!isset($this->list_fields['_batch'])) {
  284. $this->list_fields = array('_batch' => array(
  285. 'code' => '_batch',
  286. 'template' => 'Sonata/BaseApplicationBundle:CRUD:list__batch.twig',
  287. 'label' => 'batch',
  288. 'identifier' => false
  289. ) ) + $this->list_fields;
  290. }
  291. return $this->list_fields;
  292. }
  293. public function configureListFields()
  294. {
  295. }
  296. public function configureFilterFields()
  297. {
  298. }
  299. public function getFilterDatagrid()
  300. {
  301. if(!$this->filter_datagrid) {
  302. $this->filter_datagrid = new Datagrid(
  303. $this->getClass(),
  304. $this->getEntityManager()
  305. );
  306. $this->filter_datagrid->setMaxPerPage($this->max_per_page);
  307. $this->configureFilterFields();
  308. $this->filter_datagrid->setFilterFields($this->filter_fields);
  309. $this->filter_datagrid->buildFilterFields();
  310. }
  311. return $this->filter_datagrid;
  312. }
  313. /**
  314. * Construct and build the form field definitions
  315. *
  316. * @return list form field definition
  317. */
  318. public function getFormFields()
  319. {
  320. return $this->form_fields;
  321. }
  322. public function getListFields()
  323. {
  324. return $this->list_fields;
  325. }
  326. public function getChoices($description)
  327. {
  328. $targets = $this->getEntityManager()
  329. ->createQueryBuilder()
  330. ->select('t')
  331. ->from($description['targetEntity'], 't')
  332. ->getQuery()
  333. ->execute();
  334. $choices = array();
  335. foreach($targets as $target) {
  336. // todo : puts this into a configuration option and use reflection
  337. foreach(array('getTitle', 'getName', '__toString') as $getter) {
  338. if(method_exists($target, $getter)) {
  339. $choices[$target->getId()] = $target->$getter();
  340. break;
  341. }
  342. }
  343. }
  344. return $choices;
  345. }
  346. public function getForm($object, $fields)
  347. {
  348. $this->container->get('session')->start();
  349. $form = new Form('data', $object, $this->container->get('validator'));
  350. foreach($fields as $name => $description) {
  351. if(!isset($description['type'])) {
  352. continue;
  353. }
  354. switch($description['type']) {
  355. case \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY:
  356. $transformer = new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\CollectionToChoiceTransformer(array(
  357. 'em' => $this->getEntityManager(),
  358. 'className' => $description['targetEntity']
  359. ));
  360. $field = new \Symfony\Component\Form\ChoiceField($name, array_merge(array(
  361. 'expanded' => true,
  362. 'multiple' => true,
  363. 'choices' => $this->getChoices($description),
  364. 'value_transformer' => $transformer,
  365. ), $description['options']));
  366. break;
  367. case \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_ONE:
  368. case \Doctrine\ORM\Mapping\ClassMetadataInfo::ONE_TO_ONE:
  369. $transformer = new \Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\EntityToIDTransformer(array(
  370. 'em' => $this->getEntityManager(),
  371. 'className' => $description['targetEntity']
  372. ));
  373. $field = new \Symfony\Component\Form\ChoiceField($name, array_merge(array(
  374. 'expanded' => false,
  375. 'choices' => $this->getChoices($description),
  376. 'value_transformer' => $transformer,
  377. ), $description['options']));
  378. break;
  379. case 'string':
  380. $field = new \Symfony\Component\Form\TextField($name, $description['options']);
  381. break;
  382. case 'text':
  383. $field = new \Symfony\Component\Form\TextareaField($name, $description['options']);
  384. break;
  385. case 'boolean':
  386. $field = new \Symfony\Component\Form\CheckboxField($name, $description['options']);
  387. break;
  388. case 'integer':
  389. $field = new \Symfony\Component\Form\IntegerField($name, $description['options']);
  390. break;
  391. case 'decimal':
  392. $field = new \Symfony\Component\Form\NumberField($name, $description['options']);
  393. break;
  394. case 'datetime':
  395. $field = new \Symfony\Component\Form\DateTimeField($name, $description['options']);
  396. break;
  397. case 'date':
  398. $field = new \Symfony\Component\Form\DateField($name, $description['options']);
  399. break;
  400. case 'choice':
  401. $field = new \Symfony\Component\Form\ChoiceField($name, $description['options']);
  402. break;
  403. case 'array':
  404. $field = new \Symfony\Component\Form\FieldGroup($name, $description['options']);
  405. $values = $description['reflection']->getValue($object);
  406. foreach((array)$values as $k => $v) {
  407. $field->add(new \Symfony\Component\Form\TextField($k));
  408. }
  409. break;
  410. default:
  411. throw new \RuntimeException(sprintf('unknow type `%s`', $description['type']));
  412. }
  413. $form->add($field);
  414. }
  415. return $form;
  416. }
  417. public function setBaseControllerName($base_controller_name)
  418. {
  419. $this->base_controller_name = $base_controller_name;
  420. }
  421. public function getBaseControllerName()
  422. {
  423. return $this->base_controller_name;
  424. }
  425. public function setBaseRoute($base_route)
  426. {
  427. $this->base_route = $base_route;
  428. }
  429. public function getBaseRoute()
  430. {
  431. return $this->base_route;
  432. }
  433. public function setConfigurationPool($configuration_pool)
  434. {
  435. $this->configuration_pool = $configuration_pool;
  436. }
  437. public function getConfigurationPool()
  438. {
  439. return $this->configuration_pool;
  440. }
  441. public function setCode($code)
  442. {
  443. $this->code = $code;
  444. }
  445. public function getCode()
  446. {
  447. return $this->code;
  448. }
  449. public function setLabel($label)
  450. {
  451. $this->label = $label;
  452. }
  453. public function getLabel()
  454. {
  455. return $this->label;
  456. }
  457. public function setFilterFields($filter_fields)
  458. {
  459. $this->filter_fields = $filter_fields;
  460. }
  461. public function getFilterFields()
  462. {
  463. return $this->filter_fields;
  464. }
  465. public function setMaxPerPage($max_per_page)
  466. {
  467. $this->max_per_page = $max_per_page;
  468. }
  469. public function getMaxPerPage()
  470. {
  471. return $this->max_per_page;
  472. }
  473. public function setFormGroups($form_groups)
  474. {
  475. $this->form_groups = $form_groups;
  476. }
  477. public function getFormGroups()
  478. {
  479. return $this->form_groups;
  480. }
  481. }