13 KiB
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
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
$config = CrudConfig::forResource(
resource: 'users',
resourceName: 'User',
title: 'Users'
);
Spalten definieren
$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
$config = $config->withPermissions(
canCreate: true,
canEdit: true,
canView: true,
canDelete: false // Delete deaktiviert
);
Bulk-Actions
$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
$config = $config->withFilterOptions([
'statuses' => ['draft', 'published', 'archived'],
'categories' => $categories,
'current_filters' => $activeFilters,
]);
Zusätzliche Actions
$config = $config->withAdditionalActions([
[
'url' => '/admin/resource/stats',
'label' => 'Statistics',
'icon' => 'chart-bar',
],
]);
Custom Table-Konfiguration
$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:
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:
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
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:
public function __construct(
private AdminPageRenderer $pageRenderer,
private AdminTableFactory $tableFactory,
private AdminFormFactory $formFactory,
private MyRepository $repository
) {}
Nachher:
public function __construct(
private CrudService $crudService,
private MyRepository $repository
) {
$this->config = CrudConfig::forResource(...);
}
Schritt 2: Index-Methode migrieren
Vorher:
public function index(): ViewResult
{
$tableConfig = AdminTableConfig::create(...);
$table = $this->tableFactory->create($tableConfig, $data);
return $this->pageRenderer->renderIndex(...);
}
Nachher:
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:
public function create(): ViewResult
{
$formConfig = new AdminFormConfig(...);
$form = $this->formFactory->create($formConfig);
return $this->pageRenderer->renderForm(...);
}
Nachher:
public function create(): ViewResult
{
return $this->crudService->renderCreate(
config: $this->config,
formFields: [...]
);
}
Schritt 4: Redirects migrieren
Vorher:
return new Redirect('/admin/resource', status: 303, flashMessage: 'Success');
Nachher:
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:
public function __construct(...)
{
$this->config = CrudConfig::forResource(...)
->withColumns([...])
->withBulkActions([...]);
}
2. Data-Transformation
Konvertiere Entities zu Arrays für die Tabelle:
$itemData = array_map(
fn($item) => $item->toArray(),
$items
);
3. Error-Handling
Verwende immer redirectWithError() für Fehler:
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: Datumsformatierungstatus: Status-Badgesboolean: Boolean-Werte mit Badgescurrency: Währungsformatierungnumber: Zahlenformatierungmasked: Maskierte Wertepreview: 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
- Einfache Controller (wie Asset Collections): Vollständige Migration möglich
- Mittlere Controller (wie Contents): Index und Redirects migrieren, LiveComponents bleiben
- 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:
'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.phpfür die vollständige API - Siehe
src/Framework/Admin/ValueObjects/CrudConfig.phpfür alle Konfigurationsoptionen - Siehe migrierte Controller für praktische Beispiele