<?php
namespace App\Entity;
use App\Config;
use App\Entity\Menu\Item;
use App\Entity\Utils\HistoryInterface;
use App\Entity\Utils\IntlInterface;
use App\Entity\Utils\ReleaseInterface;
use App\Entity\Utils\Traits\SeoTrait;
use App\Entity\Utils\Traits\TemplateTrait;
use App\Entity\Utils\Traits\TimestampTrait;
use App\Entity\Utils\Traits\VisibilityTrait;
use App\Entity\Utils\VersionInterface;
use App\Model\ContentProperties;
use App\Model\Slice;
use App\Repository\PageRepository;
use App\Utils\Helper;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class Page.
*/
#[ORM\Table(name: 'page')]
#[ORM\Entity(repositoryClass: PageRepository::class)]
class Page implements VersionInterface, ReleaseInterface, HistoryInterface, IntlInterface, Stringable
{
use TimestampTrait;
use TemplateTrait;
use VisibilityTrait;
use SeoTrait;
final public const STATUS_DRAFT = 'draft';
final public const STATUS_PENDING = 'pending';
final public const STATUS_PUBLISHED = 'published';
final public const STATUS_ARCHIVED = 'archived';
final public const STATUS_TRASH = 'trash';
final public const CACHING_NO_CACHE = 'no-cache';
final public const CACHING_BASIC = 'basic';
final public const CACHING_SIMPLE = 'simple';
final public const CACHING_AGGRESSIVE = 'aggressive';
#[ORM\Id]
#[ORM\Column(type: Types::INTEGER)]
#[ORM\GeneratedValue]
protected ?int $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank(message: 'form.error.required')]
#[Assert\Length(min: 2, max: 255, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected string $type;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank(message: 'form.error.required')]
#[Assert\Length(min: 2, max: 255, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected string $cachingLevel = self::CACHING_BASIC;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Assert\Length(min: 2, max: 255, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected ?string $globalTitle;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Assert\Length(min: 2, max: 10000, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected ?string $globalDescription = null;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank(message: 'form.error.required')]
#[Assert\Length(min: 2, max: 255, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected string $title;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Assert\Length(min: 2, max: 10000, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected ?string $description = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
protected ?array $contentProperties = [];
#[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank(message: 'form.error.required')]
protected string $locale = 'en';
#[ORM\OneToMany(mappedBy: 'master', targetEntity: 'Page', cascade: ['detach'])]
protected Collection $translations;
#[ORM\ManyToOne(targetEntity: 'Page', cascade: ['detach'], inversedBy: 'translations')]
#[ORM\JoinColumn(name: 'master_id', onDelete: 'SET NULL')]
protected $master;
#[ORM\Column(type: Types::JSON, nullable: true)]
protected ?array $slices = [];
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'page', targetEntity: 'Path', cascade: ['all'])]
protected Collection $paths;
#[ORM\Column(type: Types::JSON, nullable: true)]
protected ?array $breadcrumb;
#[ORM\Column(type: Types::STRING)]
#[Assert\NotBlank(message: 'form.error.required')]
protected string $status = self::STATUS_DRAFT;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Assert\Length(min: 2, max: 2000, minMessage: 'form.error.short', maxMessage: 'form.error.long')]
protected ?string $updateComment = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?DateTime $published = null;
#[ORM\ManyToOne(targetEntity: 'BackUser', cascade: ['persist'])]
#[ORM\JoinColumn(name: 'lastPublisher_id')]
protected ?BackUser $lastPublisher = null;
protected bool $hasBeenPublished = false;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?DateTime $expired = null;
#[ORM\ManyToMany(targetEntity: 'Tag', inversedBy: 'pages', cascade: ['detach'])]
#[ORM\JoinTable(name: 'page_tags', joinColumns: [new ORM\JoinColumn(name: 'page_id', referencedColumnName: 'id')], inverseJoinColumns: [new ORM\JoinColumn(name: 'tag_id', referencedColumnName: 'id')])]
protected Collection $tags;
#[ORM\OneToMany(mappedBy: 'page', targetEntity: Item::class, cascade: ['detach'])]
protected Collection $menuItems;
/**
* Page constructor.
*/
public function __construct(string $title, ?string $type = null, string $locale = 'en', ?string $template = null, ?string $description = null)
{
$this->tags = new ArrayCollection();
$this->translations = new ArrayCollection();
$this->menuItems = new ArrayCollection();
$this->paths = new ArrayCollection();
$this->setTitle($title);
$this->setGlobalTitle($title);
if ($locale) {
$this->setLocale($locale);
}
if ($type) {
$this->setType($type);
if ($template) {
$this->setTemplate($template);
}
$this->setDescription($description);
$this->setGlobalDescription($description);
if ($this->canBeSeen()) {
if ($this->isMulti()) {
$path = new Path(Config::getContentDefaultPath($type).Helper::toSlug($this->getTitle()), $this->getLocale());
} else {
$path = new Path(Config::getContentDefaultPath($type), $this->getLocale());
}
$this->addPath($path);
}
}
}
public function __toString(): string
{
return (string) $this->globalTitle;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): void
{
$this->type = $type;
}
public function isMulti(): bool
{
return Config::isContentMulti($this->type);
}
public function getCachingLevel(): string
{
return $this->cachingLevel;
}
public function setCachingLevel(string $cachingLevel): void
{
$this->cachingLevel = $cachingLevel;
}
public function getGlobalTitle(): string
{
return $this->globalTitle;
}
public function setGlobalTitle(string $globalTitle): void
{
$this->globalTitle = $globalTitle;
if ($this->isMaster()) {
/** @var Page $translation */
foreach ($this->translations as $translation) {
$translation->setGlobalTitle($globalTitle);
}
}
}
public function getGlobalDescription(): ?string
{
return $this->globalDescription;
}
public function setGlobalDescription(?string $globalDescription): void
{
$this->globalDescription = $globalDescription;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): void
{
$this->description = $description;
}
public function getContentProperties(): ContentProperties
{
return new ContentProperties(array_merge($this->contentProperties ?? [], [
'locale' => $this->getLocale(),
]));
}
public function setContentProperties(ContentProperties $contentProperties): void
{
$this->contentProperties = $contentProperties->export();
}
public function addSlice(Slice $slice): Page
{
$this->slices[$slice->getPosition()] = $slice->export();
return $this;
}
public function removeSlice(Slice $slice): void
{
unset($this->slices[$slice->getPosition()]);
}
public function getSlices(): ?array
{
$slices = [];
if ($this->slices) {
foreach ($this->slices as $slice) {
$slices[] = new Slice(array_merge($slice, [
'locale' => $this->getLocale(),
]));
}
}
return $slices;
}
public function getSlicesFront(): array
{
$slices = [];
if ($this->slices) {
foreach ($this->slices as $slice) {
if ('public' == $slice['visibility']) {
$slices[] = new Slice(array_merge($slice, [
'context' => 'front',
]));
}
}
}
return $slices;
}
public function getSlicesType(): ?array
{
$slices = [];
if ($this->slices) {
foreach ($this->slices as $slice) {
$slices[] = $slice['type'];
}
}
return $slices;
}
public function setSlices(array $slices): void
{
foreach ($slices as $b) {
if (!($b instanceof Slice)) {
$b = new Slice($b);
}
$this->addSlice($b);
}
}
public function removeAllSlices(): void
{
if ($this->slices && is_countable($this->slices)) {
foreach ($this->slices as $b) {
if (!($b instanceof Slice)) {
$b = new Slice($b);
}
$this->removeSlice($b);
}
}
}
public function import(array $data, bool $partial = false): void
{
$fields = $this->getArrayModel($partial);
foreach ($fields as $field) {
$setter = 'set'.ucfirst((string) $field);
if (isset($data[$field])) {
$fields[$field] = $this->{$setter}($data[$field]);
}
}
if (!$partial) {
if (isset($data['slices'])) {
$this->setSlices($data['slices']);
}
if (isset($data['contentProperties'])) {
$this->setContentProperties(new ContentProperties($data['contentProperties']));
}
if (isset($data['path']) && $this->getCurrentPath() != $data['path']) {
$oldPath = $this->hasPath($data['path']);
if (!$oldPath) {
$path = new Path($data['path'], $this->getLocale());
$this->addPath($path);
} else {
$this->getCurrentPath(false)->setType(Path::TYPE_REDIRECT);
$oldPath->setType(Path::TYPE_CURRENT);
}
}
}
}
public function export(string $locale = null): array
{
$fields = [];
foreach ($this->getArrayModel() as $field) {
$getter = 'get'.ucfirst((string) $field);
$fields[$field] = $this->{$getter}();
}
if ($this->canBeSeen()) {
$fields['path'] = $this->getCurrentPath();
}
if ($locale) {
$fields['locale'] = $locale;
}
return array_merge(
$fields,
[
'slices' => $this->slices,
'contentProperties' => $this->contentProperties,
]
);
}
public function getArrayModel(bool $partial = false): array
{
if ($partial) {
$data = array_merge(
[
'type',
'globalTitle',
'title',
'locale',
],
$this->getTemplateArrayModel()
);
} else {
$data = array_merge(
[
'type',
'globalTitle',
'globalDescription',
'title',
'description',
'locale',
],
$this->getVisibilityArrayModel(),
$this->getSeoArrayModel(),
$this->getTemplateArrayModel()
);
}
return $data;
}
public function getLocale(): string
{
return $this->locale;
}
public function setLocale(string $locale): void
{
$this->locale = $locale;
}
public function getExistingLocales(): array
{
$locales = [];
$master = $this->getMaster();
$locales[] = $master->getLocale();
/** @var Page $translation */
foreach ($this->getTranslations() as $translation) {
$locales[] = $translation->getLocale();
}
return $locales;
}
public function getTranslations(): Collection
{
$master = $this->getMaster();
return $master->translations;
}
public function getTranslation(string $locale): mixed
{
$master = $this->getMaster();
if ($master->getLocale() == $locale) {
return $master;
}
if (is_countable($master->getTranslations()) ? count($master->getTranslations()) : 0) {
/** @var Page $translation */
foreach ($master->getTranslations() as $translation) {
if ($translation->getLocale() == $locale) {
return $translation;
}
}
}
return null;
}
public function addTranslation(object $translation): void
{
$this->translations[] = $translation;
$translation->setMaster($this);
}
public function removeTranslation(object $translation): void
{
$this->translations->removeElement($translation);
$translation->setMaster(null);
}
public function removeAllTranslation(): void
{
foreach ($this->translations as $translation) {
$this->removeTranslation($translation);
}
}
public function isMaster(): bool
{
return !$this->master;
}
public function getMaster()
{
if (!$this->master) {
return $this;
}
return $this->master;
}
public function setMaster(?object $master): void
{
$this->master = $master;
}
public function addPath(Path $path): Page
{
$this->paths[] = $path;
$path->setPage($this);
return $this;
}
public function removePath(Path $path): void
{
$this->paths->removeElement($path);
$path->setPage(null);
}
public function getPaths(bool $string = false): ArrayCollection|Collection|array
{
if ($string) {
$paths = [];
/** @var Path $path */
foreach ($this->paths as $path) {
$paths[] = $path->getPath();
}
return $paths;
}
return $this->paths;
}
public function getRedirections(): array
{
$paths = [];
if (isset($this->paths)) {
/** @var Path $path */
foreach ($this->paths as $path) {
if (Path::TYPE_REDIRECT == $path->getType()) {
$paths[] = $path;
}
}
}
return $paths;
}
public function getCurrentPath(bool $string = true): string|Path|null
{
if (isset($this->paths)) {
/** @var Path $path */
foreach ($this->paths as $path) {
if (Path::TYPE_CURRENT == $path->getType()) {
return $string ? $path->getPath() : $path;
}
}
}
return null;
}
public function hasPath(string $checkPath): ?Path
{
if (isset($this->paths)) {
/** @var Path $path */
foreach ($this->paths as $path) {
if ($checkPath == $path->getPath()) {
return $path;
}
}
}
return null;
}
public function getBreadcrumb(): ?array
{
return $this->breadcrumb;
}
public function setBreadcrumb($breadcrumb): void
{
$this->breadcrumb = $breadcrumb;
}
public function isVisible(): bool
{
return 'public' == $this->visibility && Page::STATUS_PUBLISHED == $this->status;
}
public static function getPublicationStatusesHuman(): array
{
$statusesHmn = [];
$statuses = [
self::STATUS_DRAFT,
self::STATUS_PENDING,
'soon_published',
self::STATUS_PUBLISHED,
'soon_expired',
'expired',
self::STATUS_ARCHIVED,
];
foreach ($statuses as $status) {
$statusesHmn['publication.status.'.$status] = $status;
}
return $statusesHmn;
}
public static function getPublicationStatusesString(): string
{
return implode(',', self::getPublicationStatusesHuman());
}
public function isPublishedSoon(): bool
{
if (!$this->getPublished()) {
return false;
}
$today = new DateTime();
$inTwoDays = new DateTime();
$inTwoDays->add(new \DateInterval('P2D'));
if ($this->getPublished() > $today && $this->getPublished() < $inTwoDays) {
return true;
}
return false;
}
public function isExpiredSoon(): bool
{
if (!$this->getExpired()) {
return false;
}
$today = new DateTime();
$inTwoDays = new DateTime();
$inTwoDays->add(new \DateInterval('P2D'));
if ($this->getExpired() > $today && $this->getExpired() < $inTwoDays) {
return true;
}
return false;
}
public function getStatus(): string
{
return $this->status;
}
public function setStatus($status): void
{
$this->status = $status;
if (Page::STATUS_PUBLISHED == $status) {
$this->hasBeenPublished = true;
}
}
public function getUpdateComment(): ?string
{
return $this->updateComment;
}
public function setUpdateComment(?string $updateComment): void
{
$this->updateComment = $updateComment;
}
public function getPublished(): ?DateTime
{
return $this->published;
}
public function setPublished(?DateTime $published = null): void
{
$this->published = $published;
}
public function getLastPublisher(): ?BackUser
{
return $this->lastPublisher;
}
public function setLastPublisher(?BackUser $lastPublisher = null): void
{
$this->lastPublisher = $lastPublisher;
}
/**
* @throws \Exception
*/
public function updatePublisher(BackUser $lastPublisher): bool
{
if ($this->hasBeenPublished) {
$this->setLastPublisher($lastPublisher);
$this->setPublished(new DateTime());
$this->hasBeenPublished = false;
return true;
}
return false;
}
public function getExpired(): ?DateTime
{
return $this->expired;
}
public function setExpired(?DateTime $expired = null): void
{
$this->expired = $expired;
}
public function addTag(Tag $tag): void
{
$this->tags[] = $tag;
$tag->addPage($this);
}
public function removeTag(Tag $tag): void
{
$this->tags->removeElement($tag);
$tag->removePage($this);
}
public function removeAllTags(string $type): void
{
if (is_countable($this->tags)) {
foreach ($this->tags as $tag) {
if ($tag->getType() === $type) {
$this->removeTag($tag);
}
}
}
}
public function getTags(?string $type = null): Collection
{
if ($type) {
$tags = new ArrayCollection();
/** @var Tag $tag */
foreach ($this->tags as $tag) {
if ($tag->getType() == $type) {
$tags->add($tag);
}
}
return $tags;
}
return $this->tags;
}
public function hasTag(int $tagId): bool
{
foreach ($this->tags as $tag) {
if ($tag->getId() == $tagId) {
return true;
}
}
return false;
}
public function addMenuItem(Item $item): void
{
$this->menuItems[] = $item;
$item->setPage($this);
}
public function removeMenuItem(Item $item): void
{
$this->menuItems->removeElement($item);
$item->setPage($this);
}
public function getMenuItems(): Collection
{
return $this->menuItems;
}
public function cleanJson(): bool
{
$contentProperties = $this->getContentProperties();
$contentProperties->clean(Config::getContentProperties($this->type));
$this->setContentProperties($contentProperties);
$slices = $this->getSlices();
$cleanedSlices = [];
/** @var Slice $slice */
foreach ($slices as $key => $slice) {
if (Config::getSliceFields($slice->getType())) {
$slice->clean();
$cleanedSlices[] = $slice;
}
}
$this->setSlices($cleanedSlices);
return true;
}
public function hasSlicesAllowed(): bool
{
return 'free-html' != $this->getTemplate() && count(Config::getSlices($this->getTemplate()));
}
public function canBeSeen(): bool
{
return !Config::isContentHidden(self::getType());
}
public function getEntityType(): ?string
{
return 'page';
}
}