Covivo/mobicoop

View on GitHub
api/src/Article/Service/ArticleManager.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

/**
 * Copyright (c) 2020, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 */

namespace App\Article\Service;

use App\Article\Entity\Article as ArticleEntity;
use App\Article\Entity\Iframe;
use App\Article\Entity\Paragraph;
use App\Article\Entity\RssElement;
use App\Article\Entity\Section;
use App\Article\Repository\ArticleRepository;
use App\Article\Repository\ParagraphRepository;
use App\Article\Repository\SectionRepository;
use App\Article\Ressource\Article;
use Doctrine\ORM\EntityManagerInterface;
use DOMDocument;
use Psr\Log\LoggerInterface;

/**
 * Article manager service.
 *
 * @author Sylvain Briat <sylvain.briat@mobicoop.org>
 * @author Maxime Bardot <maxime.bardot@mobicoop.org>
 */
class ArticleManager
{
    public const DIRECTION_UP = 'up';
    public const DIRECTION_DOWN = 'down';

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var SectionRepository
     */
    private $sectionRepository;

    /**
     * @var ParagraphRepository
     */
    private $paragraphRepository;

    /**
     * @var ArticleRepository
     */
    private $articleRepository;

    private $articleFeed;
    private $articleFeedNumber;
    private $articleIframeMaxWidth;
    private $articleIframeMaxHeight;

    /**
     * Constructor.
     */
    public function __construct(
        EntityManagerInterface $entityManager,
        LoggerInterface $logger,
        SectionRepository $sectionRepository,
        ParagraphRepository $paragraphRepository,
        ArticleRepository $articleRepository,
        string $articleFeed,
        int $articleFeedNumber,
        int $articleIframeMaxWidth,
        int $articleIframeMaxHeight
    ) {
        $this->entityManager = $entityManager;
        $this->logger = $logger;
        $this->sectionRepository = $sectionRepository;
        $this->paragraphRepository = $paragraphRepository;
        $this->articleRepository = $articleRepository;
        $this->articleFeed = $articleFeed;
        $this->articleFeedNumber = $articleFeedNumber;
        $this->articleIframeMaxWidth = $articleIframeMaxWidth;
        $this->articleIframeMaxHeight = $articleIframeMaxHeight;
    }

    /**
     * Change the position of a section, and the position of the associated section.
     *
     * @param Section $section   The section
     * @param string  $direction The direction (up/down)
     *
     * @return Section The section
     */
    public function changeSectionPosition(Section $section, string $direction): Section
    {
        switch ($direction) {
            case self::DIRECTION_UP:
                if ($previousSection = $this->sectionRepository->findPrevious($section)) {
                    $section->setPosition($section->getPosition() - 1);
                    $previousSection->setPosition($previousSection->getPosition() + 1);
                    $this->entityManager->persist($section);
                    $this->entityManager->persist($previousSection);
                    $this->entityManager->flush();
                }

                break;

            case self::DIRECTION_DOWN:
            default:
                if ($nextSection = $this->sectionRepository->findNext($section)) {
                    $section->setPosition($section->getPosition() + 1);
                    $nextSection->setPosition($nextSection->getPosition() - 1);
                    $this->entityManager->persist($section);
                    $this->entityManager->persist($nextSection);
                    $this->entityManager->flush();
                }

                break;
        }

        return $section;
    }

    /**
     * Change the position of a paragraph, and the position of the associated paragraph.
     *
     * @param Paragraph $paragraph The paragraph
     * @param string    $direction The direction (up/down)
     *
     * @return Paragraph The paragraph
     */
    public function changeParagraphPosition(Paragraph $paragraph, string $direction): Paragraph
    {
        switch ($direction) {
            case self::DIRECTION_UP:
                if ($previousParagraph = $this->paragraphRepository->findPrevious($paragraph)) {
                    $paragraph->setPosition($paragraph->getPosition() - 1);
                    $previousParagraph->setPosition($previousParagraph->getPosition() + 1);
                    $this->entityManager->persist($paragraph);
                    $this->entityManager->persist($previousParagraph);
                    $this->entityManager->flush();
                }

                break;

            case self::DIRECTION_DOWN:
            default:
                if ($nextParagraph = $this->paragraphRepository->findNext($paragraph)) {
                    $paragraph->setPosition($paragraph->getPosition() + 1);
                    $nextParagraph->setPosition($nextParagraph->getPosition() - 1);
                    $this->entityManager->persist($paragraph);
                    $this->entityManager->persist($nextParagraph);
                    $this->entityManager->flush();
                }

                break;
        }

        return $paragraph;
    }

    /**
     * Get the external articles.
     */
    public function getLastExternalArticles(int $nbArticles = ArticleEntity::NB_EXTERNAL_ARTICLES_DEFAULT)
    {
        return $this->articleRepository->findLastExternal($nbArticles);
    }

    /**
     * Get a collection of Article.
     *
     * @param string $context (optionnal) : Context to select specific articles
     *
     * @return Article[]
     */
    public function getArticles(string $context = null): array
    {
        $articles = [];

        // Get the articles in database
        if (is_null($context) || Article::CONTEXT_INTERNAL == $context) {
            $pages = $this->articleRepository->findAll();

            foreach ($pages as $page) {
                $articles[] = $this->makeArticleFromEntity($page);
            }
        }

        // Get the RSS articles
        if (is_null($context) || Article::CONTEXT_HOME == $context) {
            $rssItems = $this->getRssFeeds();

            foreach ($rssItems as $rssItem) {
                $articles[] = $this->makeArticleFromRss($rssItem);
            }
        }

        return $articles;
    }

    /**
     * Make an Article ressource from Article (Page) entity.
     */
    private function makeArticleFromEntity(ArticleEntity $articleEntity): Article
    {
        $article = new Article($articleEntity->getId());

        $article->setTitle($articleEntity->getTitle());

        return $article;
    }

    /**
     * Make an article ressource from an RSS element.
     *
     * @param RssElement $rssElement Rss element to convert
     */
    private function makeArticleFromRss(RssElement $rssElement): Article
    {
        $article = new Article();

        $article->setTitle($rssElement->getTitle());
        $article->setDescription($rssElement->getDescription());
        $article->setImage($rssElement->getImage());
        $article->setPubDate($rssElement->getPubDate());
        $article->setLink($rssElement->getLink());

        if (!is_null($rssElement->getIframe())) {
            $rssIframe = $rssElement->getIframe();
            $article->setIFrame('<iframe src="'.$rssIframe->getSrc().'" allowfullscreen title="'.$rssIframe->getTitle().'" width="'.$rssIframe->getWidth().'" height="'.$rssIframe->getHeight().'" allow="'.$rssIframe->getAllow().'" frameborder="0"></iframe>');
        }

        return $article;
    }

    /**
     * Get Rss elements from all feeds (in .env ARTICLE_FEEDS).
     *
     * @return RssElement[]
     */
    private function getRssFeeds(): array
    {
        if ('' == $this->articleFeed) {
            return [];
        }
        $rssElements = [];

        $articleFeed = $this->articleFeed;
        $articleFeedNumber = $this->articleFeedNumber;

        // transform xml to object
        $feedResult = simplexml_load_file($articleFeed, 'SimpleXMLElement', LIBXML_NOCDATA);
        // var_dump($feedResult);

        // exit;

        $counter = -1;

        foreach ($feedResult->channel->item as $item) {
            $rssElement = new RssElement();

            $rssElement->setTitle((string) $item->title);
            $rssElement->setPubDate(date('d M Y', strtotime($item->pubDate)));

            $rssElement->setLink((string) $item->link);

            $description = (string) $item->description;

            $start = strpos($description, '<p>');
            $end = strpos($description, '</p>', $start);

            if (strlen($description) >= 255) {
                $description = substr($description, $start, $end - $start + 234).' ...';
            } else {
                $description = substr($description, $start, $end - $start + 255).' ...';
            }

            $description = strip_tags($description);

            $rssElement->setDescription(html_entity_decode($description));

            $dom = new DOMDocument();
            libxml_use_internal_errors(true);

            $content = $item->children('content', true);

            $html_string = $content->encoded;

            if (empty($html_string)) {
                $html_string = $item->description;
            }

            $dom->loadHTML($html_string);
            libxml_clear_errors();

            if ($dom->getElementsByTagName('img')->length > 0) {
                $image = utf8_decode($dom->getElementsByTagName('img')->item(0)->getAttribute('src'));
                $rssElement->setImage($image);
            }

            // If there is an iframe (usually a video) we parse it. We only take the first one.
            if ($dom->getElementsByTagName('iframe')->length > 0) {
                $iframe = new Iframe();
                $iframe->setSrc($dom->getElementsByTagName('iframe')->item(0)->getAttribute('src'));
                $iframe->setTitle($dom->getElementsByTagName('iframe')->item(0)->getAttribute('title'));

                if ((int) $dom->getElementsByTagName('iframe')->item(0)->getAttribute('width') > $this->articleIframeMaxWidth) {
                    $iframe->setWidth($this->articleIframeMaxWidth);
                } else {
                    $iframe->setWidth($dom->getElementsByTagName('iframe')->item(0)->getAttribute('width'));
                }

                if ((int) $dom->getElementsByTagName('iframe')->item(0)->getAttribute('height') > $this->articleIframeMaxHeight) {
                    $iframe->setHeight($this->articleIframeMaxHeight);
                } else {
                    $iframe->setHeight($dom->getElementsByTagName('iframe')->item(0)->getAttribute('height'));
                }

                $iframe->setAllow($dom->getElementsByTagName('iframe')->item(0)->getAttribute('allow'));

                $rssElement->setIframe($iframe);
            }

            ++$counter;
            if ($counter == $articleFeedNumber) {
                break;
            }

            $rssElements[] = $rssElement;
        }

        return $rssElements;
    }
}