SonataAdminExtension.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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\Twig\Extension;
  11. use Doctrine\Common\Util\ClassUtils;
  12. use Knp\Menu\MenuFactory;
  13. use Knp\Menu\ItemInterface;
  14. use Knp\Menu\Twig\Helper;
  15. use Psr\Log\LoggerInterface;
  16. use Sonata\AdminBundle\Admin\AdminInterface;
  17. use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
  18. use Sonata\AdminBundle\Admin\Pool;
  19. use Sonata\AdminBundle\Exception\NoValueException;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\PropertyAccess\PropertyAccess;
  22. use Symfony\Component\Routing\RouterInterface;
  23. class SonataAdminExtension extends \Twig_Extension
  24. {
  25. /**
  26. * @var \Twig_Environment
  27. */
  28. protected $environment;
  29. /**
  30. * @var Pool
  31. */
  32. protected $pool;
  33. /**
  34. * @var RouterInterface
  35. */
  36. protected $router;
  37. /**
  38. * @var Helper
  39. */
  40. protected $knpHelper;
  41. /**
  42. * @var LoggerInterface
  43. */
  44. protected $logger;
  45. /**
  46. * @param Pool $pool
  47. * @param LoggerInterface $logger
  48. */
  49. public function __construct(Pool $pool, RouterInterface $router, Helper $knpHelper, LoggerInterface $logger = null)
  50. {
  51. $this->pool = $pool;
  52. $this->logger = $logger;
  53. $this->router = $router;
  54. $this->knpHelper = $knpHelper;
  55. }
  56. /**
  57. * {@inheritdoc}
  58. */
  59. public function initRuntime(\Twig_Environment $environment)
  60. {
  61. $this->environment = $environment;
  62. }
  63. /**
  64. * {@inheritDoc}
  65. */
  66. public function getFilters()
  67. {
  68. return array(
  69. 'render_list_element' => new \Twig_Filter_Method($this, 'renderListElement', array('is_safe' => array('html'))),
  70. 'render_view_element' => new \Twig_Filter_Method($this, 'renderViewElement', array('is_safe' => array('html'))),
  71. 'render_view_element_compare' => new \Twig_Filter_Method($this, 'renderViewElementCompare', array('is_safe' => array('html'))),
  72. 'render_relation_element' => new \Twig_Filter_Method($this, 'renderRelationElement'),
  73. 'sonata_urlsafeid' => new \Twig_Filter_Method($this, 'getUrlsafeIdentifier'),
  74. 'sonata_xeditable_type' => new \Twig_Filter_Method($this, 'getXEditableType'),
  75. );
  76. }
  77. /**
  78. * {@inheritDoc}
  79. */
  80. public function getFunctions()
  81. {
  82. return array(
  83. 'sonata_knp_menu_build' => new \Twig_Function_Method($this, 'getKnpMenu'),
  84. );
  85. }
  86. /**
  87. * {@inheritDoc}
  88. */
  89. public function getTokenParsers()
  90. {
  91. return array();
  92. }
  93. /**
  94. * {@inheritDoc}
  95. */
  96. public function getName()
  97. {
  98. return 'sonata_admin';
  99. }
  100. /**
  101. * Get template
  102. *
  103. * @param FieldDescriptionInterface $fieldDescription
  104. * @param string $defaultTemplate
  105. *
  106. * @return \Twig_TemplateInterface
  107. */
  108. protected function getTemplate(FieldDescriptionInterface $fieldDescription, $defaultTemplate)
  109. {
  110. $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
  111. try {
  112. $template = $this->environment->loadTemplate($templateName);
  113. } catch (\Twig_Error_Loader $e) {
  114. $template = $this->environment->loadTemplate($defaultTemplate);
  115. if (null !== $this->logger) {
  116. $this->logger->warning(sprintf('An error occured trying to load the template "%s" for the field "%s", the default template "%s" was used instead: "%s". ', $templateName, $fieldDescription->getFieldName(), $defaultTemplate, $e->getMessage()));
  117. }
  118. }
  119. return $template;
  120. }
  121. /**
  122. * render a list element from the FieldDescription
  123. *
  124. * @param mixed $object
  125. * @param FieldDescriptionInterface $fieldDescription
  126. * @param array $params
  127. *
  128. * @return string
  129. */
  130. public function renderListElement($object, FieldDescriptionInterface $fieldDescription, $params = array())
  131. {
  132. $template = $this->getTemplate($fieldDescription, $fieldDescription->getAdmin()->getTemplate('base_list_field'));
  133. return $this->output($fieldDescription, $template, array_merge($params, array(
  134. 'admin' => $fieldDescription->getAdmin(),
  135. 'object' => $object,
  136. 'value' => $this->getValueFromFieldDescription($object, $fieldDescription),
  137. 'field_description' => $fieldDescription,
  138. )));
  139. }
  140. /**
  141. * @param FieldDescriptionInterface $fieldDescription
  142. * @param \Twig_TemplateInterface $template
  143. * @param array $parameters
  144. *
  145. * @return string
  146. */
  147. public function output(FieldDescriptionInterface $fieldDescription, \Twig_TemplateInterface $template, array $parameters = array())
  148. {
  149. $content = $template->render($parameters);
  150. if ($this->environment->isDebug()) {
  151. return sprintf("\n<!-- START \n fieldName: %s\n template: %s\n compiled template: %s\n -->\n%s\n<!-- END - fieldName: %s -->",
  152. $fieldDescription->getFieldName(),
  153. $fieldDescription->getTemplate(),
  154. $template->getTemplateName(),
  155. $content,
  156. $fieldDescription->getFieldName()
  157. );
  158. }
  159. return $content;
  160. }
  161. /**
  162. * return the value related to FieldDescription, if the associated object does no
  163. * exists => a temporary one is created
  164. *
  165. * @param object $object
  166. * @param FieldDescriptionInterface $fieldDescription
  167. * @param array $params
  168. *
  169. * @throws \RuntimeException
  170. *
  171. * @return mixed
  172. */
  173. public function getValueFromFieldDescription($object, FieldDescriptionInterface $fieldDescription, array $params = array())
  174. {
  175. if (isset($params['loop']) && $object instanceof \ArrayAccess) {
  176. throw new \RuntimeException('remove the loop requirement');
  177. }
  178. $value = null;
  179. try {
  180. $value = $fieldDescription->getValue($object);
  181. } catch (NoValueException $e) {
  182. if ($fieldDescription->getAssociationAdmin()) {
  183. $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
  184. }
  185. }
  186. return $value;
  187. }
  188. /**
  189. * render a view element
  190. *
  191. * @param FieldDescriptionInterface $fieldDescription
  192. * @param mixed $object
  193. *
  194. * @return string
  195. */
  196. public function renderViewElement(FieldDescriptionInterface $fieldDescription, $object)
  197. {
  198. $template = $this->getTemplate($fieldDescription, 'SonataAdminBundle:CRUD:base_show_field.html.twig');
  199. try {
  200. $value = $fieldDescription->getValue($object);
  201. } catch (NoValueException $e) {
  202. $value = null;
  203. }
  204. return $this->output($fieldDescription, $template, array(
  205. 'field_description' => $fieldDescription,
  206. 'object' => $object,
  207. 'value' => $value,
  208. 'admin' => $fieldDescription->getAdmin(),
  209. ));
  210. }
  211. /**
  212. * render a compared view element
  213. *
  214. * @param FieldDescriptionInterface $fieldDescription
  215. * @param mixed $baseObject
  216. * @param mixed $compareObject
  217. *
  218. * @return string
  219. */
  220. public function renderViewElementCompare(FieldDescriptionInterface $fieldDescription, $baseObject, $compareObject)
  221. {
  222. $template = $this->getTemplate($fieldDescription, 'SonataAdminBundle:CRUD:base_show_field.html.twig');
  223. try {
  224. $baseValue = $fieldDescription->getValue($baseObject);
  225. } catch (NoValueException $e) {
  226. $baseValue = null;
  227. }
  228. try {
  229. $compareValue = $fieldDescription->getValue($compareObject);
  230. } catch (NoValueException $e) {
  231. $compareValue = null;
  232. }
  233. $baseValueOutput = $template->render(array(
  234. 'admin' => $fieldDescription->getAdmin(),
  235. 'field_description' => $fieldDescription,
  236. 'value' => $baseValue,
  237. ));
  238. $compareValueOutput = $template->render(array(
  239. 'field_description' => $fieldDescription,
  240. 'admin' => $fieldDescription->getAdmin(),
  241. 'value' => $compareValue,
  242. ));
  243. // Compare the rendered output of both objects by using the (possibly) overridden field block
  244. $isDiff = $baseValueOutput !== $compareValueOutput;
  245. return $this->output($fieldDescription, $template, array(
  246. 'field_description' => $fieldDescription,
  247. 'value' => $baseValue,
  248. 'value_compare' => $compareValue,
  249. 'is_diff' => $isDiff,
  250. 'admin' => $fieldDescription->getAdmin(),
  251. ));
  252. }
  253. /**
  254. * @throws \RunTimeException
  255. *
  256. * @param mixed $element
  257. * @param FieldDescriptionInterface $fieldDescription
  258. *
  259. * @return mixed
  260. */
  261. public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
  262. {
  263. if (!is_object($element)) {
  264. return $element;
  265. }
  266. $propertyPath = $fieldDescription->getOption('associated_property');
  267. if (null === $propertyPath) {
  268. // For BC kept associated_tostring option behavior
  269. $method = $fieldDescription->getOption('associated_tostring', '__toString');
  270. if (!method_exists($element, $method)) {
  271. throw new \RuntimeException(sprintf(
  272. 'You must define an `associated_property` option or create a `%s::__toString` method to the field option %s from service %s is ',
  273. get_class($element),
  274. $fieldDescription->getName(),
  275. $fieldDescription->getAdmin()->getCode()
  276. ));
  277. }
  278. return call_user_func(array($element, $method));
  279. }
  280. if (is_callable($propertyPath)) {
  281. return $propertyPath($element);
  282. }
  283. return PropertyAccess::createPropertyAccessor()->getValue($element, $propertyPath);
  284. }
  285. /**
  286. * Get the identifiers as a string that is save to use in an url.
  287. *
  288. * @param object $model
  289. * @param AdminInterface $admin
  290. *
  291. * @return string string representation of the id that is save to use in an url
  292. */
  293. public function getUrlsafeIdentifier($model, AdminInterface $admin = null)
  294. {
  295. if (is_null($admin)) {
  296. $admin = $this->pool->getAdminByClass(
  297. ClassUtils::getClass($model)
  298. );
  299. }
  300. return $admin->getUrlsafeIdentifier($model);
  301. }
  302. /**
  303. * @param $type
  304. *
  305. * @return string|bool
  306. */
  307. public function getXEditableType($type)
  308. {
  309. $mapping = array(
  310. 'boolean' => 'select',
  311. 'text' => 'text',
  312. 'textarea' => 'textarea',
  313. 'email' => 'email',
  314. 'string' => 'text',
  315. 'smallint' => 'text',
  316. 'bigint' => 'text',
  317. 'integer' => 'number',
  318. 'decimal' => 'number',
  319. 'currency' => 'number',
  320. 'percent' => 'number',
  321. 'url' => 'url',
  322. );
  323. return isset($mapping[$type]) ? $mapping[$type] : false;
  324. }
  325. /**
  326. * Get KnpMenu
  327. *
  328. * @param Request $request
  329. *
  330. * @return ItemInterface
  331. */
  332. public function getKnpMenu(Request $request = null)
  333. {
  334. $menuFactory = new MenuFactory();
  335. $menu = $menuFactory
  336. ->createItem('root')
  337. ->setExtra('request', $request)
  338. ;
  339. foreach ($this->pool->getAdminGroups() as $name => $group) {
  340. // Check if the menu group is built by a menu provider
  341. if (isset($group['provider'])) {
  342. $subMenu = $this->knpHelper->get($group['provider']);
  343. $menu->addChild($subMenu)
  344. ->setAttributes(array(
  345. 'icon' => $group['icon'],
  346. 'label_catalogue' => $group['label_catalogue']
  347. ))
  348. ->setExtra('roles', $group['roles']);
  349. continue;
  350. }
  351. // The menu group is built by config
  352. $menu
  353. ->addChild($name, array('label' => $group['label']))
  354. ->setAttributes(
  355. array(
  356. 'icon' => $group['icon'],
  357. 'label_catalogue' => $group['label_catalogue'],
  358. )
  359. )
  360. ->setExtra('roles', $group['roles'])
  361. ;
  362. foreach ($group['items'] as $item) {
  363. if (array_key_exists('admin', $item) && $item['admin'] != null) {
  364. $admin = $this->pool->getInstance($item['admin']);
  365. // skip menu item if no `list` url is available or user doesn't have the LIST access rights
  366. if (!$admin->hasRoute('list') || !$admin->isGranted('LIST')) {
  367. continue;
  368. }
  369. $label = $admin->getLabel();
  370. $route = $admin->generateUrl('list');
  371. $translationDomain = $admin->getTranslationDomain();
  372. } else {
  373. $label = $item['label'];
  374. $route = $this->router->generate($item['route'], $item['route_params']);
  375. $translationDomain = $group['label_catalogue'];
  376. $admin = null;
  377. }
  378. $menu[$name]
  379. ->addChild($label, array('uri' => $route))
  380. ->setExtra('translationdomain', $translationDomain)
  381. ->setExtra('admin', $admin)
  382. ;
  383. }
  384. if (0 === count($menu[$name]->getChildren())) {
  385. $menu->removeChild($name);
  386. }
  387. }
  388. return $menu;
  389. }
  390. }