Commit | Line | Data |
---|---|---|
11ade432 | 1 | <?php |
a9229942 | 2 | |
11ade432 | 3 | namespace wcf\data; |
a9229942 | 4 | |
11ade432 | 5 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
f6e164d8 | 6 | use wcf\system\event\EventHandler; |
f1c01719 | 7 | use wcf\system\exception\SystemException; |
11ade432 AE |
8 | use wcf\system\WCF; |
9 | ||
10 | /** | |
11 | * Abstract class for a list of database objects. | |
a9229942 TD |
12 | * |
13 | * @author Marcel Werk | |
14 | * @copyright 2001-2019 WoltLab GmbH | |
15 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
16 | * @package WoltLabSuite\Core\Data | |
11ade432 | 17 | */ |
a9229942 TD |
18 | abstract class DatabaseObjectList implements \Countable, ITraversableObject |
19 | { | |
20 | /** | |
21 | * class name | |
22 | * @var string | |
23 | */ | |
24 | public $className = ''; | |
25 | ||
26 | /** | |
27 | * class name of the object decorator; if left empty, no decorator is used | |
28 | * @var string | |
29 | */ | |
30 | public $decoratorClassName = ''; | |
31 | ||
32 | /** | |
33 | * object class name | |
34 | * @var string | |
35 | */ | |
36 | public $objectClassName = ''; | |
37 | ||
38 | /** | |
39 | * result objects | |
40 | * @var DatabaseObject[] | |
41 | */ | |
42 | public $objects = []; | |
43 | ||
44 | /** | |
45 | * ids of result objects | |
46 | * @var int[] | |
47 | */ | |
48 | public $objectIDs; | |
49 | ||
50 | /** | |
51 | * sql offset | |
52 | * @var int | |
53 | */ | |
54 | public $sqlOffset = 0; | |
55 | ||
56 | /** | |
57 | * sql limit | |
58 | * @var int | |
59 | */ | |
60 | public $sqlLimit = 0; | |
61 | ||
62 | /** | |
63 | * sql order by statement | |
64 | * @var string | |
65 | */ | |
66 | public $sqlOrderBy = ''; | |
67 | ||
68 | /** | |
69 | * sql select parameters | |
70 | * @var string | |
71 | */ | |
72 | public $sqlSelects = ''; | |
73 | ||
74 | /** | |
75 | * sql select joins which are necessary for where statements | |
76 | * @var string | |
77 | */ | |
78 | public $sqlConditionJoins = ''; | |
79 | ||
80 | /** | |
81 | * sql select joins | |
82 | * @var string | |
83 | */ | |
84 | public $sqlJoins = ''; | |
85 | ||
86 | /** | |
87 | * enables the automatic usage of the qualified shorthand | |
88 | * @var bool | |
89 | */ | |
90 | public $useQualifiedShorthand = true; | |
91 | ||
92 | /** | |
93 | * sql conditions | |
94 | * @var PreparedStatementConditionBuilder | |
95 | */ | |
96 | protected $conditionBuilder; | |
97 | ||
98 | /** | |
99 | * current iterator index | |
100 | * @var int | |
101 | */ | |
102 | protected $index = 0; | |
103 | ||
104 | /** | |
105 | * list of index to object relation | |
106 | * @var int[] | |
107 | */ | |
108 | protected $indexToObject; | |
109 | ||
110 | /** | |
111 | * Creates a new DatabaseObjectList object. | |
112 | */ | |
113 | public function __construct() | |
114 | { | |
115 | // set class name | |
116 | if (empty($this->className)) { | |
117 | $className = \get_called_class(); | |
118 | ||
119 | if (\mb_substr($className, -4) == 'List') { | |
120 | $this->className = \mb_substr($className, 0, -4); | |
121 | } | |
122 | } | |
123 | ||
124 | if (!empty($this->decoratorClassName)) { | |
125 | // validate decorator class name | |
126 | if (!\is_subclass_of($this->decoratorClassName, DatabaseObjectDecorator::class)) { | |
127 | throw new SystemException("'" . $this->decoratorClassName . "' should extend '" . DatabaseObjectDecorator::class . "'"); | |
128 | } | |
129 | ||
130 | $objectClassName = $this->objectClassName ?: $this->className; | |
131 | $baseClassName = \call_user_func([$this->decoratorClassName, 'getBaseClass']); | |
132 | if ($objectClassName != $baseClassName && !\is_subclass_of($baseClassName, $objectClassName)) { | |
133 | throw new SystemException("'" . $this->decoratorClassName . "' can't decorate objects of class '" . $objectClassName . "'"); | |
134 | } | |
135 | } | |
136 | ||
137 | $this->conditionBuilder = new PreparedStatementConditionBuilder(); | |
138 | ||
139 | EventHandler::getInstance()->fireAction($this, 'init'); | |
140 | } | |
141 | ||
142 | /** | |
143 | * Counts the number of objects. | |
144 | * | |
145 | * @return int | |
146 | */ | |
147 | public function countObjects() | |
148 | { | |
149 | $sql = "SELECT COUNT(*) | |
150 | FROM " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . " | |
151 | " . $this->sqlConditionJoins . " | |
152 | " . $this->getConditionBuilder(); | |
153 | $statement = WCF::getDB()->prepareStatement($sql); | |
154 | $statement->execute($this->getConditionBuilder()->getParameters()); | |
155 | ||
156 | return $statement->fetchSingleColumn(); | |
157 | } | |
158 | ||
159 | /** | |
160 | * Reads the object ids from database. | |
161 | */ | |
162 | public function readObjectIDs() | |
163 | { | |
164 | $this->objectIDs = []; | |
165 | $sql = "SELECT " . $this->getDatabaseTableAlias() . "." . $this->getDatabaseTableIndexName() . " AS objectID | |
166 | FROM " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . " | |
167 | " . $this->sqlConditionJoins . " | |
168 | " . $this->getConditionBuilder() . " | |
169 | " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : ''); | |
170 | $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset); | |
171 | $statement->execute($this->getConditionBuilder()->getParameters()); | |
172 | $this->objectIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); | |
173 | } | |
174 | ||
175 | /** | |
176 | * Reads the objects from database. | |
177 | */ | |
178 | public function readObjects() | |
179 | { | |
180 | if ($this->objectIDs !== null) { | |
181 | if (empty($this->objectIDs)) { | |
182 | return; | |
183 | } | |
184 | ||
185 | $objectIdPlaceholder = "?" . \str_repeat(',?', \count($this->objectIDs) - 1); | |
186 | ||
187 | $sql = "SELECT " . (!empty($this->sqlSelects) ? $this->sqlSelects . ($this->useQualifiedShorthand ? ',' : '') : '') . " | |
188 | " . ($this->useQualifiedShorthand ? $this->getDatabaseTableAlias() . '.*' : '') . " | |
189 | FROM " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . " | |
190 | " . $this->sqlJoins . " | |
191 | WHERE " . $this->getDatabaseTableAlias() . "." . $this->getDatabaseTableIndexName() . " IN ({$objectIdPlaceholder}) | |
192 | " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : ''); | |
193 | $statement = WCF::getDB()->prepareStatement($sql); | |
194 | $statement->execute($this->objectIDs); | |
195 | $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className)); | |
196 | } else { | |
197 | $sql = "SELECT " . (!empty($this->sqlSelects) ? $this->sqlSelects . ($this->useQualifiedShorthand ? ',' : '') : '') . " | |
198 | " . ($this->useQualifiedShorthand ? $this->getDatabaseTableAlias() . '.*' : '') . " | |
199 | FROM " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . " | |
200 | " . $this->sqlJoins . " | |
201 | " . $this->getConditionBuilder() . " | |
202 | " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : ''); | |
203 | $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset); | |
204 | $statement->execute($this->getConditionBuilder()->getParameters()); | |
205 | $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className)); | |
206 | } | |
207 | ||
208 | // decorate objects | |
209 | if (!empty($this->decoratorClassName)) { | |
210 | foreach ($this->objects as &$object) { | |
211 | $object = new $this->decoratorClassName($object); | |
212 | } | |
213 | unset($object); | |
214 | } | |
215 | ||
216 | // use table index as array index | |
217 | $objects = $this->indexToObject = []; | |
218 | foreach ($this->objects as $object) { | |
219 | $objectID = $object->getObjectID(); | |
220 | $objects[$objectID] = $object; | |
221 | ||
222 | $this->indexToObject[] = $objectID; | |
223 | } | |
224 | $this->objectIDs = $this->indexToObject; | |
225 | $this->objects = $objects; | |
226 | } | |
227 | ||
228 | /** | |
229 | * Returns the object ids of the list. | |
230 | * | |
231 | * @return int[] | |
232 | */ | |
233 | public function getObjectIDs() | |
234 | { | |
235 | return $this->objectIDs; | |
236 | } | |
237 | ||
238 | /** | |
239 | * Sets the object ids. | |
240 | * | |
241 | * @param int[] $objectIDs | |
242 | */ | |
243 | public function setObjectIDs(array $objectIDs) | |
244 | { | |
245 | $this->objectIDs = \array_merge($objectIDs); | |
246 | } | |
247 | ||
248 | /** | |
249 | * Returns the objects of the list. | |
250 | * | |
251 | * @return DatabaseObject[] | |
252 | */ | |
253 | public function getObjects() | |
254 | { | |
255 | return $this->objects; | |
256 | } | |
257 | ||
258 | /** | |
259 | * Returns the condition builder object. | |
260 | * | |
261 | * @return PreparedStatementConditionBuilder | |
262 | */ | |
263 | public function getConditionBuilder() | |
264 | { | |
265 | return $this->conditionBuilder; | |
266 | } | |
267 | ||
268 | /** | |
269 | * Sets the condition builder dynamically. | |
270 | * | |
271 | * @param PreparedStatementConditionBuilder $conditionBuilder | |
272 | * @since 5.3 | |
273 | */ | |
274 | public function setConditionBuilder(PreparedStatementConditionBuilder $conditionBuilder) | |
275 | { | |
276 | $this->conditionBuilder = $conditionBuilder; | |
277 | } | |
278 | ||
279 | /** | |
280 | * Returns the name of the database table. | |
281 | * | |
282 | * @return string | |
283 | */ | |
284 | public function getDatabaseTableName() | |
285 | { | |
286 | return \call_user_func([$this->className, 'getDatabaseTableName']); | |
287 | } | |
288 | ||
289 | /** | |
290 | * Returns the name of the database table. | |
291 | * | |
292 | * @return string | |
293 | */ | |
294 | public function getDatabaseTableIndexName() | |
295 | { | |
296 | return \call_user_func([$this->className, 'getDatabaseTableIndexName']); | |
297 | } | |
298 | ||
299 | /** | |
300 | * Returns the name of the database table alias. | |
301 | * | |
302 | * @return string | |
303 | */ | |
304 | public function getDatabaseTableAlias() | |
305 | { | |
306 | return \call_user_func([$this->className, 'getDatabaseTableAlias']); | |
307 | } | |
308 | ||
309 | /** | |
310 | * @inheritDoc | |
311 | */ | |
312 | public function count() | |
313 | { | |
314 | return \count($this->objects); | |
315 | } | |
316 | ||
317 | /** | |
318 | * @inheritDoc | |
319 | */ | |
320 | public function current() | |
321 | { | |
322 | $objectID = $this->indexToObject[$this->index]; | |
323 | ||
324 | return $this->objects[$objectID]; | |
325 | } | |
326 | ||
327 | /** | |
328 | * CAUTION: This methods does not return the current iterator index, | |
329 | * but the object key which maps to that index. | |
330 | * | |
331 | * @see \Iterator::key() | |
332 | */ | |
333 | public function key() | |
334 | { | |
335 | return $this->indexToObject[$this->index]; | |
336 | } | |
337 | ||
338 | /** | |
339 | * @inheritDoc | |
340 | */ | |
341 | public function next() | |
342 | { | |
343 | $this->index++; | |
344 | } | |
345 | ||
346 | /** | |
347 | * @inheritDoc | |
348 | */ | |
349 | public function rewind() | |
350 | { | |
351 | $this->index = 0; | |
352 | } | |
353 | ||
354 | /** | |
355 | * @inheritDoc | |
356 | */ | |
357 | public function valid() | |
358 | { | |
359 | return isset($this->indexToObject[$this->index]); | |
360 | } | |
361 | ||
362 | /** | |
363 | * @inheritDoc | |
364 | */ | |
365 | public function seek($index) | |
366 | { | |
367 | $this->index = $index; | |
368 | ||
369 | if (!$this->valid()) { | |
370 | throw new \OutOfBoundsException(); | |
371 | } | |
372 | } | |
373 | ||
374 | /** | |
375 | * @inheritDoc | |
376 | */ | |
377 | public function seekTo($objectID) | |
378 | { | |
379 | $this->index = \array_search($objectID, $this->indexToObject); | |
380 | ||
381 | if ($this->index === false) { | |
382 | throw new SystemException("object id '" . $objectID . "' is invalid"); | |
383 | } | |
384 | } | |
385 | ||
386 | /** | |
387 | * @inheritDoc | |
388 | */ | |
389 | public function search($objectID) | |
390 | { | |
391 | try { | |
392 | $this->seekTo($objectID); | |
393 | ||
394 | return $this->current(); | |
395 | } catch (SystemException $e) { | |
396 | return; | |
397 | } | |
398 | } | |
399 | ||
400 | /** | |
401 | * Returns the only object in this list or `null` if the list is empty. | |
402 | * | |
403 | * @return DatabaseObject|null | |
404 | * @throws \BadMethodCallException if list contains more than one object | |
405 | */ | |
406 | public function getSingleObject() | |
407 | { | |
408 | if (\count($this->objects) > 1) { | |
409 | throw new \BadMethodCallException("Cannot get a single object when the list contains " . \count($this->objects) . " objects."); | |
410 | } | |
411 | ||
412 | if (empty($this->objects)) { | |
c0b28aa2 | 413 | return null; |
a9229942 TD |
414 | } |
415 | ||
416 | return \reset($this->objects); | |
417 | } | |
11ade432 | 418 | } |