| Current Path : /var/www/html/reddsis/app/Services/ |
| Current File : /var/www/html/reddsis/app/Services/PortalHandler.php |
<?php
namespace App\Services;
use App\Http\Helpers\SettingHelper;
use App\Models\Backend\FrontendPage;
use App\Models\Backend\FrontendSite;
use App\Models\Backend\Setting;
use App\Models\Backend\Visit;
use App\Models\Backend\menu\frontend\Menu;
use App\Models\Backend\menu\frontend\MenuCategory;
use App\Models\Backend\menu\frontend\RoleMapping;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ViewErrorBag;
class PortalHandler
{
public function __construct(
protected FrontendSiteCacheService $cache,
protected BladeSyncService $sync
) {}
/**
* Main handler untuk render frontend site.
*
* 1. Ambil meta site dari cache
* 2. Cari page (default atau ikut slug)
* 3. Ambil menu assignments untuk role public
* 4. Build menu tree (parent-child)
* 5. Render entry script → components → page layout → header/footer → site layout
*
* Return HTML string siap render.
*/
public function handle(string $siteSlug, ?string $pageSlug = null): string
{
$meta = $this->cache->get($siteSlug);
abort_unless($meta, 404);
$site = FrontendSite::where('slug', $siteSlug)->first();
if (!$site) {
abort(404);
}
if (!$site->status) {
$siteName = $meta['name'] ?? $siteSlug;
return view('frontend.empty-site', [
'siteName' => $siteName,
'siteSlug' => $siteSlug,
'state' => 'site-inactive',
])->render();
}
if ($pageSlug) {
$page = FrontendPage::where('site_fk', $site->id)->where('slug', $pageSlug)->where('status', true)->first();
} else {
$page = $site->defaultPage()->where('status', true)->first();
}
if (!$page) {
$siteName = $meta['name'] ?? $siteSlug;
$hasPages = FrontendPage::where('site_fk', $site->id)->exists();
$state = $hasPages ? 'pages-inactive' : 'no-pages';
return view('frontend.empty-site', compact('siteName', 'siteSlug', 'state'))->render();
}
// Senarai semua pages dalam site (guna untuk navbar fallback)
$sitePages = $meta['pages'] ?? [];
$pageList = collect($sitePages)->map(fn($p, $slug) => [
'slug' => $slug,
'name' => $p['name'] ?? $slug,
'url' => "/{$slug}",
])->values();
// Ambil menu assignments untuk public kat site ni (navbar category)
$navbarCategoryId = MenuCategory::where('category_name', 'Navbar Public')->value('category_id')
?? MenuCategory::where('category_name', 'main-navbar')->value('category_id');
$publicMappings = RoleMapping::with(['menu', 'category'])
->where('role_code', 'public')
->where('category_id', $navbarCategoryId)
->where(function ($q) use ($site) {
$q->where('site_id', $site->id)
->orWhere('site_id', 0);
})
->where('status', true)
->orderBy('sort')
->get()
->filter(fn ($m) => $m->menu && $m->menu->menu_status)
->values();
// Build nested menu tree berdasarkan parent_id → menu_id hierarchy
$buildTree = function($items, $parentId = 0) use (&$buildTree) {
$tree = [];
foreach ($items as $item) {
if ((int) $item->parent_id === $parentId) {
$itemArray = $item->menu->toArray();
$children = $buildTree($items, (int) $item->menu_id);
if ($children) $itemArray['children'] = $children;
$tree[] = $itemArray;
}
}
return $tree;
};
$translateMenuName = function ($name) {
return t($name);
};
$menuTree = $buildTree($publicMappings);
// Translate menu names
array_walk_recursive($menuTree, function (&$v, $k) use ($translateMenuName) {
if ($k === 'menu_name') $v = $translateMenuName($v);
});
// Fallback: guna semua page site sebagai menu rata (flat)
if ($publicMappings->isEmpty()) {
$menuTree = $pageList->map(fn ($p) => [
'menu_name' => $translateMenuName($p['name']),
'menu_link' => $p['url'],
'menu_icon' => null,
'children' => [],
])->toArray();
$allMenus = $pageList->map(fn ($p) => (object) [
'menu_name' => $translateMenuName($p['name']),
'menu_link' => $p['url'],
'menu_icon' => null,
]);
} else {
$allMenus = $publicMappings->map(fn ($m) => $m->menu);
}
// Translate allMenus
$allMenus = $allMenus->map(function ($m) use ($translateMenuName) {
if (isset($m->menu_name)) $m->menu_name = $translateMenuName($m->menu_name);
return $m;
});
// Footer menu — category "Footer Links"
$footerCategoryId = MenuCategory::where('category_name', 'Footer Links')->value('category_id');
$footerMappings = RoleMapping::with(['menu'])
->where('role_code', 'public')
->where('category_id', $footerCategoryId)
->where(function ($q) use ($site) {
$q->where('site_id', $site->id)->orWhere('site_id', 0);
})
->where('status', true)
->orderBy('sort')
->get()
->filter(fn ($m) => $m->menu && $m->menu->menu_status)
->values();
$footerMenus = $footerMappings->map(fn ($m) => $m->menu);
if ($footerMappings->isEmpty()) {
$footerMenus = collect();
}
// Translate footer menu names
$footerMenus = $footerMenus->map(function ($m) use ($translateMenuName) {
if (isset($m->menu_name)) $m->menu_name = $translateMenuName($m->menu_name);
return $m;
});
// Visitor stats
$totalVisits = Visit::totalVisits();
$todayVisits = Visit::todayVisits();
$uniqueIps = Visit::uniqueIps();
// Variable untuk passed ke blade components
$_favicon = SettingHelper::get('favicon', 'favicon.ico');
$_faviconUrl = (!str_starts_with($_favicon, 'dynaweb4/') && !str_starts_with($_favicon, 'favicon'))
? Storage::url($_favicon)
: asset($_favicon);
$_faviconVer = Setting::where('key', 'favicon')->value('updated_at')?->timestamp ?? 0;
$lang = session('locale', 'ms');
$vars = [
'lang' => $lang,
'siteSlug' => $siteSlug,
'siteName' => $meta['name'] ?? '',
'sitePages' => $pageList,
'menus' => $allMenus,
'menuTree' => $menuTree,
'footerMenus' => $footerMenus,
'totalVisits' => $totalVisits,
'todayVisits' => $todayVisits,
'uniqueIps' => $uniqueIps,
'_faviconUrl' => "{$_faviconUrl}?v={$_faviconVer}",
];
// Shared object — supaya @php variable dalam entry script boleh dikongsi
// Guna $_shared->nama = 'value' dalam entry script,
// then $_shared->nama boleh diguna di component/pageContent/layout
$shared = new \stdClass();
$vars['_shared'] = $shared;
// Entry Script — init preprocessing (run first, wajib load)
// Variables declared with @php $var = value in the entry script are
// automatically captured and forwarded to components/page content/layout.
$entryOutput = '';
$entryBlade = Storage::get("ENTRY/PAGE/{$siteSlug}/{$page->slug}.blade.php");
if ($entryBlade) {
// Validate: entry_script tidak boleh mengandungi {!! !!} syntax
if (preg_match('/\{!!\s*\$/', $entryBlade)) {
$errorMsg = "Entry Script for page '{$page->slug}' contains {!! \$... !!} syntax. "
. "Entry Script is for @php logic only. Move component output to Page Content.";
\Illuminate\Support\Facades\Log::error($errorMsg);
abort(500, $errorMsg);
}
$compiled = Blade::compileString($entryBlade);
$__entryTmp = tempnam(sys_get_temp_dir(), 'opencode_entry') . '.php';
file_put_contents($__entryTmp, $compiled);
$__entryResult = (function () use ($__entryTmp, $vars) {
$__origKeys = array_keys($vars);
extract($vars);
ob_start();
include $__entryTmp;
$output = ob_get_clean();
$all = get_defined_vars();
$new = [];
foreach ($all as $k => $v) {
if (!in_array($k, $__origKeys, true) && !in_array($k, [
'__origKeys', 'vars', 'output', 'all', 'new', 'k', 'v', '__entryTmp'
], true)) {
$new[$k] = $v;
}
}
return ['output' => $output, 'newVars' => $new];
})();
$entryOutput = $__entryResult['output'];
$vars = array_merge($vars, $__entryResult['newVars']);
unlink($__entryTmp);
}
$vars['__entry__'] = $entryOutput;
// Render setiap component page (hero, features, etc)
foreach ($page->components as $comp) {
$compCode = $comp->component ? $comp->component->code : $comp->code;
$blade = Storage::get("PHP/{$compCode}.blade.php");
if ($blade) {
try {
$vars[$comp->code] = Blade::render($blade, $vars);
} catch (\Throwable $e) {
$vars[$comp->code] = '<div style="padding:16px;margin:8px 0;border:2px solid #ef4444;border-radius:8px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
<strong style="color:#dc2626;">⚠ Component Error: ' . e($comp->name) . ' ($' . e($comp->code) . ')</strong>
<p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
</div>';
}
} else {
$vars[$comp->code] = '';
}
}
// Render Page Content
$pageHtml = '';
$pageContentBlade = Storage::get("PAGE_CONTENT/{$siteSlug}/{$page->slug}.blade.php");
if ($pageContentBlade) {
try {
$pageHtml = Blade::render($pageContentBlade, $vars);
} catch (\Throwable $e) {
$pageHtml = '<div style="padding:16px;margin:8px 0;border:2px solid #ef4444;border-radius:8px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
<strong style="color:#dc2626;">⚠ Page Content Error</strong>
<p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
</div>';
}
} elseif ($entryOutput) {
$pageHtml = $entryOutput;
}
// Render header
$header = '';
if ($site->header_fk && $site->header) {
$headerBlade = Storage::get("PHP/{$site->header->code}.blade.php");
if ($headerBlade) {
try {
$header = Blade::render($headerBlade, $vars);
} catch (\Throwable $e) {
$header = '<div style="padding:12px;border:2px solid #ef4444;border-radius:8px;margin:4px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
<strong style="color:#dc2626;">⚠ Header Error (' . e($site->header->code) . ')</strong>
<p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
</div>';
}
}
}
// Render footer
$footer = '';
if ($site->footer_fk && $site->footer) {
$footerBlade = Storage::get("PHP/{$site->footer->code}.blade.php");
if ($footerBlade) {
try {
$footer = Blade::render($footerBlade, $vars);
} catch (\Throwable $e) {
$footer = '<div style="padding:12px;border:2px solid #ef4444;border-radius:8px;margin:4px;background:#fef2f2;font-family:sans-serif;font-size:13px;">
<strong style="color:#dc2626;">⚠ Footer Error (' . e($site->footer->code) . ')</strong>
<p style="margin:4px 0 0;color:#991b1b;">' . e($e->getMessage()) . '</p>
</div>';
}
}
}
// Page layout → override site layout kalau use_custom_layout enabled
$siteLayout = Storage::get("LAYOUTS/SITE/{$site->slug}.blade.php");
$layoutVars = array_merge($vars, [
'__header__' => $header,
'__page__' => $pageHtml,
'__footer__' => $footer,
'errors' => session('errors') ?: new ViewErrorBag,
]);
try {
if ($page->use_custom_layout) {
// If a dedicated PAGE layout exists, use it so the page can
// keep/replace header/footer as needed while still rendering
// the shared page content as __page__.
$pageLayout = Storage::get("LAYOUTS/PAGE/{$siteSlug}/{$page->slug}.blade.php");
if ($pageLayout) {
$html = Blade::render($pageLayout, $layoutVars);
} elseif ($pageContentBlade) {
// Fully standalone page content: render directly.
$html = $pageHtml;
} elseif ($siteLayout) {
$html = Blade::render($siteLayout, $layoutVars);
} else {
$html = $pageHtml;
}
} elseif ($siteLayout) {
$html = Blade::render($siteLayout, $layoutVars);
} else {
$html = $pageHtml;
}
} catch (HttpResponseException $e) {
throw $e;
} catch (\Throwable $e) {
$html = '<div style="max-width:800px;margin:40px auto;padding:24px;border:2px solid #ef4444;border-radius:12px;background:#fef2f2;font-family:sans-serif;">
<h2 style="color:#dc2626;margin:0 0 8px;">⚠ Layout Rendering Error</h2>
<p style="color:#991b1b;margin:0 0 12px;">' . e($e->getMessage()) . '</p>
<hr style="border:none;border-top:1px solid #fca5a5;margin:12px 0;">
<p style="font-size:12px;color:#b91c1c;">Site: ' . e($site->name) . ' | Page: ' . e($page->name) . '</p>
</div>';
}
return $html;
}
}