'array', ]; protected static function booted(): void { static::deleting(function (GuidePage $page) { // CASCADE on from_page_id is handled by FK, but incoming edges need cleanup GuidePageEdge::where('to_page_id', $page->id)->delete(); }); } public function getUriAttribute(): string { return route('guides.pages.show', $this->id); } public function getNormalizedContentAttribute(): string { return static::normalizeRichTextContent($this->content); } public static function normalizeRichTextContent(?string $content): string { if (blank($content)) { return ''; } $content = preg_replace_callback( '~(?:https?:)?//[^"\'\s<>()]+(?/storage/guide-pages/[^"\'\s<>()]*)~i', static fn (array $matches): string => $matches['path'], $content, ) ?? $content; return preg_replace( '~(?<=["\'])storage/guide-pages/~i', '/storage/guide-pages/', $content, ) ?? $content; } public static function uploadedAttachmentUrl(string $path): string { return '/storage/'.ltrim($path, '/'); } public function guide() { return $this->belongsTo(Guide::class); } public function outgoingEdges() { return $this->hasMany(GuidePageEdge::class, 'from_page_id')->orderBy('sort'); } public function incomingEdges() { return $this->hasMany(GuidePageEdge::class, 'to_page_id'); } public function nextPages() { return $this->belongsToMany(self::class, 'guide_page_edges', 'from_page_id', 'to_page_id') ->withPivot('label', 'sort') ->orderByPivot('sort'); } public function previousPages() { return $this->belongsToMany(self::class, 'guide_page_edges', 'to_page_id', 'from_page_id') ->withPivot('label', 'sort'); } public function isEntry(): bool { return ! $this->incomingEdges()->exists(); } }