ldc-groupe/app/Models/News.php

549 lines
16 KiB
PHP

<?php
namespace App\Models;
use App\Contracts\TranslatableModel;
use App\Enums\News\NewsStatusEnum;
use App\Helpers\TranslationHelper;
use App\Models\NewsTag;
use App\Traits\Models\HasCache;
use App\Traits\Models\HasTranslations;
use App\Traits\Models\HasUwilImages;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Laravel\Scout\Searchable;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use Spatie\Sitemap\Tags\Url;
use Uwil\SitemapGenerator\Interfaces\Sitemapable;
use Uwil\SitemapGenerator\Lib\SitemapHelper;
/**
* @property string $seo_title
* SEO title of the news.
* @property string $seo_description
* SEO description of the news.
* @property string $title
* Title of the news.
* @property string $slug
* Slug of the news.
* @property string $picture
* Main img of the news.
* @property string $picture_alt
* Alt text for the picture.
* @property string $picture_title
* Title for the picture.
* @property string $content
* Content of the news.
* @property boolean $inverted
* Are image and content inverted in front
* @property string $vimeo_id
* Optionnal vimeo_id
* @property boolean $has_show_picture
* Show the picture in the show page
* @property \App\Enums\News\NewsStatusEnum $status
* Status of the news.
* @property-read \Illuminate\Support\Carbon $created_at
* Date on which the news was created.
* @property-read \Illuminate\Support\Carbon $updated_at
* Date on which the news was updated.
* @property-read \Illuminate\Support\Carbon $deleted_at
* Date on which the news was updated.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\NewsCategory> $categories
* Associated news categories of the news.
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\NewsTag> $tags
* Associated news tags of the news.
*/
class News extends Model implements TranslatableModel, LocalizedUrlRoutable, Sitemapable
{
use ActivityLog;
use HasCache;
use HasFactory;
use HasTranslations;
use SoftDeletes;
use HasCache;
use HasUwilImages {
HasUwilImages::inertiaFill as traitInertiaFill;
}
use Searchable;
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName(): string
{
return request()->routeIs('back.*') ? 'id' : 'slug';
}
/**
* Get the value of the model's localized route key.
*
* @param string $locale
* @return string
* @phpcs:disable
*/
public function getLocalizedRouteKey($locale): string
{
// phpcs:enable
return $this->getTranslation(
$this->getRouteKeyName(),
$locale
);
}
/**
* Champs contenant des images dynamiques à gérer.
*
* @var array
*/
protected array $imageFields = [
'picture'
];
/**
* Champs contenant des wysiwyg avec images dynamiques à gérer.
*
* @var array
*/
protected array $wysiwygFields = [
'content'
];
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'seo_title',
'seo_description',
'title',
'slug',
'content',
'status',
'picture',
'picture_alt',
'picture_title',
'published_at',
'unpublished_at',
'inverted',
'vimeo_id',
'has_show_picture',
];
/**
* The translatable columns.
*
* @var list<string>
*/
public $translatable = [
'seo_title',
'seo_description',
'title',
'slug',
'content',
'picture_alt',
'picture_title',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'status' => NewsStatusEnum::class,
'published_at' => 'datetime',
'unpublished_at' => 'datetime',
];
/**
* Relations should be included in the cache.
*
* @var list<string>
*/
protected static $cacheRelations = [
'categories',
'tags',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
self::manageFields($model);
});
static::updating(function (self $model) {
self::manageFields($model);
});
static::restoring(function (self $model) {
$model->status = NewsStatusEnum::unpublished;
});
}
// * METHODS
/**
* Manage model fields.
*
* @param self $model
* @return void
*/
private static function manageFields(self $model): void
{
// Check unique fields.
$slug = Str::slug($model->title);
TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title');
TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug', 'title');
$model->setAttribute('slug', $slug);
// Check translations.
self::validateFallbackTranslations($model);
// Set status.
self::makeStatusSense($model);
}
/**
* Override HasUwilImages trait method to fill the model without relations.
*
* @param array $attributes The attributes to fill the model with.
* @return void
*/
public function inertiaFill(array $attributes): void
{
// Ommit some relations.
$attributes = Arr::except($attributes, ['categories', 'tags']);
$this->traitInertiaFill($attributes);
}
/**
* Adjusts model status using current status and publish dates.
* Case 1: publish model when all null -> set published_at date.
* Case 2: unpublish model when all null -> nothing to do.
* Case 3: publish model when published_at is already set -> only change status.
* Case 4: unpublish model when published_at is already set -> can't update model.
* Case 5: publish model when unpublished_at is already set -> can't update model.
* Case 6: unpublish model when unpublished_at is already set -> only change status.
*
* @param self $model
* @return void
*/
private static function makeStatusSense(self $model): void
{
/** @var \Carbon\Carbon|null */
$published_at = $model->published_at;
/** @var \Carbon\Carbon|null */
$unpublished_at = $model->unpublished_at;
$wantedStatus = $model->status;
// Check if dates are already set.
if ($published_at or $unpublished_at) {
$model->status = $model->planificated_status;
}
// If there is a diff, no update.
if ($wantedStatus !== $model->status) {
session()->flash('error', \trans(
'La publication est modifiée en concordance avec les dates enregistrées.
Pour modifier l\'état de publication, veuillez modifier ou enlever les dates correspondantes.'
));
}
// Set default published_at date when publishing model.
if (!$published_at && $model->status->equals(NewsStatusEnum::published)) {
$model->published_at = Carbon::now();
}
}
/**
* Getter the current status following planification dates planificated_status.
*
* @return \App\Enums\News\NewsStatusEnum
*/
protected function getPlanificatedStatusAttribute(): \App\Enums\News\NewsStatusEnum
{
/** @var \Carbon\Carbon|null */
$published_at = $this->published_at;
/** @var \Carbon\Carbon|null */
$unpublished_at = $this->unpublished_at;
if ($published_at and $unpublished_at) {
return ($unpublished_at->greaterThanOrEqualTo(now()) and $published_at->lessThan(now()))
? NewsStatusEnum::published
: NewsStatusEnum::unpublished;
}
if ($published_at) {
return $published_at->lessThan(now())
? NewsStatusEnum::published
: NewsStatusEnum::unpublished;
}
if ($unpublished_at) {
return $unpublished_at->greaterThanOrEqualTo(now())
? NewsStatusEnum::published
: NewsStatusEnum::unpublished;
}
return NewsStatusEnum::unpublished;
}
/**
* Get formated published_at date.
*
* @return string
*/
public function publishedAtFormatted(): string
{
return Carbon::parse($this->published_at)->format('d/m/Y');
}
/**
* Get a comma-separated list of all category labels for this news item.
*
* @return string|null
*/
public function getTagLabelsAttribute(): ?string
{
if (empty($this->tags)) {
return null;
}
$labels = collect($this->tags)
->map(fn($cat) => $cat->name ?? null)
->filter()
->values();
return $labels->join(', ');
}
/**
* Get data related to the first category of this news item.
*
* @return \App\Models\NewsTag|null
*/
public function getFirstTagDataAttribute(): ?\App\Models\NewsTag
{
if (empty($this->tags) || empty($this->tags[0])) {
return null;
}
return $this->tags[0];
}
/**
* Assert fallback locale translation is present.
*
* @param self $model
* @return void
* @throws \Illuminate\Validation\ValidationException Translations missing.
*/
public static function validateFallbackTranslations(self $model): void
{
if (!$model->status->equals(NewsStatusEnum::published)) {
return;
}
TranslationHelper::validateFallbackTranslations($model);
}
/**
* Get the url to access a specific resource.
*
* @param integer $id Id field of the target model.
* @return string|null
*/
public static function getAccessModel(int $id): ?string
{
$targetModel = self::getModelsCached()->firstWhere('id', $id);
if (!$targetModel) {
return null;
}
return sprintf('/%s', $targetModel->slug);
}
// * MEILISEARCH
/**
* Get model's setting to apply on scout search (meilisearch).
*
* @var array
*/
public static $searchSettings = [
'filterableAttributes' => ['status'],
'sortableAttributes' => ['status']
];
/**
* Get fields that we want to keep in the current locale.
*
* @param string $currentLocale
* @return array
*/
public static function searchTransFields(string $currentLocale): array
{
return [
"title_$currentLocale",
"slug_$currentLocale",
"content_$currentLocale",
];
}
/**
* Get enum status of the model.
*
* @return integer
*/
public static function searchStatusEnum(): int
{
return NewsStatusEnum::published->value;
}
/**
* Get front route.
*
* @return string
*/
public static function searchRouteName(): string
{
return 'front.news.show';
}
/**
* Get model identificable field.
*
* @return string
*/
public static function searchMainField(): string
{
return 'title';
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
$locales = config('laravellocalization.supportedLocales');
$data = [
'id' => $this->id,
'status' => $this->status->value,
];
foreach ($locales as $locale => $_) {
$data["title_$locale"] = $this->hasTranslation('title', $locale)
? $this->getTranslation('title', $locale)
: null;
$data["slug_$locale"] = $this->hasTranslation('slug', $locale)
? $this->getTranslation('slug', $locale)
: null;
$data["content_$locale"] = $this->hasTranslation('content', $locale)
? strip_tags($this->getTranslation('content', $locale))
: null;
}
return $data;
}
/**
* Get all published news by category.
*
* @param string $category
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\News>
*/
public static function getPublishedNewsByCategoryInCurrentLocale(string $category)
{
$locale = App::getLocale();
return News::getModelsCached()
->where('status', NewsStatusEnum::published)
->filter(function ($news) use ($category, $locale) {
return !empty($news->getTranslation('slug', $locale, false)) && $news->categories->contains(
fn ($cat) => $cat->slug === $category
);
});
}
// * SITEMAP
/**
* toSitemapTag method
*
* @return Url|string|array
*/
public function toSitemapTag(): Url | string | array
{
$model = $this;
$locales = $model->locales();
$sitemapUrls = [];
foreach ($locales as $locale) {
if (in_array($locale, config('sitemap-generator.supported_locales'))) {
$sitemapUrls[] = SitemapHelper::generateUrl(
$model->getLocalizedUrl($locale),
$model,
$locales,
currentLocale: $locale,
isNews: true
);
}
}
return $sitemapUrls;
}
/**
* Get localized URL from Mcamara\LaravelLocalization.
*
* @param string $locale
* @return string
*/
public function getLocalizedUrl(string $locale): string
{
return LaravelLocalization::getURLFromRouteNameTranslated(
$locale,
'routes.front.news',
['news' => $this]
) ?? route('front.news.show', $this);
}
// * RELATIONSHIPS
/**
* The relation with NewsCategory.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function categories(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphedByMany(
NewsCategory::class,
'newsable',
'newsables',
'news_id',
'newsable_id'
);
}
/**
* The relation with NewsTag.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphedByMany(
NewsTag::class,
'newsable',
'newsables',
'news_id',
'newsable_id'
)->withTimestamps();
}
}