Some checks failed
Deploy Application / deploy (push) Has been cancelled
523 lines
13 KiB
Markdown
523 lines
13 KiB
Markdown
# 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
|
|
|
|
|
|
|