mein-sterntours/app/Services/NavigationTreeService.php
2026-01-23 17:34:40 +01:00

681 lines
20 KiB
PHP

<?php
namespace App\Services;
use App\Models\Page;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class NavigationTreeService
{
/**
* Cache-Zeit in Minuten
* @var int
*/
protected $cacheTime = 60;
/**
* Ob Caching aktiviert ist
* @var bool
*/
protected $cacheEnabled = true;
/**
* Aktiviert oder deaktiviert Caching
*
* @param bool $enabled
* @return $this
*/
public function setCacheEnabled(bool $enabled): self
{
$this->cacheEnabled = $enabled;
return $this;
}
/**
* Setzt die Cache-Zeit
*
* @param int $minutes
* @return $this
*/
public function setCacheTime(int $minutes): self
{
$this->cacheTime = $minutes;
return $this;
}
/**
* Löscht den Navigation-Cache
*
* @return void
*/
public function clearCache(): void
{
Cache::forget('navigation_tree_full');
Cache::forget('navigation_tree_active');
Cache::forget('navigation_flat_full');
Cache::forget('navigation_flat_active');
// Lösche auch alle Subtree-Caches
$keys = Cache::get('navigation_subtree_keys', []);
foreach ($keys as $key) {
Cache::forget($key);
}
Cache::forget('navigation_subtree_keys');
}
/**
* Gibt den kompletten Navigationsbaum zurück
*
* @param bool $onlyActive Nur aktive und sichtbare Seiten zurückgeben
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
* @return array
*/
public function getNavigationTree(bool $onlyActive = false, bool $onlyShowInNavi = false): array
{
$cacheKey = $onlyActive ? 'navigation_tree_active' : 'navigation_tree_full';
if ($this->cacheEnabled) {
return Cache::remember($cacheKey, $this->cacheTime, function () use ($onlyActive, $onlyShowInNavi) {
return $this->buildNavigationTree($onlyActive, $onlyShowInNavi);
});
}
return $this->buildNavigationTree($onlyActive, $onlyShowInNavi);
}
/**
* Gibt den Navigationsbaum wie im Frontend zurück (nur Länderseiten mit Children)
*
* @param bool $includeHidden Auch ausgeblendete Pages anzeigen
* @return array
*/
public function getFrontendNavigationTree(bool $includeHidden = false): array
{
$cacheKey = 'navigation_tree_frontend_' . ($includeHidden ? 'with_hidden' : 'visible');
if ($this->cacheEnabled) {
return Cache::remember($cacheKey, $this->cacheTime, function () use ($includeHidden) {
return $this->buildFrontendNavigationTree($includeHidden);
});
}
return $this->buildFrontendNavigationTree($includeHidden);
}
/**
* Baut den Frontend-Navigationsbaum auf
*
* @param bool $includeHidden
* @return array
*/
protected function buildFrontendNavigationTree(bool $includeHidden = false): array
{
$tree = [];
// 1. Länderseiten (Hauptnavigation)
$countryPages = $this->getCountryPages($includeHidden);
foreach ($countryPages as $page) {
$node = $this->buildFrontendNode($page, $includeHidden);
if ($node) {
$node['section'] = 'Länder-Navigation';
$tree[] = $node;
}
}
// 2. Ferienwohnungen (USEDOM)
$fewoPages = $this->getFewoPages($includeHidden);
if (!empty($fewoPages)) {
$tree[] = [
'is_section_separator' => true,
'title' => 'USEDOM Ferienwohnungen',
'icon' => 'isv-fewo',
'section' => 'Ferienwohnungen'
];
foreach ($fewoPages as $page) {
$node = $this->buildFrontendNode($page, $includeHidden);
if ($node) {
$node['section'] = 'Ferienwohnungen';
$tree[] = $node;
}
}
}
// 3. Weitere wichtige Seiten (Mehr-Menü)
$morePages = $this->getMoreMenuPages($includeHidden);
if (!empty($morePages)) {
$tree[] = [
'is_section_separator' => true,
'title' => 'Weitere Seiten (Mehr-Menü)',
'icon' => 'fa fa-ellipsis-v',
'section' => 'Mehr'
];
foreach ($morePages as $page) {
$node = $this->buildFrontendNode($page, $includeHidden);
if ($node) {
$node['section'] = 'Mehr';
$tree[] = $node;
}
}
}
return $tree;
}
/**
* Hole alle Länderseiten
*
* @param bool $includeHidden
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getCountryPages(bool $includeHidden = false)
{
$query = Page::whereNull('parent_id')
->whereNotNull('country_id')
->orderBy('order')
->orderBy('title');
if (!$includeHidden) {
$query->where('show_in_navi', 1);
$query->where('status', 1);
}
return $query->get();
}
/**
* Hole Ferienwohnungs-Übersichtsseite mit Children
*
* @param bool $includeHidden
* @return array
*/
protected function getFewoPages(bool $includeHidden = false)
{
// Suche die Hauptseite "Ferienwohnungen"
$query = Page::where('slug', 'ferienwohnungen')
->orWhere('real_url_path', '/ferienwohnungen');
if (!$includeHidden) {
$query->where('status', 1);
}
$fewoMainPage = $query->first();
if (!$fewoMainPage) {
return [];
}
// Nur die Hauptseite zurückgeben, Children werden über buildFrontendNode geladen
return [$fewoMainPage];
}
/**
* Hole Seiten für "Mehr"-Menü
*
* @param bool $includeHidden
* @return array
*/
protected function getMoreMenuPages(bool $includeHidden = false)
{
// Typische Slugs/Pfade aus dem Mehr-Menü mit ihren möglichen Children
$morePages = [
'ueber-uns' => true, // Könnte Children haben
'reiseversicherung' => false, // Keine Children
'reisefuehrer' => true, // Könnte Children haben
'reisemagazin' => true, // Könnte Children haben
'reisenews' => true // Könnte Children haben
];
$pages = [];
foreach ($morePages as $slug => $hasChildren) {
$query = Page::where('slug', $slug)
->orWhere('real_url_path', '/' . $slug);
if (!$includeHidden) {
$query->where('status', 1);
}
$page = $query->first();
if ($page) {
$pages[] = $page;
}
}
return $pages;
}
/**
* Baut einen Frontend-Node auf (mit Children gruppiert nach beforeTitle)
*
* @param Page $page
* @param bool $includeHidden
* @param bool $loadChildren Soll Children geladen werden?
* @return array|null
*/
protected function buildFrontendNode(Page $page, bool $includeHidden = false, bool $loadChildren = true): ?array
{
$node = $this->buildNodeData($page, true);
if (!$loadChildren) {
$node['children'] = [];
$node['has_children'] = false;
return $node;
}
// Hole Children
$query = Page::where('parent_id', $page->id)
->orderBy('order')
->orderBy('title');
if (!$includeHidden) {
$query->where('show_in_navi', 1);
$query->where('status', 1);
}
$children = $query->get();
// Gruppiere Children nach beforeTitle
$groupedChildren = [
'main' => [],
'infos' => []
];
foreach ($children as $child) {
$childNode = $this->buildNodeData($child, true);
if ($child->before_title === 'Infos') {
$groupedChildren['infos'][] = $childNode;
} else {
$groupedChildren['main'][] = $childNode;
}
}
// Baue finale Children-Liste mit Gruppierung
$finalChildren = [];
// Erst Haupt-Children
foreach ($groupedChildren['main'] as $childNode) {
$finalChildren[] = $childNode;
}
// Dann Info-Children mit Separator
if (!empty($groupedChildren['infos'])) {
$finalChildren[] = [
'is_separator' => true,
'title' => 'Infos',
'icon' => 'fa fa-info-circle'
];
foreach ($groupedChildren['infos'] as $childNode) {
$finalChildren[] = $childNode;
}
}
$node['children'] = $finalChildren;
$node['has_children'] = count($finalChildren) > 0;
return $node;
}
/**
* Baut den Navigationsbaum auf (ohne Caching)
*
* @param bool $onlyActive
* @param bool $onlyShowInNavi
* @return array
*/
protected function buildNavigationTree(bool $onlyActive = false, bool $onlyShowInNavi = false): array
{
// Hole alle Root-Seiten (ohne Parent)
$query = Page::whereNull('parent_id')
->orderBy('order')
->orderBy('title');
if ($onlyActive) {
$query->where('status', 1);
}
if ($onlyShowInNavi) {
$query->where('show_in_navi', 1);
}
$rootPages = $query->get();
$tree = [];
foreach ($rootPages as $page) {
$tree[] = $this->buildNode($page, $onlyActive, $onlyShowInNavi);
}
return $tree;
}
/**
* Gibt einen Teilbaum zurück, beginnend mit einer bestimmten Page
*
* @param int $rootId Die ID der Root-Page
* @param bool $onlyActive Nur aktive Seiten zurückgeben
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
* @return array|null
*/
public function getNavigationSubTree(int $rootId, bool $onlyActive = false, bool $onlyShowInNavi = false): ?array
{
$cacheKey = "navigation_subtree_{$rootId}_" . ($onlyActive ? 'active' : 'full');
if ($this->cacheEnabled) {
// Speichere Cache-Key für späteres Löschen
$keys = Cache::get('navigation_subtree_keys', []);
if (!in_array($cacheKey, $keys)) {
$keys[] = $cacheKey;
Cache::put('navigation_subtree_keys', $keys, $this->cacheTime);
}
return Cache::remember($cacheKey, $this->cacheTime, function () use ($rootId, $onlyActive, $onlyShowInNavi) {
return $this->buildSubTree($rootId, $onlyActive, $onlyShowInNavi);
});
}
return $this->buildSubTree($rootId, $onlyActive, $onlyShowInNavi);
}
/**
* Baut einen Teilbaum auf (ohne Caching)
*
* @param int $rootId
* @param bool $onlyActive
* @param bool $onlyShowInNavi
* @return array|null
*/
protected function buildSubTree(int $rootId, bool $onlyActive = false, bool $onlyShowInNavi = false): ?array
{
$page = Page::find($rootId);
if (!$page) {
return null;
}
return $this->buildNode($page, $onlyActive, $onlyShowInNavi);
}
/**
* Gibt eine flache Liste aller Navigationspunkte zurück
*
* @param bool $onlyActive Nur aktive Seiten zurückgeben
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
* @return array
*/
public function getFlatNavigationList(bool $onlyActive = false, bool $onlyShowInNavi = false): array
{
$cacheKey = $onlyActive ? 'navigation_flat_active' : 'navigation_flat_full';
if ($this->cacheEnabled) {
return Cache::remember($cacheKey, $this->cacheTime, function () use ($onlyActive, $onlyShowInNavi) {
return $this->buildFlatList($onlyActive, $onlyShowInNavi);
});
}
return $this->buildFlatList($onlyActive, $onlyShowInNavi);
}
/**
* Baut eine flache Liste auf (ohne Caching)
*
* @param bool $onlyActive
* @param bool $onlyShowInNavi
* @return array
*/
protected function buildFlatList(bool $onlyActive = false, bool $onlyShowInNavi = false): array
{
$query = Page::orderBy('order')
->orderBy('title');
if ($onlyActive) {
$query->where('status', 1);
}
if ($onlyShowInNavi) {
$query->where('show_in_navi', 1);
}
$pages = $query->get();
$list = [];
foreach ($pages as $page) {
$list[] = $this->buildNodeData($page, false);
}
return $list;
}
/**
* Baut einen einzelnen Knoten mit allen Kindern rekursiv auf
*
* @param Page $page
* @param bool $onlyActive
* @param bool $onlyShowInNavi
* @return array
*/
protected function buildNode(Page $page, bool $onlyActive = false, bool $onlyShowInNavi = false): array
{
$node = $this->buildNodeData($page, true);
// Hole alle Child-Seiten
$query = Page::where('parent_id', $page->id)
->orderBy('order')
->orderBy('title');
if ($onlyActive) {
$query->where('status', 1);
}
if ($onlyShowInNavi) {
$query->where('show_in_navi', 1);
}
$children = $query->get();
$childNodes = [];
foreach ($children as $child) {
$childNodes[] = $this->buildNode($child, $onlyActive, $onlyShowInNavi);
}
$node['children'] = $childNodes;
$node['has_children'] = count($childNodes) > 0;
return $node;
}
/**
* Erstellt die Datenstruktur für einen einzelnen Navigationspunkt
*
* @param Page $page
* @param bool $includeRelations Beziehungen laden (TravelProgram, etc.)
* @return array
*/
protected function buildNodeData(Page $page, bool $includeRelations = true): array
{
$data = [
'id' => $page->id,
'title' => $page->title,
'title_short' => $page->title_short,
'before_title' => $page->before_title,
'slug' => $page->slug,
'real_url_path' => $page->real_url_path,
'url' => $page->real_url_path ?: $this->buildUrlPath($page),
'status' => $page->status,
'show_in_navi' => $page->show_in_navi,
'order' => $page->order,
'template' => $page->template,
'lvl' => $page->lvl,
'parent_id' => $page->parent_id,
// SEO-Informationen
'pagetitle' => $page->pagetitle,
'description' => $page->description,
'keywords' => $page->keywords,
'canonical_url' => $page->canonical_url,
// Tree-Informationen
'lft' => $page->lft,
'rgt' => $page->rgt,
'tree_root' => $page->tree_root,
// Box-Informationen (für Karten/Boxen in der Navigation)
'box_body' => $page->box_body,
'box_image_url' => $page->box_image_url,
'box_star' => $page->box_star,
'box_discount' => $page->box_discount,
// Zusätzliche Flags
'is_travel_program' => !is_null($page->travel_program),
'is_fewo_lodging' => !is_null($page->fewo_lodging),
'is_country_page' => !is_null($page->country_id),
];
if ($includeRelations) {
// Lade Beziehungen wenn benötigt
if ($page->travel_program) {
$travelProgram = $page->travel_program_content;
if ($travelProgram) {
$data['travel_program'] = [
'id' => $travelProgram->id,
'title' => $travelProgram->title,
'subtitle' => $travelProgram->subtitle,
'status' => $travelProgram->status,
'position' => $travelProgram->position,
'duration' => $travelProgram->duration,
'price_from' => $travelProgram->price_from,
];
}
}
if ($page->fewo_lodging) {
$fewoLodging = $page->fewo_lodging();
if ($fewoLodging) {
$fewo = $fewoLodging->first();
if ($fewo) {
$data['fewo_lodging'] = [
'id' => $fewo->id,
'name' => $fewo->name,
'status' => $fewo->status,
];
}
}
}
if ($page->country_id) {
$country = $page->travel_country;
if ($country) {
$data['country'] = [
'id' => $country->id,
'name' => $country->name,
'title' => $country->title,
'slug' => $country->slug,
];
}
}
}
// CMS Settings (JSON)
if ($page->cms_settings) {
try {
$data['cms_settings'] = json_decode($page->cms_settings, true);
} catch (\Exception $e) {
$data['cms_settings'] = null;
}
}
return $data;
}
/**
* Baut den URL-Pfad für eine Page rekursiv auf (Fallback wenn real_url_path nicht gesetzt)
*
* @param Page $page
* @return string
*/
protected function buildUrlPath(Page $page): string
{
$slugs = [];
$current = $page;
// Sammle alle Slugs von unten nach oben
while ($current) {
if ($current->slug) {
array_unshift($slugs, $current->slug);
}
$current = $current->parent_page;
}
return '/' . implode('/', $slugs);
}
/**
* Zählt die Anzahl der Knoten im Baum
*
* @param array $tree
* @return int
*/
public function countNodes(array $tree): int
{
$count = 0;
foreach ($tree as $node) {
$count++; // Zähle den aktuellen Knoten
if (isset($node['children']) && is_array($node['children'])) {
$count += $this->countNodes($node['children']);
}
}
return $count;
}
/**
* Findet einen Knoten anhand seiner ID im Baum
*
* @param array $tree
* @param int $nodeId
* @return array|null
*/
public function findNodeById(array $tree, int $nodeId): ?array
{
foreach ($tree as $node) {
if ($node['id'] === $nodeId) {
return $node;
}
if (isset($node['children']) && is_array($node['children'])) {
$found = $this->findNodeById($node['children'], $nodeId);
if ($found) {
return $found;
}
}
}
return null;
}
/**
* Gibt den Breadcrumb-Pfad für eine bestimmte Page zurück
*
* @param int $pageId
* @return array
*/
public function getBreadcrumb(int $pageId): array
{
$page = Page::find($pageId);
if (!$page) {
return [];
}
$breadcrumb = [];
$current = $page;
while ($current) {
array_unshift($breadcrumb, [
'id' => $current->id,
'title' => $current->title,
'title_short' => $current->title_short,
'url' => $current->real_url_path ?: $this->buildUrlPath($current),
'slug' => $current->slug,
]);
$current = $current->parent_page;
}
return $breadcrumb;
}
}