e8359db1d8be2401601fbcdb1277c54324576c9e
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / DatabaseObjectList.class.php
1 <?php
2
3 namespace wcf\data;
4
5 use wcf\system\database\util\PreparedStatementConditionBuilder;
6 use wcf\system\event\EventHandler;
7 use wcf\system\exception\SystemException;
8 use wcf\system\WCF;
9
10 /**
11 * Abstract class for a list of database objects.
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
17 */
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)) {
413 return;
414 }
415
416 return \reset($this->objects);
417 }
418 }