23-01-2026
This commit is contained in:
parent
8fd1f4d451
commit
389d5d1820
59 changed files with 9642 additions and 883 deletions
312
dev/frontend-navigation/BACKEND-UI.md
Normal file
312
dev/frontend-navigation/BACKEND-UI.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# Backend-UI für Navigation API
|
||||
|
||||
Die Backend-UI bietet eine benutzerfreundliche grafische Oberfläche zur Verwaltung und Visualisierung des Navigationsbaums **genau wie im Frontend (header.html.twig)**.
|
||||
|
||||
## Zugriff
|
||||
|
||||
**URL:** `/navigation-api`
|
||||
**Permission:** `crm-nav-api`
|
||||
**Menü:** Navigation API (Seitenmenü)
|
||||
|
||||
## Frontend-Struktur
|
||||
|
||||
Die Backend-UI zeigt den Navigationsbaum **exakt wie im Frontend** mit allen Bereichen:
|
||||
|
||||
### 🌍 Länder-Navigation
|
||||
|
||||
- ✅ **Länderseiten** (Pages mit `country_id`) als Root-Level
|
||||
- ✅ **Sortierung** nach `order` und `title`
|
||||
- ✅ **Gruppierung** der Children nach `beforeTitle` (Haupt / Infos)
|
||||
- ✅ **titleShort** wird verwendet statt vollständigem Titel
|
||||
- ✅ Toggle zum Auf-/Zuklappen der Children
|
||||
|
||||
### 🏠 USEDOM Ferienwohnungen
|
||||
|
||||
- ✅ **Ferienwohnungs-Übersicht** als Hauptseite
|
||||
- ✅ **Einzelne FeWos** als Children
|
||||
- ✅ Sortiert nach `order` und `title`
|
||||
|
||||
### 📑 Mehr-Menü Seiten
|
||||
|
||||
- ✅ **Über uns**
|
||||
- ✅ **Reiseversicherung**
|
||||
- ✅ **Reiseführer** (mit möglichen Children)
|
||||
- ✅ **Reisemagazin** (mit möglichen Children)
|
||||
- ✅ **Reisenews** (mit möglichen Children)
|
||||
|
||||
### Allgemein
|
||||
|
||||
- ✅ **Ausgeblendete Pages** werden angezeigt (Badge "Ausgeblendet")
|
||||
- ✅ **Section-Separators** trennen die Bereiche visuell
|
||||
|
||||
## Features
|
||||
|
||||
### 📊 Live-Statistiken
|
||||
|
||||
Oben auf der Seite werden wichtige Kennzahlen angezeigt:
|
||||
|
||||
- **Gesamt Seiten:** Alle Pages in der Datenbank
|
||||
- **Aktive Seiten:** Nur sichtbare und aktive Pages
|
||||
- **Reiseprogramme:** Anzahl der TravelProgram-Pages
|
||||
- **Länderseiten:** Anzahl der Country-Pages
|
||||
|
||||
Die Statistiken werden beim Laden der Seite automatisch aktualisiert.
|
||||
|
||||
### 🌳 Interaktiver Navigationsbaum
|
||||
|
||||
Der Hauptbereich zeigt den hierarchischen Navigationsbaum mit folgenden Features:
|
||||
|
||||
#### Visuelle Darstellung
|
||||
|
||||
- **Icons:**
|
||||
|
||||
- ⭐ Stern (gelb) = Länderseite (Root-Level)
|
||||
- ✈️ Flugzeug = Reiseprogramm
|
||||
- 🏠 Haus = Ferienwohnung
|
||||
- ℹ️ Info-Circle = Infos-Gruppe
|
||||
- 📄 Dokument = Normale Seite
|
||||
|
||||
- **Badges:**
|
||||
|
||||
- 🔴 **Inaktiv** = Status ist 0
|
||||
- ⚠️ **Ausgeblendet** = show_in_navi ist 0 (wird trotzdem angezeigt!)
|
||||
- 🔵 **Reiseprogramm** = Hat TravelProgram
|
||||
- 💙 **FeWo** = Hat FewoLodging
|
||||
- 💚 **Land** = Hat Country
|
||||
- ⚪ **Gruppe: Infos** = beforeTitle ist gesetzt
|
||||
|
||||
- **Hierarchie:**
|
||||
|
||||
- **Root-Level:** Länderseiten (fett, grauer Hintergrund, blaue Linie links)
|
||||
- **Children:** Eingerückt, normale Darstellung
|
||||
- **Separator "Infos":** Grauer Block trennt Haupt- von Info-Seiten
|
||||
- Toggle-Buttons nur bei Länderseiten
|
||||
|
||||
- **Besonderheiten:**
|
||||
- **titleShort** wird angezeigt (wie im Frontend)
|
||||
- **Gruppierung** nach beforeTitle (Haupt, dann Infos)
|
||||
- **Ausgeblendete** Pages haben gelbes Badge aber sind sichtbar
|
||||
|
||||
#### Interaktion
|
||||
|
||||
- **Auf-/Zuklappen:**
|
||||
|
||||
- Klick auf Pfeil: Einzelnen Knoten auf-/zuklappen
|
||||
- "Alle aufklappen": Zeigt kompletten Baum
|
||||
- "Alle zuklappen": Zeigt nur Root-Ebene
|
||||
|
||||
- **Hover-Effekt:**
|
||||
- Hintergrund wird grau beim Überfahren mit der Maus
|
||||
|
||||
### 🔍 Suche
|
||||
|
||||
Die Suchfunktion durchsucht alle Navigationspunkte nach:
|
||||
|
||||
- **Titel:** Page-Titel
|
||||
- **Slug:** URL-freundlicher Name
|
||||
- **URL:** Vollständiger Pfad
|
||||
|
||||
**Verwendung:**
|
||||
|
||||
1. Suchbegriff eingeben
|
||||
2. Enter drücken oder auf Suchbutton klicken
|
||||
3. Gefundene Knoten werden gelb markiert
|
||||
4. Parent-Knoten werden automatisch aufgeklappt
|
||||
|
||||
### 🎯 Filter
|
||||
|
||||
**"Mit ausgeblendeten" / "Nur sichtbare"**
|
||||
|
||||
- **Mit ausgeblendeten (Standard):** Zeigt alle Pages inkl. ausgeblendete (show_in_navi=0)
|
||||
- **Nur sichtbare:** Zeigt nur Pages mit show_in_navi=1 (wie im Frontend)
|
||||
|
||||
Der aktive Filter wird durch die Button-Farbe angezeigt:
|
||||
|
||||
- Blau = Mit ausgeblendeten
|
||||
- Primär = Nur sichtbare
|
||||
|
||||
Ausgeblendete Pages werden mit einem gelben Badge "Ausgeblendet" markiert.
|
||||
|
||||
### 📥 Export
|
||||
|
||||
**JSON-Export-Funktion:**
|
||||
|
||||
- Klick auf "Export JSON" lädt eine JSON-Datei herunter
|
||||
- Dateiname: `navigation-tree-YYYY-MM-DD-HHMMSS.json`
|
||||
- Inhalt: Kompletter Navigationsbaum entsprechend aktuellem Filter
|
||||
- Format: Pretty-printed JSON mit UTF-8 Encoding
|
||||
|
||||
**Verwendungszwecke:**
|
||||
|
||||
- Backup der Navigationsstruktur
|
||||
- Import in andere Systeme
|
||||
- Analyse und Dokumentation
|
||||
- Debugging
|
||||
|
||||
### 🔄 Cache-Verwaltung
|
||||
|
||||
**Cache leeren:**
|
||||
|
||||
- Klick auf "Cache leeren"
|
||||
- Bestätigung erforderlich
|
||||
- Löscht alle gecachten Navigationsdaten
|
||||
- Wird automatisch nach 60 Minuten erneuert
|
||||
|
||||
**Wann Cache leeren?**
|
||||
|
||||
- Nach Änderungen an Pages
|
||||
- Nach Import/Export von Daten
|
||||
- Bei veralteten Anzeigen
|
||||
- Nach Struktur-Änderungen
|
||||
|
||||
## Technische Details
|
||||
|
||||
### API-Calls
|
||||
|
||||
Die UI nutzt folgende Endpunkte:
|
||||
|
||||
```javascript
|
||||
// Statistiken laden
|
||||
GET /navigation-api/stats
|
||||
|
||||
// Navigationsbaum laden (Frontend-Struktur)
|
||||
GET /navigation-api/data?include_hidden=0|1
|
||||
|
||||
// Suche durchführen
|
||||
GET /navigation-api/search?query=...
|
||||
|
||||
// Export starten (Frontend-Struktur)
|
||||
GET /navigation-api/export?include_hidden=0|1
|
||||
|
||||
// Cache leeren
|
||||
POST /navigation-api/clear-cache
|
||||
```
|
||||
|
||||
**Unterschied zu API-Endpunkten:**
|
||||
|
||||
- `/api/navigation/*` = Kompletter Baum (API)
|
||||
- `/navigation-api/*` = Frontend-Struktur (nur Länderseiten)
|
||||
|
||||
### Performance
|
||||
|
||||
- **Caching:** 60 Minuten Server-seitig
|
||||
- **Lazy Loading:** Kinder werden nur bei Bedarf gerendert
|
||||
- **Optimierte Queries:** Eager Loading von Relationships
|
||||
- **Frontend-Rendering:** jQuery-basiert, schnell auch bei 1000+ Knoten
|
||||
|
||||
### Browser-Kompatibilität
|
||||
|
||||
- ✅ Chrome/Edge (aktuell)
|
||||
- ✅ Firefox (aktuell)
|
||||
- ✅ Safari (aktuell)
|
||||
- ⚠️ IE11 (eingeschränkt)
|
||||
|
||||
## Styling
|
||||
|
||||
### Farben
|
||||
|
||||
- **Primary (Blau):** Reiseprogramme, aktive Aktionen
|
||||
- **Success (Grün):** Aktive Pages, Länder
|
||||
- **Info (Cyan):** Ferienwohnungen
|
||||
- **Warning (Gelb):** "Nicht in Navi", Suchmarkierungen
|
||||
- **Danger (Rot):** Inaktive Pages, Löschen-Aktionen
|
||||
|
||||
### Responsive Design
|
||||
|
||||
Die UI ist responsive und funktioniert auf verschiedenen Bildschirmgrößen:
|
||||
|
||||
- **Desktop (>1200px):** Volle Features, 4 Statistik-Karten
|
||||
- **Tablet (768-1199px):** 2 Statistik-Karten pro Zeile
|
||||
- **Mobile (<768px):** 1 Statistik-Karte pro Zeile, vereinfachte Toolbar
|
||||
|
||||
## Shortcuts
|
||||
|
||||
Keine Keyboard-Shortcuts implementiert (noch).
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
1. **Sehr große Bäume (>10.000 Knoten):**
|
||||
|
||||
- Kann zu langsamem Rendering führen
|
||||
- Empfehlung: Filter verwenden
|
||||
|
||||
2. **Suche:**
|
||||
|
||||
- Nur client-seitig, keine Server-Suche
|
||||
- Bei vielen Ergebnissen kann es unübersichtlich werden
|
||||
|
||||
3. **Keine Bearbeitung:**
|
||||
- Nur Anzeige, keine Inline-Bearbeitung
|
||||
- Änderungen müssen über CMS erfolgen
|
||||
|
||||
## Zukünftige Erweiterungen
|
||||
|
||||
Mögliche Features für die Zukunft:
|
||||
|
||||
- [ ] Drag & Drop zum Verschieben von Knoten
|
||||
- [ ] Inline-Bearbeitung von Titeln
|
||||
- [ ] Bulk-Operationen (Status ändern, löschen, etc.)
|
||||
- [ ] Mehr Filter-Optionen (Template, Level, etc.)
|
||||
- [ ] Keyboard-Shortcuts
|
||||
- [ ] Pagination für sehr große Bäume
|
||||
- [ ] Graphische Visualisierung (D3.js Tree)
|
||||
- [ ] Breadcrumb-Anzeige für selektierten Knoten
|
||||
- [ ] Export in andere Formate (CSV, XML)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Baum lädt nicht
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Browser-Konsole öffnen (F12)
|
||||
2. Fehler prüfen
|
||||
3. Netzwerk-Tab prüfen: Status-Code der API-Calls
|
||||
4. Backend-Logs prüfen
|
||||
|
||||
### Problem: Statistiken zeigen "-"
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. API-Endpunkt `/navigation-api/stats` prüfen
|
||||
2. Browser-Konsole auf Fehler prüfen
|
||||
3. Permission `crm-tp-na` prüfen
|
||||
|
||||
### Problem: Cache wird nicht geleert
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. CSRF-Token prüfen
|
||||
2. POST-Request erfolgreich? (Netzwerk-Tab)
|
||||
3. Server-Logs prüfen
|
||||
4. Cache-System aktiv? (config/cache.php)
|
||||
|
||||
### Problem: Suche funktioniert nicht
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. JavaScript-Fehler in Konsole?
|
||||
2. jQuery geladen?
|
||||
3. Suchbegriff korrekt eingegeben?
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen oder Fragen:
|
||||
|
||||
1. Dokumentation lesen: `dev/frontend-navigation/README.md`
|
||||
2. API-Dokumentation: `dev/frontend-navigation/navigation-api.md`
|
||||
3. Test-Tools verwenden: `dev/frontend-navigation/test-api.html`
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0 (Initial Release)
|
||||
|
||||
- ✅ Navigationsbaum-Visualisierung
|
||||
- ✅ Live-Statistiken
|
||||
- ✅ Suche
|
||||
- ✅ Filter (Alle/Aktive)
|
||||
- ✅ Export (JSON)
|
||||
- ✅ Cache-Verwaltung
|
||||
- ✅ Responsive Design
|
||||
- ✅ Icon-basierte Typerkennung
|
||||
- ✅ Badge-System für Status
|
||||
256
dev/frontend-navigation/KernelControllerListener.php
Normal file
256
dev/frontend-navigation/KernelControllerListener.php
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
|
||||
* @date 12/02/2016
|
||||
*/
|
||||
|
||||
namespace AppBundle\Listener;
|
||||
|
||||
|
||||
use AppBundle\AppBundle;
|
||||
use AppBundle\Controller\DefaultController;
|
||||
use AppBundle\Entity\Page;
|
||||
use AppBundle\Util;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class KernelControllerListener
|
||||
{
|
||||
private $em;
|
||||
private $controllerResolver;
|
||||
|
||||
public function __construct(EntityManager $entityManager, ControllerResolverInterface $controllerResolver)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
$this->controllerResolver = $controllerResolver;
|
||||
}
|
||||
|
||||
private function setSessionAttributeByTime($request, $key)
|
||||
{
|
||||
|
||||
$session = $request->getSession();
|
||||
$session->set('_open_side_about', '');
|
||||
$session->set('_open_side_search', '');
|
||||
|
||||
if ($key === 'default') { //is default visit
|
||||
if (!$session->get('default_visit')) { //first visit
|
||||
$session->set('default_visit', true);
|
||||
$session->set('_open_side_about', 'open');
|
||||
$session->set('_open_side_search', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 'api') { //is api = Reiseführer
|
||||
if (!$session->get('api_visit')) { //first visit
|
||||
$session->set('api_visit', true);
|
||||
$session->set('_open_side_about', 'open');
|
||||
}
|
||||
}
|
||||
}
|
||||
public function onKernelController(FilterControllerEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
$session = $request->getSession();
|
||||
Util::setMySession('search_request_b', $session->get('search_request_b'));
|
||||
Util::setMySession('search_request_e', $session->get('search_request_e'));
|
||||
Util::setMySession('search_request_c', $session->get('search_request_c'));
|
||||
|
||||
if ($request->get('_controller') === 'AppBundle\Controller\DefaultController::homeAction') {
|
||||
$this->setSessionAttributeByTime($request, "default");
|
||||
}
|
||||
if ($request->get('_controller') == 'AppBundle\Controller\DefaultController::defaultAction') {
|
||||
|
||||
$repo = $this->em->getRepository('AppBundle:Page');
|
||||
$path = preg_replace('/^\/?(.*?)\/?$/', '$1', $request->getPathInfo());
|
||||
/** @var Page $node */
|
||||
$node = null;
|
||||
|
||||
// Try to find by url path. It's possible that the path consists of two parts:
|
||||
// - the beginning part represents a node
|
||||
// - the ending part represents a handler
|
||||
// e.g. /path/to/travel/program/buchen ("buchen" is the handler part)
|
||||
$pathArray = explode('/', $path);
|
||||
$restOfPath = '';
|
||||
$curPath = $path;
|
||||
//search for entry in new tree objects
|
||||
$api = Util::loadFromApi('cms/search', ['url' => $curPath]);
|
||||
while (!empty($pathArray)) {
|
||||
if (!$api) {
|
||||
$node = $repo->findOneBy(['realUrlPath' => '/' . $curPath]);
|
||||
if ($node) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$restOfPath = '/' . array_pop($pathArray) . $restOfPath;
|
||||
$curPath = implode('/', $pathArray);
|
||||
}
|
||||
//find and try 301
|
||||
//find => to
|
||||
$redirects = [
|
||||
'/reisefuehrer/tuerkei' => 'tuerkei-reisen/reisefuehrer',
|
||||
'/reisefuehrer/jordanien' => 'jordanien-reisen/reisefuehrer',
|
||||
'/reisefuehrer/oman' => 'oman-reisen/reisefuehrer',
|
||||
'/reisefuehrer/israel' => 'israel-reisen/israel-reisefuehrer',
|
||||
'/reisefuehrer/aegypten' => 'aegypten-reisen/aegypten-reisefuehrer',
|
||||
'/reisemagazin/tuerkei-reisemagazin' => 'tuerkei-reisen/tuerkei-reisemagazin',
|
||||
'/reisemagazin/aegypten' => 'aegypten-reisen/aegypten-reisemagazin',
|
||||
'/reisemagazin/israel' => 'israel-reisen/israel-reisemagazin',
|
||||
'/reisemagazin/jordanien' => 'jordanien-reisen/jordanien-reisemagazin',
|
||||
'/reisemagazin/marokko' => 'marokko-urlaub/marokko-reisemagazin'
|
||||
|
||||
];
|
||||
//301
|
||||
foreach ($redirects as $find => $to) {
|
||||
if (strpos($restOfPath, $find) !== false) {
|
||||
$restOfPath = str_replace($find, $to, $restOfPath);
|
||||
$protocol = 'https';
|
||||
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") {
|
||||
$protocol = 'http';
|
||||
}
|
||||
header("Location: " . $protocol . "://" . $_SERVER["HTTP_HOST"] . "/" . $restOfPath, true, 301);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
//load content from API, is found by cms/search
|
||||
|
||||
if ($api) {
|
||||
$this->setSessionAttributeByTime($request, "api");
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:iqTravelGuide');
|
||||
$request->attributes->set('api', $api);
|
||||
$request->attributes->set('template', 'TravelGuide');
|
||||
try {
|
||||
$controller = $this->controllerResolver->getController($request);
|
||||
} catch (\LogicException $e) {
|
||||
// If there is no controller action, call the default action and pass the template name
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:Default');
|
||||
$request->attributes->set('template', "Default");
|
||||
}
|
||||
$event->setController($controller ?? $this->controllerResolver->getController($request));
|
||||
return;
|
||||
}
|
||||
$this->setSessionAttributeByTime($request, "default");
|
||||
|
||||
if (!$node) {
|
||||
// Now try to find a page by tracing a page node path using the page nodes' slugs
|
||||
|
||||
$pathArray = explode('/', $path);
|
||||
|
||||
while (!empty($pathArray)) {
|
||||
$restOfPath = '/' . implode('/', $pathArray);
|
||||
$slug = array_shift($pathArray);
|
||||
|
||||
$qb = $repo->createQueryBuilder('p');
|
||||
|
||||
$qb->where($qb->expr()->eq('p.slug', ':slug'));
|
||||
$qb->setParameter('slug', $slug);
|
||||
|
||||
if ($node != null) {
|
||||
$qb->andWhere($qb->expr()->eq('p.parent', ':parentId'));
|
||||
$qb->setParameter('parentId', $node->getId());
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNull('p.parent'));
|
||||
}
|
||||
|
||||
$qb->setMaxResults(1);
|
||||
$childNode = $qb->getQuery()->getOneOrNullResult();
|
||||
if (!$childNode) {
|
||||
$whitelist = [
|
||||
'buchen',
|
||||
'berechne-gesamtpreis',
|
||||
'show_nationality_country_text',
|
||||
'pdf',
|
||||
];
|
||||
if (!in_array($slug, $whitelist)) {
|
||||
throw new HttpException(404, 'Seite nicht gefunden: ' . $slug);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($node) {
|
||||
|
||||
// Avoid database calls to parent later
|
||||
$childNode->setParent($node);
|
||||
}
|
||||
$node = $childNode;
|
||||
}
|
||||
if ($node && $node->getRealUrlPath() && $node->getRealUrlPath() != '/' . $path) {
|
||||
// If there realUrlPath is set and the slug path differs from realUrlPath, then the slug path is
|
||||
// not a valid URL. Otherwise, there would be two different URLs representing the same page.
|
||||
$event->setController(function () {
|
||||
throw new NotFoundHttpException('Invalid URL');
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!$node) {
|
||||
// Search for a redirect entry
|
||||
$redirect = $this->em->getRepository('AppBundle:Redirect')->findOneBy(['sourceUrlPath' => '/' . $path]);
|
||||
if ($redirect) {
|
||||
$redirectUrl = $redirect->getPage()->getUrlPath();
|
||||
$event->setController(function () use ($redirectUrl) {
|
||||
return new RedirectResponse($redirectUrl, 301);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($node) {
|
||||
|
||||
if ($node->getStatus() == 0) {
|
||||
throw new NotFoundHttpException('Inactive page');
|
||||
}
|
||||
|
||||
$request->attributes->set('page', $node);
|
||||
|
||||
if ($node->getTravelProgram() != null && (
|
||||
$restOfPath == '/buchen' || $restOfPath == '/berechne-gesamtpreis' || $restOfPath == '/show_nationality_country_text')) {
|
||||
// Special case: Booking actions
|
||||
$request->attributes->set('travelProgramPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:Booking:index');
|
||||
} elseif ($restOfPath && $node->getTravelProgram() !== null && (
|
||||
$restOfPath === '/pdf')) {
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:pdf');
|
||||
} elseif ($node->getTravelProgram() !== null) {
|
||||
if ($node->getTravelProgram()->getStatus() == 0) {
|
||||
throw new \NotFoundHttpException('Inactive travel program');
|
||||
}
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:travelProgram');
|
||||
} elseif (
|
||||
$node->getFewoLodging() != null &&
|
||||
($restOfPath === '/buchen' || $restOfPath === '/berechne-gesamtpreis')
|
||||
) {
|
||||
$request->attributes->set('fewoTravelProgramPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:FewoBooking:index');
|
||||
} elseif ($node->getFewoLodging() !== null) {
|
||||
$request->attributes->set('fewoLodgingPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:fewoLodging');
|
||||
} else {
|
||||
$handler = $node->getTemplate() ? ucfirst($node->getTemplate()) : 'Default';
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:' . $handler);
|
||||
if ($node->getTemplate()) {
|
||||
try {
|
||||
$controller = $this->controllerResolver->getController($request);
|
||||
} catch (\LogicException $e) {
|
||||
// If there is no controller action, call the default action and pass the template name
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:Default');
|
||||
$request->attributes->set('template', $node->getTemplate());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
$event->setController($controller ?? $this->controllerResolver->getController($request));
|
||||
}
|
||||
}
|
||||
}
|
||||
180
dev/frontend-navigation/PageRepository.php
Normal file
180
dev/frontend-navigation/PageRepository.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace AppBundle\Entity;
|
||||
|
||||
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
|
||||
/**
|
||||
* PageRepository
|
||||
*
|
||||
* This class was generated by the Doctrine ORM. Add your own custom
|
||||
* repository methods below.
|
||||
*/
|
||||
class PageRepository extends NestedTreeRepository
|
||||
{
|
||||
/**
|
||||
* @param Page $page
|
||||
* @return Page[]|array
|
||||
*
|
||||
* @todo Optimize performance by adapting search algorithm's optimizations
|
||||
*/
|
||||
public function getChildrenWithTravelProgramsAndDates(Page $page)
|
||||
{
|
||||
$pages = $this->getChildrenQueryBuilder($page)
|
||||
->leftJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->andWhere('tp.status > 0')
|
||||
->andWhere('node.status > 0')
|
||||
->orderBy('node.order')
|
||||
->addOrderBy('tp.position')
|
||||
->addOrderBy('node.title')
|
||||
->getQuery()
|
||||
->execute();
|
||||
/** @var Page $childPage */
|
||||
foreach ($pages as &$childPage) {
|
||||
if ($childPage->getTravelProgram()) {
|
||||
$this->getEntityManager()->getRepository('AppBundle:TravelPeriod')->getTrueTravelPeriods(
|
||||
$childPage->getTravelProgram()
|
||||
);
|
||||
}
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function findWithTravelProgramsOfCountry(TravelCountry $country)
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.travelProgram', 'tp')
|
||||
->innerJoin('tp.countries', 'c')
|
||||
->where('c.id = ' . $country->getId())
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status = 1')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Page[]
|
||||
*/
|
||||
public function findOffers()
|
||||
{
|
||||
$ret = [];
|
||||
$countries = $this->getEntityManager()->getRepository('AppBundle:TravelCountry')->findAll();
|
||||
foreach ($countries as $country) {
|
||||
$ret = array_merge(
|
||||
$ret,
|
||||
$this->createQueryBuilder('node')
|
||||
->innerJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->innerJoin('tp.countries', 'c')
|
||||
->where('c.id = ' . $country->getId())
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status = 1')
|
||||
->orderBy('node.order')
|
||||
->addOrderBy('tp.position')
|
||||
->addOrderBy('node.title')
|
||||
->setMaxResults(3)
|
||||
->getQuery()
|
||||
->execute()
|
||||
);
|
||||
}
|
||||
shuffle($ret);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function findCountryPages()
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.country', 'country')
|
||||
->where('node.status > 0')
|
||||
->andWhere('node.template = \'overview\'')
|
||||
->andWhere('node.lvl = 0')
|
||||
->andWhere('node.order > 0')
|
||||
->orderBy('node.order,node.title')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findTopCountryNavPages()
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.country', 'country')
|
||||
->leftJoin('node.children', 'childPage', Expr\Join::WITH, 'childPage.status > 0')
|
||||
->addSelect('childPage')
|
||||
->where('node.status > 0')
|
||||
->andWhere('node.template = \'overview\'')
|
||||
->andWhere('node.lvl = 0')
|
||||
->andWhere('node.order > 0')
|
||||
->orderBy('node.order,node.title, childPage.order, childPage.title')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findFeedbacks($rootPageId)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('node');
|
||||
return $qb
|
||||
->where($qb->expr()->eq('node.parent', $rootPageId))
|
||||
->andWhere('node.showInNavi = 1')
|
||||
->andWhere('node.status = 1')
|
||||
->orderBy('node.order')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findParentsWithShowNav($rootPageId)
|
||||
{
|
||||
|
||||
$qb = $this->createQueryBuilder('node');
|
||||
$pages = $qb->innerJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->where($qb->expr()->eq('node.parent', $rootPageId))
|
||||
->andWhere('node.showInNavi = 1')
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status > 0')
|
||||
->orderBy('node.order')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
foreach ($pages as &$childPage) {
|
||||
if ($childPage->getTravelProgram()) {
|
||||
// var_dump($childPage->getTravelProgram()->getId());
|
||||
// $this->getEntityManager()->getRepository('AppBundle:TravelPeriod')->getTrueTravelPeriods($childPage->getTravelProgram());
|
||||
}
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Page $page
|
||||
*
|
||||
* @return Page[]|\Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getSiblings(Page $page)
|
||||
{
|
||||
$parent = $page->getParent();
|
||||
if (!$parent) {
|
||||
// On purpose, we don't treat root pages as if they were siblings
|
||||
return [];
|
||||
}
|
||||
$siblings = $parent->getChildren();
|
||||
foreach ($siblings as &$sibling) {
|
||||
$sibling->setParent($parent);
|
||||
}
|
||||
|
||||
// Da diese Methode nur für die Navigation verwendet wird, kann man hier vorfiltern
|
||||
$filteredSiblings = [];
|
||||
foreach ($siblings as &$sibling) {
|
||||
if ($sibling->getStatus() == 1 && $sibling->getShowInNavi() == 1) {
|
||||
$filteredSiblings[] = $sibling;
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredSiblings;
|
||||
}
|
||||
}
|
||||
197
dev/frontend-navigation/README.md
Normal file
197
dev/frontend-navigation/README.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# Frontend Navigation - Backend API
|
||||
|
||||
Dieses Verzeichnis enthält die Dokumentation und Referenzimplementierungen für die Frontend-Navigation.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Navigation-API stellt die komplette Seitenstruktur des CMS als hierarchischen Baum oder flache Liste zur Verfügung.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### Backend-UI (Admin-Interface)
|
||||
|
||||
Zugriff über: **Navigation API** im Seitenmenü (Permission: `crm-nav-api`)
|
||||
|
||||
Die Backend-UI zeigt die **komplette Frontend-Navigation**:
|
||||
|
||||
- 📊 Live-Statistiken (Gesamt, Aktiv, Programme, Länder)
|
||||
- 🌍 **Länder-Navigation** mit Children (gruppiert nach Haupt/Infos)
|
||||
- 🏠 **USEDOM Ferienwohnungen** mit allen FeWos
|
||||
- 📑 **Mehr-Menü Seiten** (Über uns, Reiseversicherung, Reiseführer, Reisemagazin, Reisenews)
|
||||
- 🔍 Volltext-Suche über Titel, Slug und URL
|
||||
- 🌳 Interaktiver Nested Tree mit Auf-/Zuklappen
|
||||
- 🎯 Filter (Mit ausgeblendeten/Nur sichtbare)
|
||||
- 📥 JSON-Export
|
||||
- 🔄 Cache-Verwaltung
|
||||
|
||||
### API-Endpunkte
|
||||
|
||||
```
|
||||
GET /api/navigation/tree - Kompletter Navigationsbaum
|
||||
GET /api/navigation/tree/active - Nur aktive Navigationspunkte
|
||||
GET /api/navigation/tree/{rootId} - Teilbaum ab einer bestimmten Page
|
||||
GET /api/navigation/flat - Flache Liste aller Pages
|
||||
GET /api/navigation/breadcrumb/{pageId} - Breadcrumb-Pfad
|
||||
```
|
||||
|
||||
### Beispiel-Verwendung
|
||||
|
||||
```javascript
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
fetch("/api/navigation/tree")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log(data.data); // Navigationsbaum
|
||||
});
|
||||
```
|
||||
|
||||
## Dateien
|
||||
|
||||
- **README.md** - Diese Datei (Übersicht)
|
||||
- **BACKEND-UI.md** - Dokumentation der Backend-UI (Admin-Interface)
|
||||
- **navigation.md** - Dokumentation der Frontend-Routing-Logik (Symfony-basiert)
|
||||
- **navigation-api.md** - Vollständige API-Dokumentation mit Beispielen
|
||||
- **test-api.php** - CLI-Test-Script für alle API-Endpunkte
|
||||
- **test-api.html** - Browser-basierte interaktive Test-Oberfläche
|
||||
- **KernelControllerListener.php** - Referenz: Symfony KernelControllerListener
|
||||
- **PageRepository.php** - Referenz: Doctrine Page Repository
|
||||
|
||||
## Implementierung
|
||||
|
||||
Die API wurde im Laravel-Backend implementiert:
|
||||
|
||||
### Backend-Komponenten
|
||||
|
||||
**API-Layer:**
|
||||
|
||||
1. **API Controller:** `app/Http/Controllers/API/NavigationController.php`
|
||||
|
||||
- REST API-Endpunkte
|
||||
- JSON-Responses
|
||||
|
||||
2. **Web Controller:** `app/Http/Controllers/NavigationTreeController.php`
|
||||
|
||||
- Backend-UI (Admin-Interface)
|
||||
- Daten-Export und Cache-Management
|
||||
|
||||
3. **Service:** `app/Services/NavigationTreeService.php`
|
||||
|
||||
- Business-Logik
|
||||
- Rekursiver Baum-Aufbau
|
||||
- Caching (60 Min.)
|
||||
- Helper-Methoden (Breadcrumb, Node-Suche, etc.)
|
||||
|
||||
4. **Model:** `app/Models/Page.php`
|
||||
|
||||
- Eloquent Model für die Page-Entität
|
||||
- Relationships (parent, children, travel_program, etc.)
|
||||
|
||||
5. **Views:** `resources/views/navigation/index.blade.php`
|
||||
|
||||
- Interaktive Baum-Visualisierung
|
||||
- Suche und Filter
|
||||
- Statistiken
|
||||
|
||||
6. **Routes:**
|
||||
- API: `routes/api.php`
|
||||
- Web: `routes/web.php` (Permission: `crm-tp-na`)
|
||||
|
||||
## Features
|
||||
|
||||
### Navigationsbaum
|
||||
|
||||
- ✅ Hierarchische Struktur mit unbegrenzter Tiefe
|
||||
- ✅ Alle Page-Eigenschaften enthalten (SEO, Template, Status, etc.)
|
||||
- ✅ Beziehungen zu TravelProgram, FewoLodging, Country
|
||||
- ✅ Filterung nach Status und Sichtbarkeit
|
||||
- ✅ Sortierung nach Order und Title
|
||||
|
||||
### Zusätzliche Funktionen
|
||||
|
||||
- ✅ Breadcrumb-Generierung
|
||||
- ✅ Teilbaum-Abfrage
|
||||
- ✅ Flache Listen-Ansicht
|
||||
- ✅ Node-Zählung
|
||||
- ✅ URL-Pfad-Generierung
|
||||
|
||||
### Performance
|
||||
|
||||
- Effiziente Queries mit Eager Loading
|
||||
- Rekursive Baum-Konstruktion
|
||||
- Unterstützung für Nested Set Tree (lft/rgt)
|
||||
- Caching-ready
|
||||
|
||||
## Datenstruktur
|
||||
|
||||
Jeder Navigationspunkt enthält:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Seitentitel",
|
||||
"slug": "seitentitel",
|
||||
"url": "/seitentitel",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "default",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false,
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
```
|
||||
|
||||
Siehe `navigation-api.md` für eine vollständige Feldbeschreibung.
|
||||
|
||||
## Routing-Logik (Frontend)
|
||||
|
||||
Das Frontend verwendet einen `KernelControllerListener` (Symfony), der:
|
||||
|
||||
1. **API-Lookup** prüft (externe Reiseführer)
|
||||
2. **Datenbank-Suche** durchführt (Page-Entität)
|
||||
3. **Redirects** verarbeitet (301-Weiterleitungen)
|
||||
4. **Controller zuweist** basierend auf Content-Type
|
||||
|
||||
Siehe `navigation.md` für Details zur Routing-Logik.
|
||||
|
||||
## Unterschiede Frontend/Backend
|
||||
|
||||
| Aspekt | Frontend (Symfony) | Backend (Laravel) |
|
||||
| ---------- | ----------------------------- | -------------------------- |
|
||||
| Framework | Symfony 2.x | Laravel 5.x |
|
||||
| ORM | Doctrine | Eloquent |
|
||||
| Routing | KernelControllerListener | API Routes |
|
||||
| Tree-Logik | Nested Set + Parent Traversal | Parent-Child Relationships |
|
||||
| API | External (IQ API) | Internal (Laravel) |
|
||||
|
||||
## Wartung
|
||||
|
||||
### Neue Felder hinzufügen
|
||||
|
||||
1. Füge das Feld in `NavigationTreeService::buildNodeData()` hinzu
|
||||
2. Aktualisiere die Dokumentation in `navigation-api.md`
|
||||
|
||||
### Performance optimieren
|
||||
|
||||
1. Füge Caching in `NavigationTreeService` hinzu
|
||||
2. Verwende Eager Loading für Relations
|
||||
3. Implementiere Pagination für große Bäume
|
||||
|
||||
### Tests
|
||||
|
||||
Empfohlene Tests:
|
||||
|
||||
- Unit Tests für `NavigationTreeService`
|
||||
- API Tests für alle Endpunkte
|
||||
- Performance Tests für große Navigationsbäume
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen zur API siehe:
|
||||
|
||||
- `navigation-api.md` - Vollständige API-Dokumentation
|
||||
- `navigation.md` - Frontend Routing-Dokumentation
|
||||
325
dev/frontend-navigation/header.html.twig
Normal file
325
dev/frontend-navigation/header.html.twig
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
<div id="topBar" class="">
|
||||
<div class="container-fluid">
|
||||
{% if content.info.office_important_note_active == 1 %}
|
||||
<ul class="top-links block">
|
||||
<li class="icon">
|
||||
<a class="dropdown-toggle no-text-underline" data-toggle="dropdown" data-hover="dropdown" href="#"><i class="fa fa-info"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-infos">
|
||||
<div class="dropdown-menu-header">
|
||||
<span><i class="fa fa-info"></i> aktuelle Infos</span>
|
||||
</div>
|
||||
<div class="dropdown-menu-body">
|
||||
{{ content.info.office_important_note }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li style="overflow: hidden;width: 100%;">
|
||||
<div id="marquee" class="marquee"><span> {{ content.info.office_important_note }}</span></div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="top-links block wrap" id="topNavAccordion">
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopTravelDates" role="button" aria-expanded="false" aria-controls="collapseTopTravelDates">
|
||||
<i class="fa fa-plane"></i> Reisetermine
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
<!-- ab Montag um 09:00 Uhr -->
|
||||
<!-- bis xxx Uhr -->
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopTravelDates">
|
||||
<div class="dropdown-menu-body">
|
||||
<div class="badge badge-default btn-block">
|
||||
<span class="text-default">{{ header_travel_program.title }} <br>
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
<tr>
|
||||
<th class="text-left">Hinflug</th>
|
||||
<th class="text-left">Rückflug</th>
|
||||
<th class="text-left">Preis p. P.</th>
|
||||
</tr>
|
||||
|
||||
{% set last_name = "" %}
|
||||
{% for travel_date in header_travel_program.travelDates('header') if travel_date.status >= 0 %}
|
||||
{% if loop.index <= 6 %}
|
||||
{% if last_name != travel_date.name %}
|
||||
{% set last_name = travel_date.name %}
|
||||
<tr>
|
||||
<td class="text-left">{{ travel_date.start|date }}</td>
|
||||
<td class="text-left">{{ travel_date.end|date }}</td>
|
||||
<td class="text-left">
|
||||
<strong>
|
||||
{% if travel_date.prices[3] is defined %}
|
||||
{% if travel_date.prices[3].available == "1" %}
|
||||
{% if travel_date.prices[3].effectiveDiscountPrice %}
|
||||
<a href="{{ header_travel_program.page.urlPath }}" style="color: #558c55; text-decoration: underline;">
|
||||
ab {{ travel_date.prices[3].effectiveDiscountPrice|number_format }} €
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a href="{{ header_travel_program.page.getUrlPathBefore }}">weitere Rundreisen ansehen</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopPhone" role="button" aria-expanded="false" aria-controls="collapseTopPhone">
|
||||
<i class="fa fa-phone-square"></i> 030 - 700 94 100 •
|
||||
{% if(content.available.phone.active) %}
|
||||
<span class="text-success">erreichbar</span>
|
||||
{% else %}
|
||||
erreichbar
|
||||
{% endif %}
|
||||
{{ content.available.phone.content }}
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
<!-- ab Montag um 09:00 Uhr -->
|
||||
<!-- bis xxx Uhr -->
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopPhone">
|
||||
<div class="dropdown-menu-body">
|
||||
<p><a href="tel:030 - 700 94 100" class="btn btn-secondary btn-sm btn-block text-center"><i class="fa fa-phone-square text-success" ></i> 030 - 700 94 100</a></p>
|
||||
<hr>
|
||||
<div class="badge badge-default btn-block">
|
||||
{% if(content.available.phone.active) %}
|
||||
<span class="text-success"><i class="fa fa-check-circle fa-lg"></i></span> Wir sind zur Zeit telefonisch zu erreichen.
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-times-circle fa-lg"></i></span> Wir sind zur Zeit telefonisch nicht zu erreichen.
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
{% for key, val in phone %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 50%">{{ val.day }} <span class="text-muted pull-right"> {{ val.date }}</span></td>
|
||||
{% if val.active == 0 %}
|
||||
<td colspan="3">geschlossen</td>
|
||||
{% else %}
|
||||
<td>{{ val.from }}</td><td>-</td><td>{{ val.to }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopLocal" role="button" aria-expanded="false" aria-controls="collapseTopLocal">
|
||||
<i class="fa fa-clock-o"></i> Reisebüro
|
||||
{% if(content.available.local.active) %}
|
||||
<span class="text-success">geöffnet</span> •
|
||||
{% else %}
|
||||
<span class="text-danger">geschlossen</span> •
|
||||
{% endif %}
|
||||
{{ content.available.local.content }}
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopLocal">
|
||||
<div class="dropdown-menu-body">
|
||||
<div class="badge badge-default btn-block">
|
||||
{% if(content.available.local.active) %}
|
||||
<span class="text-success"><i class="fa fa-check-circle fa-lg"></i></span> Unsere Büro ist aktuell geöffnet.
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-times-circle fa-lg"></i></span> Unsere Büro ist aktuell geschlossen.
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
{% for key, val in local %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 50%">{{ val.day }} <span class="text-muted pull-right"> {{ val.date }}</span></td>
|
||||
{% if val.active == 0 %}
|
||||
<td colspan="3">geschlossen</td>
|
||||
{% else %}
|
||||
<td>{{ val.from }}</td><td>-</td><td>{{ val.to }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopContact" role="button" aria-expanded="false" aria-controls="collapseTopContact"><i class="fa fa-envelope"></i> Kontakt • Formular • Terminvereinbarung <i class="fa fa-caret-collapse"></i></a>
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopContact">
|
||||
<div class="dropdown-menu-body">
|
||||
{{ content.info.office_appointment | raw }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="header" class="sticky clearfix">
|
||||
<!-- TOP NAV -->
|
||||
<header id="topNav">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button class="btn btn-mobile" data-toggle="collapse" data-target=".nav-main-collapse">
|
||||
<i class="fa fa-bars"></i> Menü
|
||||
</button>
|
||||
<button class="btn btn-primary btn-mobile-info myanimated my_fadein">
|
||||
<i class="fa fa-info"></i>
|
||||
</button>
|
||||
<!-- Logo -->
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ asset('images/wlogo.png') }}" alt="Stern Tours">
|
||||
</a>
|
||||
|
||||
<div class="navbar-collapse nav-main-collapse collapse">
|
||||
<nav class="nav-main">
|
||||
<ul class="topMain nav nav-pills nav-main md-pull-left">
|
||||
<li class=" active"><!-- HOME -->
|
||||
<a href="/" title="Kulturreisen" itemprop="">
|
||||
<i class="fa fa-home fa-lg"></i> <span class="hidden-md hidden-lg">Kulturreisen</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{% for nav_page in nav_pages if nav_page.country is not empty %}
|
||||
{% if nav_page.showInNavi == 1 %}
|
||||
{# @var nav_page \AppBundle\Entity\Page #}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" href="{{ nav_page.urlPath }}">
|
||||
<i class="fa fa-star"></i> {{ nav_page.title|replace({'Reisen': ''}) }} <span class="hidden-md hidden-lg">Reisen</span>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<h4><i class="fa fa-star"></i> <a href="{{ nav_page.urlPath }}" title="{{ nav_page.title }}">
|
||||
{{ nav_page.title|replace({'Reisen': ''}) }} Reisen
|
||||
</a></h4>
|
||||
</li>
|
||||
|
||||
{% for childnav_page in nav_page.children %}
|
||||
{# @var childnav_page \AppBundle\Entity\Page #}
|
||||
|
||||
{% if(childnav_page.beforeTitle == "Infos") %}
|
||||
<li>
|
||||
<h4><i class="fa fa-info-circle"></i> Infos</h4>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="{{ childnav_page.urlPath }}" title="{{ childnav_page.title }}">
|
||||
{{ childnav_page.titleShort }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="topMain nav nav-pills nav-main md-pull-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle extra-margin-left" href="/ferienwohnungen">
|
||||
<span class="text-usedom">USEDOM</span> <span class="hidden-md">Ferienwohnungen</span> <span class="hidden-sm hidden-lg hidden-xs">FeWo</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen" title="Usedom Ferienwohnungen"><i class="isv-fewo"></i> Übersicht </a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo1-strandstr29" title="FeWo 1 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 1 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo2-strandstr29" title="FeWo 2 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 2 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo3-strandstr29" title="FeWo 3 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 3 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo4-strandstr29" title="FeWo 4 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 4 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo1-triftweg10" title="FeWo 1 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 1 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo2-triftweg10" title="FeWo 2 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 2 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo3-triftweg10" title="FeWo 3 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 3 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo4-triftweg10" title="FeWo 4 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 4 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" href="#">
|
||||
<i class="fa fa-ellipsis-v fa-lg"></i> <span class="hidden-md hidden-lg">mehr</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/tuerkei-reisen" title="Türkei Reisen"><i class="fa fa-star"></i> Türkei Reisen </a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/israel-reisen" title="Israel Reisen"><i class="fa fa-star"></i> Israel Reisen </a>
|
||||
</li>
|
||||
<!--
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/usbekistan-reisen" title="Usbekistan Reisen"><i class="fa fa-star"></i> Usbekistan Reisen </a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/marokko-urlaub" title="Marokko Reisen"><i class="fa fa-star"></i> Marokko Reisen</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="divider"></li>
|
||||
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ueber-uns" title="Über uns"><i class="fa fa-users"></i> Über uns</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reiseversicherung" title="Reiseversicherung">
|
||||
<i class="fa fa-shield"></i> Reiseversicherung
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisefuehrer" title="Reiseführer">
|
||||
<i class="fa fa-flag"></i> Reiseführer
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisemagazin" title="Reisemagazin">
|
||||
<i class="fa fa-book"></i> Reisemagazin
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisenews" title="Reisenews">
|
||||
<i class="fa fa-newspaper-o"></i> Reisenews
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
414
dev/frontend-navigation/navigation-api.md
Normal file
414
dev/frontend-navigation/navigation-api.md
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
# Navigation API Dokumentation
|
||||
|
||||
Die Navigation-API stellt Endpunkte bereit, um die Frontend-Navigationsstruktur im Backend abzurufen.
|
||||
|
||||
## Basis-URL
|
||||
|
||||
Alle Endpunkte sind unter `/api/navigation/` verfügbar.
|
||||
|
||||
## Endpunkte
|
||||
|
||||
### 1. Kompletter Navigationsbaum
|
||||
|
||||
**GET** `/api/navigation/tree`
|
||||
|
||||
Gibt den kompletten hierarchischen Navigationsbaum mit allen Seiten zurück.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"title_short": null,
|
||||
"before_title": null,
|
||||
"slug": "startseite",
|
||||
"real_url_path": "/",
|
||||
"url": "/",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "home",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"pagetitle": "Willkommen",
|
||||
"description": "Startseite",
|
||||
"keywords": "start, home",
|
||||
"canonical_url": null,
|
||||
"lft": 1,
|
||||
"rgt": 10,
|
||||
"tree_root": 1,
|
||||
"box_body": null,
|
||||
"box_image_url": null,
|
||||
"box_star": null,
|
||||
"box_discount": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false,
|
||||
"cms_settings": null,
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Über uns",
|
||||
"slug": "ueber-uns",
|
||||
"url": "/ueber-uns",
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
],
|
||||
"has_children": true
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total_nodes": 42,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Nur aktive Navigationspunkte
|
||||
|
||||
**GET** `/api/navigation/tree/active`
|
||||
|
||||
Gibt nur aktive Seiten zurück (`status = 1` und `show_in_navi = 1`).
|
||||
|
||||
**Response:** Gleiche Struktur wie oben, aber gefiltert.
|
||||
|
||||
---
|
||||
|
||||
### 3. Teilbaum ab einem bestimmten Knoten
|
||||
|
||||
**GET** `/api/navigation/tree/{rootId}`
|
||||
|
||||
Gibt einen Teilbaum zurück, beginnend mit der angegebenen Page-ID.
|
||||
|
||||
**Parameter:**
|
||||
|
||||
- `rootId` (integer): Die ID der Seite, ab der der Baum aufgebaut werden soll
|
||||
|
||||
**Beispiel:** `/api/navigation/tree/5`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 5,
|
||||
"title": "Reisen",
|
||||
"slug": "reisen",
|
||||
"url": "/reisen",
|
||||
"children": [
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Türkei",
|
||||
"slug": "tuerkei",
|
||||
"url": "/reisen/tuerkei",
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
],
|
||||
"has_children": true
|
||||
},
|
||||
"meta": {
|
||||
"total_nodes": 15,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Flache Liste (ohne Hierarchie)
|
||||
|
||||
**GET** `/api/navigation/flat`
|
||||
|
||||
Gibt alle Navigationspunkte als flache Liste zurück (ohne parent-child Beziehung).
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"slug": "startseite",
|
||||
"url": "/",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "home",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Über uns",
|
||||
"slug": "ueber-uns",
|
||||
"url": "/ueber-uns",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 2,
|
||||
"template": "default",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total_nodes": 42,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Breadcrumb-Pfad
|
||||
|
||||
**GET** `/api/navigation/breadcrumb/{pageId}`
|
||||
|
||||
Gibt den Breadcrumb-Pfad für eine bestimmte Seite zurück (von der Wurzel bis zur Zielseite).
|
||||
|
||||
**Parameter:**
|
||||
|
||||
- `pageId` (integer): Die ID der Seite, für die der Breadcrumb erstellt werden soll
|
||||
|
||||
**Beispiel:** `/api/navigation/breadcrumb/15`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"title_short": null,
|
||||
"url": "/",
|
||||
"slug": "startseite"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Reisen",
|
||||
"title_short": null,
|
||||
"url": "/reisen",
|
||||
"slug": "reisen"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Türkei",
|
||||
"title_short": null,
|
||||
"url": "/reisen/tuerkei",
|
||||
"slug": "tuerkei"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Istanbul",
|
||||
"title_short": null,
|
||||
"url": "/reisen/tuerkei/istanbul",
|
||||
"slug": "istanbul"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"depth": 4,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Felder-Beschreibung
|
||||
|
||||
Jeder Navigationspunkt enthält folgende Informationen:
|
||||
|
||||
### Basis-Informationen
|
||||
|
||||
- `id`: Eindeutige ID der Seite
|
||||
- `title`: Vollständiger Titel
|
||||
- `title_short`: Kurzer Titel (optional)
|
||||
- `before_title`: Text vor dem Titel (optional)
|
||||
- `slug`: URL-freundlicher Name
|
||||
- `real_url_path`: Der definierte URL-Pfad aus der Datenbank
|
||||
- `url`: Der vollständige URL-Pfad (entweder `real_url_path` oder automatisch generiert)
|
||||
|
||||
### Navigationsinformationen
|
||||
|
||||
- `status`: Status der Seite (0 = inaktiv, 1 = aktiv)
|
||||
- `show_in_navi`: Soll in Navigation angezeigt werden (0 = nein, 1 = ja)
|
||||
- `order`: Sortierreihenfolge
|
||||
- `lvl`: Hierarchie-Ebene (0 = Root)
|
||||
- `parent_id`: ID der Eltern-Seite (null = Root)
|
||||
|
||||
### SEO-Informationen
|
||||
|
||||
- `pagetitle`: SEO-Titel
|
||||
- `description`: Meta-Beschreibung
|
||||
- `keywords`: Meta-Keywords
|
||||
- `canonical_url`: Kanonische URL
|
||||
|
||||
### Template & Layout
|
||||
|
||||
- `template`: Template-Name (z.B. "home", "overview", "default")
|
||||
- `box_body`: Inhalt für Box-Darstellung
|
||||
- `box_image_url`: Bild-URL für Box
|
||||
- `box_star`: Sternebewertung
|
||||
- `box_discount`: Rabatt-Information
|
||||
|
||||
### Tree-Struktur
|
||||
|
||||
- `lft`, `rgt`: Nested Set Tree Werte
|
||||
- `tree_root`: ID der Baumwurzel
|
||||
|
||||
### Content-Type Flags
|
||||
|
||||
- `is_travel_program`: Ist ein Reiseprogramm
|
||||
- `is_fewo_lodging`: Ist eine Ferienwohnung
|
||||
- `is_country_page`: Ist eine Länderseite
|
||||
|
||||
### Beziehungen (wenn vorhanden)
|
||||
|
||||
**Travel Program:**
|
||||
|
||||
```json
|
||||
"travel_program": {
|
||||
"id": 123,
|
||||
"title": "7 Tage Istanbul",
|
||||
"subtitle": "Die schönsten Orte",
|
||||
"status": 1,
|
||||
"position": 1,
|
||||
"duration": 7,
|
||||
"price_from": 899.00
|
||||
}
|
||||
```
|
||||
|
||||
**Fewo Lodging:**
|
||||
|
||||
```json
|
||||
"fewo_lodging": {
|
||||
"id": 45,
|
||||
"name": "Villa am Meer",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Country:**
|
||||
|
||||
```json
|
||||
"country": {
|
||||
"id": 10,
|
||||
"name": "Türkei",
|
||||
"title": "Türkei Reisen",
|
||||
"slug": "tuerkei"
|
||||
}
|
||||
```
|
||||
|
||||
### Hierarchie-Informationen
|
||||
|
||||
- `children`: Array von Child-Knoten (gleiche Struktur)
|
||||
- `has_children`: Boolean, ob Kinder vorhanden sind
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
Bei Fehlern wird ein JSON-Response mit folgendem Format zurückgegeben:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Fehlerbeschreibung"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes:**
|
||||
|
||||
- `200`: Erfolgreiche Anfrage
|
||||
- `404`: Seite/Ressource nicht gefunden
|
||||
- `500`: Serverfehler
|
||||
|
||||
---
|
||||
|
||||
## Verwendungsbeispiele
|
||||
|
||||
### JavaScript/Fetch
|
||||
|
||||
```javascript
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
fetch("/api/navigation/tree")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Navigation Tree:", data.data);
|
||||
console.log("Total Nodes:", data.meta.total_nodes);
|
||||
});
|
||||
|
||||
// Breadcrumb für eine Seite abrufen
|
||||
fetch("/api/navigation/breadcrumb/15")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Breadcrumb:", data.data);
|
||||
});
|
||||
```
|
||||
|
||||
### PHP/Laravel
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
$response = Http::get('http://yourdomain.com/api/navigation/tree');
|
||||
$navigationTree = $response->json()['data'];
|
||||
|
||||
// Teilbaum abrufen
|
||||
$response = Http::get('http://yourdomain.com/api/navigation/tree/5');
|
||||
$subTree = $response->json()['data'];
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
# Kompletten Baum
|
||||
curl -X GET http://yourdomain.com/api/navigation/tree
|
||||
|
||||
# Nur aktive Navigationspunkte
|
||||
curl -X GET http://yourdomain.com/api/navigation/tree/active
|
||||
|
||||
# Breadcrumb
|
||||
curl -X GET http://yourdomain.com/api/navigation/breadcrumb/15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance-Hinweise
|
||||
|
||||
- Der komplette Navigationsbaum kann bei großen Websites viele Knoten enthalten
|
||||
- Verwenden Sie `/api/navigation/tree/active` für Frontend-Navigationen
|
||||
- Verwenden Sie `/api/navigation/tree/{rootId}` um nur relevante Teilbäume zu laden
|
||||
- Erwägen Sie Caching für häufig abgerufene Navigationsstrukturen
|
||||
|
||||
---
|
||||
|
||||
## Implementierung
|
||||
|
||||
Die Navigation-API basiert auf folgenden Komponenten:
|
||||
|
||||
1. **Controller:** `App\Http\Controllers\API\NavigationController`
|
||||
2. **Service:** `App\Services\NavigationTreeService`
|
||||
3. **Model:** `App\Models\Page`
|
||||
4. **Routen:** Definiert in `routes/api.php`
|
||||
|
||||
Der Service verwendet rekursive Methoden, um den hierarchischen Baum aufzubauen und kann einfach erweitert werden.
|
||||
47
dev/frontend-navigation/navigation.md
Normal file
47
dev/frontend-navigation/navigation.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Routing & Navigations-Logik (KernelControllerListener)
|
||||
|
||||
Die Klasse `src/AppBundle/Listener/KernelControllerListener.php` implementiert die zentrale Routing-Logik für das CMS. Sie greift ein, wenn der Symfony-Router den Request an `AppBundle\Controller\DefaultController::defaultAction` leitet.
|
||||
|
||||
## Funktionsweise
|
||||
|
||||
Der Listener analysiert den Request-Pfad (`pathInfo`) und entscheidet, welcher Controller tatsächlich ausgeführt werden soll.
|
||||
|
||||
### 1. API-Lookup (Priorität 1)
|
||||
|
||||
Zuerst wird geprüft, ob der Pfad über einen externen API-Call (`Util::loadFromApi('cms/search', ...)`) aufgelöst werden kann.
|
||||
|
||||
- **Ziel-Controller:** `AppBundle:Cms:iqTravelGuide`
|
||||
- **Template:** `TravelGuide`
|
||||
|
||||
### 2. Datenbank-Suche (Page Entity)
|
||||
|
||||
Wenn die API nichts zurückgibt, wird in der lokalen Datenbank (`AppBundle:Page`) gesucht.
|
||||
|
||||
- **Methode A (Direkt):** Suche nach Übereinstimmung im Feld `realUrlPath`.
|
||||
- **Methode B (Tree-Traversierung):** Der Pfad wird anhand der Slashes (`/`) zerlegt. Es wird versucht, den Pfad hierarchisch über `slug` und die Eltern-Kind-Beziehung (`parent`) aufzulösen. Die anfällige Nested-Set-Logik (`lft`/`rgt`, `lvl`) wird hier bewusst umgangen, um 404-Fehler bei inkonsistenten Trees zu vermeiden.
|
||||
|
||||
### 3. Redirects
|
||||
|
||||
- **Hardcoded:** Es gibt eine Liste fester 301-Weiterleitungen (z.B. alte Reiseführer-URLs).
|
||||
- **Datenbank:** Tabelle `AppBundle:Redirect`. Wenn keine Page gefunden wird, wird hier geprüft.
|
||||
|
||||
### 4. Controller-Zuweisung
|
||||
|
||||
Sobald eine `Page`-Entität (`$node`) gefunden wurde, wird der Controller basierend auf dem Inhaltstyp bestimmt:
|
||||
|
||||
| Inhaltstyp | URL-Suffix | Controller | Action |
|
||||
| ----------------- | ------------------------- | ----------------------- | ------------------------- |
|
||||
| **TravelProgram** | `/buchen`, `/berechne...` | `AppBundle:Booking` | `index` |
|
||||
| **TravelProgram** | `/pdf` | `AppBundle:Cms` | `pdf` |
|
||||
| **TravelProgram** | _(keins)_ | `AppBundle:Cms` | `travelProgram` |
|
||||
| **FewoLodging** | `/buchen` | `AppBundle:FewoBooking` | `index` |
|
||||
| **FewoLodging** | _(keins)_ | `AppBundle:Cms` | `fewoLodging` |
|
||||
| **Standard Page** | _(keins)_ | `AppBundle:Cms` | _Dyn. nach Template-Name_ |
|
||||
|
||||
**Fallback für Standard Pages:**
|
||||
Wenn die Page ein Template (z.B. "About") definiert hat, versucht das System `AppBundle:Cms:About` aufzurufen. Existiert diese Action nicht, wird `AppBundle:Cms:Default` verwendet und der Template-Name als Parameter übergeben.
|
||||
|
||||
### 5. Fehlerbehandlung
|
||||
|
||||
- **404:** Wenn Pfad nicht gefunden und nicht in der Whitelist (`buchen`, `pdf` etc.) -> `HttpException(404)`.
|
||||
- **Inaktiv:** Wenn Page-Status `0` ist -> `NotFoundHttpException`.
|
||||
496
dev/frontend-navigation/test-api.html
Normal file
496
dev/frontend-navigation/test-api.html
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Navigation API Tester</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.endpoint-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.endpoint-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.endpoint-method {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.method-get {
|
||||
background: #61affe;
|
||||
}
|
||||
|
||||
.method-post {
|
||||
background: #49cc90;
|
||||
}
|
||||
|
||||
.endpoint-url {
|
||||
background: #f7f7f7;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.endpoint-description {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.test-button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.test-button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.test-button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-data {
|
||||
background: #f7f7f7;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.test-all {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
transition: background 0.3s;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.test-all:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary h2 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.total {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.stat-card.error {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🗺️ Navigation API Tester</h1>
|
||||
|
||||
<div class="intro">
|
||||
<p>Dieses Tool testet alle verfügbaren Endpunkte der Navigation API und zeigt die Ergebnisse in einer strukturierten Form.</p>
|
||||
</div>
|
||||
|
||||
<div class="config">
|
||||
<label for="baseUrl"><strong>Basis-URL:</strong></label>
|
||||
<input type="text" id="baseUrl" value="http://localhost/api/navigation" placeholder="http://localhost/api/navigation">
|
||||
</div>
|
||||
|
||||
<button class="test-all" onclick="testAllEndpoints()">Alle Endpunkte testen</button>
|
||||
|
||||
<!-- Endpoint 1: Kompletter Navigationsbaum -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Kompletter Navigationsbaum</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt den kompletten hierarchischen Navigationsbaum mit allen Seiten zurück.
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree', 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 2: Aktive Navigationspunkte -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Nur aktive Navigationspunkte</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree/active</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt nur aktive Seiten zurück (status = 1 und show_in_navi = 1).
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree/active', 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree-active"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 3: Teilbaum -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Teilbaum ab Root-ID</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree/{rootId}</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt einen Teilbaum zurück, beginnend mit der angegebenen Page-ID.<br>
|
||||
<input type="number" id="subtreeId" placeholder="Root Page ID" value="1" style="margin-top: 10px; padding: 8px; width: 200px;">
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree/' + document.getElementById('subtreeId').value, 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree-subtree"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 4: Flache Liste -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Flache Liste</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/flat</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt alle Navigationspunkte als flache Liste zurück (ohne parent-child Beziehung).
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('flat', 'GET')">Testen</button>
|
||||
<div class="result" id="result-flat"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 5: Breadcrumb -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Breadcrumb-Pfad</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/breadcrumb/{pageId}</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt den Breadcrumb-Pfad für eine bestimmte Seite zurück.<br>
|
||||
<input type="number" id="breadcrumbId" placeholder="Page ID" value="1" style="margin-top: 10px; padding: 8px; width: 200px;">
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('breadcrumb/' + document.getElementById('breadcrumbId').value, 'GET')">Testen</button>
|
||||
<div class="result" id="result-breadcrumb"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 6: Cache leeren -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Cache leeren</div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/cache/clear</div>
|
||||
<div class="endpoint-description">
|
||||
Löscht den kompletten Navigation-Cache.
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('cache/clear', 'POST')">Testen</button>
|
||||
<div class="result" id="result-cache-clear"></div>
|
||||
</div>
|
||||
|
||||
<!-- Zusammenfassung -->
|
||||
<div class="summary" id="summary">
|
||||
<h2>Test-Zusammenfassung</h2>
|
||||
<div class="summary-stats">
|
||||
<div class="stat-card total">
|
||||
<div class="stat-number" id="stat-total">0</div>
|
||||
<div class="stat-label">Gesamt</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-number" id="stat-success">0</div>
|
||||
<div class="stat-label">Erfolgreich</div>
|
||||
</div>
|
||||
<div class="stat-card error">
|
||||
<div class="stat-number" id="stat-error">0</div>
|
||||
<div class="stat-label">Fehlgeschlagen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testResults = {};
|
||||
|
||||
function getBaseUrl() {
|
||||
return document.getElementById('baseUrl').value.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
async function testEndpoint(endpoint, method = 'GET') {
|
||||
const resultId = 'result-' + endpoint.replace(/\//g, '-').replace(/\d+/g, 'subtree');
|
||||
const resultDiv = document.getElementById(resultId);
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'result';
|
||||
resultDiv.innerHTML = '<div class="result-header">Teste...<span class="loading"></span></div>';
|
||||
|
||||
const url = getBaseUrl() + '/' + endpoint;
|
||||
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const endTime = performance.now();
|
||||
const duration = Math.round(endTime - startTime);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
testResults[endpoint] = response.ok;
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.className = 'result success';
|
||||
let html = '<div class="result-header">✓ Erfolgreich</div>';
|
||||
html += '<div class="result-meta">';
|
||||
html += `<strong>HTTP Status:</strong> ${response.status}<br>`;
|
||||
html += `<strong>Dauer:</strong> ${duration}ms<br>`;
|
||||
|
||||
if (data.meta) {
|
||||
html += '<strong>Metadaten:</strong><br>';
|
||||
for (const [key, value] of Object.entries(data.meta)) {
|
||||
html += ` ${key}: ${value}<br>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (data.data) {
|
||||
html += '<div class="result-data">';
|
||||
html += JSON.stringify(data.data, null, 2);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
} else {
|
||||
resultDiv.className = 'result error';
|
||||
let html = '<div class="result-header">✗ Fehler</div>';
|
||||
html += '<div class="result-meta">';
|
||||
html += `<strong>HTTP Status:</strong> ${response.status}<br>`;
|
||||
html += `<strong>Dauer:</strong> ${duration}ms<br>`;
|
||||
if (data.error) {
|
||||
html += `<strong>Fehlermeldung:</strong> ${data.error}`;
|
||||
}
|
||||
html += '</div>';
|
||||
resultDiv.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
testResults[endpoint] = false;
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="result-header">✗ Fehler</div>
|
||||
<div class="result-meta">
|
||||
<strong>Fehlermeldung:</strong> ${error.message}<br>
|
||||
<br>
|
||||
Mögliche Ursachen:<br>
|
||||
• Server läuft nicht<br>
|
||||
• CORS-Probleme<br>
|
||||
• Falsche Base-URL<br>
|
||||
• Netzwerkfehler
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAllEndpoints() {
|
||||
testResults = {};
|
||||
|
||||
const endpoints = [
|
||||
{ path: 'tree', method: 'GET' },
|
||||
{ path: 'tree/active', method: 'GET' },
|
||||
{ path: 'tree/1', method: 'GET' },
|
||||
{ path: 'flat', method: 'GET' },
|
||||
{ path: 'breadcrumb/1', method: 'GET' },
|
||||
{ path: 'cache/clear', method: 'POST' }
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(endpoint.path, endpoint.method);
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Kurze Pause zwischen Tests
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const total = Object.keys(testResults).length;
|
||||
const success = Object.values(testResults).filter(r => r === true).length;
|
||||
const error = total - success;
|
||||
|
||||
document.getElementById('stat-total').textContent = total;
|
||||
document.getElementById('stat-success').textContent = success;
|
||||
document.getElementById('stat-error').textContent = error;
|
||||
document.getElementById('summary').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
249
dev/frontend-navigation/test-api.php
Normal file
249
dev/frontend-navigation/test-api.php
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test-Script für die Navigation API
|
||||
*
|
||||
* Dieses Script testet alle verfügbaren Endpunkte der Navigation API
|
||||
* und zeigt die Ergebnisse in einer strukturierten Form.
|
||||
*
|
||||
* Verwendung:
|
||||
* 1. Passe die BASE_URL an deine Umgebung an
|
||||
* 2. Führe aus: php test-api.php
|
||||
*/
|
||||
|
||||
// Konfiguration
|
||||
define('BASE_URL', 'http://localhost'); // Passe dies an deine Umgebung an
|
||||
define('API_PREFIX', '/api/navigation');
|
||||
|
||||
// Terminal Farben
|
||||
define('COLOR_GREEN', "\033[0;32m");
|
||||
define('COLOR_RED', "\033[0;31m");
|
||||
define('COLOR_YELLOW', "\033[0;33m");
|
||||
define('COLOR_BLUE', "\033[0;34m");
|
||||
define('COLOR_RESET', "\033[0m");
|
||||
|
||||
/**
|
||||
* Führt einen API-Request aus
|
||||
*/
|
||||
function apiRequest($endpoint, $method = 'GET')
|
||||
{
|
||||
$url = BASE_URL . API_PREFIX . $endpoint;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$startTime = microtime(true);
|
||||
$response = curl_exec($ch);
|
||||
$endTime = microtime(true);
|
||||
$duration = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $error,
|
||||
'duration' => $duration
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'http_code' => $httpCode,
|
||||
'data' => json_decode($response, true),
|
||||
'duration' => $duration
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabe-Helfer
|
||||
*/
|
||||
function printHeader($text)
|
||||
{
|
||||
echo "\n" . COLOR_BLUE . str_repeat('=', 80) . COLOR_RESET . "\n";
|
||||
echo COLOR_BLUE . $text . COLOR_RESET . "\n";
|
||||
echo COLOR_BLUE . str_repeat('=', 80) . COLOR_RESET . "\n\n";
|
||||
}
|
||||
|
||||
function printSuccess($text)
|
||||
{
|
||||
echo COLOR_GREEN . "✓ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printError($text)
|
||||
{
|
||||
echo COLOR_RED . "✗ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printInfo($text)
|
||||
{
|
||||
echo COLOR_YELLOW . "ℹ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printJson($data, $maxDepth = 3, $currentDepth = 0)
|
||||
{
|
||||
if ($currentDepth >= $maxDepth) {
|
||||
echo "[... gekürzt nach Tiefe $maxDepth]\n";
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet einen einzelnen Endpunkt
|
||||
*/
|
||||
function testEndpoint($name, $endpoint, $method = 'GET', $showFullData = false)
|
||||
{
|
||||
printInfo("Teste: $name");
|
||||
echo " Endpunkt: $endpoint\n";
|
||||
echo " Methode: $method\n";
|
||||
|
||||
$result = apiRequest($endpoint, $method);
|
||||
|
||||
if (!$result['success']) {
|
||||
printError("Request fehlgeschlagen: " . $result['error']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$httpCode = $result['http_code'];
|
||||
$data = $result['data'];
|
||||
$duration = $result['duration'];
|
||||
|
||||
echo " HTTP Status: $httpCode\n";
|
||||
echo " Dauer: {$duration}ms\n";
|
||||
|
||||
if ($httpCode === 200) {
|
||||
printSuccess("Erfolgreich");
|
||||
|
||||
if (isset($data['success']) && $data['success']) {
|
||||
if (isset($data['meta'])) {
|
||||
echo "\n Metadaten:\n";
|
||||
foreach ($data['meta'] as $key => $value) {
|
||||
echo " $key: $value\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($showFullData && isset($data['data'])) {
|
||||
echo "\n Daten (gekürzt):\n";
|
||||
$preview = $data['data'];
|
||||
if (is_array($preview) && count($preview) > 2) {
|
||||
$preview = array_slice($preview, 0, 2);
|
||||
echo " " . json_encode($preview, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
echo " ... und " . (count($data['data']) - 2) . " weitere Einträge\n";
|
||||
} else {
|
||||
printJson($preview, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printError("HTTP-Fehler $httpCode");
|
||||
if (isset($data['error'])) {
|
||||
echo " Fehlermeldung: " . $data['error'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
return $httpCode === 200;
|
||||
}
|
||||
|
||||
// Hauptprogramm
|
||||
printHeader("Navigation API Test Suite");
|
||||
|
||||
echo "Base URL: " . BASE_URL . "\n";
|
||||
echo "API Prefix: " . API_PREFIX . "\n\n";
|
||||
|
||||
$results = [];
|
||||
|
||||
// Test 1: Kompletter Navigationsbaum
|
||||
printHeader("Test 1: Kompletter Navigationsbaum");
|
||||
$results['tree'] = testEndpoint(
|
||||
"Kompletter Navigationsbaum",
|
||||
"/tree",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 2: Nur aktive Navigationspunkte
|
||||
printHeader("Test 2: Nur aktive Navigationspunkte");
|
||||
$results['tree_active'] = testEndpoint(
|
||||
"Aktive Navigationspunkte",
|
||||
"/tree/active",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 3: Teilbaum (ersetze 1 mit einer existierenden Page-ID)
|
||||
printHeader("Test 3: Teilbaum ab Root-ID 1");
|
||||
$results['subtree'] = testEndpoint(
|
||||
"Teilbaum ab Root-ID 1",
|
||||
"/tree/1",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 4: Flache Liste
|
||||
printHeader("Test 4: Flache Liste aller Navigationspunkte");
|
||||
$results['flat'] = testEndpoint(
|
||||
"Flache Liste",
|
||||
"/flat",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 5: Breadcrumb (ersetze 1 mit einer existierenden Page-ID)
|
||||
printHeader("Test 5: Breadcrumb für Page-ID 1");
|
||||
$results['breadcrumb'] = testEndpoint(
|
||||
"Breadcrumb für Page-ID 1",
|
||||
"/breadcrumb/1",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 6: Cache leeren
|
||||
printHeader("Test 6: Cache leeren");
|
||||
$results['cache_clear'] = testEndpoint(
|
||||
"Cache leeren",
|
||||
"/cache/clear",
|
||||
"POST",
|
||||
false
|
||||
);
|
||||
|
||||
// Zusammenfassung
|
||||
printHeader("Test-Zusammenfassung");
|
||||
|
||||
$total = count($results);
|
||||
$passed = count(array_filter($results));
|
||||
$failed = $total - $passed;
|
||||
|
||||
echo "Gesamt: $total Tests\n";
|
||||
printSuccess("Erfolgreich: $passed");
|
||||
if ($failed > 0) {
|
||||
printError("Fehlgeschlagen: $failed");
|
||||
}
|
||||
|
||||
$percentage = round(($passed / $total) * 100, 1);
|
||||
echo "\nErfolgsrate: $percentage%\n";
|
||||
|
||||
if ($percentage === 100.0) {
|
||||
printSuccess("\nAlle Tests bestanden! 🎉");
|
||||
} else {
|
||||
printError("\nEinige Tests sind fehlgeschlagen. Bitte überprüfen Sie die Ausgabe oben.");
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Weitere Hinweise
|
||||
printHeader("Weitere Informationen");
|
||||
echo "Vollständige API-Dokumentation: dev/frontend-navigation/navigation-api.md\n";
|
||||
echo "README: dev/frontend-navigation/README.md\n\n";
|
||||
echo "Hinweis: Wenn Tests fehlschlagen, überprüfen Sie:\n";
|
||||
echo " 1. Ist die BASE_URL korrekt?\n";
|
||||
echo " 2. Läuft der Server?\n";
|
||||
echo " 3. Sind Page-Einträge in der Datenbank vorhanden?\n";
|
||||
echo " 4. Sind die Routen korrekt registriert?\n\n";
|
||||
Loading…
Add table
Add a link
Reference in a new issue