681 lines
20 KiB
PHP
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;
|
|
}
|
|
}
|