<?php
namespace App\Entity\Content;
use App\Entity\Content\Common\DiscriminatorInterface;
use App\Entity\Content\Common\DiscriminatorTrait;
use App\Entity\Content\Common\Props\HeadlineTrait;
use App\Entity\Content\Common\Props\TimestampTrait;
use App\Entity\Feed\AbstractEntry;
use App\Entity\Feed\Entry\AbstractContentEntry;
use App\Entity\Shared\UlidIdentifiableInterface;
use App\Entity\Shared\UlidIdentifiableTrait;
use App\Entity\System\School;
use App\Entity\System\SocialAccount;
use App\Enum\ChannelEnum;
use App\Model\Async\StringSemaphoreInterface;
use App\Model\Async\StringSemaphoreTrait;
use App\Model\Content\ContentInterface;
use App\Model\Content\ObjectInterface;
use App\Util\Bitwise;
use App\Util\Errors;
use Cms\ContainerBundle\Entity\Container;
use Cms\CoreBundle\Model\Interfaces\Blameable;
use Cms\CoreBundle\Model\Interfaces\Loggable\LoggableInterface;
use Cms\CoreBundle\Model\Interfaces\Timestampable;
use Cms\Modules\AlertBundle\Model\Alert\AlertData;
use Cms\TenantBundle\Model as Tenantable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
/**
* @ORM\Entity(
* repositoryClass = "App\Doctrine\Repository\Content\ObjectRepository",
* )
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(
* name = DiscriminatorInterface::COLUMN__NAME,
* type = DiscriminatorInterface::COLUMN__TYPE,
* length = 64,
* )
* @ORM\DiscriminatorMap(AbstractObject::DISCRS)
* @ORM\Table(
* name = "sn__content__object",
* indexes = {
* @ORM\Index(
* name = "idx__discr",
* columns = {
* "discr",
* },
* ),
* },
* )
*/
abstract class AbstractObject
implements
Tenantable\TenantableInterface,
UlidIdentifiableInterface,
Timestampable\TimestampableInterface,
Blameable\BlameableInterface,
DiscriminatorInterface,
ContentInterface,
ObjectInterface,
LoggableInterface,
StringSemaphoreInterface
{
const DISCR = null;
const DISCRS = [
Alerts\Alert\AlertObject::DISCR => Alerts\Alert\AlertObject::class,
Events\Event\EventObject::DISCR => Events\Event\EventObject::class,
Exhibits\Gallery\GalleryObject::DISCR => Exhibits\Gallery\GalleryObject::class,
Exhibits\Video\VideoObject::DISCR => Exhibits\Video\VideoObject::class,
Posts\Post\PostObject::DISCR => Posts\Post\PostObject::class,
];
const CLASSES__ALTERATION = null;
const ROUTING_SLUG = null;
public const HEADLINE_LIMIT = 100;
use Tenantable\TenantableTrait;
use UlidIdentifiableTrait;
use Timestampable\TimestampableTrait;
use Blameable\BlameableTrait;
use DiscriminatorTrait;
use HeadlineTrait;
use TimestampTrait;
use StringSemaphoreTrait;
/**
* @var School|null
*
* @ORM\ManyToOne(
* targetEntity = School::class,
* )
* @ORM\JoinColumn(
* fieldName = "school",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "CASCADE",
* )
*
* @Groups({"school", "school_minimal"})
*
*/
protected ?School $school = null;
/**
* @var AbstractContentEntry|null
*
* @ORM\OneToOne(
* targetEntity = AbstractContentEntry::class,
* mappedBy = "object",
* cascade = {"remove"}
* )
*/
protected ?AbstractContentEntry $entry = null;
/**
* @var Container|null
*
* @ORM\ManyToOne(
* targetEntity = Container::class,
* )
* @ORM\JoinColumn(
* fieldName = "department",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "CASCADE",
* )
*
* @Groups({"department", "department_minimal"})
*
*/
protected ?Container $department = null;
/**
* @var int
*
* @ORM\Column(
* type = "bigint",
* nullable = false,
* options = {
* "default" = 0,
* },
* )
*
* @Groups("object")
*
* https://github.com/symfony/symfony/issues/54418
* @Context(denormalizationContext = {AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT = true})
*
*/
protected int $schoolTypes = 0;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = ObjectInterface::VISIBILITIES__PUBLISHED,
* },
* )
*
* @Groups("object_visibility")
*
*/
protected int $visibility = ObjectInterface::VISIBILITIES__PUBLISHED;
/**
* @var Collection|null
*/
protected Collection $alterations;
/**
* @var Collection|null
*/
protected Collection $drafts;
/**
* @var int|null
*
* @ORM\Column(
* type = "integer",
* nullable = true,
* )
*/
protected ?int $migrationId = null;
/**
* @var UuidInterface|null
*
* @ORM\Column(
* type = "uuid",
* nullable = true,
* )
*/
protected ?UuidInterface $migrationUid = null;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* type = "datetime",
* nullable = true,
* )
*
* @Groups("object_scheduled_at")
*
*/
protected ?DateTimeInterface $scheduledAt = null;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = ChannelEnum::NONE,
* "unsigned" = true,
* },
* )
*
* @Groups("object")
*
*/
protected int $channels = ChannelEnum::WEBSITE | ChannelEnum::APP;
/**
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = AlertData::BEHAVIORS__NONE,
* },
* )
*
* @Groups("object")
*
*/
protected int $websiteBehavior = AlertData::BEHAVIORS__NONE;
/**
* @var int|null
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* options = {
* "default" = AlertData::LEVELS__INFORMATIVE,
* },
* )
*
* @Groups("object")
*
*/
protected int $websiteLevel = AlertData::LEVELS__INFORMATIVE;
/**
* @var DateTimeInterface|null
*
* @ORM\Column(
* type = "datetime",
* nullable = true,
* )
*
* @Groups("object")
*
*/
protected ?DateTimeInterface $websiteEndDateTime = null;
/**
* @var array|null
*
* @ORM\Column(
* type = "json",
* nullable = true,
* )
*/
protected ?array $socialPosts = [];
/**
* @param ContentInterface|null $content
*/
public function __construct(?ContentInterface $content = null)
{
$this->alterations = new ArrayCollection();
$this->drafts = new ArrayCollection();
if ($content) {
$this->copy($content);
}
}
/**
* @return int|null
*/
public function getMigrationId(): ?int
{
return $this->migrationId;
}
/**
* @param int|null $migrationId
* @return $this
*/
public function setMigrationId(?int $migrationId): self
{
$this->migrationId = $migrationId;
return $this;
}
/**
* @return UuidInterface|null
*/
public function getMigrationUid(): ?UuidInterface
{
return $this->migrationUid;
}
/**
* @param string|UuidInterface|null $migrationUid
* @return $this
*/
public function setMigrationUid($migrationUid): self
{
if ($migrationUid) {
if (is_string($migrationUid)) {
$migrationUid = Uuid::fromString($migrationUid);
}
if ( ! $migrationUid instanceof UuidInterface) {
throw new \Exception();
}
}
$this->migrationUid = $migrationUid ?: null;
return $this;
}
/**
* {@inheritDoc}
*/
public static function getAlterationClass(): ?string
{
return static::CLASSES__ALTERATION;
}
/**
* {@inheritDoc}
* @return Collection|AbstractDraft[]
*/
public function getDrafts(): iterable
{
return $this->drafts;
}
/**
* {@inheritDoc}
* @return Collection|AbstractAlteration[]
*/
public function getAlterations(): iterable
{
return $this->alterations;
}
/**
* @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 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 int
*/
public function getSchoolTypes(): int
{
return $this->schoolTypes;
}
/**
* @return string[]
*/
public function getSchoolTypesName(): array
{
return array_filter(
array_map(static function ($type) {
return array_search($type, School::TYPES);
}, Bitwise::explode($this->schoolTypes))
);
}
/**
* @param int|null $schoolTypes
* @return $this
*/
public function setSchoolTypes(?int $schoolTypes): self
{
$this->schoolTypes = $schoolTypes ?: 0;
return $this;
}
/**
* @return int
*/
public function getVisibility(): int
{
return $this->visibility;
}
/**
* @return string
*/
public function getVisibilityName(): string
{
return array_search($this->getVisibility(), ObjectInterface::VISIBILITIES);
}
/**
* @param int $visibility
* @return $this
*/
public function setVisibility(int $visibility): self
{
if ( ! in_array($visibility, ObjectInterface::VISIBILITIES)) {
throw new \LogicException();
}
$this->visibility = $visibility;
return $this;
}
/**
* @return bool
*/
public function isUnpublished(): bool
{
return $this->getVisibility() === ObjectInterface::VISIBILITIES__UNPUBLISHED;
}
/**
* @return bool
*/
public function isHidden(): bool
{
return $this->getVisibility() === ObjectInterface::VISIBILITIES__HIDDEN;
}
/**
* @return bool
*/
public function isPublished(): bool
{
return $this->getVisibility() === ObjectInterface::VISIBILITIES__PUBLISHED;
}
/**
* @return AbstractEntry|null
*/
public function getEntry(): ?AbstractEntry
{
return $this->entry;
}
/**
* @param AbstractEntry|null $entry
* @return $this
*/
public function setEntry(?AbstractEntry $entry): self
{
$this->entry = $entry;
return $this;
}
/**
* @return DateTimeInterface|null
*/
public function getScheduledAt(): ?DateTimeInterface
{
return $this->scheduledAt;
}
/**
* @param DateTimeInterface|null $scheduledAt
* @return self
*/
public function setScheduledAt(?DateTimeInterface $scheduledAt): self
{
$this->scheduledAt = $scheduledAt;
return $this;
}
/**
* @return int
*/
public function getChannels(): int
{
return $this->channels;
}
/**
* @return string[]
*/
public function getChannelsName(): array
{
return array_filter(
array_map(static function ($channel) {
return array_search($channel, ChannelEnum::ALL_CHANNELS);
}, Bitwise::explode($this->channels))
);
}
/**
* @param int $channels
* @return self
*/
public function setChannels(int $channels): self
{
$this->channels = $channels;
return $this;
}
/**
* @param int $channel
* @return bool
*/
public function hasChannel(int $channel): bool
{
return (($this->getChannels() & $channel) === $channel);
}
/**
* @return int
*/
public function getWebsiteBehavior(): int
{
return $this->websiteBehavior;
}
/**
* @return string
*/
public function getWebsiteBehaviorName(): string
{
return array_search($this->getWebsiteBehavior(), AlertData::BEHAVIORS);
}
/**
* @param int $websiteBehavior
* @return self
*/
public function setWebsiteBehavior(int $websiteBehavior): self
{
$this->websiteBehavior = $websiteBehavior;
return $this;
}
/**
* @return int
*/
public function getWebsiteLevel(): int
{
return $this->websiteLevel;
}
/**
* @return string
*/
public function getWebsiteLevelName(): string
{
return array_search($this->getWebsiteLevel(), AlertData::LEVELS);
}
/**
* @param int $websiteLevel
* @return self
*/
public function setWebsiteLevel(int $websiteLevel): self
{
$this->websiteLevel = $websiteLevel;
return $this;
}
/**
* @return DateTimeInterface|null
*/
public function getWebsiteEndDateTime(): ?DateTimeInterface
{
return $this->websiteEndDateTime;
}
/**
* @param DateTimeInterface|null $websiteEndDateTime
* @return self
*/
public function setWebsiteEndDateTime(?DateTimeInterface $websiteEndDateTime): self
{
$this->websiteEndDateTime = $websiteEndDateTime;
return $this;
}
public function getReview(): array
{
return [
'channels' => $this->getChannels(),
'scheduledAt' => $this->getScheduledAt(),
'scheduled' => (bool)$this->getScheduledAt(),
'websiteEndDateTime' => $this->getWebsiteEndDateTime(),
'websiteLevel' => $this->getWebsiteLevel(),
'websiteBehavior' => $this->getWebsiteBehavior(),
'schoolTypes' => Bitwise::explode($this->getSchoolTypes()),
];
}
public function setReview(array $review): self
{
if (empty($review)) {
return $this;
}
if (isset($review['scheduled']) && $review['scheduled'] === false) {
$review['scheduledAt'] = null;
}
if (isset($review['scheduledAt']) && $review['scheduledAt'] instanceof DateTimeInterface) {
$this->setVisibility(ObjectInterface::VISIBILITIES__UNPUBLISHED);
}
return $this
->setScheduledAt($review['scheduledAt'] ?? null)
->setChannels($review['channels'] ?? ChannelEnum::NONE)
->setWebsiteEndDateTime($review['websiteEndDateTime'] ?? null)
->setWebsiteLevel($review['websiteLevel'] ?? AlertData::LEVELS__INFORMATIVE)
->setWebsiteBehavior($review['websiteBehavior'] ?? AlertData::BEHAVIORS__NONE)
->setSchoolTypes(array_sum($review['schoolTypes'] ?? []))
;
}
/**
* @return string
*/
abstract public function __toString(): string;
/**
* {@inheritDoc}
*/
public function getLoggableDetails(): array
{
return [
'id' => (string) $this->getId(),
'title' => $this->__toString(),
'type' => static::ROUTING_SLUG,
];
}
/**
* @param string $socialType
* @param string $accountId
* @param string|null $postId
* @param \Throwable|string $result
* @return $this
*/
public function addSocialPost(
string $socialType,
string $accountId,
?string $postId,
$result
): self
{
// make sure that the social type is legit
if ( ! array_key_exists($socialType, SocialAccount::DISCRS)) {
throw new \LogicException();
}
// for backwards compatibility, make sure we have an array to work with
if ($this->socialPosts === null) {
$this->socialPosts = [];
}
// make sure this social type exists
if ( ! array_key_exists($socialType, $this->socialPosts)) {
$this->socialPosts[$socialType] = [];
}
// set the specific account details
$this->socialPosts[$socialType][$accountId] = [
'id' => $postId,
];
switch (true) {
case $result && is_string($result):
$this->socialPosts[$socialType][$accountId]['url'] = $result;
break;
case $result instanceof \Throwable:
$this->socialPosts[$socialType][$accountId]['error'] = Errors::jsonSerialize(
$result,
false,
);
break;
}
return $this;
}
/**
* @return array
*/
public function getSocialPosts(): array
{
return $this->socialPosts ?? [];
}
}