*/ protected $fillable = [ 'id_key', 'title', 'slug', 'content', 'page_id', 'config_id', 'status', 'isLocked', 'isRoot', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'content' => 'array', 'status' => PageStatusEnum::class, 'isLocked' => 'boolean', 'isRoot' => 'boolean', ]; /** * The translatable columns. * * @var list */ public $translatable = [ 'slug', 'title', 'content', ]; /** * Relations should be included in the cache. * * @var list */ protected static $cacheRelations = [ 'config', 'parentPage', 'childPages', ]; /** * Perform any actions required after the model boots. * * @return void */ protected static function booted(): void { static::creating(function (self $model) { self::manageFields($model); // Set id_key from title. $idKey = self::generateUniqueField( 'dynamic_pages', 'id_key', Str::slug($model->title), $model->id ); $model->id_key = $idKey; }); static::updating(function (self $model) { self::manageFields($model); self::checkParentsStatus($model); }); static::updated(function (self $model) { self::setAllChildPageStatus($model); DynamicPageHelper::manageImgs($model); }); static::restoring(function (self $model) { if ($model->config_id && $model->config()->onlyTrashed()->exists()) { throw ValidationException::withMessages([ 'error' => trans('crud.messages.cannot_be_restored', [ 'model' => Str::ucfirst(trans_choice('models.classes.dynamic-page', 1)) ]), ]); } $model->status = PageStatusEnum::draft; $model->isRoot = false; }); static::forceDeleted(function (self $model) { DynamicPageHelper::manageImgs($model, true); }); } // * METHODS /** * Generate unique value from model field * * @param string $table * @param string $field * @param string $baseValue * @param mixed $ignoreId * @return string */ public static function generateUniqueField( string $table, string $field, string $baseValue, int|string|null $ignoreId = null ): string { $value = $baseValue; $counter = 1; // Boucle tant que le slug existe en base while (true) { $query = DB::table($table)->where($field, $value); if ($ignoreId) { $query->where('id', '!=', $ignoreId); } if (!$query->exists()) { // On a trouvé un slug unique return $value; } // Sinon on incrémente le suffixe $value = $baseValue . '_' . $counter; $counter++; } } /** * Unpublished all children pages if change status published from draft/unpublished. * * @param self $model * @return void */ private static function setAllChildPageStatus(self $model): void { if ( !$model->status->equals(PageStatusEnum::published) && !$model->getOriginal('status')->equals($model->status) ) { $model->childPages->map(function (DynamicPage $page) use ($model) { $page->status = $model->status; $page->saveOrFail(); }); } } /** * Publish current page if all parents are published. * * @param self $model * @return void * @throws \Illuminate\Validation\ValidationException Parent must be published. */ private static function checkParentsStatus(self $model): void { if ($model->status->equals(PageStatusEnum::published) && !self::getParentsStatus($model)) { throw ValidationException::withMessages([ 'error' => trans('validation.parent_page'), ]); } } /** * Check if all parent of all associated page are published. * * @param self $model * @return boolean */ private static function getParentsStatus(self $model): bool { $parent = $model->parentPage; while ($parent) { if (!$parent->status->equals(PageStatusEnum::published)) { return false; } $parent = $parent->parentPage; } return true; } /** * Manage model fields. * * @param self $model * @return void */ private static function manageFields(self $model): void { // Check unique fields. if ($model->isRoot) { self::assertRootIsUnique($model); } else { $slug = $model->slug ?? Str::slug($model->title); TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title', $model->page_id, 'page_id'); TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug', $model->page_id, 'page_id'); $model->setAttribute('slug', $slug); } // Check translations. self::validateFallbackTranslations($model); } /** * Assert there is only one root page. * * @param self $model * @return void */ public static function assertRootIsUnique(self $model): void { $oldDynamicPageRoot = self::getModelsCached() ->where('id', '!=', $model->getKey()) ->where('isRoot', true) ->first(); if ($oldDynamicPageRoot) { $oldDynamicPageRoot->status = PageStatusEnum::unpublished; $oldDynamicPageRoot->isRoot = false; $oldDynamicPageRoot->saveQuietly(); } } /** * 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(PageStatusEnum::published)) { return; } TranslationHelper::validateFallbackTranslations($model); } /** * Get the view of a specific dynamic page. * * @return string */ public function getView(): string { /** @var string $view The view used for the route. */ $view = PageConfig::$defaultView; // Check if the view in config is defined and can be used. if (!empty($this->config->view)) { $candidate = sprintf('front.pages.%s', $this->config->view); if (view()->exists($candidate)) { $view = $candidate; } } // Check if a view with the slug exists. if ($view === PageConfig::$defaultView && !empty($this->slug)) { $candidate = sprintf('front.pages.%s', $this->slug); if (view()->exists($candidate)) { $view = $candidate; } } return $view; } /** * 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|null { $targetModel = self::getModelsCached()->firstWhere('id', $id); if (!$targetModel) { return null; } $url = RouteHelper::routeNameChecker("front.dynamic-pages.{$targetModel->id_key}"); return parse_url($url, PHP_URL_PATH) ?? "/"; } /** * Get localized path from the locale. * * @param string $locale * @return string|null */ public function getLocalizedPath(string $locale): ?string { $slug = $this->getTranslation('slug', $locale); if (! $slug) { return null; } $pages = static::getModelsCached() ->where('status', PageStatusEnum::published) ->load('parentPage') ->keyBy('id'); $segments = DynamicPageHelper::getAllDynamicPageSegments($this, $pages, $locale); $segmentsPath = implode('/', $segments); return $segmentsPath ? "{$segmentsPath}/{$slug}" : $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. * * @return array */ public static function searchTransFields(): array { $currentLocale = app()->getLocale(); return [ "title_$currentLocale", "slug_$currentLocale", "content_$currentLocale", ]; } /** * Get enum status of the model. * * @return integer */ public static function searchStatusEnum(): int { return PageStatusEnum::published->value; } /** * Get front route. * * @param string|null $idKey * @return string */ public static function searchRouteName(string|null $idKey): string { $currentLocale = app()->getLocale(); return "front.dynamic-pages.$idKey.$currentLocale"; } /** * Get model identificable field. * * @return string */ public static function searchMainField(): string { return 'title'; } /** * Get fields that we want to keep in the current locale. * * @param string $currentLocale * @return array */ public static function searchableTransFields(string $currentLocale): array { return [ "title_$currentLocale", "slug_$currentLocale", "content_$currentLocale", ]; } /** * Get the indexable data array for the model. * * @return array */ public function toSearchableArray(): array { $locales = LaravelLocalization::getSupportedLanguagesKeys(); $data = [ 'id' => $this->id, 'id_key' => $this->id_key, '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; $content = $this->hasTranslation('content', $locale) ? strip_tags(collect($this->getTranslation('content', $locale)) ->flatten() ->filter(fn($value, $key) => is_string($value) && !empty($value) && !is_numeric($value) && !in_array($key, ['order', 'id']) && !str_starts_with($key, 'image_') && !preg_match('/storage.*\.(?:png|jpe?g|gif|pdf|webp)/i', $value)) ->values() ->implode(' ')) : null; $data["content_$locale"] = $content; } //end foreach return $data; } // * SITEMAP /** * toSitemapTag method * * @return Url|string|array */ public function toSitemapTag(): Url | string | array { $sitemapLines = []; $locales = array_keys(config('laravellocalization.supportedLocales', ['fr' => "Français"])); foreach ($locales as $locale) { $slug = $this->getTranslation('slug', $locale, false); // Homepage has no slug. if ($slug || $this->isRoot) { $sitemapLines[] = SitemapHelper::generateUrl( $this->getLocalizedUrl($locale), locales: $this->locales(), getAlternate: fn (string $locale) => $this->getLocalizedUrl($locale) ); } } return $sitemapLines; } /** * Get localized URL. * * @param string $locale * @return string|false */ public function getLocalizedUrl(string $locale): string|false { return in_array($locale, $this->locales()) ? route("front.dynamic-pages.$this->id_key.$locale") : false; } // * RELATIONSHIPS /** * Get children pages for this page. * * @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\DynamicPage,$this> */ public function childPages(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(DynamicPage::class, 'page_id'); } /** * Get parent page for this page. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\DynamicPage,$this> */ public function parentPage(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(DynamicPage::class, 'page_id'); } /** * Get the config for this page. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\PageConfig,$this> */ public function config(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(PageConfig::class, 'config_id'); } }