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