SonataAdminExtension.php 13 KB

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