# Einheitliches CRUD-System ## Übersicht Das einheitliche CRUD-System bietet eine zentrale Abstraktion für alle Admin-CRUD-Operationen im Framework. Es basiert auf Komposition statt Vererbung und folgt den Framework-Prinzipien. ## Architektur ### Komponenten - **CrudService**: Zentrale Service-Klasse für CRUD-Operationen - **CrudConfig**: Value Object für CRUD-Konfiguration - **AdminPageRenderer**: Rendering von Admin-Seiten (wird von CrudService verwendet) - **AdminTableFactory**: Erstellung von Tabellen (wird von CrudService verwendet) - **AdminFormFactory**: Erstellung von Formularen (wird von CrudService verwendet) ### Design-Prinzipien - **Komposition statt Vererbung**: Keine Base-Controller-Klassen - **Value Objects**: CrudConfig als immutable Value Object - **Dependency Injection**: Alle Abhängigkeiten werden injiziert - **Separation of Concerns**: Controller enthalten nur Business-Logik, Rendering wird delegiert ## Verwendung ### Grundlegende CRUD-Implementierung ```php final readonly class MyResourceController { private CrudConfig $config; public function __construct( private CrudService $crudService, private MyResourceRepository $repository ) { $this->config = CrudConfig::forResource( resource: 'my-resource', resourceName: 'My Resource', title: 'My Resources' )->withColumns([ ['field' => 'name', 'label' => 'Name', 'sortable' => true, 'searchable' => true], ['field' => 'created_at', 'label' => 'Created', 'sortable' => true, 'formatter' => 'date'], ]); } #[Route('/admin/my-resource', Method::GET)] public function index(Request $request): ViewResult { $items = $this->repository->findAll(); $itemData = array_map(fn($item) => $item->toArray(), $items); return $this->crudService->renderIndex( config: $this->config, items: $itemData, request: $request ); } #[Route('/admin/my-resource/create', Method::GET)] public function create(): ViewResult { return $this->crudService->renderCreate( config: $this->config, formFields: [ 'name' => [ 'type' => 'text', 'label' => 'Name', 'required' => true, ], ] ); } #[Route('/admin/my-resource', Method::POST)] public function store(Request $request): Redirect { try { $item = $this->repository->create($request->body->toArray()); return $this->crudService->redirectAfterCreate( config: $this->config, request: $request, itemId: $item->id->toString() ); } catch (\Throwable $e) { return $this->crudService->redirectWithError( 'Failed to create: ' . $e->getMessage(), $request->body->toArray() ); } } #[Route('/admin/my-resource/{id}/edit', Method::GET)] public function edit(string $id): ViewResult { $item = $this->repository->findById($id); return $this->crudService->renderEdit( config: $this->config, id: $id, formFields: [ 'name' => [ 'type' => 'text', 'label' => 'Name', 'required' => true, ], ], itemData: $item->toArray() ); } #[Route('/admin/my-resource/{id}', Method::PUT)] public function update(string $id, Request $request): Redirect { try { $this->repository->update($id, $request->body->toArray()); return $this->crudService->redirectAfterUpdate( config: $this->config, request: $request, itemId: $id ); } catch (\Throwable $e) { return $this->crudService->redirectWithError( 'Failed to update: ' . $e->getMessage(), $request->body->toArray() ); } } #[Route('/admin/my-resource/{id}', Method::DELETE)] public function destroy(string $id): Redirect { try { $this->repository->delete($id); return $this->crudService->redirectAfterDelete($this->config); } catch (\Throwable $e) { return $this->crudService->redirectWithError( 'Failed to delete: ' . $e->getMessage() ); } } } ``` ## CrudConfig Konfiguration ### Grundkonfiguration ```php $config = CrudConfig::forResource( resource: 'users', resourceName: 'User', title: 'Users' ); ``` ### Spalten definieren ```php $config = $config->withColumns([ // Format 1: Mit 'field' Key ['field' => 'name', 'label' => 'Name', 'sortable' => true, 'searchable' => true], // Format 2: Key ist der Feldname 'email' => ['label' => 'Email', 'sortable' => true, 'formatter' => 'email'], // Mit Formatter 'created_at' => ['label' => 'Created', 'sortable' => true, 'formatter' => 'date'], // Mit CSS-Klasse 'status' => ['label' => 'Status', 'formatter' => 'status', 'class' => 'text-center'], ]); ``` ### Berechtigungen konfigurieren ```php $config = $config->withPermissions( canCreate: true, canEdit: true, canView: true, canDelete: false // Delete deaktiviert ); ``` ### Bulk-Actions ```php $config = $config->withBulkActions([ [ 'label' => 'Delete', 'action' => 'delete', 'method' => 'DELETE', 'style' => 'danger', 'confirm' => 'Delete Selected Items', ], [ 'label' => 'Publish', 'action' => 'publish', 'method' => 'POST', 'style' => 'primary', ], ]); ``` ### Filter-Optionen ```php $config = $config->withFilterOptions([ 'statuses' => ['draft', 'published', 'archived'], 'categories' => $categories, 'current_filters' => $activeFilters, ]); ``` ### Zusätzliche Actions ```php $config = $config->withAdditionalActions([ [ 'url' => '/admin/resource/stats', 'label' => 'Statistics', 'icon' => 'chart-bar', ], ]); ``` ### Custom Table-Konfiguration ```php $config = $config->withTableConfig([ 'editUrlTemplate' => '/admin/custom-resource/{id}/edit', 'viewUrlTemplate' => '/admin/custom-resource/{id}/view', 'rowActions' => true, ]); ``` ## Erweiterte Verwendung ### Custom Filter-Logik Für komplexe Filter bleibt die Logik im Controller: ```php public function index(Request $request): ViewResult { // Custom filter logic $filters = MyFilters::fromArray($request->query->toArray()); $items = $this->repository->findWithFilters($filters); // Data enrichment $itemData = array_map(function ($item) { $data = $item->toArray(); $data['custom_field'] = $this->enrichData($item); return $data; }, $items); // Use CrudService with filter options $configWithFilters = $this->config->withFilterOptions([ 'filter_options' => $this->getFilterOptions(), 'current_filters' => $filters->toArray(), ]); return $this->crudService->renderIndex( config: $configWithFilters, items: $itemData, request: $request ); } ``` ### LiveComponents für spezielle Formulare Für komplexe Formulare können LiveComponents verwendet werden: ```php public function create(): ViewResult { // Use LiveComponent for complex form $component = $this->componentRegistry->resolve(...); return $this->pageRenderer->renderLiveForm( resource: 'my-resource', component: $component, title: 'Create Resource' ); } public function store(Request $request): Redirect { // After successful creation, use CrudService redirect return $this->crudService->redirectAfterCreate( config: $this->config, request: $request, itemId: $item->id->toString() ); } ``` ### Custom Show-View ```php public function show(string $id): ViewResult { $item = $this->repository->findById($id); return $this->crudService->renderShow( config: $this->config, id: $id, fields: [ ['label' => 'Name', 'value' => $item->name, 'type' => 'text'], ['label' => 'Status', 'value' => $item->status, 'type' => 'badge', 'color' => 'success'], ], metadata: [ 'created_at' => $item->createdAt->toIso8601(), 'updated_at' => $item->updatedAt->toIso8601(), ], relatedItems: [ 'related_data' => $this->getRelatedData($item), ], actions: [ ['type' => 'link', 'url' => "/admin/resource/{$id}/custom", 'label' => 'Custom Action'], ] ); } ``` ## Migration bestehender Controller ### Schritt 1: Dependencies ersetzen **Vorher:** ```php public function __construct( private AdminPageRenderer $pageRenderer, private AdminTableFactory $tableFactory, private AdminFormFactory $formFactory, private MyRepository $repository ) {} ``` **Nachher:** ```php public function __construct( private CrudService $crudService, private MyRepository $repository ) { $this->config = CrudConfig::forResource(...); } ``` ### Schritt 2: Index-Methode migrieren **Vorher:** ```php public function index(): ViewResult { $tableConfig = AdminTableConfig::create(...); $table = $this->tableFactory->create($tableConfig, $data); return $this->pageRenderer->renderIndex(...); } ``` **Nachher:** ```php public function index(Request $request): ViewResult { $items = $this->repository->findAll(); $itemData = array_map(fn($item) => $item->toArray(), $items); return $this->crudService->renderIndex( config: $this->config, items: $itemData, request: $request ); } ``` ### Schritt 3: Create/Edit migrieren **Vorher:** ```php public function create(): ViewResult { $formConfig = new AdminFormConfig(...); $form = $this->formFactory->create($formConfig); return $this->pageRenderer->renderForm(...); } ``` **Nachher:** ```php public function create(): ViewResult { return $this->crudService->renderCreate( config: $this->config, formFields: [...] ); } ``` ### Schritt 4: Redirects migrieren **Vorher:** ```php return new Redirect('/admin/resource', status: 303, flashMessage: 'Success'); ``` **Nachher:** ```php return $this->crudService->redirectAfterCreate( config: $this->config, request: $request, itemId: $item->id->toString() ); ``` ## Best Practices ### 1. CrudConfig im Constructor erstellen Erstelle die CrudConfig einmal im Constructor, nicht in jeder Methode: ```php public function __construct(...) { $this->config = CrudConfig::forResource(...) ->withColumns([...]) ->withBulkActions([...]); } ``` ### 2. Data-Transformation Konvertiere Entities zu Arrays für die Tabelle: ```php $itemData = array_map( fn($item) => $item->toArray(), $items ); ``` ### 3. Error-Handling Verwende immer `redirectWithError()` für Fehler: ```php try { // ... } catch (\Throwable $e) { return $this->crudService->redirectWithError( 'Failed: ' . $e->getMessage(), $request->body->toArray() // Preserve input for form ); } ``` ### 4. Custom-Logik bleibt im Controller Komplexe Filter, Data-Enrichment und spezielle Features bleiben im Controller. CrudService übernimmt nur das Standard-Rendering. ### 5. LiveComponents für komplexe Formulare Für sehr komplexe Formulare (z.B. mit Drag & Drop, Live-Updates) verwende LiveComponents statt Standard-Formulare. ## Verfügbare Formatter - `date`: Datumsformatierung - `status`: Status-Badges - `boolean`: Boolean-Werte mit Badges - `currency`: Währungsformatierung - `number`: Zahlenformatierung - `masked`: Maskierte Werte - `preview`: Preview-Format (z.B. für Bilder) ## URL-Templates Standard-URLs werden automatisch generiert: - Create: `/admin/{resource}/create` - Edit: `/admin/{resource}/edit/{id}` - View: `/admin/{resource}/view/{id}` - Delete: `/admin/{resource}/delete/{id}` Custom URLs können über `withTableConfig()` konfiguriert werden. ## Beispiele Siehe `src/Framework/Admin/Examples/CampaignCrudController.php` für ein vollständiges Beispiel. ## Migration-Guide 1. **Einfache Controller** (wie Asset Collections): Vollständige Migration möglich 2. **Mittlere Controller** (wie Contents): Index und Redirects migrieren, LiveComponents bleiben 3. **Komplexe Controller** (wie Assets): Hybrid-Ansatz - Standard-CRUD über Service, Custom-Features bleiben ## Troubleshooting ### Problem: Spalten werden nicht angezeigt **Lösung**: Stelle sicher, dass die Spalten-Namen in `withColumns()` mit den Keys in den Daten übereinstimmen. ### Problem: Formular-Felder werden nicht angezeigt **Lösung**: Verwende das korrekte Format für `formFields`: ```php 'field_name' => [ 'type' => 'text', 'label' => 'Label', 'required' => true, ] ``` ### Problem: Redirects funktionieren nicht **Lösung**: Stelle sicher, dass `Request $request` als Parameter übergeben wird. ## Weitere Informationen - Siehe `src/Framework/Admin/Services/CrudService.php` für die vollständige API - Siehe `src/Framework/Admin/ValueObjects/CrudConfig.php` für alle Konfigurationsoptionen - Siehe migrierte Controller für praktische Beispiele