Discussion provider support for articles
authorAlexander Ebert <ebert@woltlab.com>
Tue, 26 Jun 2018 10:19:11 +0000 (12:19 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 26 Jun 2018 10:19:11 +0000 (12:19 +0200)
See #2615
See #2585
See WoltLab/com.woltlab.wbb#299
See WoltLab/com.woltlab.wbb#301

14 files changed:
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/templates/article.tpl
com.woltlab.wcf/templates/articleComments.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/articleListItems.tpl
com.woltlab.wcf/templates/boxArticleList.tpl
wcfsetup/install/files/lib/data/article/Article.class.php
wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php
wcfsetup/install/files/lib/page/AbstractArticlePage.class.php
wcfsetup/install/files/lib/page/ArticlePage.class.php
wcfsetup/install/files/lib/system/article/discussion/AbstractArticleDiscussionProvider.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/article/discussion/CommentArticleDiscussionProvider.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/article/discussion/IArticleDiscussionProvider.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/article/discussion/VoidArticleDiscussionProvider.class.php [new file with mode: 0644]

index 20a83cd7c8b8a6e57f1dd2889b1a4d4af5500e91..30445adad8a7d691a4f2370f5efa795bd9f49d31 100644 (file)
                        <definitionname>com.woltlab.wcf.label.objectType</definitionname>
                        <classname>wcf\system\label\object\type\ArticleCategoryLabelObjectTypeHandler</classname>
                </type>
+               <type>
+                       <name>com.woltlab.wcf.article.comment</name>
+                       <definitionname>com.woltlab.wcf.article.discussionProvider</definitionname>
+                       <classname>wcf\system\article\discussion\CommentArticleDiscussionProvider</classname>
+               </type>
                <!-- /articles -->
                
                <type>
index 8fcb1f0b50a06baebd1f4f140144d4c222c8f0da..61a6f0227bd383818efd1008c83fca84b04913a4 100644 (file)
                        <name>com.woltlab.wcf.sitemap.object</name>
                        <interfacename>wcf\system\sitemap\object\ISitemapObjectObjectType</interfacename>
                </definition>
+               
+               <definition>
+                       <name>com.woltlab.wcf.article.discussionProvider</name>
+                       <interfacename>wcf\system\article\discussion\IArticleDiscussionProvider</interfacename>
+               </definition>
        </import>
        
        <delete>
index 4e0fde5bfdea92410ddec43814dca92dadced738..1bc906b4e6194dc6c2372730717d349040da1539 100644 (file)
                                        <meta itemprop="dateModified" content="{@$article->time|date:'c'}">
                                </li>
                                
-                               {if $article->enableComments}
+                               {if $article->getDiscussionProvider()->getDiscussionCountPhrase()}
                                        <li itemprop="interactionStatistic" itemscope itemtype="http://schema.org/InteractionCounter">
                                                <span class="icon icon16 fa-comments"></span>
-                                               <span>{lang}wcf.article.articleComments{/lang}</span>
+                                               <span>{$article->getDiscussionProvider()->getDiscussionCountPhrase()}</span>
                                                <meta itemprop="interactionType" content="http://schema.org/CommentAction">
-                                               <meta itemprop="userInteractionCount" content="{@$article->comments}">
+                                               <meta itemprop="userInteractionCount" content="{@$article->getDiscussionProvider()->getDiscussionCount()}">
                                        </li>
                                {/if}
                                
 
 {event name='beforeComments'}
 
-{if $article->enableComments}
-       {if $commentList|count || $commentCanAdd}
-               <section id="comments" class="section sectionContainerList">
-                       <h2 class="sectionTitle">{lang}wcf.global.comments{/lang}{if $article->comments} <span class="badge">{#$article->comments}</span>{/if}</h2>
-                       
-                       {include file='__commentJavaScript' commentContainerID='articleCommentList'}
-                       
-                       <ul id="articleCommentList" class="commentList containerList" data-can-add="{if $commentCanAdd}true{else}false{/if}" data-object-id="{@$articleContentID}" data-object-type-id="{@$commentObjectTypeID}" data-comments="{@$commentList->countObjects()}" data-last-comment-time="{@$lastCommentTime}">
-                               {if $commentCanAdd}{include file='commentListAddComment' wysiwygSelector='articleCommentListAddComment'}{/if}
-                               {include file='commentList'}
-                       </ul>
-               </section>
-       {/if}
-{/if}
+{@$article->getDiscussionProvider()->renderDiscussions()}
 
 {if MODULE_LIKE && ARTICLE_ENABLE_LIKE}
        <script data-relocate="true">
diff --git a/com.woltlab.wcf/templates/articleComments.tpl b/com.woltlab.wcf/templates/articleComments.tpl
new file mode 100644 (file)
index 0000000..dcb3879
--- /dev/null
@@ -0,0 +1,12 @@
+{if $commentList|count || $commentCanAdd}
+       <section id="comments" class="section sectionContainerList">
+               <h2 class="sectionTitle">{lang}wcf.global.comments{/lang}{if $article->comments} <span class="badge">{#$article->comments}</span>{/if}</h2>
+               
+               {include file='__commentJavaScript' commentContainerID='articleCommentList'}
+               
+               <ul id="articleCommentList" class="commentList containerList" data-can-add="{if $commentCanAdd}true{else}false{/if}" data-object-id="{@$articleContentID}" data-object-type-id="{@$commentObjectTypeID}" data-comments="{@$commentList->countObjects()}" data-last-comment-time="{@$lastCommentTime}">
+                       {if $commentCanAdd}{include file='commentListAddComment' wysiwygSelector='articleCommentListAddComment'}{/if}
+                       {include file='commentList'}
+               </ul>
+       </section>
+{/if}
index f5badadbb25a05b558f44d4e834bd54972ac7cdc..072e06d7fcd1796061d267a5c7099bd621d2f90c 100644 (file)
                                                                        {@$article->time|time}
                                                                </li>
                                                                
-                                                               {if $article->enableComments}
+                                                               {if $article->getDiscussionProvider()->getDiscussionCountPhrase()}
                                                                        <li>
                                                                                <span class="icon icon16 fa-comments"></span>
-                                                                               {lang}wcf.article.articleComments{/lang}
+                                                                               {$article->getDiscussionProvider()->getDiscussionCountPhrase()}
                                                                        </li>
                                                                {/if}
                                                                
@@ -63,4 +63,4 @@
                        </a>
                </li>
        {/foreach}
-</ul>
\ No newline at end of file
+</ul>
index b562cbc35d4da305b90a6eb101982552a12f720a..881d56121f591cadcc0e7eef4078f9bb999dbf25 100644 (file)
@@ -13,7 +13,7 @@
                                                        {elseif $boxSortField == 'views'}
                                                                {lang article=$boxArticle}wcf.article.articleViews{/lang}
                                                        {elseif $boxSortField == 'comments'}
-                                                               {lang article=$boxArticle}wcf.article.articleComments{/lang}
+                                                               {$boxArticle->getDiscussionProvider()->getDiscussionCountPhrase()}
                                                        {elseif $boxSortField == 'cumulativeLikes'}
                                                                {if MODULE_LIKE && $__wcf->getSession()->getPermission('user.like.canViewLike') && ($boxArticle->likes || $boxArticle->dislikes)}
                                                                        <span class="wcfLikeCounter{if $boxArticle->cumulativeLikes > 0} likeCounterLiked{elseif $boxArticle->cumulativeLikes < 0}likeCounterDisliked{/if}">
@@ -45,7 +45,7 @@
                                                
                                                <li>
                                                        <span class="icon icon16 fa-comments"></span>
-                                                       {lang article=$boxArticle}wcf.article.articleComments{/lang}
+                                                       {$boxArticle->getDiscussionProvider()->getDiscussionCountPhrase()}
                                                </li>
                                        </ul>
                                </a>
index 0318fd08530632d9d1845742022fc9a5ae20fe29..5806e17ad6cddd84ae70063180bd546a309a0595 100644 (file)
@@ -5,6 +5,10 @@ use wcf\data\article\category\ArticleCategory;
 use wcf\data\article\content\ArticleContent;
 use wcf\data\DatabaseObject;
 use wcf\data\ILinkableObject;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\article\discussion\CommentArticleDiscussionProvider;
+use wcf\system\article\discussion\IArticleDiscussionProvider;
+use wcf\system\article\discussion\VoidArticleDiscussionProvider;
 use wcf\system\WCF;
 
 /**
@@ -65,6 +69,12 @@ class Article extends DatabaseObject implements ILinkableObject {
         */
        protected $category;
        
+       /**
+        * @var IArticleDiscussionProvider
+        * @since 3.2
+        */
+       protected $discussionProvider;
+       
        /**
         * Returns true if the active user can delete this article.
         *
@@ -241,4 +251,69 @@ class Article extends DatabaseObject implements ILinkableObject {
                
                return $this->category;
        }
+       
+       /**
+        * Sets the discussion provider for this article.
+        * 
+        * @param       IArticleDiscussionProvider      $discussionProvider
+        * @since       3.2
+        */
+       public function setDiscussionProvider(IArticleDiscussionProvider $discussionProvider) {
+               $this->discussionProvider = $discussionProvider;
+       }
+       
+       /**
+        * Returns the responsible discussion provider for this article.
+        * 
+        * @return      IArticleDiscussionProvider
+        * @since       3.2
+        */
+       public function getDiscussionProvider() {
+               if ($this->discussionProvider === null) {
+                       foreach (self::getAllDiscussionProviders() as $discussionProvider) {
+                               if (call_user_func([$discussionProvider, 'isResponsible'], $this)) {
+                                       $this->setDiscussionProvider(new $discussionProvider($this));
+                                       break;
+                               }
+                       }
+                       
+                       if ($this->discussionProvider === null) {
+                               throw new \RuntimeException('No discussion provider has claimed to be responsible for the article #' . $this->articleID);
+                       }
+               }
+               
+               return $this->discussionProvider;
+       }
+       
+       /**
+        * Returns the list of the available discussion providers.
+        * 
+        * @return      string[]
+        * @since       3.2
+        */
+       public static function getAllDiscussionProviders(): array {
+               /** @var string[] $discussionProviders */
+               static $discussionProviders;
+               
+               if ($discussionProviders === null) {
+                       $discussionProviders = [];
+                       
+                       $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.article.discussionProvider');
+                       $commentProvider = '';
+                       foreach ($objectTypes as $objectType) {
+                               // the comment and the "void" provider should always be the last in the list
+                               if ($objectType->className === CommentArticleDiscussionProvider::class) {
+                                       $commentProvider = $objectType->className;
+                                       continue;
+                               }
+                               
+                               $discussionProviders[] = $objectType->className;
+                       }
+                       
+                       $discussionProviders[] = $commentProvider;
+                       $discussionProviders[] = VoidArticleDiscussionProvider::class;
+               }
+               
+               return $discussionProviders;
+       }
 }
index 5d1acce04608e902176cd5da7d828137a6286888..c6278d22522f05b8b4d3f215fa79f566190c5267 100644 (file)
@@ -81,7 +81,12 @@ class ViewableArticleList extends ArticleList {
                        $contentList->getConditionBuilder()->add('(article_content.languageID IS NULL OR article_content.languageID = ?)', [WCF::getLanguage()->languageID]);
                        $contentList->readObjects();
                        foreach ($contentList as $articleContent) {
-                               $this->objects[$articleContent->articleID]->setArticleContent($articleContent);
+                               $article = $this->objects[$articleContent->articleID];
+                               $article->setArticleContent($articleContent);
+                               
+                               // Some providers do pre-populate internal caches in order to retrieve the data
+                               // for many objects in a single step.
+                               $article->getDiscussionProvider();
                        }
                }
                
index ea5675d33924d8c65e85f96fe70c5c4cf82ac518..8aed647262369ead090956072023b4244ad31033 100644 (file)
@@ -85,6 +85,7 @@ abstract class AbstractArticlePage extends AbstractPage {
                }
                
                $this->article = ViewableArticle::getArticle($this->articleContent->articleID, false);
+               $this->article->getDiscussionProvider()->setArticleContent($this->articleContent->getDecoratedObject());
                $this->category = $this->article->getCategory();
                
                // update interface language
index 3bbfe0da3e9fd4fd20079809a55cd80a86803b78..ffcfeb933b15196df319e5a120dbd3e090123dcb 100644 (file)
@@ -139,12 +139,14 @@ class ArticlePage extends AbstractArticlePage {
                WCF::getTPL()->assign([
                        'previousArticle' => $this->previousArticle,
                        'nextArticle' => $this->nextArticle,
-                       'commentCanAdd' => WCF::getSession()->getPermission('user.article.canAddComment'),
-                       'commentList' => $this->commentList,
-                       'commentObjectTypeID' => $this->commentObjectTypeID,
-                       'lastCommentTime' => $this->commentList ? $this->commentList->getMinCommentTime() : 0,
-                       'likeData' => (MODULE_LIKE && $this->commentList) ? $this->commentList->getLikeData() : [],
-                       'articleLikeData' => $this->articleLikeData
+                       'articleLikeData' => $this->articleLikeData,
+                       
+                       // nullified values for backwards-compatibility
+                       'commentCanAdd' => 0,
+                       'commentList' => null,
+                       'commentObjectTypeID' => 0,
+                       'lastCommentTime' => 0,
+                       'likeData' => [],
                ]);
        }
 }
diff --git a/wcfsetup/install/files/lib/system/article/discussion/AbstractArticleDiscussionProvider.class.php b/wcfsetup/install/files/lib/system/article/discussion/AbstractArticleDiscussionProvider.class.php
new file mode 100644 (file)
index 0000000..0385555
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\article\discussion;
+use wcf\data\article\Article;
+use wcf\data\article\content\ArticleContent;
+
+/**
+ * Default implementation for discussion provider for articles. Any actual implementation
+ * should derive from this class for forwards-compatibility.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Article\Discussion
+ * @since       3.2
+ */
+abstract class AbstractArticleDiscussionProvider implements IArticleDiscussionProvider {
+       /**
+        * @var Article
+        */
+       protected $article;
+       
+       /**
+        * @var ArticleContent 
+        */
+       protected $articleContent;
+       
+       /**
+        * AbstractArticleDiscussionProvider constructor.
+        *
+        * @param       Article         $article
+        */
+       public function __construct(Article $article) {
+               $this->article = $article;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setArticleContent(ArticleContent $articleContent) {
+               $this->articleContent = $articleContent;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/article/discussion/CommentArticleDiscussionProvider.class.php b/wcfsetup/install/files/lib/system/article/discussion/CommentArticleDiscussionProvider.class.php
new file mode 100644 (file)
index 0000000..9f521a4
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\article\discussion;
+use wcf\data\article\Article;
+use wcf\system\comment\CommentHandler;
+use wcf\system\WCF;
+
+/**
+ * The built-in discussion provider using the native comment system.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Article\Discussion
+ * @since       3.2
+ */
+class CommentArticleDiscussionProvider extends AbstractArticleDiscussionProvider {
+       /**
+        * @inheritDoc
+        */
+       public function getDiscussionCount(): int {
+               return $this->article->comments;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getDiscussionCountPhrase(): string {
+               return WCF::getLanguage()->getDynamicVariable('wcf.article.articleComments', ['article' => $this->article]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function renderDiscussions(): string {
+               $commentCanAdd = WCF::getSession()->getPermission('user.article.canAddComment');
+               $commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.articleComment');
+               $commentManager = CommentHandler::getInstance()->getObjectType($commentObjectTypeID)->getProcessor();
+               $commentList = CommentHandler::getInstance()->getCommentList($commentManager, $commentObjectTypeID, $this->articleContent->articleContentID);
+               
+               WCF::getTPL()->assign([
+                       'commentCanAdd' => $commentCanAdd,
+                       'commentList' => $commentList,
+                       'commentObjectTypeID' => $commentObjectTypeID,
+                       'lastCommentTime' => $commentList->getMinCommentTime(),
+                       'likeData' => (MODULE_LIKE) ? $commentList->getLikeData() : [],
+               ]);
+               
+               return WCF::getTPL()->fetch('articleComments');
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function isResponsible(Article $article): bool {
+               return !!$article->enableComments;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/article/discussion/IArticleDiscussionProvider.class.php b/wcfsetup/install/files/lib/system/article/discussion/IArticleDiscussionProvider.class.php
new file mode 100644 (file)
index 0000000..e9e5877
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\article\discussion;
+use wcf\data\article\Article;
+use wcf\data\article\content\ArticleContent;
+
+/**
+ * Discussion provider for articles.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Article\Discussion
+ * @since       3.2
+ */
+interface IArticleDiscussionProvider {
+       /**
+        * Returns the number of discussion items.
+        * 
+        * @return      int
+        */
+       public function getDiscussionCount(): int;
+       
+       /**
+        * Returns the simple phrase "X <discussions>" that is used for both the statistics
+        * and the meta data in the article's headline.
+        * 
+        * @return      string
+        */
+       public function getDiscussionCountPhrase(): string;
+       
+       /**
+        * Renders the input and display section of the associated discussion.
+        * 
+        * @return      string
+        */
+       public function renderDiscussions(): string;
+       
+       /**
+        * Sets the content object required for the separate discussions per article language.
+        * 
+        * @param       ArticleContent          $articleContent
+        */
+       public function setArticleContent(ArticleContent $articleContent);
+       
+       /**
+        * Returning true will assign this provider to the article, otherwise the next
+        * possible provider is being evaluated.
+        *
+        * @param       Article         $article
+        * @return      bool
+        */
+       public static function isResponsible(Article $article): bool;
+}
diff --git a/wcfsetup/install/files/lib/system/article/discussion/VoidArticleDiscussionProvider.class.php b/wcfsetup/install/files/lib/system/article/discussion/VoidArticleDiscussionProvider.class.php
new file mode 100644 (file)
index 0000000..8e5e960
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+namespace wcf\system\article\discussion;
+use wcf\data\article\Article;
+
+/**
+ * Represents a non-existing discussion provider and is used when there is no other
+ * type of discussion being available. This provider is always being evaluated last.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Article\Discussion
+ * @since       3.2
+ */
+class VoidArticleDiscussionProvider extends AbstractArticleDiscussionProvider {
+       /**
+        * @inheritDoc
+        */
+       public function getDiscussionCount(): int {
+               return 0;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getDiscussionCountPhrase(): string {
+               return '';
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function renderDiscussions(): string {
+               return '';
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function isResponsible(Article $article): bool {
+               return true;
+       }
+}