3 use wcf\system\database\util\PreparedStatementConditionBuilder
;
4 use wcf\system\event\EventHandler
;
5 use wcf\system\exception\SystemException
;
9 * Abstract class for a list of database objects.
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
16 abstract class DatabaseObjectList
implements \Countable
, ITraversableObject
{
21 public $className = '';
24 * class name of the object decorator; if left empty, no decorator is used
27 public $decoratorClassName = '';
33 public $objectClassName = '';
37 * @var DatabaseObject[]
42 * ids of result objects
45 public $objectIDs = null;
51 public $sqlOffset = 0;
60 * sql order by statement
63 public $sqlOrderBy = '';
66 * sql select parameters
69 public $sqlSelects = '';
72 * sql select joins which are necessary for where statements
75 public $sqlConditionJoins = '';
81 public $sqlJoins = '';
84 * enables the automatic usage of the qualified shorthand
87 public $useQualifiedShorthand = true;
91 * @var PreparedStatementConditionBuilder
93 protected $conditionBuilder = null;
96 * current iterator index
102 * list of index to object relation
105 protected $indexToObject = null;
108 * Creates a new DatabaseObjectList object.
110 public function __construct() {
112 if (empty($this->className
)) {
113 $className = get_called_class();
115 if (mb_substr($className, -4) == 'List') {
116 $this->className
= mb_substr($className, 0, -4);
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."'");
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."'");
133 $this->conditionBuilder
= new PreparedStatementConditionBuilder();
135 EventHandler
::getInstance()->fireAction($this, 'init');
139 * Counts the number of objects.
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());
151 return $statement->fetchSingleColumn();
155 * Reads the object ids from database.
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
);
170 * Reads the objects from database.
172 public function readObjects() {
173 if ($this->objectIDs
!== null) {
174 if (empty($this->objectIDs
)) {
177 $sql = "SELECT ".(!empty($this->sqlSelects
) ?
$this->sqlSelects
.($this->useQualifiedShorthand ?
',' : '') : '')."
178 ".($this->useQualifiedShorthand ?
$this->getDatabaseTableAlias().'.*' : '')."
179 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
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
));
188 $sql = "SELECT ".(!empty($this->sqlSelects
) ?
$this->sqlSelects
.($this->useQualifiedShorthand ?
',' : '') : '')."
189 ".($this->useQualifiedShorthand ?
$this->getDatabaseTableAlias().'.*' : '')."
190 FROM ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
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
));
200 if (!empty($this->decoratorClassName
)) {
201 foreach ($this->objects
as &$object) {
202 $object = new $this->decoratorClassName($object);
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;
213 $this->indexToObject
[] = $objectID;
215 $this->objectIDs
= $this->indexToObject
;
216 $this->objects
= $objects;
220 * Returns the object ids of the list.
224 public function getObjectIDs() {
225 return $this->objectIDs
;
229 * Sets the object ids.
231 * @param integer[] $objectIDs
233 public function setObjectIDs(array $objectIDs) {
234 $this->objectIDs
= array_merge($objectIDs);
238 * Returns the objects of the list.
240 * @return DatabaseObject[]
242 public function getObjects() {
243 return $this->objects
;
247 * Returns the condition builder object.
249 * @return PreparedStatementConditionBuilder
251 public function getConditionBuilder() {
252 return $this->conditionBuilder
;
256 * Sets the condition builder dynamically.
258 * @param PreparedStatementConditionBuilder $conditionBuilder
261 public function setConditionBuilder(PreparedStatementConditionBuilder
$conditionBuilder) {
262 $this->conditionBuilder
= $conditionBuilder;
266 * Returns the name of the database table.
270 public function getDatabaseTableName() {
271 return call_user_func([$this->className
, 'getDatabaseTableName']);
275 * Returns the name of the database table.
279 public function getDatabaseTableIndexName() {
280 return call_user_func([$this->className
, 'getDatabaseTableIndexName']);
284 * Returns the name of the database table alias.
288 public function getDatabaseTableAlias() {
289 return call_user_func([$this->className
, 'getDatabaseTableAlias']);
295 public function count() {
296 return count($this->objects
);
302 public function current() {
303 $objectID = $this->indexToObject
[$this->index
];
304 return $this->objects
[$objectID];
308 * CAUTION: This methods does not return the current iterator index,
309 * but the object key which maps to that index.
311 * @see \Iterator::key()
313 public function key() {
314 return $this->indexToObject
[$this->index
];
320 public function next() {
327 public function rewind() {
334 public function valid() {
335 return isset($this->indexToObject
[$this->index
]);
341 public function seek($index) {
342 $this->index
= $index;
344 if (!$this->valid()) {
345 throw new \
OutOfBoundsException();
352 public function seekTo($objectID) {
353 $this->index
= array_search($objectID, $this->indexToObject
);
355 if ($this->index
=== false) {
356 throw new SystemException("object id '".$objectID."' is invalid");
363 public function search($objectID) {
365 $this->seekTo($objectID);
366 return $this->current();
368 catch (SystemException
$e) {
374 * Returns the only object in this list or `null` if the list is empty.
376 * @return DatabaseObject|null
377 * @throws \BadMethodCallException if list contains more than one object
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.");
384 if (empty($this->objects
)) {
388 return reset($this->objects
);