From 19aad3be37fa3adfdfe4118b70c62ebe482479b1 Mon Sep 17 00:00:00 2001 From: Benedict Massolle Date: Wed, 1 Jun 2022 09:24:35 +0200 Subject: [PATCH] Implement feed provider to allow decoration of feed generation and item transformation --- .../src/Controller/NewsFeedController.php | 45 +--- .../src/Event/TransformFeedItemEvent.php | 61 ----- .../TransformFeedItemListener.php | 150 ------------ news-bundle/src/Feed/FeedProvider.php | 226 ++++++++++++++++++ .../src/Feed/FeedProviderInterface.php | 35 +++ news-bundle/src/Resources/config/services.yml | 23 +- 6 files changed, 284 insertions(+), 256 deletions(-) delete mode 100644 news-bundle/src/Event/TransformFeedItemEvent.php delete mode 100644 news-bundle/src/EventListener/TransformFeedItemListener.php create mode 100644 news-bundle/src/Feed/FeedProvider.php create mode 100644 news-bundle/src/Feed/FeedProviderInterface.php diff --git a/news-bundle/src/Controller/NewsFeedController.php b/news-bundle/src/Controller/NewsFeedController.php index 06c2178b9fd..06ee0c0b1f6 100644 --- a/news-bundle/src/Controller/NewsFeedController.php +++ b/news-bundle/src/Controller/NewsFeedController.php @@ -12,15 +12,16 @@ namespace Contao\NewsBundle\Controller; +use Contao\CoreBundle\Asset\ContaoContext; use Contao\CoreBundle\Controller\AbstractController; use Contao\CoreBundle\Routing\Page\DynamicRouteInterface; use Contao\CoreBundle\Routing\Page\PageRoute; use Contao\CoreBundle\ServiceAnnotation\Page; -use Contao\NewsBundle\Event\TransformFeedItemEvent; +use Contao\NewsBundle\Feed\FeedProviderInterface; use Contao\NewsModel; use Contao\PageModel; use Contao\StringUtil; -use FeedIo\Feed; +use FeedIo\Feed\Item; use FeedIo\Specification; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -45,7 +46,7 @@ class NewsFeedController extends AbstractController implements DynamicRouteInter 'rss' => '.xml', ]; - public function __construct(private Specification $specification) + public function __construct(private readonly ContaoContext $contaoContext, private readonly FeedProviderInterface $feedProvider, private readonly Specification $specification) { } @@ -53,40 +54,18 @@ public function __invoke(Request $request, PageModel $pageModel): Response { $this->initializeContaoFramework(); - // TODO: Make $feed extendable for third-party code - $feed = new Feed(); - $feed->setTitle($pageModel->title); - $feed->setDescription($pageModel->description); - $feed->setLanguage($pageModel->language); + $staticUrl = $this->contaoContext->getStaticUrl(); + $baseUrl = $staticUrl ?: $request->getSchemeAndHttpHost(); - $lastModified = null; + $this->feedProvider->setContext($pageModel, $request, $baseUrl); - $archives = StringUtil::deserialize($pageModel->newsArchives); - $featured = match ($pageModel->feedFeatured) { - 'featured' => true, - 'unfeatured' => false, - default => null, - }; - $newsModel = $this->getContaoAdapter(NewsModel::class); - - // TODO: Make this extendable for third-party code - if ($pageModel->maxFeedItems > 0) { - $articles = $newsModel->findPublishedByPids($archives, $featured, $pageModel->maxFeedItems); - } else { - $articles = $newsModel->findPublishedByPids($archives, $featured); - } + $feed = $this->feedProvider->getFeed(); + $articles = $this->feedProvider->getItems(); + $lastModified = null; if (null !== $articles) { foreach ($articles as $article) { - $event = new TransformFeedItemEvent($article, $pageModel, $request); - $this->container->get('event_dispatcher')->dispatch($event, TransformFeedItemEvent::NAME); - - $item = $event->getItem(); - - if (null === $item) { - continue; - } - + $item = $this->feedProvider->getItemFromModel($article); $lastModified = null === $lastModified ? $item->getLastModified() : min($lastModified, $item->getLastModified()); $feed->add($item); $this->tagResponse($article); @@ -101,7 +80,7 @@ public function __invoke(Request $request, PageModel $pageModel): Response $this->setCacheHeaders($response, $pageModel); - foreach ($archives as $archive) { + foreach (StringUtil::deserialize($pageModel->newsArchives, true) as $archive) { $this->tagResponse('contao.db.tl_news_archive.'.$archive); } diff --git a/news-bundle/src/Event/TransformFeedItemEvent.php b/news-bundle/src/Event/TransformFeedItemEvent.php deleted file mode 100644 index edd5ae273fa..00000000000 --- a/news-bundle/src/Event/TransformFeedItemEvent.php +++ /dev/null @@ -1,61 +0,0 @@ -model = $model; - $this->pageModel = $pageModel; - $this->request = $request; - } - - public function getModel(): NewsModel - { - return $this->model; - } - - public function getPageModel(): PageModel - { - return $this->pageModel; - } - - public function getRequest(): Request - { - return $this->request; - } - - public function getItem(): ?ItemInterface - { - return $this->item ?? null; - } - - public function setItem(ItemInterface $item): void - { - $this->item = $item; - } -} diff --git a/news-bundle/src/EventListener/TransformFeedItemListener.php b/news-bundle/src/EventListener/TransformFeedItemListener.php deleted file mode 100644 index a3293ad6285..00000000000 --- a/news-bundle/src/EventListener/TransformFeedItemListener.php +++ /dev/null @@ -1,150 +0,0 @@ -getModel(); - $pageModel = $event->getPageModel(); - $request = $event->getRequest(); - - $staticUrl = $this->contaoContext->getStaticUrl(); - $baseUrl = $staticUrl ?: $request->getSchemeAndHttpHost(); - - $item = new Item(); - - $item->setTitle($model->headline) - ->setLastModified((new \DateTime())->setTimestamp($model->date)) - ->setLink(News::generateNewsUrl($model, false, true)) - ; - - $item->setContent($this->getItemDescription($model, $item, $pageModel->feedSource, $request)); - - /** @var UserModel $authorModel */ - if (($authorModel = $model->getRelated('author')) instanceof UserModel) { - $item->setAuthor((new Author())->setName($authorModel->name)); - } - - // Add the article image as enclosure - if ($model->addImage) { - $this->addEnclosures($item, $model->singleSRC, $baseUrl, $pageModel->imgSize); - } - - // Enclosures - if ($model->addEnclosure) { - $this->addEnclosures($item, StringUtil::deserialize($model->enclosure, true), $baseUrl, $pageModel->imgSize); - } - - $event->setItem($item); - } - - private function getItemDescription(NewsModel $model, Item $item, string $feedSource, Request $request): string - { - $environment = $this->framework->getAdapter(Environment::class); - $controller = $this->framework->getAdapter(Controller::class); - $contentModel = $this->framework->getAdapter(ContentModel::class); - - $description = $model->teaser ?? ''; - - // Prepare the description - if ('source_text' === $feedSource) { - $elements = $contentModel->findPublishedByPidAndTable($model->id, 'tl_news'); - - if (null !== $elements) { - $description = ''; - // TODO: Is this still necessary? - // Overwrite the request (see #7756) - $environment->set('request', $item->getLink()); - - foreach ($elements as $element) { - $description .= $controller->getContentElement($element); - $this->cacheTags->tagWithModelInstance($element); - } - - $environment->set('request', $request->getUri()); - } - } - - $description = $this->insertTags->replaceInline($description); - - return $controller->convertRelativeUrls($description, $item->getLink()); - } - - private function addEnclosures(Item $item, array|string $enclosures, string $baseUrl, string $imageSize = null): void - { - if (\is_string($enclosures)) { - $enclosures = [$enclosures]; - } - - $size = StringUtil::deserialize($imageSize, true); - - $filesAdapter = $this->framework->getAdapter(FilesModel::class); - $files = $filesAdapter->findMultipleByUuids($enclosures); - - if (null === $files) { - return; - } - - while ($files->next()) { - $file = new File($files->path); - - $fileUrl = $baseUrl.'/'.$file->path; - $fileSize = $file->filesize; - - if ($size && $file->isImage) { - $image = $this->imageFactory->create(Path::join($this->projectDir, $file->path), $size); - $fileUrl = $baseUrl.'/'.$image->getUrl($this->projectDir); - $file = new File(Path::makeRelative($image->getPath(), $this->projectDir)); - $fileSize = $file->exists() ? $file->filesize : null; - } - - $media = (new Media()) - ->setUrl($fileUrl) - ->setType($file->mime) - ; - - if ($fileSize) { - $media->setLength($fileSize); - } - - $item->addMedia($media); - } - } -} diff --git a/news-bundle/src/Feed/FeedProvider.php b/news-bundle/src/Feed/FeedProvider.php new file mode 100644 index 00000000000..aed8c97eff6 --- /dev/null +++ b/news-bundle/src/Feed/FeedProvider.php @@ -0,0 +1,226 @@ +pageModel = $pageModel; + $this->request = $request; + $this->baseUrl = $baseUrl; + } + + public function getFeed(): FeedInterface + { + $this->throwIfContextNotSet(); + + $feed = new Feed(); + + $feed->setTitle($this->pageModel->title) + ->setDescription($this->pageModel->description) + ->setLanguage($this->pageModel->language); + + return $feed; + } + + public function getItems(): ?Collection + { + $this->throwIfContextNotSet(); + + $archives = StringUtil::deserialize($this->pageModel->newsArchives); + $featured = match ($this->pageModel->feedFeatured) { + 'featured' => true, + 'unfeatured' => false, + default => null, + }; + $newsModel = $this->framework->getAdapter(NewsModel::class); + + if ($this->pageModel->maxFeedItems > 0) { + return $newsModel->findPublishedByPids($archives, $featured, $this->pageModel->maxFeedItems, 0, [ + 'return' => 'Collection' + ]); + } else { + return $newsModel->findPublishedByPids($archives, $featured, 0, 0, [ + 'return' => 'Collection' + ]); + } + } + + public function getItemFromModel(NewsModel $model): ItemInterface + { + $this->throwIfContextNotSet(); + + $item = new Feed\Item(); + $item->setTitle($this->getTitle($model, $item)) + ->setLastModified($this->getLastModified($model, $item)) + ->setLink($this->getLink($model, $item)) + ->setPublicId($this->getPublicId($model, $item)) + ; + + $author = $this->getAuthor($model, $item); + if ($author) { + $item->setAuthor($author); + } + + $enclosures = $this->getEnclosures($model, $item); + + foreach ($enclosures as $enclosure) { + $item->addMedia($enclosure); + } + + return $item; + } + + protected function getTitle(NewsModel $model, ItemInterface $item): string + { + return $model->headline; + } + + protected function getLink(NewsModel $model, ItemInterface $item): string + { + return News::generateNewsUrl($model, false, true); + } + + protected function getPublicId(NewsModel $model, ItemInterface $item): string + { + return $item->getLink(); + } + + protected function getContent(NewsModel $model, ItemInterface $item): string + { + $environment = $this->framework->getAdapter(Environment::class); + $controller = $this->framework->getAdapter(Controller::class); + $contentModel = $this->framework->getAdapter(ContentModel::class); + + $description = $model->teaser ?? ''; + + // Prepare the description + if ('source_text' === $this->pageModel->feedSource) { + $elements = $contentModel->findPublishedByPidAndTable($model->id, 'tl_news'); + + if (null !== $elements) { + $description = ''; + // TODO: Is this still necessary? + // Overwrite the request (see #7756) + $environment->set('request', $item->getLink()); + + foreach ($elements as $element) { + $description .= $controller->getContentElement($element); + $this->cacheTags->tagWithModelInstance($element); + } + + $environment->set('request', $this->request->getUri()); + } + } + + $description = $this->insertTags->replaceInline($description); + + return $controller->convertRelativeUrls($description, $item->getLink()); + } + + protected function getAuthor(NewsModel $model, ItemInterface $item): ?AuthorInterface + { + /** @var UserModel $authorModel */ + if (($authorModel = $model->getRelated('author')) instanceof UserModel) { + return (new Author())->setName($authorModel->name); + } + + return null; + } + + protected function getEnclosures(NewsModel $model, ItemInterface $item): array + { + $enclosures = []; + $uuids = []; + + if ($model->singleSRC) { + $uuids[] = $model->singleSRC; + } + + if ($model->addEnclosure) { + $uuids = [...$uuids, ...StringUtil::deserialize($model->enclosure, true)]; + } + + $size = StringUtil::deserialize($this->pageModel->imgSize, true); + + $filesAdapter = $this->framework->getAdapter(FilesModel::class); + $files = $filesAdapter->findMultipleByUuids($uuids); + + if (null === $files) { + return []; + } + + while ($files->next()) { + $file = new File($files->path); + + $fileUrl = $this->baseUrl . '/' . $file->path; + $fileSize = $file->filesize; + + if ($size && $file->isImage) { + $image = $this->imageFactory->create(Path::join($this->projectDir, $file->path), $size); + $fileUrl = $this->baseUrl . '/' . $image->getUrl($this->projectDir); + $file = new File(Path::makeRelative($image->getPath(), $this->projectDir)); + $fileSize = $file->exists() ? $file->filesize : null; + } + + $media = (new Media()) + ->setUrl($fileUrl) + ->setType($file->mime); + + if ($fileSize) { + $media->setLength($fileSize); + } + + $enclosures[] = $media; + } + + return $enclosures; + } + + protected function getLastModified(NewsModel $model, ItemInterface $item): DateTime + { + return (new DateTime())->setTimestamp($model->date); + } + + private function throwIfContextNotSet(): void + { + if (null === $this->pageModel || null === $this->request || null === $this->baseUrl) { + throw new RuntimeException('You must set the request context first by calling `setContext`'); + } + } +} diff --git a/news-bundle/src/Feed/FeedProviderInterface.php b/news-bundle/src/Feed/FeedProviderInterface.php new file mode 100644 index 00000000000..446b83d92a9 --- /dev/null +++ b/news-bundle/src/Feed/FeedProviderInterface.php @@ -0,0 +1,35 @@ +