ernestwisniewski/kbin

View on GitHub
src/Entity/Post.php

Summary

Maintainability
A
35 mins
Test Coverage
C
71%
<?php

// SPDX-FileCopyrightText: 2023 /kbin contributors <https://kbin.pub/>
//
// SPDX-License-Identifier: AGPL-3.0-only

declare(strict_types=1);

namespace App\Entity;

use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Contracts\CommentInterface;
use App\Entity\Contracts\FavouriteInterface;
use App\Entity\Contracts\RankingInterface;
use App\Entity\Contracts\ReportInterface;
use App\Entity\Contracts\TagInterface;
use App\Entity\Contracts\VisibilityInterface;
use App\Entity\Contracts\VotableInterface;
use App\Entity\Traits\ActivityPubActivityTrait;
use App\Entity\Traits\CreatedAtTrait;
use App\Entity\Traits\EditedAtTrait;
use App\Entity\Traits\RankingTrait;
use App\Entity\Traits\VisibilityTrait;
use App\Entity\Traits\VotableTrait;
use App\Repository\PostRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Webmozart\Assert\Assert;

#[Entity(repositoryClass: PostRepository::class)]
#[Index(columns: ['visibility', 'is_adult'], name: 'post_visibility_adult_idx')]
#[Index(columns: ['visibility'], name: 'post_visibility_idx')]
#[Index(columns: ['is_adult'], name: 'post_adult_idx')]
#[Index(columns: ['ranking'], name: 'post_ranking_idx')]
#[Index(columns: ['score'], name: 'post_score_idx')]
#[Index(columns: ['comment_count'], name: 'post_comment_count_idx')]
#[Index(columns: ['created_at'], name: 'post_created_at_idx')]
#[Index(columns: ['last_active'], name: 'post_last_active_at_idx')]
#[Index(columns: ['body_ts'], name: 'post_body_ts_idx')]
#[Index(columns: ['tags'], name: 'post_tags_idx')]
#[Index(columns: ['ap_id'], name: 'post_ap_id_idx')]
class Post implements VotableInterface, CommentInterface, VisibilityInterface, RankingInterface, ReportInterface, FavouriteInterface, TagInterface, ActivityPubActivityInterface
{
    use VotableTrait;
    use RankingTrait;
    use VisibilityTrait;
    use ActivityPubActivityTrait;
    use EditedAtTrait;
    use CreatedAtTrait {
        CreatedAtTrait::__construct as createdAtTraitConstruct;
    }

    #[ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
    #[JoinColumn(nullable: false)]
    public User $user;
    #[ManyToOne(targetEntity: Magazine::class, inversedBy: 'posts')]
    #[JoinColumn(nullable: false, onDelete: 'CASCADE')]
    public ?Magazine $magazine;
    #[ManyToOne(targetEntity: Image::class, cascade: ['persist'])]
    #[JoinColumn]
    public ?Image $image = null;
    #[Column(type: 'string', length: 255, nullable: true)]
    public string $slug;
    #[Column(type: 'text', length: 15000, nullable: true)]
    public ?string $body = null;
    #[Column(type: 'string')]
    public string $lang = 'en';
    #[Column(type: 'integer')]
    public int $commentCount = 0;
    #[Column(type: 'integer', options: ['default' => 0])]
    public int $favouriteCount = 0;
    #[Column(type: 'integer')]
    public int $score = 0;
    #[Column(type: 'boolean')]
    public bool $isAdult = false;
    #[Column(type: 'boolean', options: ['default' => false])]
    public bool $sticky = false;
    #[Column(type: 'datetimetz')]
    public ?\DateTime $lastActive;
    #[Column(type: 'string', nullable: true)]
    public ?string $ip = null;
    #[Column(type: 'json', nullable: true, options: ['jsonb' => true])]
    public ?array $tags = null;
    #[Column(type: 'json', nullable: true, options: ['jsonb' => true])]
    public ?array $mentions = null;
    #[OneToMany(mappedBy: 'post', targetEntity: PostComment::class, orphanRemoval: true)]
    public Collection $comments;
    #[OneToMany(mappedBy: 'post', targetEntity: PostVote::class, cascade: [
        'persist',
        'remove',
    ], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
    public Collection $votes;
    #[OneToMany(mappedBy: 'post', targetEntity: PostReport::class, cascade: ['remove'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
    public Collection $reports;
    #[OneToMany(mappedBy: 'post', targetEntity: PostFavourite::class, cascade: ['remove'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
    public Collection $favourites;
    #[OneToMany(mappedBy: 'post', targetEntity: PostCreatedNotification::class, cascade: ['remove'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
    public Collection $notifications;
    public array $children = [];
    #[Id]
    #[GeneratedValue]
    #[Column(type: 'integer')]
    private int $id;
    #[Column(type: 'text', nullable: true, insertable: false, updatable: false, options: ['default' => 'english'])]
    private $bodyTs;

    public function __construct(
        ?string $body,
        Magazine $magazine,
        User $user,
        bool $isAdult,
        string $ip = null
    ) {
        $this->body = $body;
        $this->magazine = $magazine;
        $this->user = $user;
        $this->isAdult = $isAdult;
        $this->ip = $ip;
        $this->comments = new ArrayCollection();
        $this->votes = new ArrayCollection();
        $this->reports = new ArrayCollection();
        $this->favourites = new ArrayCollection();
        $this->notifications = new ArrayCollection();

        $this->createdAtTraitConstruct();
        $this->lastActive = new \DateTime();
    }

    public function updateLastActive(): void
    {
        $this->comments->get(-1);

        $criteria = Criteria::create()
            ->orderBy(['createdAt' => 'DESC'])
            ->setMaxResults(1);

        $lastComment = $this->comments->matching($criteria)->first();

        if ($lastComment) {
            $this->lastActive = \DateTime::createFromImmutable($lastComment->createdAt);
        } else {
            $this->lastActive = \DateTime::createFromImmutable($this->getCreatedAt());
        }
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getBestComments(User $user = null): Collection
    {
        $criteria = Criteria::create()
            ->orderBy(['upVotes' => 'DESC', 'createdAt' => 'ASC']);

        $comments = $this->comments->matching($criteria);
        $comments = $this->handlePrivateComments($comments, $user);
        $comments = new ArrayCollection($comments->slice(0, 2));

        if (!\count(array_filter($comments->toArray(), fn ($comment) => $comment->countUpVotes() > 0))) {
            return $this->getLastComments();
        }

        $iterator = $comments->getIterator();
        $iterator->uasort(function ($a, $b) {
            return ($a->createdAt < $b->createdAt) ? -1 : 1;
        });

        return new ArrayCollection(iterator_to_array($iterator));
    }

    private function handlePrivateComments(ArrayCollection $comments, ?User $user): ArrayCollection
    {
        return $comments->filter(function (PostComment $val) use ($user) {
            if ($user && VisibilityInterface::VISIBILITY_PRIVATE === $val->getVisibility()) {
                return $user->isFollower($val->user);
            }

            return VisibilityInterface::VISIBILITY_VISIBLE === $val->getVisibility();
        });
    }

    public function getLastComments(User $user = null): Collection
    {
        $criteria = Criteria::create()
            ->orderBy(['createdAt' => 'ASC']);

        $comments = $this->comments->matching($criteria);

        $comments = $this->handlePrivateComments($comments, $user);

        return new ArrayCollection($comments->slice(-2, 2));
    }

    public function softDelete(): void
    {
        $this->visibility = VisibilityInterface::VISIBILITY_SOFT_DELETED;
    }

    public function trash(): void
    {
        $this->visibility = VisibilityInterface::VISIBILITY_TRASHED;
    }

    public function restore(): void
    {
        $this->visibility = VisibilityInterface::VISIBILITY_VISIBLE;
    }

    public function isAuthor(User $user): bool
    {
        return $user === $this->user;
    }

    public function getShortTitle(?int $length = 60): string
    {
        $body = wordwrap($this->body ?? '', $length);
        $body = explode("\n", $body);

        return trim($body[0]).(isset($body[1]) ? '...' : '');
    }

    public function getCommentCount(): int
    {
        return $this->commentCount;
    }

    public function getUniqueCommentCount(): int
    {
        $users = [];
        $count = 0;
        foreach ($this->comments as $comment) {
            if (!\in_array($comment->user, $users)) {
                $users[] = $comment->user;
                ++$count;
            }
        }

        return $count;
    }

    public function getScore(): int
    {
        return $this->score;
    }

    public function getMagazine(): ?Magazine
    {
        return $this->magazine;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function isFavored(User $user): bool
    {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('user', $user));

        return $this->favourites->matching($criteria)->count() > 0;
    }

    public function isAdult(): bool
    {
        return $this->isAdult || $this->magazine->isAdult;
    }

    public function getTags(): array
    {
        return array_values($this->tags ?? []);
    }

    public function countCommentsNewestThan(\DateTime $time, User $excludedUser): int
    {
        $criteria = Criteria::create()
            ->andWhere(Criteria::expr()->neq('user', $excludedUser))
            ->andWhere(Criteria::expr()->gt('createdAt', \DateTimeImmutable::createFromMutable($time)));

        return $this->comments->matching($criteria)->count();
    }

    public function __sleep()
    {
        return [];
    }
}