Add explicit `return null;` statements
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / DatabaseObjectList.class.php
index 3078a0caf20f1e98a27b9cded6056374e3f9e332..4762ea07c6e3d5a9f334d951bf07200dcac70fae 100644 (file)
@@ -1,5 +1,7 @@
 <?php
+
 namespace wcf\data;
+
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\event\EventHandler;
 use wcf\system\exception\SystemException;
@@ -7,356 +9,410 @@ use wcf\system\WCF;
 
 /**
  * Abstract class for a list of database objects.
- * 
- * @author     Marcel Werk
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\Data
+ *
+ * @author  Marcel Werk
+ * @copyright   2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data
  */
-abstract class DatabaseObjectList implements \Countable, ITraversableObject {
-       /**
-        * class name
-        * @var string
-        */
-       public $className = '';
-       
-       /**
-        * class name of the object decorator; if left empty, no decorator is used
-        * @var string
-        */
-       public $decoratorClassName = '';
-       
-       /**
-        * object class name
-        * @var string
-        */
-       public $objectClassName = '';
-       
-       /**
-        * result objects
-        * @var DatabaseObject[]
-        */
-       public $objects = [];
-       
-       /**
-        * ids of result objects
-        * @var integer[]
-        */
-       public $objectIDs = null;
-       
-       /**
-        * sql offset
-        * @var integer
-        */
-       public $sqlOffset = 0;
-       
-       /**
-        * sql limit
-        * @var integer
-        */
-       public $sqlLimit = 0;
-       
-       /**
-        * sql order by statement
-        * @var string
-        */
-       public $sqlOrderBy = '';
-       
-       /**
-        * sql select parameters
-        * @var string
-        */
-       public $sqlSelects = '';
-       
-       /**
-        * sql select joins which are necessary for where statements
-        * @var string
-        */
-       public $sqlConditionJoins = '';
-       
-       /**
-        * sql select joins
-        * @var string
-        */
-       public $sqlJoins = '';
-       
-       /**
-        * enables the automatic usage of the qualified shorthand
-        * @var boolean
-        */
-       public $useQualifiedShorthand = true;
-       
-       /**
-        * sql conditions
-        * @var PreparedStatementConditionBuilder
-        */
-       protected $conditionBuilder = null;
-       
-       /**
-        * current iterator index
-        * @var integer
-        */
-       protected $index = 0;
-       
-       /**
-        * list of index to object relation
-        * @var integer[]
-        */
-       protected $indexToObject = null;
-       
-       /**
-        * Creates a new DatabaseObjectList object.
-        */
-       public function __construct() {
-               // set class name
-               if (empty($this->className)) {
-                       $className = get_called_class();
-                       
-                       if (mb_substr($className, -4) == 'List') {
-                               $this->className = mb_substr($className, 0, -4);
-                       }
-               }
-               
-               if (!empty($this->decoratorClassName)) {
-                       // validate decorator class name
-                       if (!is_subclass_of($this->decoratorClassName, DatabaseObjectDecorator::class)) {
-                               throw new SystemException("'".$this->decoratorClassName."' should extend '".DatabaseObjectDecorator::class."'");
-                       }
-                       
-                       $objectClassName = $this->objectClassName ?: $this->className;
-                       $baseClassName = call_user_func([$this->decoratorClassName, 'getBaseClass']);
-                       if ($objectClassName != $baseClassName && !is_subclass_of($baseClassName, $objectClassName)) {
-                               throw new SystemException("'".$this->decoratorClassName."' can't decorate objects of class '".$objectClassName."'");
-                       }
-               }
-               
-               $this->conditionBuilder = new PreparedStatementConditionBuilder();
-               
-               EventHandler::getInstance()->fireAction($this, 'init');
-       }
-       
-       /**
-        * Counts the number of objects.
-        * 
-        * @return      integer
-        */
-       public function countObjects() {
-               $sql = "SELECT  COUNT(*)
-                       FROM    ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
-                       ".$this->sqlConditionJoins."
-                       ".$this->getConditionBuilder();
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute($this->getConditionBuilder()->getParameters());
-               
-               return $statement->fetchSingleColumn();
-       }
-       
-       /**
-        * Reads the object ids from database.
-        */
-       public function readObjectIDs() {
-               $this->objectIDs = [];
-               $sql = "SELECT  ".$this->getDatabaseTableAlias().".".$this->getDatabaseTableIndexName()." AS objectID
-                       FROM    ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
-                               ".$this->sqlConditionJoins."
-                               ".$this->getConditionBuilder()."
-                               ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
-               $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
-               $statement->execute($this->getConditionBuilder()->getParameters());
-               $this->objectIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
-       }
-       
-       /**
-        * Reads the objects from database.
-        */
-       public function readObjects() {
-               if ($this->objectIDs !== null) {
-                       if (empty($this->objectIDs)) {
-                               return;
-                       }
-                       $sql = "SELECT  ".(!empty($this->sqlSelects) ? $this->sqlSelects.($this->useQualifiedShorthand ? ',' : '') : '')."
-                                       ".($this->useQualifiedShorthand ? $this->getDatabaseTableAlias().'.*' : '')."
-                               FROM    ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
-                                       ".$this->sqlJoins."
-                               WHERE   ".$this->getDatabaseTableAlias().".".$this->getDatabaseTableIndexName()." IN (?".str_repeat(',?', count($this->objectIDs) - 1).")
-                                       ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
-                       $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute($this->objectIDs);
-                       $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
-               }
-               else {
-                       $sql = "SELECT  ".(!empty($this->sqlSelects) ? $this->sqlSelects.($this->useQualifiedShorthand ? ',' : '') : '')."
-                                       ".($this->useQualifiedShorthand ? $this->getDatabaseTableAlias().'.*' : '')."
-                               FROM    ".$this->getDatabaseTableName()." ".$this->getDatabaseTableAlias()."
-                                       ".$this->sqlJoins."
-                                       ".$this->getConditionBuilder()."
-                                       ".(!empty($this->sqlOrderBy) ? "ORDER BY ".$this->sqlOrderBy : '');
-                       $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
-                       $statement->execute($this->getConditionBuilder()->getParameters());
-                       $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
-               }
-               
-               // decorate objects
-               if (!empty($this->decoratorClassName)) {
-                       foreach ($this->objects as &$object) {
-                               $object = new $this->decoratorClassName($object);
-                       }
-                       unset($object);
-               }
-               
-               // use table index as array index
-               $objects = $this->indexToObject = [];
-               foreach ($this->objects as $object) {
-                       $objectID = $object->getObjectID();
-                       $objects[$objectID] = $object;
-                       
-                       $this->indexToObject[] = $objectID;
-               }
-               $this->objectIDs = $this->indexToObject;
-               $this->objects = $objects;
-       }
-       
-       /**
-        * Returns the object ids of the list.
-        * 
-        * @return      integer[]
-        */
-       public function getObjectIDs() {
-               return $this->objectIDs;
-       }
-       
-       /**
-        * Sets the object ids.
-        * 
-        * @param       integer[]       $objectIDs
-        */
-       public function setObjectIDs(array $objectIDs) {
-               $this->objectIDs = array_merge($objectIDs);
-       }
-       
-       /**
-        * Returns the objects of the list.
-        * 
-        * @return      DatabaseObject[]
-        */
-       public function getObjects() {
-               return $this->objects;
-       }
-       
-       /**
-        * Returns the condition builder object.
-        * 
-        * @return      PreparedStatementConditionBuilder
-        */
-       public function getConditionBuilder() {
-               return $this->conditionBuilder;
-       }
-       
-       /**
-        * Returns the name of the database table.
-        * 
-        * @return      string
-        */
-       public function getDatabaseTableName() {
-               return call_user_func([$this->className, 'getDatabaseTableName']);
-       }
-       
-       /**
-        * Returns the name of the database table.
-        * 
-        * @return      string
-        */
-       public function getDatabaseTableIndexName() {
-               return call_user_func([$this->className, 'getDatabaseTableIndexName']);
-       }
-       
-       /**
-        * Returns the name of the database table alias.
-        * 
-        * @return      string
-        */
-       public function getDatabaseTableAlias() {
-               return call_user_func([$this->className, 'getDatabaseTableAlias']);
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function count() {
-               return count($this->objects);
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function current() {
-               $objectID = $this->indexToObject[$this->index];
-               return $this->objects[$objectID];
-       }
-       
-       /**
-        * CAUTION: This methods does not return the current iterator index,
-        * but the object key which maps to that index.
-        * 
-        * @see \Iterator::key()
-        */
-       public function key() {
-               return $this->indexToObject[$this->index];
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function next() {
-               ++$this->index;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function rewind() {
-               $this->index = 0;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function valid() {
-               return isset($this->indexToObject[$this->index]);
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function seek($index) {
-               $this->index = $index;
-               
-               if (!$this->valid()) {
-                       throw new \OutOfBoundsException();
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function seekTo($objectID) {
-               $this->index = array_search($objectID, $this->indexToObject);
-               
-               if ($this->index === false) {
-                       throw new SystemException("object id '".$objectID."' is invalid");
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function search($objectID) {
-               try {
-                       $this->seekTo($objectID);
-                       return $this->current();
-               }
-               catch (SystemException $e) {
-                       return null;
-               }
-       }
+abstract class DatabaseObjectList implements \Countable, ITraversableObject
+{
+    /**
+     * class name
+     * @var string
+     */
+    public $className = '';
+
+    /**
+     * class name of the object decorator; if left empty, no decorator is used
+     * @var string
+     */
+    public $decoratorClassName = '';
+
+    /**
+     * object class name
+     * @var string
+     */
+    public $objectClassName = '';
+
+    /**
+     * result objects
+     * @var DatabaseObject[]
+     */
+    public $objects = [];
+
+    /**
+     * ids of result objects
+     * @var int[]
+     */
+    public $objectIDs;
+
+    /**
+     * sql offset
+     * @var int
+     */
+    public $sqlOffset = 0;
+
+    /**
+     * sql limit
+     * @var int
+     */
+    public $sqlLimit = 0;
+
+    /**
+     * sql order by statement
+     * @var string
+     */
+    public $sqlOrderBy = '';
+
+    /**
+     * sql select parameters
+     * @var string
+     */
+    public $sqlSelects = '';
+
+    /**
+     * sql select joins which are necessary for where statements
+     * @var string
+     */
+    public $sqlConditionJoins = '';
+
+    /**
+     * sql select joins
+     * @var string
+     */
+    public $sqlJoins = '';
+
+    /**
+     * enables the automatic usage of the qualified shorthand
+     * @var bool
+     */
+    public $useQualifiedShorthand = true;
+
+    /**
+     * sql conditions
+     * @var PreparedStatementConditionBuilder
+     */
+    protected $conditionBuilder;
+
+    /**
+     * current iterator index
+     * @var int
+     */
+    protected $index = 0;
+
+    /**
+     * list of index to object relation
+     * @var int[]
+     */
+    protected $indexToObject;
+
+    /**
+     * Creates a new DatabaseObjectList object.
+     */
+    public function __construct()
+    {
+        // set class name
+        if (empty($this->className)) {
+            $className = \get_called_class();
+
+            if (\mb_substr($className, -4) == 'List') {
+                $this->className = \mb_substr($className, 0, -4);
+            }
+        }
+
+        if (!empty($this->decoratorClassName)) {
+            // validate decorator class name
+            if (!\is_subclass_of($this->decoratorClassName, DatabaseObjectDecorator::class)) {
+                throw new SystemException("'" . $this->decoratorClassName . "' should extend '" . DatabaseObjectDecorator::class . "'");
+            }
+
+            $objectClassName = $this->objectClassName ?: $this->className;
+            $baseClassName = \call_user_func([$this->decoratorClassName, 'getBaseClass']);
+            if ($objectClassName != $baseClassName && !\is_subclass_of($baseClassName, $objectClassName)) {
+                throw new SystemException("'" . $this->decoratorClassName . "' can't decorate objects of class '" . $objectClassName . "'");
+            }
+        }
+
+        $this->conditionBuilder = new PreparedStatementConditionBuilder();
+
+        EventHandler::getInstance()->fireAction($this, 'init');
+    }
+
+    /**
+     * Counts the number of objects.
+     *
+     * @return  int
+     */
+    public function countObjects()
+    {
+        $sql = "SELECT  COUNT(*)
+                FROM    " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . "
+                " . $this->sqlConditionJoins . "
+                " . $this->getConditionBuilder();
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute($this->getConditionBuilder()->getParameters());
+
+        return $statement->fetchSingleColumn();
+    }
+
+    /**
+     * Reads the object ids from database.
+     */
+    public function readObjectIDs()
+    {
+        $this->objectIDs = [];
+        $sql = "SELECT  " . $this->getDatabaseTableAlias() . "." . $this->getDatabaseTableIndexName() . " AS objectID
+                FROM    " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . "
+                " . $this->sqlConditionJoins . "
+                " . $this->getConditionBuilder() . "
+                " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : '');
+        $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
+        $statement->execute($this->getConditionBuilder()->getParameters());
+        $this->objectIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
+    }
+
+    /**
+     * Reads the objects from database.
+     */
+    public function readObjects()
+    {
+        if ($this->objectIDs !== null) {
+            if (empty($this->objectIDs)) {
+                return;
+            }
+
+            $objectIdPlaceholder = "?" . \str_repeat(',?', \count($this->objectIDs) - 1);
+
+            $sql = "SELECT  " . (!empty($this->sqlSelects) ? $this->sqlSelects . ($this->useQualifiedShorthand ? ',' : '') : '') . "
+                            " . ($this->useQualifiedShorthand ? $this->getDatabaseTableAlias() . '.*' : '') . "
+                    FROM    " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . "
+                            " . $this->sqlJoins . "
+                    WHERE   " . $this->getDatabaseTableAlias() . "." . $this->getDatabaseTableIndexName() . " IN ({$objectIdPlaceholder})
+                            " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : '');
+            $statement = WCF::getDB()->prepareStatement($sql);
+            $statement->execute($this->objectIDs);
+            $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
+        } else {
+            $sql = "SELECT  " . (!empty($this->sqlSelects) ? $this->sqlSelects . ($this->useQualifiedShorthand ? ',' : '') : '') . "
+                            " . ($this->useQualifiedShorthand ? $this->getDatabaseTableAlias() . '.*' : '') . "
+                    FROM    " . $this->getDatabaseTableName() . " " . $this->getDatabaseTableAlias() . "
+                    " . $this->sqlJoins . "
+                    " . $this->getConditionBuilder() . "
+                    " . (!empty($this->sqlOrderBy) ? "ORDER BY " . $this->sqlOrderBy : '');
+            $statement = WCF::getDB()->prepareStatement($sql, $this->sqlLimit, $this->sqlOffset);
+            $statement->execute($this->getConditionBuilder()->getParameters());
+            $this->objects = $statement->fetchObjects(($this->objectClassName ?: $this->className));
+        }
+
+        // decorate objects
+        if (!empty($this->decoratorClassName)) {
+            foreach ($this->objects as &$object) {
+                $object = new $this->decoratorClassName($object);
+            }
+            unset($object);
+        }
+
+        // use table index as array index
+        $objects = $this->indexToObject = [];
+        foreach ($this->objects as $object) {
+            $objectID = $object->getObjectID();
+            $objects[$objectID] = $object;
+
+            $this->indexToObject[] = $objectID;
+        }
+        $this->objectIDs = $this->indexToObject;
+        $this->objects = $objects;
+    }
+
+    /**
+     * Returns the object ids of the list.
+     *
+     * @return  int[]
+     */
+    public function getObjectIDs()
+    {
+        return $this->objectIDs;
+    }
+
+    /**
+     * Sets the object ids.
+     *
+     * @param int[] $objectIDs
+     */
+    public function setObjectIDs(array $objectIDs)
+    {
+        $this->objectIDs = \array_merge($objectIDs);
+    }
+
+    /**
+     * Returns the objects of the list.
+     *
+     * @return  DatabaseObject[]
+     */
+    public function getObjects()
+    {
+        return $this->objects;
+    }
+
+    /**
+     * Returns the condition builder object.
+     *
+     * @return  PreparedStatementConditionBuilder
+     */
+    public function getConditionBuilder()
+    {
+        return $this->conditionBuilder;
+    }
+
+    /**
+     * Sets the condition builder dynamically.
+     *
+     * @param PreparedStatementConditionBuilder $conditionBuilder
+     * @since   5.3
+     */
+    public function setConditionBuilder(PreparedStatementConditionBuilder $conditionBuilder)
+    {
+        $this->conditionBuilder = $conditionBuilder;
+    }
+
+    /**
+     * Returns the name of the database table.
+     *
+     * @return  string
+     */
+    public function getDatabaseTableName()
+    {
+        return \call_user_func([$this->className, 'getDatabaseTableName']);
+    }
+
+    /**
+     * Returns the name of the database table.
+     *
+     * @return  string
+     */
+    public function getDatabaseTableIndexName()
+    {
+        return \call_user_func([$this->className, 'getDatabaseTableIndexName']);
+    }
+
+    /**
+     * Returns the name of the database table alias.
+     *
+     * @return  string
+     */
+    public function getDatabaseTableAlias()
+    {
+        return \call_user_func([$this->className, 'getDatabaseTableAlias']);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function count()
+    {
+        return \count($this->objects);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function current()
+    {
+        $objectID = $this->indexToObject[$this->index];
+
+        return $this->objects[$objectID];
+    }
+
+    /**
+     * CAUTION: This methods does not return the current iterator index,
+     * but the object key which maps to that index.
+     *
+     * @see \Iterator::key()
+     */
+    public function key()
+    {
+        return $this->indexToObject[$this->index];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function next()
+    {
+        $this->index++;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function rewind()
+    {
+        $this->index = 0;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function valid()
+    {
+        return isset($this->indexToObject[$this->index]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function seek($index)
+    {
+        $this->index = $index;
+
+        if (!$this->valid()) {
+            throw new \OutOfBoundsException();
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function seekTo($objectID)
+    {
+        $this->index = \array_search($objectID, $this->indexToObject);
+
+        if ($this->index === false) {
+            throw new SystemException("object id '" . $objectID . "' is invalid");
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function search($objectID)
+    {
+        try {
+            $this->seekTo($objectID);
+
+            return $this->current();
+        } catch (SystemException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Returns the only object in this list or `null` if the list is empty.
+     *
+     * @return  DatabaseObject|null
+     * @throws  \BadMethodCallException     if list contains more than one object
+     */
+    public function getSingleObject()
+    {
+        if (\count($this->objects) > 1) {
+            throw new \BadMethodCallException("Cannot get a single object when the list contains " . \count($this->objects) . " objects.");
+        }
+
+        if (empty($this->objects)) {
+            return null;
+        }
+
+        return \reset($this->objects);
+    }
 }