<?php
namespace App\Entity\Feed;
use App\Entity\Content\Common\DiscriminatorInterface;
use App\Entity\Content\Common\DiscriminatorTrait;
use App\Entity\Content\Common\Props\SlugInterface;
use App\Entity\Feed\Entry;
use App\Entity\Shared\UlidIdentifiableInterface;
use App\Entity\Shared\UlidIdentifiableTrait;
use App\Entity\System\School;
use App\Model\Content\Media\AbstractMedia;
use App\Model\Content\Media\MediaCollection;
use App\Util\Bitwise;
use App\Util\Html;
use Cms\ContainerBundle\Entity\Container;
use Cms\CoreBundle\Model\Interfaces\Blameable\BlameableInterface;
use Cms\CoreBundle\Model\Interfaces\Blameable\BlameableTrait;
use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableInterface;
use Cms\CoreBundle\Model\Interfaces\Timestampable\TimestampableTrait;
use Cms\CoreBundle\Service\Slugger;
use Cms\TenantBundle\Model\TenantableInterface;
use Cms\TenantBundle\Model\TenantableTrait;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Html2Text\Html2Text;
use Reinder83\BinaryFlags\Bits;
use Spatie\Url\Url;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
/**
* @ORM\Entity(
* repositoryClass = "App\Doctrine\Repository\Feed\EntryRepository",
* )
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(
* name = DiscriminatorInterface::COLUMN__NAME,
* type = DiscriminatorInterface::COLUMN__TYPE,
* )
* @ORM\DiscriminatorMap(AbstractEntry::DISCRS)
* @ORM\Table(
* name = "sys__feed__entry",
* )
*/
abstract class AbstractEntry
implements
UlidIdentifiableInterface,
TenantableInterface,
TimestampableInterface,
BlameableInterface,
DiscriminatorInterface,
SlugInterface
{
const DISCRS = [
Entry\AbstractContentEntry::DISCR => Entry\AbstractContentEntry::class,
Entry\ContentEventEntry::DISCR => Entry\ContentEventEntry::class,
Entry\ContentGalleryEntry::DISCR => Entry\ContentGalleryEntry::class,
Entry\ContentPostEntry::DISCR => Entry\ContentPostEntry::class,
Entry\ContentVideoEntry::DISCR => Entry\ContentVideoEntry::class,
];
const DISCR = null;
const TYPE_CLASSES = [
self::TYPES__EVENT => Entry\ContentEventEntry::class,
self::TYPES__GALLERY => Entry\ContentGalleryEntry::class,
self::TYPES__POST => Entry\ContentPostEntry::class,
self::TYPES__VIDEO => Entry\ContentVideoEntry::class,
];
const TYPES__EVENT = 'event';
const TYPES__GALLERY = 'gallery';
const TYPES__POST = 'post';
const TYPES__VIDEO = 'video';
public const PERMISSION_MAP = [
self::TYPES__POST => 'campussuite.cms.modules.news.manage',
self::TYPES__EVENT => 'campussuite.cms.modules.calendar.manage',
self::TYPES__GALLERY => 'campussuite.cms.modules.gallery.manage',
self::TYPES__VIDEO => 'campussuite.cms.modules.gallery.manage',
];
const ROUTING_SLUG = 'feed';
const VISIBILITIES = [
'unpublished' => self::VISIBILITIES__UNPUBLISHED,
'hidden' => self::VISIBILITIES__HIDDEN,
'published' => self::VISIBILITIES__PUBLISHED,
];
const VISIBILITIES__UNPUBLISHED = Bits::BIT_1;
const VISIBILITIES__HIDDEN = Bits::BIT_2;
const VISIBILITIES__PUBLISHED = Bits::BIT_3;
const DEFAULT_PREVIEW_LENGTH = 50;
use UlidIdentifiableTrait;
use TenantableTrait;
use TimestampableTrait;
use BlameableTrait;
use DiscriminatorTrait;
/**
* @var School|null
*
* @ORM\ManyToOne(
* targetEntity = School::class,
* )
* @ORM\JoinColumn(
* name = "school",
* referencedColumnName = "id",
* onDelete = "CASCADE",
* )
*
* @Groups({"school", "school_minimal"})
*
*/
protected ?School $school = null;
/**
* @var int
*
* @ORM\Column(
* type = "bigint",
* nullable = false,
* options = {
* "default" = 0,
* },
* )
*
* @Groups("entry")
* https://github.com/symfony/symfony/issues/54418
* @Context(denormalizationContext = {AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT = true})
*/
protected int $schoolType = 0;
/**
* @var Container|null
*
* @ORM\ManyToOne(
* targetEntity = Container::class,
* )
* @ORM\JoinColumn(
* name = "department",
* referencedColumnName = "id",
* onDelete = "CASCADE",
* )
*
* @Groups({"department", "department_minimal"})
*
*/
protected ?Container $department = null;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* type = "datetime",
* nullable = false,
* )
*
* @Groups("entry_timestamp")
*
*/
protected ?DateTimeInterface $timestamp = null;
/**
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = false,
* )
*
* @Groups("entry")
*
*/
protected ?string $label = null;
/**
* @var string|null
*
* @ORM\Column(
* type = "text",
* nullable = true,
* )
*
* @Groups("entry")
*/
protected ?string $preview = null;
/**
* @var array|string[]
*/
protected array $previewChunks = [];
/**
* @var MediaCollection|AbstractMedia[]|null
*
* @ORM\Column(
* type = "media_array",
* nullable = false,
* )
*
* @Groups("media")
*
*/
protected ?MediaCollection $media = null;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = AbstractEntry::VISIBILITIES__PUBLISHED,
* },
* )
*
* @Groups("entry_visibility")
*
*/
protected int $visibility = self::VISIBILITIES__PUBLISHED;
/**
* @var bool
*
* @ORM\Column(
* type = "boolean",
* nullable = false,
* options = {
* "default" = true,
* },
* )
*/
protected bool $boosted = true;
/**
* @var bool
*
* @ORM\Column(
* type = "boolean",
* nullable = false,
* options = {
* "default" = false,
* },
* )
*
* @Groups("entry")
*
*/
protected bool $pinned = false;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* type = "datetime_immutable",
* nullable = true,
* )
*
* @Groups("entry")
*
*/
protected ?DateTimeInterface $pinnedAt = null;
/**
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $link = null;
/**
* @return string|null
*/
public function getLink(): ?string
{
return $this->link;
}
/**
* @param string|null $link
* @return $this
*/
public function setLink(?string $link): self
{
$this->link = trim($link) ?: null;
return $this;
}
/**
* @return bool
*/
public function isLinkInternal(): bool
{
return ($this->getLink() === '/' || preg_match('/^\\/[^\\/]/', $this->getLink()));
}
/**
* @return bool
*/
public function isLinkExternal(): bool
{
return ( ! $this->isLinkInternal());
}
/**
* @return Url|null
*/
public function getLinkAsUrl(): ?Url
{
return $this->getLink() ? Url::fromString($this->getLink()) : null;
}
/**
* @return School|null
*/
public function getSchool(): ?School
{
return $this->school;
}
/**
* @param School|null $school
* @return $this
*/
public function setSchool(?School $school): self
{
$this->school = $school;
return $this;
}
/**
* @return int
*/
public function getSchoolType(): int
{
return $this->schoolType;
}
/**
* @return string[]
*/
public function getSchoolTypeName(): array
{
return array_filter(
array_map(static function ($type) {
return array_search($type, School::TYPES);
}, Bitwise::explode($this->schoolType))
);
}
/**
* @param int $schoolType
* @return $this
*/
public function setSchoolType(int $schoolType): self
{
$this->schoolType = max(0, $schoolType);
return $this;
}
/**
* @return Container|null
*/
public function getDepartment(): ?Container
{
return $this->department;
}
/**
* @param Container|null $department
* @return $this
*/
public function setDepartment(?Container $department): self
{
$this->department = $department;
return $this;
}
/**
* @return DateTimeInterface|null
*/
public function getTimestamp(): ?DateTimeInterface
{
return $this->timestamp;
}
/**
* @param DateTimeInterface $timestamp
* @return $this
*/
public function setTimestamp(DateTimeInterface $timestamp): self
{
$this->timestamp = $timestamp;
return $this;
}
/**
* @return string|null
*/
public function getLabel(): ?string
{
return $this->label;
}
/**
* @param string $label
* @return $this
*/
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
/**
* @param int|null $max
* @return string|null
*/
public function getPreview(?int $max = null): ?string
{
return ($max !== null) ? implode(' ', $this->getPreviewChunks($max)) : $this->preview;
}
/**
* @param string|null $preview
* @return $this
*/
public function setPreview(?string $preview): self
{
$this->preview = (new Html2Text($preview, ['do_links' => 'none']))->getText() ?: null;
$this->previewChunks = [];
return $this;
}
/**
* @param int|null $max
* @return array|string[]
*/
public function getPreviewChunks(?int $max = null): array
{
$preview = $this->getPreview();
if ( ! $preview) {
return [];
}
if ( ! $this->previewChunks) {
$this->previewChunks = explode(' ', $preview);
}
return ($max !== null) ? array_slice($this->previewChunks, 0, $max ?: self::DEFAULT_PREVIEW_LENGTH) : $this->previewChunks;
}
/**
* @return AbstractMedia|null
*/
public function peekMedia(): ?AbstractMedia
{
if ($this->getMedia() && $this->getMedia()[0] instanceof AbstractMedia) {
return $this->getMedia()[0];
}
return null;
}
/**
* @return MediaCollection|AbstractMedia[]
*/
public function getMedia(): MediaCollection
{
if ( ! $this->media) {
$this->media = new MediaCollection();
}
return $this->media;
}
/**
* @param MediaCollection|AbstractMedia[]|null $media
* @return $this
*/
public function setMedia(?MediaCollection $media): self
{
$this->media = $media ?: new MediaCollection();
return $this;
}
/**
* @param $entity
* @return $this
*/
abstract public function populate($entity): self;
/**
* @return string|null
*/
abstract public function getType(): string;
/**
* @return int
*/
public function getVisibility(): int
{
return $this->visibility;
}
/**
* @return string
*/
public function getVisibilityName(): string
{
return array_search($this->getVisibility(), self::VISIBILITIES);
}
/**
* @param int $visibility
* @return $this
*/
public function setVisibility(int $visibility): self
{
if ( ! in_array($visibility, self::VISIBILITIES)) {
throw new \LogicException();
}
$this->visibility = $visibility;
return $this;
}
/**
* @return bool
*/
public function isUnpublished(): bool
{
return $this->getVisibility() === self::VISIBILITIES__UNPUBLISHED;
}
/**
* @return bool
*/
public function isHidden(): bool
{
return $this->getVisibility() === self::VISIBILITIES__HIDDEN;
}
/**
* @return bool
*/
public function isPublished(): bool
{
return $this->getVisibility() === self::VISIBILITIES__PUBLISHED;
}
/**
* @return bool
*/
public function isBoosted(): bool
{
return $this->boosted;
}
/**
* @param bool $boosted
* @return $this
*/
public function setBoosted(bool $boosted): self
{
$this->boosted = $boosted;
return $this;
}
/**
* @return bool
*/
public function isPinned(): bool
{
return $this->pinned;
}
/**
* @return DateTimeInterface|null
*/
public function getPinnedAt(): ?DateTimeInterface
{
return $this->pinnedAt;
}
/**
* @param DateTimeInterface|null $pinnedAt
* @return $this
*/
public function setPinnedAt(?DateTimeInterface $pinnedAt): self
{
$this->pinnedAt = $pinnedAt;
$this->pinned = (bool) $pinnedAt;
return $this;
}
/**
* @return string
*/
public function __toString(): string
{
return $this->getId();
}
/**
* {@inheritDoc}
*/
public function getSlug(): ?string
{
return Slugger::slug($this->getLabel(), true) ?: null;
}
/**
* {@inheritDoc}
*/
public function setSlug(?string $slug): SlugInterface
{
return $this;
}
/**
* @return AbstractMedia|null
*/
public function getFeature(): ?AbstractMedia
{
return $this->getMedia()->getFeature();
}
}