diff --git a/Classes/Domain/Model/News.php b/Classes/Domain/Model/News.php index bb9b2df7b38e8e27d713819b17714712fa5f92b1..d042f4b004717147ad9e35f700652254dc8786b1 100644 --- a/Classes/Domain/Model/News.php +++ b/Classes/Domain/Model/News.php @@ -107,9 +107,9 @@ class News extends CategoryAndNews { */ public function __construct() { parent::__construct(); - $this->relatedNews = new ObjectStorage(); $this->tags = new ObjectStorage(); $this->newsAuthor = new ObjectStorage(); + $this->relatedNews = new ObjectStorage(); } /** diff --git a/Classes/Domain/Repository/NewsRepository.php b/Classes/Domain/Repository/NewsRepository.php index 7bf40bf35b5b468295d6c7b1c19d391b39681284..c854896fb78dd5fd95c879354ff043a993101740 100644 --- a/Classes/Domain/Repository/NewsRepository.php +++ b/Classes/Domain/Repository/NewsRepository.php @@ -26,8 +26,10 @@ namespace SGalinski\SgNews\Domain\Repository; * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ -use SGalinski\SgNews\Domain\Model\Author; use SGalinski\SgNews\Domain\Model\News; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\Generic\QueryResult; use TYPO3\CMS\Extbase\Persistence\QueryInterface; use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; @@ -505,4 +507,97 @@ class NewsRepository extends AbstractRepository { $query->matching($query->equals('uid', $uid)); return $query->execute()->getFirst(); } + + /** + * This method finds news related by Tag or Category to the given news record + * + * @param News $news The news to find related news to + * @param int $limit Limit the amount of related news + * @return QueryResultInterface + */ + public function findRelated(News $news, int $limit = 0) { + $connection = $this->getConnection(); + $qb = $connection->createQueryBuilder(); + // We need to build the constraint for the tags/categories + $constraints = []; + $tags = $news->getTags(); + if ($tags->count() > 0) { + $tagConstraints = []; + foreach ($tags as $tag) { + $qb->leftJoin('pages', 'sys_category_records_mm', 'tags', 'pages.uid=tags.uid_foreign'); + $tagConstraints[] = $qb->expr()->eq('tags.uid_local', $tag->getUid()); + } + $constraints[] = $qb->expr()->eq('tags.tablenames', $qb->createNamedParameter('pages')); + $constraints[] = $qb->expr()->eq('tags.fieldname', $qb->createNamedParameter('tx_sgnews_tags')); + $constraints[] = $qb->expr()->orX($tagConstraints); + } else { + $constraints[] = $qb->expr()->eq('pid', $news->getPid()); + } + + // here we fetch the lastUpdated of the $limit amount of news with newer lastUpdated dates + $result = $qb->select('lastUpdated') + ->from('pages') + ->where( + $qb->expr()->eq('doktype', $qb->createNamedParameter(News::DOK_TYPE_NEWS, Connection::PARAM_INT)), + $qb->expr()->gt('lastUpdated', $news->getLastUpdated()->getTimestamp()), + $qb->expr()->andX(...$constraints) + ) + ->setMaxResults($limit) + ->orderBy('lastUpdated', 'desc') + ->execute(); + $newest = $result->fetchOne(); + + // Here we fetch the lastUpdated of the $limit amount of news with older lastUpdated dates + $result = $qb->select('lastUpdated') + ->from('pages') + ->where( + $qb->expr()->eq('doktype', $qb->createNamedParameter(News::DOK_TYPE_NEWS, Connection::PARAM_INT)), + $qb->expr()->lt('lastUpdated', $news->getLastUpdated()->getTimestamp()), + $qb->expr()->andX(...$constraints) + ) + ->setMaxResults($limit) + ->orderBy('lastUpdated', 'asc') + ->execute(); + $oldest = $result->fetchOne(); + + $query = $this->createQuery(); + $query->getQuerySettings()->setRespectStoragePage(FALSE); + $query->setOrderings([ + 'crdate' => QueryInterface::ORDER_DESCENDING + ]); + $constraints = []; + if ($newest) { + $constraints[] = $query->lessThanOrEqual('lastUpdated', $newest); + } + if ($oldest) { + $constraints[] = $query->greaterThanOrEqual('lastUpdated', $oldest); + } + + // Now we fetch the $limit amount of news via extbase query and limit them to the newest and oldest date + // remember, we fetched the oldest and newest date from the $limit amount of news newer and older then + // the given news. If we limit the result of the following query to $limit, we get $limit amount of news + // "around" the given news, where newer news are preferred due to the ordering. + $tags = $news->getTags(); + if ($tags->count() > 0) { + foreach ($tags as $tag) { + $constraints[] = $query->contains('tags', $tag); + } + } else { + $constraints[] = $query->equals('pid', $news->getPid()); + } + + $query->matching( + $constraints + ); + if ($limit > 0) { + $query->setLimit($limit); + } + + return $query->execute(); + } + + protected function getConnection(): Connection { + return GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('pages'); + } } diff --git a/Classes/ViewHelpers/RelatedViewHelper.php b/Classes/ViewHelpers/RelatedViewHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..f49aeecbb0ba4c8109d328bceabf4588c9232f23 --- /dev/null +++ b/Classes/ViewHelpers/RelatedViewHelper.php @@ -0,0 +1,64 @@ +<?php + +namespace SGalinski\SgNews\ViewHelpers; + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; + +class RelatedViewHelper extends AbstractViewHelper { + /** + * Initialize the view helper arguments + */ + public function initializeArguments() { + $this->registerArgument( + 'news', + 'SGalinski\SgNews\Domain\Model\News', + 'The news record from which to find related news', + TRUE + ); + $this->registerArgument('limit', 'int', 'Limit the amount of related news to display', FALSE, 5); + $this->registerArgument('as', 'string', 'The name of the iteration variable', TRUE); + $this->registerArgument('iteration', 'string', 'The name of the variable to store iteration information (index, cycle, isFirst, isLast, isEven, isOdd)'); + } + + /** + * Render the ViewHelper + * It works like the for-ViewHelper by running through the child content and adding the related news records to it + * + * @return string + */ + public function render() { + $news = $this->arguments['news']; + $newsRepository = GeneralUtility::makeInstance(NewsRepository::class); + $related = $newsRepository->findRelated($news); + $templateVariableContainer = $this->renderingContext->getVariableProvider(); + $output = ''; + if (isset($this->arguments['iteration'])) { + $iterationData = [ + 'index' => 0, + 'cycle' => 1, + 'total' => count($related) + ]; + } + foreach ($related as $relatedNews) { + $templateVariableContainer->add($this->arguments['as'], $relatedNews); + if (isset($this->arguments['iteration'])) { + $iterationData['isFirst'] = $iterationData['cycle'] === 1; + $iterationData['isLast'] = $iterationData['cycle'] === $iterationData['total']; + $iterationData['isEven'] = $iterationData['cycle'] % 2 === 0; + $iterationData['isOdd'] = !$iterationData['isEven']; + $templateVariableContainer->add($this->arguments['iteration'], $iterationData); + $iterationData['index']++; + $iterationData['cycle']++; + } + + $output .= $this->renderChildren(); + $templateVariableContainer->remove($this->arguments['as']); + if (isset($this->arguments['iteration'])) { + $templateVariableContainer->remove($this->arguments['iteration']); + } + } + + return $output; + } +} diff --git a/Resources/Private/Templates/SingleView/SingleView.html b/Resources/Private/Templates/SingleView/SingleView.html index b9f098afd3e8a431d37aefe3ae39537627407321..ab6e7239814fa3c32bfc1b70b2a9ba16df61c955 100644 --- a/Resources/Private/Templates/SingleView/SingleView.html +++ b/Resources/Private/Templates/SingleView/SingleView.html @@ -179,21 +179,40 @@ </div> <f:if condition="{newsMetaData.news.relatedNews}"> - <div class="tx-sgnews-single-related"> - <h3> - <f:translate key="frontend.singleview.relatedArticles" /> - </h3> - - <ul> - <f:for each="{newsMetaData.news.relatedNews}" as="relatedNewsEntry"> - <li> - <a href="{f:uri.page(pageUid: '{relatedNewsEntry.uid}')}"> - {relatedNewsEntry.subtitleWithFallbackToTitle} - </a> - </li> - </f:for> - </ul> - </div> + <f:then> + <div class="tx-sgnews-single-related"> + <h3> + <f:translate key="frontend.singleview.relatedArticles" /> + </h3> + + <ul> + <f:for each="{newsMetaData.news.relatedNews}" as="relatedNewsEntry"> + <li> + <a href="{f:uri.page(pageUid: '{relatedNewsEntry.uid}')}"> + {relatedNewsEntry.subtitleWithFallbackToTitle} + </a> + </li> + </f:for> + </ul> + </div> + </f:then> + <f:else> + <div class="tx-sgnews-single-related"> + <h3> + <f:translate key="frontend.singleview.relatedArticles" /> + </h3> + + <ul> + <sg:related news="{newsMetaData.news}" limit="5" as="relatedNewsEntry"> + <li> + <a href="{f:uri.page(pageUid: '{relatedNewsEntry.uid}')}"> + {relatedNewsEntry.subtitleWithFallbackToTitle} + </a> + </li> + </sg:related> + </ul> + </div> + </f:else> </f:if> </div> </div>