fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
522
docs/claude/crud-system.md
Normal file
522
docs/claude/crud-system.md
Normal file
@@ -0,0 +1,522 @@
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user