PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / Framework / Models / ModelQueryBuilder.php

ModelQueryBuilder.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at src/Framework/Models/ModelQueryBuilder.php

254 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Framework\Models;
4
5 use Give\Framework\Database\DB;
6 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
7 use Give\Framework\Models\Contracts\ModelCrud;
8 use Give\Framework\QueryBuilder\Clauses\Having;
9 use Give\Framework\QueryBuilder\Clauses\RawSQL;
10 use Give\Framework\QueryBuilder\Clauses\Select;
11 use Give\Framework\QueryBuilder\QueryBuilder;
12
13 /**
14 * @since 2.19.6
15 *
16 * @template M
17 */
18 class ModelQueryBuilder extends QueryBuilder
19 {
20 /**
21 * @var class-string<M>
22 */
23 protected $model;
24
25 /**
26 * @param class-string<M> $modelClass
27 */
28 public function __construct($modelClass)
29 {
30 if (!is_subclass_of($modelClass, Model::class)) {
31 throw new InvalidArgumentException("$modelClass must be an instance of " . Model::class);
32 }
33
34 $this->model = $modelClass;
35 }
36
37 /**
38 * Returns the number of rows returned by a query
39 *
40 * @since 4.18.0 Honor an explicit column in grouped counts by summing the column's non-null values per group.
41 * @since 4.18.0 Preserve SELECT aliases referenced by HAVING when counting a grouped query.
42 * @since 4.18.0 Count the groups of a grouped query rather than the first group's rows.
43 * @since 2.24.0
44 *
45 * @param null|string $column
46 */
47 public function count($column = null): int
48 {
49 $column = ( ! $column || $column === '*') ? '1' : trim($column);
50
51 /*
52 * A grouped query returns one row per group and get_row() reads only the first of them,
53 * so the number of groups is what the caller is actually asking for.
54 */
55 if ($this->groupByColumns) {
56 return +DB::get_row($this->getGroupCountSQL($column))->count;
57 }
58
59 if ('1' === $column) {
60 $this->selects = [];
61 }
62 $this->selects[] = new RawSQL('SELECT COUNT(%1s) AS count', $column);
63
64 return +parent::get()->count;
65 }
66
67 /**
68 * @since 3.6.0
69 *
70 * @param int $perPage
71 * @param int $page
72 *
73 * @return ModelQueryBuilder
74 */
75 public function paginate($perPage, $page = 1): ModelQueryBuilder
76 {
77 return $this
78 ->limit($perPage)
79 ->offset(($page - 1) * $perPage);
80 }
81
82 /**
83 * Get row
84 *
85 * @since 2.19.6
86 *
87 * @return M|null
88 *
89 * @param int $output For inheritance compatibility only, unused.
90 */
91 public function get($output = OBJECT)
92 {
93 $row = DB::get_row($this->getSQL(), OBJECT);
94
95 if (!$row) {
96 return null;
97 }
98
99 return $this->getRowAsModel($row);
100 }
101
102 /**
103 * Get results
104 *
105 * @since 2.19.6
106 *
107 * @return M[]|null
108 *
109 * @param int $output For inheritance compatibility only, unused.
110 */
111 public function getAll($output = OBJECT)
112 {
113 $results = DB::get_results($this->getSQL(), OBJECT);
114
115 if (!$results) {
116 return null;
117 }
118
119 if (isset($this->model)) {
120 return $this->getAllAsModel($results);
121 }
122
123 return $results;
124 }
125
126 /**
127 * Get row as model
128 *
129 * @since 2.19.6
130 *
131 * @param object|null $row
132 *
133 * @return M|null
134 */
135 protected function getRowAsModel($row)
136 {
137 $model = $this->model;
138
139 if (!method_exists($model, 'fromQueryBuilderObject')) {
140 throw new InvalidArgumentException("fromQueryBuilderObject missing from $model");
141 }
142
143 return $model::fromQueryBuilderObject($row);
144 }
145
146 /**
147 * Get results as models
148 *
149 * @since 2.19.6
150 *
151 * @param object[] $results
152 *
153 * @return M[]|null
154 */
155 protected function getAllAsModel($results)
156 {
157 /** @var ModelCrud $model */
158 $model = $this->model;
159
160 if (!method_exists($model, 'fromQueryBuilderObject')) {
161 throw new InvalidArgumentException("fromQueryBuilderObject missing from $model");
162 }
163
164 return array_map(static function ($object) use ($model) {
165 return $model::fromQueryBuilderObject($object);
166 }, $results);
167 }
168
169 /**
170 * Wraps the grouped query so that its rows, one per group, are what gets counted. A
171 * COUNT(DISTINCT ...) over the grouped columns would drop every group holding a NULL.
172 *
173 * An explicit column counts the column's non-null values, so each group contributes its
174 * non-null count and the wrapper sums them instead of counting groups.
175 *
176 * The grouped columns are aliased because a derived table rejects duplicate column names, and
177 * the ordering and paging are dropped because neither changes the number of groups. SELECT
178 * entries whose aliases a HAVING clause references are kept, since the replaced select list
179 * would otherwise leave HAVING pointing at an alias that no longer exists.
180 *
181 * @since 4.18.0
182 *
183 * @param string $column
184 */
185 private function getGroupCountSQL($column = null): string
186 {
187 $innerSelects = [];
188
189 if ($column && '1' !== $column) {
190 $innerSelects[] = DB::prepare('COUNT(%1s) AS nonNullCount', $column);
191 }
192
193 foreach ($this->groupByColumns as $index => $groupByColumn) {
194 $innerSelects[] = "{$groupByColumn} AS groupedColumn{$index}";
195 }
196
197 foreach ($this->getHavingReferencedSelects() as $select) {
198 $innerSelects[] = $select;
199 }
200
201 $this->selects = [new RawSQL('SELECT ' . implode(', ', $innerSelects))];
202 $this->orderBys = [];
203 $this->limit = null;
204 $this->offset = null;
205
206 if ($column && '1' !== $column) {
207 return "SELECT SUM(nonNullCount) AS count FROM ({$this->getSQL()}) AS groupedQuery";
208 }
209
210 return "SELECT COUNT(*) AS count FROM ({$this->getSQL()}) AS groupedQuery";
211 }
212
213 /**
214 * Renders the SELECT entries whose aliases a HAVING clause references, since the grouped
215 * select list that count() builds would otherwise leave HAVING pointing at a missing alias.
216 *
217 * @since 4.18.0
218 *
219 * @return string[]
220 */
221 private function getHavingReferencedSelects(): array
222 {
223 $referencedAliases = [];
224
225 foreach ($this->havings as $having) {
226 if ($having instanceof Having) {
227 $referencedAliases[] = $having->column;
228 }
229 }
230
231 if ( ! $referencedAliases) {
232 return [];
233 }
234
235 $selects = [];
236
237 foreach ($this->selects as $select) {
238 if ($select instanceof Select) {
239 if (in_array($select->alias, $referencedAliases, true)) {
240 $selects[] = DB::prepare('%1s AS %2s', $select->column, $select->alias);
241 }
242 } elseif ($select instanceof RawSQL) {
243 if (preg_match('/\s+AS\s+([^\s,]+)\s*$/i', $select->sql, $matches) &&
244 in_array(trim($matches[1], '`\'"'), $referencedAliases, true)
245 ) {
246 $selects[] = $select->sql;
247 }
248 }
249 }
250
251 return $selects;
252 }
253 }
254