Add abstract object tree node used for selection form fields
authorMatthias Schmidt <gravatronics@live.com>
Sun, 2 Jun 2019 07:31:02 +0000 (09:31 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 2 Jun 2019 07:31:02 +0000 (09:31 +0200)
12 files changed:
wcfsetup/install/files/lib/data/DatabaseObject.class.php
wcfsetup/install/files/lib/data/IIDObject.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IObjectTreeNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IPollContainer.class.php
wcfsetup/install/files/lib/data/IVersionTrackerObject.class.php
wcfsetup/install/files/lib/data/TObjectTreeNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryNode.class.php
wcfsetup/install/files/lib/data/like/object/ILikeObject.class.php
wcfsetup/install/files/lib/system/form/builder/field/TSelectionFormField.class.php
wcfsetup/install/files/lib/system/request/IRouteController.class.php
wcfsetup/install/files/lib/system/tagging/ITagged.class.php
wcfsetup/install/files/lib/system/user/notification/object/IUserNotificationObject.class.php

index c7ea75db6f29357b230a86dd680bd8ae55745bd0..99e9056411877b1951936b8fe985e5a68721d22c 100644 (file)
@@ -10,7 +10,7 @@ use wcf\system\WCF;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Data
  */
-abstract class DatabaseObject implements IStorableObject {
+abstract class DatabaseObject implements IIDObject, IStorableObject {
        /**
         * database table for this object
         * @var string
diff --git a/wcfsetup/install/files/lib/data/IIDObject.class.php b/wcfsetup/install/files/lib/data/IIDObject.class.php
new file mode 100644 (file)
index 0000000..4ff7595
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Provides a method to access the unique id of an object.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data
+ * @since      5.2
+ */
+interface IIDObject {
+       /**
+        * Returns the unique id of the object.
+        * 
+        * @return      integer
+        */
+       public function getObjectID();
+}
diff --git a/wcfsetup/install/files/lib/data/IObjectTreeNode.class.php b/wcfsetup/install/files/lib/data/IObjectTreeNode.class.php
new file mode 100644 (file)
index 0000000..4d6b15a
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every node of a database object tree has to implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data
+ * @since      5.2
+ */
+interface IObjectTreeNode extends \Countable, IIDObject, \RecursiveIterator {
+       /**
+        * Adds the given node as child node and sets the child node's parent node to this node.
+        *
+        * @param       IObjectTreeNode         $child          added child node
+        * @throws      \InvalidArgumentException               if given object is no (deocrated) instance of this class
+        */
+       public function addChild(IObjectTreeNode $child);
+       
+       /**
+        * Returns the depth of the node within the tree.
+        * 
+        * The minimum depth is `1`.
+        * 
+        * @return      integer
+        */
+       public function getDepth();
+       
+       /**
+        * Returns the number of open parent nodes.
+        * 
+        * @return      integer
+        */
+       public function getOpenParentNodes();
+       
+       /**
+        * Returns `true` if this node is the last sibling and `false` otherwise.
+        * 
+        * @return      boolean
+        */
+       public function isLastSibling();
+       
+       /**
+        * Sets the parent node of this node.
+        *
+        * @param       IObjectTreeNode         $parentNode     parent node
+        * @throws      \InvalidArgumentException               if given object is no (deocrated) instance of this class
+        */
+       public function setParentNode(IObjectTreeNode $parentNode);
+}
index a898c69f63df814da1db5d815168af9ee613cae6..a176cad398cbe075452ed5db384d7b0c8225e147 100644 (file)
@@ -10,14 +10,7 @@ namespace wcf\data;
  * @package    WoltLabSuite\Core\Data
  * @since      5.2
  */
-interface IPollContainer extends IPollObject {
-       /**
-        * Returns the id of the poll container.
-        *
-        * @return      integer
-        */
-       public function getObjectID();
-       
+interface IPollContainer extends IIDObject, IPollObject {
        /**
         * Returns the id of the poll that belongs to this object or `null` if there is no such poll.
         *
index 1342a11ebe55d3094c791301703062e4a33b3bbb..ef29e38437f5508ef550a329418a1b03519db0ab 100644 (file)
@@ -10,18 +10,11 @@ namespace wcf\data;
  * @package    WoltLabSuite\Core\Data
  * @since      3.1
  */
-interface IVersionTrackerObject extends IUserContent {
+interface IVersionTrackerObject extends IIDObject, IUserContent {
        /**
         * Returns the link to the object's edit page.
         * 
         * @return      string
         */
        public function getEditLink();
-       
-       /**
-        * Returns the object's unique id.
-        * 
-        * @return      integer
-        */
-       public function getObjectID();
 }
diff --git a/wcfsetup/install/files/lib/data/TObjectTreeNode.class.php b/wcfsetup/install/files/lib/data/TObjectTreeNode.class.php
new file mode 100644 (file)
index 0000000..3a05e1e
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+namespace wcf\data;
+use wcf\util\ClassUtil;
+
+/**
+ * Default implementation of `IObjectTreeNode`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data
+ * @since      5.2
+ */
+trait TObjectTreeNode {
+       /**
+        * child nodes
+        * @var TObjectTreeNode[]
+        */
+       protected $children = [];
+       
+       /**
+        * current iterator key
+        * @var integer
+        */
+       protected $index = 0;
+       
+       /**
+        * parent node object
+        * @var TObjectTreeNode
+        */
+       protected $parentNode = null;
+       
+       /**
+        * Adds the given node as child node and sets the child node's parent node to this node.
+        * 
+        * @param       IObjectTreeNode         $child          added child node
+        * @throws      \InvalidArgumentException               if given object is no (deocrated) instance of this class
+        */
+       public function addChild(IObjectTreeNode $child) {
+               if (!($child instanceof $this) && !ClassUtil::isDecoratedInstanceOf($child, static::class)) {
+                       throw new \InvalidArgumentException("Child has to be a (decorated) instance of '" . static::class . "', but instance of '" . get_class($child) . "' given.");
+               }
+               
+               $child->setParentNode($this);
+               
+               $this->children[] = $child;
+       }
+       
+       /**
+        * Returns the number of child nodes.
+        * 
+        * @return      integer
+        */
+       public function count() {
+               return count($this->children);
+       }
+       
+       /**
+        * Return the currently iterated child node.
+        * 
+        * @return      IObjectTreeNode
+        */
+       public function current() {
+               return $this->children[$this->index];
+       }
+       
+       /**
+        * Returns an iterator for the currently iterated child node by returning the node itself.
+        * 
+        * @return      IObjectTreeNode
+        */
+       public function getChildren() {
+               return $this->children[$this->index];
+       }
+       
+       /**
+        * Returns the depth of the node within the tree.
+        * 
+        * The minimum depth is `1`.
+        * 
+        * @return      integer
+        */
+       public function getDepth() {
+               $element = $this;
+               $depth = 1;
+               
+               while ($element->parentNode->parentNode !== null) {
+                       $depth++;
+                       $element = $element->parentNode;
+               }
+               
+               return $depth;
+       }
+       
+       /**
+        * Returns the number of open parent nodes.
+        * 
+        * @return      integer
+        */
+       public function getOpenParentNodes() {
+               $element = $this;
+               $i = 0;
+               
+               while ($element->parentNode->parentNode !== null && $element->isLastSibling()) {
+                       $i++;
+                       $element = $element->parentNode;
+               }
+               
+               return $i;
+       }
+
+       /**
+        * Returns `true` if the node as any children and return `false` otherwise.
+        * 
+        * @return      boolean
+        */
+       public function hasChildren() {
+               return !empty($this->children);
+       }
+       
+       /**
+        * Return the key of the currently iterated child node.
+        * 
+        * @return      integer
+        */
+       public function key() {
+               return $this->index;
+       }
+       
+       /**
+        * Returns `true` if this node is the last sibling and `false` otherwise.
+        * 
+        * @return      boolean
+        */
+       public function isLastSibling() {
+               foreach ($this->parentNode as $key => $child) {
+                       if ($child === $this) {
+                               return $key === count($this->parentNode) - 1;
+                       }
+               }
+               
+               throw new \LogicException("Unreachable");
+       }
+       
+       /**
+        * Moves the iteration forward to next child node.
+        */
+       public function next() {
+               $this->index++;
+       }
+       
+       /**
+        * Rewind the iteration to the first child node.
+        */
+       public function rewind() {
+               $this->index = 0;
+       }
+       
+       /**
+        * Sets the parent node of this node.
+        * 
+        * @param       IObjectTreeNode         $parentNode     parent node
+        * @throws      \InvalidArgumentException               if given object is no (deocrated) instance of this class
+        */
+       public function setParentNode(IObjectTreeNode $parentNode) {
+               if (!($parentNode instanceof $this) && !ClassUtil::isDecoratedInstanceOf($parentNode, static::class)) {
+                       throw new \InvalidArgumentException("Parent has to be a (decorated) instance of '" . static::class . "', but instance of '" . get_class($parentNode) . "' given.");
+               }
+               
+               $this->parentNode = $parentNode;
+       }
+       
+       /**
+        * Returns `true` if current iteration position is valid and `false` otherwise.
+        * 
+        * @return      boolean
+        */
+       public function valid() {
+               return isset($this->children[$this->index]);
+       }
+}
index 4cadf7c0a73ff5e10a5ee09a85b13494f16f5a93..113264ef8793915ffbe5ef50f72052a55026c173 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 namespace wcf\data\category;
 use wcf\data\DatabaseObjectDecorator;
+use wcf\data\IObjectTreeNode;
+use wcf\data\TObjectTreeNode;
 
 /**
  * Represents a category node.
@@ -13,154 +15,14 @@ use wcf\data\DatabaseObjectDecorator;
  * @method     Category        getDecoratedObject()
  * @mixin      Category
  */
-class CategoryNode extends DatabaseObjectDecorator implements \RecursiveIterator, \Countable {
-       /**
-        * child category nodes
-        * @var CategoryNode[]
-        */
-       protected $children = [];
-       
-       /**
-        * current iterator key
-        * @var integer
-        */
-       protected $index = 0;
-       
-       /**
-        * parent node object
-        * @var CategoryNode
-        */
-       protected $parentNode = null;
+class CategoryNode extends DatabaseObjectDecorator implements IObjectTreeNode {
+       use TObjectTreeNode;
        
        /**
         * @inheritDoc
         */
        protected static $baseClass = Category::class;
        
-       /**
-        * Adds the given category node as child node.
-        * 
-        * @param       CategoryNode            $categoryNode
-        */
-       public function addChild(CategoryNode $categoryNode) {
-               $categoryNode->setParentNode($this);
-               
-               $this->children[] = $categoryNode;
-       }
-       
-       /**
-        * Sets parent node object.
-        * 
-        * @param       CategoryNode            $parentNode
-        */
-       public function setParentNode(CategoryNode $parentNode) {
-               $this->parentNode = $parentNode;
-       }
-       
-       /**
-        * Returns true if this element is the last sibling.
-        * 
-        * @return      boolean
-        */
-       public function isLastSibling() {
-               foreach ($this->parentNode as $key => $child) {
-                       if ($child === $this) {
-                               if ($key == count($this->parentNode) - 1) return true;
-                               return false;
-                       }
-               }
-       }
-       
-       /**
-        * Returns the number of open parent nodes.
-        * 
-        * @return      integer
-        */
-       public function getOpenParentNodes() {
-               $element = $this;
-               $i = 0;
-               
-               while ($element->parentNode->parentNode != null && $element->isLastSibling()) {
-                       $i++;
-                       $element = $element->parentNode;
-               }
-               
-               return $i;
-       }
-       
-       /**
-        * Returns node depth.
-        *
-        * @return      integer
-        */
-       public function getDepth() {
-               $element = $this;
-               $depth = 1;
-               
-               while ($element->parentNode->parentNode != null) {
-                       $depth++;
-                       $element = $element->parentNode;
-               }
-               
-               return $depth;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function count() {
-               return count($this->children);
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function current() {
-               return $this->children[$this->index];
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function getChildren() {
-               return $this->children[$this->index];
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function hasChildren() {
-               return !empty($this->children);
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function key() {
-               return $this->index;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function next() {
-               $this->index++;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function rewind() {
-               $this->index = 0;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function valid() {
-               return isset($this->children[$this->index]);
-       }
-       
        /**
         * Returns true if this category is visible in a nested menu item list.
         *
index d53b71a7c133b7c29f8013aceef71366d503a0cf..b692397c80b8e29a56233eed50911f4f84fe153d 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\data\like\object;
+use wcf\data\IIDObject;
 use wcf\data\like\Like;
 use wcf\data\object\type\ObjectType;
 use wcf\data\IDatabaseObjectProcessor;
@@ -13,7 +14,7 @@ use wcf\data\ITitledObject;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Data\Like\Object
  */
-interface ILikeObject extends IDatabaseObjectProcessor, ITitledObject {
+interface ILikeObject extends IDatabaseObjectProcessor, IIDObject, ITitledObject {
        /**
         * Returns the url to this likeable.
         * 
@@ -28,13 +29,6 @@ interface ILikeObject extends IDatabaseObjectProcessor, ITitledObject {
         */
        public function getUserID();
        
-       /**
-        * Returns the id of this object.
-        * 
-        * @return      integer
-        */
-       public function getObjectID();
-       
        /**
         * Returns the likeable object type previously set via `setObjectType()`.
         * 
index 257d15313ec631472b0860d571af0b80e23b3ffc..78975c002b13c4d8eef16444de79840187a62515 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\form\builder\field;
 use wcf\data\DatabaseObjectList;
+use wcf\data\IObjectTreeNode;
 use wcf\data\ITitledObject;
 use wcf\system\WCF;
 use wcf\util\ClassUtil;
@@ -100,47 +101,53 @@ trait TSelectionFormField {
         */
        public function options($options, $nestedOptions = false, $labelLanguageItems = true) {
                if ($nestedOptions) {
-                       if (!is_array($options) && !is_callable($options)) {
-                               throw new \InvalidArgumentException("The given nested options are neither an array nor a callable, " . gettype($options) . " given.");
+                       if (!is_array($options) && !($options instanceof \Traversable) && !is_callable($options)) {
+                               throw new \InvalidArgumentException("The given nested options are neither iterable nor a callable, " . gettype($options) . " given.");
                        }
                }
-               else if (!is_array($options) && !is_callable($options) && !($options instanceof DatabaseObjectList)) {
-                       throw new \InvalidArgumentException("The given options are neither an array, a callable nor an instance of '" . DatabaseObjectList::class . "', " . gettype($options) . " given.");
+               else if (!is_array($options) && !($options instanceof \Traversable) && !is_callable($options)) {
+                       throw new \InvalidArgumentException("The given options are neither iterable nor a callable, " . gettype($options) . " given.");
                }
                
                if (is_callable($options)) {
                        $options = $options();
                        
                        if ($nestedOptions) {
-                               if (!is_array($options) && !($options instanceof DatabaseObjectList)) {
-                                       throw new \UnexpectedValueException("The nested options callable is expected to return an array, " . gettype($options) . " returned.");
+                               if (!is_array($options) && !($options instanceof \Traversable)) {
+                                       throw new \UnexpectedValueException("The nested options callable is expected to return an iterable value, " . gettype($options) . " returned.");
                                }
                        }
-                       else if (!is_array($options) && !($options instanceof DatabaseObjectList)) {
-                               throw new \UnexpectedValueException("The options callable is expected to return an array or an instance of '" . DatabaseObjectList::class . "', " . gettype($options) . " returned.");
+                       else if (!is_array($options) && !($options instanceof \Traversable)) {
+                               throw new \UnexpectedValueException("The options callable is expected to return an iterable value, " . gettype($options) . " returned.");
                        }
                        
                        return $this->options($options, $nestedOptions, $labelLanguageItems);
                }
-               else if ($options instanceof DatabaseObjectList) {
+               else if ($options instanceof \Traversable) {
                        // automatically read objects
-                       if ($options->objectIDs === null) {
+                       if ($options instanceof DatabaseObjectList && $options->objectIDs === null) {
                                $options->readObjects();
                        }
                        
-                       $dboOptions = [];
-                       foreach ($options as $object) {
-                               if (!ClassUtil::isDecoratedInstanceOf($object, ITitledObject::class)) {
-                                       throw new \InvalidArgumentException("The database objects in the passed list must implement '" . ITitledObject::class . "'.");
-                               }
-                               if (!$object::getDatabaseTableIndexIsIdentity()) {
-                                       throw new \InvalidArgumentException("The database objects in the passed list must must have an index that identifies the objects.");
+                       if ($nestedOptions) {
+                               $collectedOptions = [];
+                               foreach ($options as $key => $object) {
+                                       if (!($object instanceof IObjectTreeNode)) {
+                                               throw new \InvalidArgumentException("Nested traversable options must implement '" . IObjectTreeNode::class . "'.");
+                                       }
+                                       
+                                       $collectedOptions[] = [
+                                               'depth' => $object->getDepth() - 1,
+                                               'label' => $object,
+                                               'value' => $object->getObjectID()
+                                       ];
                                }
                                
-                               $dboOptions[$object->getObjectID()] = $object->getTitle();
+                               $options = $collectedOptions;
+                       }
+                       else {
+                               $options = iterator_to_array($options);
                        }
-                       
-                       $options = $dboOptions;
                }
                
                $this->options = [];
@@ -161,8 +168,16 @@ trait TSelectionFormField {
                                }
                                
                                // validate label
-                               if (is_object($option['label']) && method_exists($option['label'], '__toString')) {
-                                       $option['label'] = (string) $option['label'];
+                               if (is_object($option['label'])) {
+                                       if (method_exists($option['label'], '__toString')) {
+                                               $option['label'] = (string)$option['label'];
+                                       }
+                                       else if ($option['label'] instanceof ITitledObject || ClassUtil::isDecoratedInstanceOf($option['label'], ITitledObject::class)) {
+                                               $option['label'] = $option['label']->getTitle();
+                                       }
+                                       else {
+                                               throw new \InvalidArgumentException("Nested option with key '{$key}' contain invalid label of type " . gettype($option['label']) . ".");
+                                       }
                                }
                                else if (!is_string($option['label']) && !is_numeric($option['label'])) {
                                        throw new \InvalidArgumentException("Nested option with key '{$key}' contain invalid label of type " . gettype($option['label']) . ".");
@@ -202,8 +217,16 @@ trait TSelectionFormField {
                                        throw new \InvalidArgumentException("Non-nested options must not contain any array. Array given for value '{$value}'.");
                                }
                                
-                               if (is_object($label) && method_exists($label, '__toString')) {
-                                       $label = (string) $label;
+                               if (is_object($label)) {
+                                       if (method_exists($label, '__toString')) {
+                                               $label = (string)$label;
+                                       }
+                                       else if ($label instanceof ITitledObject || ClassUtil::isDecoratedInstanceOf($label, ITitledObject::class)) {
+                                               $label = $label->getTitle();
+                                       }
+                                       else {
+                                               throw new \InvalidArgumentException("Options contain invalid label of type " . gettype($label) . ".");
+                                       }
                                }
                                else if (!is_string($label) && !is_numeric($label)) {
                                        throw new \InvalidArgumentException("Options contain invalid label of type " . gettype($label) . ".");
index f4ef29b767db1eb60932136cf05b7794cce947e0..0f7b8e6bf5345633b165b7f8a864d8b6a84c3e81 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\request;
+use wcf\data\IIDObject;
 use wcf\data\ITitledObject;
 
 /**
@@ -10,11 +11,4 @@ use wcf\data\ITitledObject;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Request
  */
-interface IRouteController extends ITitledObject {
-       /**
-        * Returns the id of the object.
-        * 
-        * @return      integer
-        */
-       public function getObjectID();
-}
+interface IRouteController extends IIDObject, ITitledObject {}
index a714671d8aa94525bfd4ef9a4a5345cbdc2bcad3..da10bda3fbcb1263c8b0458b12af516f21a05d98 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\tagging;
+use wcf\data\IIDObject;
 
 /**
  * Any tagged object has to implement this interface.
@@ -9,14 +10,7 @@ namespace wcf\system\tagging;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Tagging
  */
-interface ITagged {
-       /**
-        * Returns the id of the tagged object.
-        * 
-        * @return      integer         the id to get
-        */
-       public function getObjectID();
-       
+interface ITagged extends IIDObject {
        /**
         * Returns the taggable type of this tagged object.
         * 
index d15ee276afd5b87fa9ee45ed5d54990312d99978..3d19ff8967e70efaed8493eb33b2b917bf4726d3 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\user\notification\object;
 use wcf\data\IDatabaseObjectProcessor;
+use wcf\data\IIDObject;
 use wcf\data\ITitledObject;
 
 /**
@@ -11,14 +12,7 @@ use wcf\data\ITitledObject;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\User\Notification\Object
  */
-interface IUserNotificationObject extends IDatabaseObjectProcessor, ITitledObject {
-       /**
-        * Returns the ID of this object.
-        * 
-        * @return      integer
-        */
-       public function getObjectID();
-       
+interface IUserNotificationObject extends IDatabaseObjectProcessor, IIDObject, ITitledObject {
        /**
         * Returns the url of this object.
         *