'boolean', ]; protected $appends = [ 'full_src', ]; /** * URL کامل تصویر — همان قرارداد قبلی API (src = آدرس قابل نمایش). */ public function getSrcAttribute(): ?string { return self::publicUrl($this->attributes['src'] ?? null); } public function getFullSrcAttribute(): ?string { return $this->src; } public static function publicUrl(?string $path): ?string { if ($path === null || trim($path) === '') { return null; } $trimmed = trim($path); if (preg_match('/^https?:\/\//i', $trimmed)) { return self::rewriteWrongPublicUrl($trimmed) ?? $trimmed; } $relative = self::normalizeRelativeStoragePath($trimmed); $base = self::resolvePublicBaseUrl(); if ($relative === '') { return null; } if ($base === '') { return $relative; } return $base.'/'.$relative; } /** * مسیر نسبی داخل public/storage — حتی اگر قبلاً URL کامل اشتباه در DB ذخیره شده باشد. */ public static function normalizeRelativeStoragePath(string $path): string { $path = trim($path); if (preg_match('#/storage/(.+)$#i', $path, $matches)) { return 'storage/'.ltrim($matches[1], '/'); } $relative = ltrim($path, '/'); if (! str_starts_with($relative, 'storage/')) { $relative = 'storage/'.$relative; } return $relative; } /** * اصلاح URLهای قدیمی: https://site.ir/storage/... → https://site.ir/server/storage/... */ public static function rewriteWrongPublicUrl(string $url): ?string { if (! preg_match('#^(https?://[^/]+)/storage/(.+)$#i', $url, $matches)) { return null; } $base = self::resolvePublicBaseUrl(); if ($base === '') { return null; } if (str_contains($base, '/server') && ! str_contains($matches[1].'/storage/', '/server/storage/')) { return $base.'/storage/'.$matches[2]; } return null; } /** * پایهٔ URL فایل‌های storage. * در درخواست HTTP، دامنهٔ همان درخواست اولویت دارد (جلوگیری از URL اشتباه وقتی settings دامنهٔ ماژول دیگر است). */ public static function resolvePublicBaseUrl(): string { $fromRequest = self::baseUrlFromRequest(); if ($fromRequest !== '') { return $fromRequest; } $fromSettings = self::baseUrlFromSettings(); if ($fromSettings !== '') { return self::ensureAbsoluteUrl($fromSettings); } $domain = config('app.domain'); if (is_string($domain) && $domain !== '' && ! str_contains($domain, 'localhost')) { return self::ensureAbsoluteUrl(self::ensureServerPublicBase(rtrim($domain, '/'))); } $appUrl = config('app.url'); if (is_string($appUrl) && $appUrl !== '' && ! str_contains($appUrl, 'localhost')) { return self::ensureAbsoluteUrl(self::ensureServerPublicBase(rtrim($appUrl, '/'))); } return ''; } private static function baseUrlFromRequest(): string { try { if (app()->bound('request')) { $request = request(); $host = $request?->getSchemeAndHttpHost(); if (is_string($host) && $host !== '' && ! str_contains($host, 'localhost')) { return self::ensureAbsoluteUrl(self::ensureServerPublicBase($host)); } } } catch (\Throwable) { } return ''; } private static function ensureAbsoluteUrl(string $base): string { $base = rtrim(trim($base), '/'); if ($base === '') { return ''; } if (! preg_match('/^https?:\/\//i', $base)) { $base = 'https://'.ltrim($base, '/'); } return $base; } private static function baseUrlFromSettings(): string { try { $rows = Setting::query() ->whereIn('key', ['bace_url', 'server_url', 'admin_url', 'app_url']) ->where('is_active', 1) ->pluck('value', 'key'); foreach (['server_url', 'bace_url'] as $key) { $value = trim((string) ($rows[$key] ?? '')); if ($value !== '') { return rtrim($value, '/'); } } $adminUrl = trim((string) ($rows['admin_url'] ?? '')); if ($adminUrl !== '' && preg_match('#/admin#i', $adminUrl)) { return rtrim(preg_replace('#/admin/?$#i', '/server', rtrim($adminUrl, '/')), '/'); } $siteUrl = trim((string) ($rows['app_url'] ?? '')); if ($siteUrl !== '') { $siteUrl = rtrim($siteUrl, '/'); if (! preg_match('#/server$#i', $siteUrl)) { $siteUrl .= '/server'; } return $siteUrl; } } catch (\Throwable) { } return ''; } private static function ensureServerPublicBase(string $base): string { if (preg_match('#/(server|api)(/|$)#i', $base)) { return rtrim($base, '/'); } return rtrim($base, '/').'/server'; } }