SonataAdminExtension.php 12 KB

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