Merge pull request #1028 from Gravatronics/enhancement/getObjectID
[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\exception\SystemException;
5 use wcf\system\WCF;
6 use wcf\util\ClassUtil;
7 use wcf\util\StringUtil;
8
9 /**
10 * Abstract class for a list of database objects.
11 *
12 * @author Marcel Werk
13 * @copyright 2001-2012 WoltLab GmbH
14 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
15 * @package com.woltlab.wcf
16 * @subpackage data
17 * @category Community Framework
18 */
19 abstract class DatabaseObjectList implements \Countable, ITraversableObject {
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 array<wcf\data\DatabaseObject>
41 */
42 public $objects = array();
43
44 /**
45 * ids of result objects
46 * @var array<integer>
47 */
48 public $objectIDs = null;
49
50 /**
51 * sql offset
52 * @var integer
53 */
54 public $sqlOffset = 0;
55
56 /**
57 * sql limit
58 * @var integer
59 */
60 public $sqlLimit = 20;
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 boolean
89 */
90 public $useQualifiedShorthand = true;
91
92 /**
93 * sql conditions
94 * @var wcf\system\database\util\PreparedStatementConditionBuilder
95 */
96 protected $conditionBuilder = null;
97
98 /**
99 * current iterator index
100 * @var integer
101 */
102 protected $index = 0;
103
104 /**
105 * list of index to object relation
106 * @var array<integer>
107 */
108 protected $indexToObject = null;
109
110 /**
111 * Creates a new DatabaseObjectList object.
112 */
113 public function __construct() {
114 // set class name
115 if (empty($this->className)) {
116 $className = get_called_class();
117
118 if (StringUtil::substring($className, -4) == 'List') {
119 $this->className = StringUtil::substring($className, 0, -4);
120 }
121 }
122
123 if (!empty($this->decoratorClassName)) {
124 // validate decorator class name
125 if (!ClassUtil::isInstanceOf($this->decoratorClassName, 'wcf\data\DatabaseObjectDecorator')) {
126 throw new SystemException("'".$this->decoratorClassName."' should extend 'wcf\data\DatabaseObjectDecorator'");
127 }
128
129 $objectClassName = $this->objectClassName ?: $this->className;
130 $baseClassName = call_user_func(array($this->decoratorClassName, 'getBaseClass'));
131 if ($objectClassName != $baseClassName && !ClassUtil::isInstanceOf($baseClassName, $objectClassName)) {
132 throw new SystemException("'".$this->decoratorClassName."' can't decorate objects of class '".$objectClassName."'");
133 }
134 }
135
136 $this->conditionBuilder = new PreparedStatementConditionBuilder();
137 }
138
139 /**
140 * Counts the number of objects.
141 *
142 * @return integer
143 */
144 public function countObjects() {
145 $sql = "SELECT COUNT(*) AS count
146 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
147 ".$this->sqlConditionJoins."
148 ".$this->getConditionBuilder()->__toString();
149 $statement = WCF::getDB()->prepareStatement($sql);
150 $statement->execute($this->getConditionBuilder()->getParameters());
151 $row = $statement->fetchArray();
152 return $row['count'];
153 }
154
155 /**
156 * Reads the object ids from database.
157 */
158 public function readObjectIDs() {
159 $this->objectIDs = array();
160 $sql = "SELECT ".$this->getDatabaseTableAlias().".".$this->getDatabaseTableIndexName()." AS objectID
161 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
162 ".$this->sqlConditionJoins."
163 ".$this->getConditionBuilder()->__toString()."
164 ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
165 $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
166 $statement->execute($this->getConditionBuilder()->getParameters());
167 while ($row = $statement->fetchArray()) {
168 $this->objectIDs[] = $row['objectID'];
169 }
170 }
171
172 /**
173 * Reads the objects from database.
174 */
175 public function readObjects() {
176 if ($this->objectIDs !== null) {
177 if (empty($this->objectIDs)) {
178 return;
179 }
180 $sql = "SELECT ".(!empty($this->sqlSelects) ? $this->sqlSelects.($this->useQualifiedShorthand ? ',' : '') : '')."
181 ".($this->useQualifiedShorthand ? $this->getDatabaseTableAlias().'.*' : '')."
182 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
183 ".$this->sqlJoins."
184 WHERE ".$this->getDatabaseTableAlias().".".$this->getDatabaseTableIndexName()." IN (?".str_repeat(',?', count($this->objectIDs) - 1).")
185 ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
186 $statement = WCF::getDB()->prepareStatement($sql);
187 $statement->execute($this->objectIDs);
188 $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
189 }
190 else {
191 $sql = "SELECT ".(!empty($this->sqlSelects) ? $this->sqlSelects.($this->useQualifiedShorthand ? ',' : '') : '')."
192 ".($this->useQualifiedShorthand ? $this->getDatabaseTableAlias().'.*' : '')."
193 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
194 ".$this->sqlJoins."
195 ".$this->getConditionBuilder()->__toString()."
196 ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
197 $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
198 $statement->execute($this->getConditionBuilder()->getParameters());
199 $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
200 }
201
202 // decorate objects
203 if (!empty($this->decoratorClassName)) {
204 foreach ($this->objects as &$object) {
205 $object = new $this->decoratorClassName($object);
206 }
207 unset($object);
208 }
209
210 // use table index as array index
211 $objects = array();
212 foreach ($this->objects as $object) {
213 $objectID = $object->getObjectID();
214 $objects[$objectID] = $object;
215
216 $this->indexToObject[] = $objectID;
217 }
218 $this->objectIDs = $this->indexToObject;
219 $this->objects = $objects;
220 }
221
222 /**
223 * Returns the object ids of the list.
224 *
225 * @return array<integer>
226 */
227 public function getObjectIDs() {
228 return $this->objectIDs;
229 }
230
231 /**
232 * Sets the object ids.
233 *
234 * @param array<integer> $objectIDs
235 */
236 public function setObjectIDs(array $objectIDs) {
237 $this->objectIDs = $objectIDs;
238 }
239
240 /**
241 * Returns the objects of the list.
242 *
243 * @return array<wcf\data\DatabaseObject>
244 */
245 public function getObjects() {
246 return $this->objects;
247 }
248
249 /**
250 * Returns the condition builder object.
251 *
252 * @return wcf\system\database\util\PreparedStatementConditionBuilder
253 */
254 public function getConditionBuilder() {
255 return $this->conditionBuilder;
256 }
257
258 /**
259 * Returns the name of the database table.
260 *
261 * @return string
262 */
263 public function getDatabaseTableName() {
264 return call_user_func(array($this->className, 'getDatabaseTableName'));
265 }
266
267 /**
268 * Returns the name of the database table.
269 *
270 * @return string
271 */
272 public function getDatabaseTableIndexName() {
273 return call_user_func(array($this->className, 'getDatabaseTableIndexName'));
274 }
275
276 /**
277 * Returns the name of the database table alias.
278 *
279 * @return string
280 */
281 public function getDatabaseTableAlias() {
282 return call_user_func(array($this->className, 'getDatabaseTableAlias'));
283 }
284
285 /**
286 * @see \Countable::count()
287 */
288 public function count() {
289 return count($this->objects);
290 }
291
292 /**
293 * @see \Iterator::current()
294 */
295 public function current() {
296 $objectID = $this->indexToObject[$this->index];
297 return $this->objects[$objectID];
298 }
299
300 /**
301 * CAUTION: This methods does not return the current iterator index,
302 * rather than the object key which maps to that index.
303 *
304 * @see \Iterator::key()
305 */
306 public function key() {
307 return $this->indexToObject[$this->index];
308 }
309
310 /**
311 * @see \Iterator::next()
312 */
313 public function next() {
314 ++$this->index;
315 }
316
317 /**
318 * @see \Iterator::rewind()
319 */
320 public function rewind() {
321 $this->index = 0;
322 }
323
324 /**
325 * @see \Iterator::valid()
326 */
327 public function valid() {
328 return isset($this->indexToObject[$this->index]);
329 }
330
331 /**
332 * @see \SeekableIterator::seek()
333 */
334 public function seek($index) {
335 $this->index = $index;
336
337 if (!$this->valid()) {
338 throw new \OutOfBoundsException();
339 }
340 }
341
342 /**
343 * @see wcf\data\ITraversableObject::seekTo()
344 */
345 public function seekTo($objectID) {
346 $this->index = array_search($objectID, $this->indexToObject);
347
348 if ($this->index === false) {
349 throw new SystemException("object id '".$objectID."' is invalid");
350 }
351 }
352
353 /**
354 * @see wcf\data\ITraversableObject::search()
355 */
356 public function search($objectID) {
357 try {
358 $this->seekTo($objectID);
359 return $this->current();
360 }
361 catch (SystemException $e) {
362 return null;
363 }
364 }
365 }