Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

DataTable.php 11 KiB

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. }