Skip to content

Commit 71789e8

Browse files
radimvaculikf3l1x
authored andcommitted
Add NetteDatabaseDataSource for raw SQL queries via Nette\Database\Explorer
Ports the contributte/datagrid-nette-database-data-source package into the main datagrid repository. Uses subquery wrapping for filter injection instead of PHPSQLParser, avoiding any extra dependencies.
1 parent 5ccbcf2 commit 71789e8

2 files changed

Lines changed: 667 additions & 0 deletions

File tree

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Datagrid\DataSource;
4+
5+
use Contributte\Datagrid\Exception\DatagridDateTimeHelperException;
6+
use Contributte\Datagrid\Filter\FilterDate;
7+
use Contributte\Datagrid\Filter\FilterDateRange;
8+
use Contributte\Datagrid\Filter\FilterMultiSelect;
9+
use Contributte\Datagrid\Filter\FilterRange;
10+
use Contributte\Datagrid\Filter\FilterSelect;
11+
use Contributte\Datagrid\Filter\FilterText;
12+
use Contributte\Datagrid\Utils\DateTimeHelper;
13+
use Contributte\Datagrid\Utils\Sorting;
14+
use Nette\Database\Explorer;
15+
use Nette\Database\ResultSet;
16+
17+
class NetteDatabaseDataSource extends FilterableDataSource implements IDataSource
18+
{
19+
20+
protected array $data = [];
21+
22+
/** @var mixed[] */
23+
protected array $queryParameters;
24+
25+
/** @var array<array{string, mixed[]}> */
26+
protected array $whereConditions = [];
27+
28+
protected ?string $orderByClause = null;
29+
30+
/**
31+
* @param mixed[] $params
32+
*/
33+
public function __construct(
34+
protected Explorer $connection,
35+
protected string $sql,
36+
array $params = [],
37+
)
38+
{
39+
$this->queryParameters = $params;
40+
}
41+
42+
public function getCount(): int
43+
{
44+
$sql = sprintf('SELECT COUNT(*) AS count FROM (%s) AS datagrid_count', $this->buildFilteredSql());
45+
46+
$row = $this->query($sql, $this->buildParams())->fetch();
47+
48+
return $row !== null ? (int) $row['count'] : 0;
49+
}
50+
51+
/**
52+
* {@inheritDoc}
53+
*/
54+
public function getData(): array
55+
{
56+
return $this->data !== []
57+
? $this->data
58+
: $this->query($this->buildFinalSql(), $this->buildParams())->fetchAll();
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
public function filterOne(array $condition): IDataSource
65+
{
66+
foreach ($condition as $column => $value) {
67+
$this->addWhereCondition(sprintf('%s = ?', $column), [$value]);
68+
}
69+
70+
return $this;
71+
}
72+
73+
/**
74+
* @phpstan-param positive-int|0 $offset
75+
* @phpstan-param positive-int|0 $limit
76+
*/
77+
public function limit(int $offset, int $limit): IDataSource
78+
{
79+
$sql = sprintf('%s LIMIT %d OFFSET %d', $this->buildFinalSql(), $limit, $offset);
80+
81+
$this->data = $this->query($sql, $this->buildParams())->fetchAll();
82+
83+
return $this;
84+
}
85+
86+
public function sort(Sorting $sorting): IDataSource
87+
{
88+
if (is_callable($sorting->getSortCallback())) {
89+
call_user_func(
90+
$sorting->getSortCallback(),
91+
$this->sql,
92+
$sorting->getSort()
93+
);
94+
95+
return $this;
96+
}
97+
98+
$sort = $sorting->getSort();
99+
100+
if ($sort !== []) {
101+
$parts = [];
102+
103+
foreach ($sort as $column => $order) {
104+
$parts[] = sprintf('%s %s', $column, $order);
105+
}
106+
107+
$this->orderByClause = implode(', ', $parts);
108+
}
109+
110+
return $this;
111+
}
112+
113+
public function getDataSource(): Explorer
114+
{
115+
return $this->connection;
116+
}
117+
118+
/**
119+
* Returns the current SQL query and its parameters.
120+
*
121+
* @return array{string, mixed[]}
122+
*/
123+
public function getQuery(): array
124+
{
125+
return [$this->buildFinalSql(), $this->buildParams()];
126+
}
127+
128+
/**
129+
* @param mixed[] $params
130+
*/
131+
protected function addWhereCondition(string $sql, array $params = []): void
132+
{
133+
$this->whereConditions[] = [$sql, $params];
134+
}
135+
136+
protected function buildFilteredSql(): string
137+
{
138+
if ($this->whereConditions === []) {
139+
return $this->sql;
140+
}
141+
142+
$whereParts = array_map(static fn (array $c): string => $c[0], $this->whereConditions);
143+
$whereClause = implode(' AND ', $whereParts);
144+
145+
return sprintf('SELECT * FROM (%s) AS datagrid_base WHERE %s', $this->sql, $whereClause);
146+
}
147+
148+
protected function buildFinalSql(): string
149+
{
150+
$sql = $this->buildFilteredSql();
151+
152+
if ($this->orderByClause !== null) {
153+
$sql .= sprintf(' ORDER BY %s', $this->orderByClause);
154+
}
155+
156+
return $sql;
157+
}
158+
159+
/**
160+
* @return mixed[]
161+
*/
162+
protected function buildParams(): array
163+
{
164+
$params = $this->queryParameters;
165+
166+
foreach ($this->whereConditions as [, $conditionParams]) {
167+
$params = array_merge($params, $conditionParams);
168+
}
169+
170+
return $params;
171+
}
172+
173+
/**
174+
* @param mixed[] $params
175+
* @return ResultSet<mixed>
176+
*/
177+
protected function query(string $sql, array $params = []): ResultSet
178+
{
179+
/** @phpstan-ignore argument.type */
180+
return $this->connection->query($sql, ...$params);
181+
}
182+
183+
protected function applyFilterDate(FilterDate $filter): void
184+
{
185+
$conditions = $filter->getCondition();
186+
187+
try {
188+
$date = DateTimeHelper::tryConvertToDateTime($conditions[$filter->getColumn()], [$filter->getPhpFormat()]);
189+
190+
$this->addWhereCondition(
191+
sprintf('DATE(%s) = ?', $filter->getColumn()),
192+
[$date->format('Y-m-d')]
193+
);
194+
} catch (DatagridDateTimeHelperException) {
195+
// ignore the invalid filter value
196+
}
197+
}
198+
199+
protected function applyFilterDateRange(FilterDateRange $filter): void
200+
{
201+
$conditions = $filter->getCondition();
202+
203+
$valueFrom = $conditions[$filter->getColumn()]['from'];
204+
$valueTo = $conditions[$filter->getColumn()]['to'];
205+
206+
if ($valueFrom) {
207+
try {
208+
$dateFrom = DateTimeHelper::tryConvertToDateTime($valueFrom, [$filter->getPhpFormat()]);
209+
$dateFrom->setTime(0, 0, 0);
210+
211+
$this->addWhereCondition(
212+
sprintf('DATE(%s) >= ?', $filter->getColumn()),
213+
[$dateFrom->format('Y-m-d')]
214+
);
215+
} catch (DatagridDateTimeHelperException) {
216+
// ignore the invalid filter value
217+
}
218+
}
219+
220+
if ($valueTo) {
221+
try {
222+
$dateTo = DateTimeHelper::tryConvertToDateTime($valueTo, [$filter->getPhpFormat()]);
223+
$dateTo->setTime(23, 59, 59);
224+
225+
$this->addWhereCondition(
226+
sprintf('DATE(%s) <= ?', $filter->getColumn()),
227+
[$dateTo->format('Y-m-d')]
228+
);
229+
} catch (DatagridDateTimeHelperException) {
230+
// ignore the invalid filter value
231+
}
232+
}
233+
}
234+
235+
protected function applyFilterRange(FilterRange $filter): void
236+
{
237+
$conditions = $filter->getCondition();
238+
239+
$valueFrom = $conditions[$filter->getColumn()]['from'];
240+
$valueTo = $conditions[$filter->getColumn()]['to'];
241+
242+
if ($valueFrom !== '') {
243+
$this->addWhereCondition(
244+
sprintf('%s >= ?', $filter->getColumn()),
245+
[$valueFrom]
246+
);
247+
}
248+
249+
if ($valueTo !== '') {
250+
$this->addWhereCondition(
251+
sprintf('%s <= ?', $filter->getColumn()),
252+
[$valueTo]
253+
);
254+
}
255+
}
256+
257+
protected function applyFilterText(FilterText $filter): void
258+
{
259+
$condition = $filter->getCondition();
260+
$operator = $filter->hasConjunctionSearch() ? 'AND' : 'OR';
261+
$or = [];
262+
$bigOrArgs = [];
263+
264+
foreach ($condition as $column => $value) {
265+
$like = '(';
266+
$args = [];
267+
268+
if ($filter->isExactSearch()) {
269+
$like .= sprintf('%s = ? %s ', $column, $operator);
270+
$args[] = $value;
271+
} else {
272+
$words = $filter->hasSplitWordsSearch() === false ? [$value] : explode(' ', $value);
273+
274+
foreach ($words as $word) {
275+
$like .= sprintf('%s LIKE ? %s ', $column, $operator);
276+
$args[] = sprintf('%%%s%%', $word);
277+
}
278+
}
279+
280+
$like = substr($like, 0, strlen($like) - (strlen($operator) + 2)) . ')';
281+
282+
$or[] = $like;
283+
$bigOrArgs = array_merge($bigOrArgs, $args);
284+
}
285+
286+
if (count($or) > 1) {
287+
$bigOr = '(' . implode(sprintf(' %s ', $operator), $or) . ')';
288+
$this->addWhereCondition($bigOr, $bigOrArgs);
289+
} else {
290+
$this->addWhereCondition((string) reset($or), $bigOrArgs);
291+
}
292+
}
293+
294+
protected function applyFilterMultiSelect(FilterMultiSelect $filter): void
295+
{
296+
$condition = $filter->getCondition();
297+
$values = $condition[$filter->getColumn()];
298+
299+
$placeholders = implode(', ', array_fill(0, count($values), '?'));
300+
$this->addWhereCondition(
301+
sprintf('%s IN (%s)', $filter->getColumn(), $placeholders),
302+
array_values($values)
303+
);
304+
}
305+
306+
protected function applyFilterSelect(FilterSelect $filter): void
307+
{
308+
foreach ($filter->getCondition() as $column => $value) {
309+
$this->addWhereCondition(sprintf('%s = ?', $column), [$value]);
310+
}
311+
}
312+
313+
}

0 commit comments

Comments
 (0)