AbstractDataTablesDatabase::getData()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 9
c 2
b 0
f 0
dl 0
loc 13
rs 9.9666
cc 3
nc 3
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebServCo\Framework\DataTables;
6
7
use WebServCo\Framework\Database\Order as DatabaseOrder;
8
use WebServCo\Framework\Exceptions\DatabaseException;
9
use WebServCo\Framework\Interfaces\ArrayObjectInterface;
10
use WebServCo\Framework\Interfaces\DatabaseInterface;
11
12
abstract class AbstractDataTablesDatabase implements \WebServCo\Framework\Interfaces\DataTablesInterface
13
{
14
    protected DatabaseInterface $db;
15
16
    /**
17
    * @param string $orderPart,
18
    */
19
    abstract protected function getQuery(string $searchPart, string $orderPart, string $limitPart): string;
20
21
    abstract protected function getRecordsTotalQuery(): string;
22
23
    public function __construct(DatabaseInterface $db)
24
    {
25
        $this->db = $db;
26
    }
27
28
    public function getResponse(Request $request): Response
29
    {
30
        $params = [];
31
        $limitPart = '';
32
33
        $columnArrayObject = $request->getColumns();
34
        [$searchPart, $searchParams] = $this->getSearchQueryPart($columnArrayObject);
35
36
        if (!\is_string($searchPart)) {
0 ignored issues
show
introduced by
The condition is_string($searchPart) is always true.
Loading history...
37
            throw new \InvalidArgumentException('"searchQueryPart" is not a string');
38
        }
39
        if (!\is_array($searchParams)) {
0 ignored issues
show
introduced by
The condition is_array($searchParams) is always true.
Loading history...
40
            throw new \InvalidArgumentException('"searchParams" is not an array');
41
        }
42
43
        $params = \array_merge($params, $searchParams);
44
45
        $orderArrayObject = $request->getOrder();
46
        $orderPart = $this->getOrderQueryPart($columnArrayObject, $orderArrayObject);
47
48
        $length = $request->getLength();
49
        if (-1 !== $length) {
50
            $limitPart = 'LIMIT ?, ?';
51
            $params[] = $request->getStart();
52
            $params[] = $length;
53
        }
54
        $query = $this->getQuery($searchPart, $orderPart, $limitPart);
55
        try {
56
            $pdoStatement = $this->db->query($query, $params);
57
        } catch (DatabaseException $e) {
58
            // Rethrow in order to pinpoint the query location in the logs.
59
            throw new DatabaseException($e->getMessage(), $e);
60
        }
61
62
        $data = $this->getData($columnArrayObject, $pdoStatement);
63
64
        $recordsFiltered = $this->getRecordsFiltered();
65
66
        // Extra query for total only if there is a search, otherwise the total is equal to the $recordsFiltered
67
        $recordsTotal = $searchPart
68
            ? $this->getRecordsTotal()
69
            : $recordsFiltered;
70
71
        return new Response(
72
            $request->getDraw(),
73
            $recordsTotal,
74
            $recordsFiltered,
75
            $data,
76
        );
77
    }
78
79
    protected function assertColumnArrayObject(ArrayObjectInterface $arrayObject): bool
80
    {
81
        if (!$arrayObject instanceof ColumnArrayObject) {
82
            throw new \InvalidArgumentException(\sprintf('Object is not an instance of %s.', 'ColumnArrayObject'));
83
        }
84
        return true;
85
    }
86
87
    protected function assertOrderArrayObject(ArrayObjectInterface $arrayObject): bool
88
    {
89
        if (!$arrayObject instanceof OrderArrayObject) {
90
            throw new \InvalidArgumentException(\sprintf('Object is not an instance of %s.', 'OrderArrayObject'));
91
        }
92
        return true;
93
    }
94
95
    /**
96
    * @return array<int,array<int|string,mixed>>
97
    */
98
    protected function getData(ArrayObjectInterface $columnArrayObject, \PDOStatement $pdoStatement): array
99
    {
100
        $this->assertColumnArrayObject($columnArrayObject);
101
        $data = [];
102
        while ($row = $pdoStatement->fetch(\PDO::FETCH_ASSOC)) {
103
            $item = [];
104
            foreach ($columnArrayObject as $column) {
105
                $name = $column->getData();
106
                $item[$name] = $row[$name] ?? null;
107
            }
108
            $data[] = $item;
109
        }
110
        return $data;
111
    }
112
113
    protected function getDatabaseColumnName(string $dataTablesColumnName): string
114
    {
115
        try {
116
            return $this->db->escapeIdentifier($dataTablesColumnName);
117
        } catch (DatabaseException $e) {
118
            // Rethrow in order to pinpoint the query location in the logs.
119
            throw new DatabaseException($e->getMessage(), $e);
120
        }
121
    }
122
123
    protected function getOrderQueryPart(
124
        ArrayObjectInterface $columnArrayObject,
125
        ArrayObjectInterface $orderArrayObject
126
    ): string {
127
        $this->assertColumnArrayObject($columnArrayObject);
128
        $this->assertOrderArrayObject($orderArrayObject);
129
        $items = [];
130
        foreach ($orderArrayObject as $order) {
131
            $columnKey = (string) $order->getColumn();
132
            $column = $columnArrayObject->offsetGet($columnKey);
133
            if (!($column instanceof Column)) {
134
                continue;
135
            }
136
137
            if (!$column->getOrderable()) {
138
                continue;
139
            }
140
141
            $dir = \strtoupper($order->getDir());
142
            $dir = \in_array($dir, [DatabaseOrder::ASC, DatabaseOrder::DESC], true)
143
                ? $dir
144
                : DatabaseOrder::ASC;
145
            $columnName = $this->getDatabaseColumnName($column->getData());
146
            $items[] = \sprintf(' %s %s', $columnName, $dir);
147
        }
148
        if (!$items) {
149
            // No order, or none of the columns are actually orderable
150
            return '';
151
        }
152
        return \sprintf(
153
            'ORDER BY%s',
154
            \implode(",", $items),
155
        );
156
    }
157
158
    protected function getRecordsFiltered(): int
159
    {
160
        try {
161
            return (int) $this->db->getColumn("SELECT FOUND_ROWS()", [], 0);
162
        } catch (DatabaseException $e) {
163
            // Rethrow in order to pinpoint the query location in the logs.
164
            throw new DatabaseException($e->getMessage(), $e);
165
        }
166
    }
167
168
    protected function getRecordsTotal(): int
169
    {
170
        try {
171
            return (int) $this->db->getColumn( // grand total - query without the search, order, limits
172
                $this->getRecordsTotalQuery(),
173
                [],
174
            );
175
        } catch (DatabaseException $e) {
176
            // Rethrow in order to pinpoint the query location in the logs.
177
            throw new DatabaseException($e->getMessage(), $e);
178
        }
179
    }
180
181
    /**
182
    * @return array<int,array<int,string>|string>
183
    */
184
    protected function getSearchQueryPart(ArrayObjectInterface $columnArrayObject): array
185
    {
186
        $this->assertColumnArrayObject($columnArrayObject);
187
        $query = '';
188
        $params = [];
189
        foreach ($columnArrayObject as $column) {
190
            if (!$column->getSearchable()) {
191
                continue;
192
            }
193
194
            $search = $column->getSearch();
195
            $searchValue = $search->getValue();
196
            if ('' === $searchValue) {
197
                continue;
198
            }
199
            // make sure it works also for "0"
200
            $query .= \sprintf(
201
                " AND %s LIKE ?",
202
                $this->getDatabaseColumnName($column->getData()),
203
            );
204
            $params[] = \sprintf('%%%s%%', $searchValue);
205
        }
206
        return [$query, $params];
207
    }
208
}
209