DataTable.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <?php
  2. declare(strict_types=1);
  3. namespace Grav\Plugin\FlexObjects\Table;
  4. use Grav\Common\Debugger;
  5. use Grav\Common\Grav;
  6. use Grav\Framework\Collection\CollectionInterface;
  7. use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
  8. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  9. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  10. use JsonSerializable;
  11. use Throwable;
  12. use Twig\Environment;
  13. use Twig\Error\LoaderError;
  14. use Twig\Error\RuntimeError;
  15. use Twig\Error\SyntaxError;
  16. use function is_array;
  17. use function is_string;
  18. /**
  19. * Class DataTable
  20. * @package Grav\Plugin\Gitea
  21. *
  22. * https://github.com/ratiw/vuetable-2/wiki/Data-Format-(JSON)
  23. * https://github.com/ratiw/vuetable-2/wiki/Sorting
  24. */
  25. class DataTable implements JsonSerializable
  26. {
  27. /** @var string */
  28. private $url;
  29. /** @var int */
  30. private $limit;
  31. /** @var int */
  32. private $page;
  33. /** @var array */
  34. private $sort;
  35. /** @var string */
  36. private $search;
  37. /** @var FlexCollectionInterface */
  38. private $collection;
  39. /** @var FlexCollectionInterface */
  40. private $filteredCollection;
  41. /** @var array */
  42. private $columns;
  43. /** @var Environment */
  44. private $twig;
  45. /** @var array */
  46. private $twig_context;
  47. /**
  48. * DataTable constructor.
  49. * @param array $params
  50. */
  51. public function __construct(array $params)
  52. {
  53. $this->setUrl($params['url'] ?? '');
  54. $this->setLimit((int)($params['limit'] ?? 10));
  55. $this->setPage((int)($params['page'] ?? 1));
  56. $this->setSort($params['sort'] ?? ['id' => 'asc']);
  57. $this->setSearch($params['search'] ?? '');
  58. }
  59. /**
  60. * @param string $url
  61. * @return void
  62. */
  63. public function setUrl(string $url): void
  64. {
  65. $this->url = $url;
  66. }
  67. /**
  68. * @param int $limit
  69. * @return void
  70. */
  71. public function setLimit(int $limit): void
  72. {
  73. $this->limit = max(1, $limit);
  74. }
  75. /**
  76. * @param int $page
  77. * @return void
  78. */
  79. public function setPage(int $page): void
  80. {
  81. $this->page = max(1, $page);
  82. }
  83. /**
  84. * @param string|string[] $sort
  85. * @return void
  86. */
  87. public function setSort($sort): void
  88. {
  89. if (is_string($sort)) {
  90. $sort = $this->decodeSort($sort);
  91. } elseif (!is_array($sort)) {
  92. $sort = [];
  93. }
  94. $this->sort = $sort;
  95. }
  96. /**
  97. * @param string $search
  98. * @return void
  99. */
  100. public function setSearch(string $search): void
  101. {
  102. $this->search = $search;
  103. }
  104. /**
  105. * @param CollectionInterface $collection
  106. * @return void
  107. */
  108. public function setCollection(CollectionInterface $collection): void
  109. {
  110. $this->collection = $collection;
  111. $this->filteredCollection = null;
  112. }
  113. /**
  114. * @return int
  115. */
  116. public function getLimit(): int
  117. {
  118. return $this->limit;
  119. }
  120. /**
  121. * @return int
  122. */
  123. public function getPage(): int
  124. {
  125. return $this->page;
  126. }
  127. /**
  128. * @return int
  129. */
  130. public function getLastPage(): int
  131. {
  132. return 1 + (int)floor(max(0, $this->getTotal()-1) / $this->getLimit());
  133. }
  134. /**
  135. * @return int
  136. */
  137. public function getTotal(): int
  138. {
  139. $collection = $this->filteredCollection ?? $this->getCollection();
  140. return $collection ? $collection->count() : 0;
  141. }
  142. /**
  143. * @return array
  144. */
  145. public function getSort(): array
  146. {
  147. return $this->sort;
  148. }
  149. /**
  150. * @return FlexCollectionInterface|null
  151. */
  152. public function getCollection(): ?FlexCollectionInterface
  153. {
  154. return $this->collection;
  155. }
  156. /**
  157. * @param int $page
  158. * @return string|null
  159. */
  160. public function getUrl(int $page): ?string
  161. {
  162. if ($page < 1 || $page > $this->getLastPage()) {
  163. return null;
  164. }
  165. return "{$this->url}.json?page={$page}&per_page={$this->getLimit()}&sort={$this->encodeSort()}";
  166. }
  167. /**
  168. * @return array
  169. */
  170. public function getColumns(): array
  171. {
  172. if (null === $this->columns) {
  173. $collection = $this->getCollection();
  174. if (!$collection) {
  175. return [];
  176. }
  177. $blueprint = $collection->getFlexDirectory()->getBlueprint();
  178. $schema = $blueprint->schema();
  179. $columns = $blueprint->get('config/admin/views/list/fields') ?? $blueprint->get('config/admin/list/fields', []);
  180. $list = [];
  181. foreach ($columns as $key => $options) {
  182. if (!isset($options['field'])) {
  183. $options['field'] = $schema->get($options['alias'] ?? $key);
  184. }
  185. if (!$options['field'] || !empty($options['field']['ignore'])) {
  186. continue;
  187. }
  188. $list[$key] = $options;
  189. }
  190. $this->columns = $list;
  191. }
  192. return $this->columns;
  193. }
  194. /**
  195. * @return array
  196. */
  197. public function getData(): array
  198. {
  199. $grav = Grav::instance();
  200. /** @var Debugger $debugger */
  201. $debugger = $grav['debugger'];
  202. $debugger->startTimer('datatable', 'Data Table');
  203. $collection = $this->getCollection();
  204. if (!$collection) {
  205. return [];
  206. }
  207. if ($this->search !== '') {
  208. $collection = $collection->search($this->search);
  209. }
  210. $columns = $this->getColumns();
  211. $collection = $collection->sort($this->getSort());
  212. $this->filteredCollection = $collection;
  213. $limit = $this->getLimit();
  214. $page = $this->getPage();
  215. $to = $page * $limit;
  216. $from = $to - $limit + 1;
  217. if ($from < 1 || $from > $this->getTotal()) {
  218. $debugger->stopTimer('datatable');
  219. return [];
  220. }
  221. $array = $collection->slice($from-1, $limit);
  222. $twig = $grav['twig'];
  223. $grav->fireEvent('onTwigSiteVariables');
  224. $this->twig = $twig->twig;
  225. $this->twig_context = $twig->twig_vars;
  226. $list = [];
  227. /** @var FlexObjectInterface $object */
  228. foreach ($array as $object) {
  229. $item = [
  230. 'id' => $object->getKey(),
  231. 'timestamp' => $object->getTimestamp()
  232. ];
  233. foreach ($columns as $name => $column) {
  234. $item[str_replace('.', '_', $name)] = $this->renderColumn($name, $column, $object);
  235. }
  236. $item['_actions_'] = $this->renderActions($object);
  237. $list[] = $item;
  238. }
  239. $debugger->stopTimer('datatable');
  240. return $list;
  241. }
  242. /**
  243. * @return array
  244. */
  245. public function jsonSerialize(): array
  246. {
  247. $data = $this->getData();
  248. $total = $this->getTotal();
  249. $limit = $this->getLimit();
  250. $page = $this->getPage();
  251. $to = $page * $limit;
  252. $from = $to - $limit + 1;
  253. $empty = empty($data);
  254. return [
  255. 'links' => [
  256. 'pagination' => [
  257. 'total' => $total,
  258. 'per_page' => $limit,
  259. 'current_page' => $page,
  260. 'last_page' => $this->getLastPage(),
  261. 'next_page_url' => $this->getUrl($page+1),
  262. 'prev_page_url' => $this->getUrl($page-1),
  263. 'from' => $empty ? null : $from,
  264. 'to' => $empty ? null : min($to, $total),
  265. ]
  266. ],
  267. 'data' => $data
  268. ];
  269. }
  270. /**
  271. * @param string $name
  272. * @param array $column
  273. * @param FlexObjectInterface $object
  274. * @return false|string
  275. * @throws Throwable
  276. * @throws LoaderError
  277. * @throws RuntimeError
  278. * @throws SyntaxError
  279. */
  280. protected function renderColumn(string $name, array $column, FlexObjectInterface $object)
  281. {
  282. $grav = Grav::instance();
  283. $flex = $grav['flex_objects'];
  284. $value = $object->getFormValue($name) ?? $object->getNestedProperty($name, $column['field']['default'] ?? null);
  285. $type = $column['field']['type'] ?? 'text';
  286. $hasLink = $column['link'] ?? null;
  287. $link = null;
  288. $authorized = $object instanceof FlexAuthorizeInterface
  289. ? ($object->isAuthorized('read') || $object->isAuthorized('update')) : true;
  290. if ($hasLink && $authorized) {
  291. $route = $grav['route']->withExtension('');
  292. $link = $route->withAddedPath($object->getKey())->withoutParams()->getUri();
  293. }
  294. $template = $this->twig->resolveTemplate(["forms/fields/{$type}/edit_list.html.twig", 'forms/fields/text/edit_list.html.twig']);
  295. return $this->twig->load($template)->render([
  296. 'value' => $value,
  297. 'link' => $link,
  298. 'field' => $column['field'],
  299. 'object' => $object,
  300. 'flex' => $flex,
  301. 'route' => $grav['route']->withExtension('')
  302. ] + $this->twig_context);
  303. }
  304. /**
  305. * @param FlexObjectInterface $object
  306. * @return false|string
  307. * @throws Throwable
  308. * @throws LoaderError
  309. * @throws RuntimeError
  310. * @throws SyntaxError
  311. */
  312. protected function renderActions(FlexObjectInterface $object)
  313. {
  314. $grav = Grav::instance();
  315. $type = $object->getFlexType();
  316. $template = $this->twig->resolveTemplate(["flex-objects/types/{$type}/list/list_actions.html.twig", 'flex-objects/types/default/list/list_actions.html.twig']);
  317. return $this->twig->load($template)->render([
  318. 'object' => $object,
  319. 'flex' => $grav['flex_objects'],
  320. 'route' => $grav['route']->withExtension('')
  321. ] + $this->twig_context);
  322. }
  323. /**
  324. * @param string $sort
  325. * @param string $fieldSeparator
  326. * @param string $orderSeparator
  327. * @return array
  328. */
  329. protected function decodeSort(string $sort, string $fieldSeparator = ',', string $orderSeparator = '|'): array
  330. {
  331. $strings = explode($fieldSeparator, $sort);
  332. $list = [];
  333. foreach ($strings as $string) {
  334. $item = explode($orderSeparator, $string, 2);
  335. $key = array_shift($item);
  336. $order = array_shift($item) === 'desc' ? 'desc' : 'asc';
  337. $list[$key] = $order;
  338. }
  339. return $list;
  340. }
  341. /**
  342. * @param string $fieldSeparator
  343. * @param string $orderSeparator
  344. * @return string
  345. */
  346. protected function encodeSort(string $fieldSeparator = ',', string $orderSeparator = '|'): string
  347. {
  348. $list = [];
  349. foreach ($this->getSort() as $key => $order) {
  350. $list[] = $key . $orderSeparator . ($order ?: 'asc');
  351. }
  352. return implode($fieldSeparator, $list);
  353. }
  354. }