lindesbs/uibuilder
Composer 安装命令:
composer require lindesbs/uibuilder
包简介
Attribute-driven CRUD UI builder for Symfony
README 文档
README
Attribut-getriebener CRUD-UI-Builder für Symfony 7.4 und 8. Aus einer Doctrine-Entity mit zwei PHP-Attributen (#[UiResource], #[UiField]) entsteht automatisch eine List-/Detail-/Bearbeiten-/Löschen-Oberfläche inklusive Routing — ohne Controller, Formular-Klassen oder Templates von Hand schreiben zu müssen.
Features
- Attribute statt Boilerplate: Resource- und Feld-Definition direkt an der Entity.
- Automatisches Routing über einen eigenen Route-Loader (
type: lindesbs_uibuilder), inkl. konfigurierbarer Routennamen (siehe#[UiResource]). - Generischer Controller für list/display/new/edit/delete/bulk_delete, austauschbar über
den
operations-Parameter. - Pagination, Sortierung und Suche in der Listenansicht, ohne dass eine Resource dafür extra Code braucht (siehe Listen: Pagination, Sortierung, Suche).
- Reine View-Fragmente, keine vollständigen Seiten: Die Templates rendern nur den
CRUD-Inhalt (Tabelle, Formular, Detailansicht) — kein
<html>/<head>, kein Layout. Die konsumierende App bindet diese Views über eigene Twig-Funktionen in ihre eigenen Templates ein, siehe Rendering. - Erweiterbare Feldtypen (
text,bool,datetime,enum,many_to_one,collection,readonly) via Tagged Service. - Austauschbarer Datenzugriff über
DataProviderInterface(Standard: Doctrine ORM), erweiterbar durch Vererbung vonDoctrineOrmDataProviderfür Scoping/Owner-Filter. - Security grob per Resource (
security) und granular pro Datensatz über einen Voter-Hook (voterAttributes) für display/edit/delete. - Optionales Resource-Dashboard, das alle registrierten Resourcen aufzählt.
- Übersetzbare Labels:
#[UiField]/#[UiResource]-Labels laufen in allen Templates durch Twigstrans-Filter (siehe Übersetzung). Operation-Konstantenklasse (Lindesbs\UiBuilder\Metadata\Operation) statt Rohstrings füroperations/visibleOn/Routen-Keys — schützt vor Tippfehlern, die sonst eine Operation stillschweigend deaktivieren würden.
Installation
Das Package wird als Composer-Path-Repository eingebunden (siehe lib/lindesbs/uibuilder in diesem Monorepo):
// composer.json der konsumierenden App
{
"repositories": [
{ "type": "path", "url": "lib/lindesbs/uibuilder", "options": { "symlink": true } }
],
"require": {
"lindesbs/uibuilder": "*@dev"
}
}
ddev composer require "lindesbs/uibuilder:*@dev"
Symfony Flex registriert das Bundle automatisch in config/bundles.php
(Lindesbs\UiBuilder\LindesbsUiBuilderBundle). Zwei Dateien müssen manuell ergänzt werden, da es keine offizielle Flex-Recipe gibt:
config/packages/lindesbs_ui_builder.yaml
lindesbs_ui_builder:
resources:
- App\Entity\Host
- App\Entity\User
- App\Entity\ApiToken
config/routes.yaml
lindesbs_ui_builder:
resource: .
type: lindesbs_uibuilder
Resourcen werden nicht automatisch entdeckt — jede Entity-Klasse muss explizit unter resources eingetragen sein.
Verwendung
use Lindesbs\UiBuilder\Attribute\UiField;
use Lindesbs\UiBuilder\Attribute\UiResource;
use Lindesbs\UiBuilder\Metadata\Operation;
#[ORM\Entity]
#[UiResource(name: 'host', label: 'Hosts')]
class Host
{
#[ORM\ManyToOne]
#[UiField(label: 'Besitzer', type: 'many_to_one', order: 10, options: [
'target_class' => User::class,
'choice_label' => 'username',
'required' => true,
])]
private ?User $owner = null;
#[ORM\Column]
#[UiField(label: 'Name', type: 'text', order: 20, options: ['required' => true])]
private string $name = '';
#[ORM\Column]
#[UiField(label: 'Gesperrt', type: 'bool', visibleOn: [Operation::LIST, Operation::DISPLAY], order: 30)]
private bool $blocked = false;
}
Das ergibt unter /studio/ui/host/ eine vollständige Liste, Detail-, Bearbeiten- und Löschen-Ansicht.
#[UiResource]
| Parameter | Beschreibung | Default |
|---|---|---|
name | Slug der Resource, taucht in Route und Pfad auf (/studio/ui/{name}) | – |
label | Anzeigename in der UI — Anzeigetext oder Übersetzungsschlüssel, läuft in Templates durch trans (siehe Übersetzung) | – |
routePrefix | Basis-Pfad | /studio/ui |
routeNamePrefix | Präfix für generierte Routennamen | uibuilder_ |
operations | Erlaubte Aktionen, als Operation::*-Konstanten (Operation::LIST, DISPLAY, NEW, EDIT, DELETE, BULK_DELETE) | [Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::DELETE] |
security | An denyAccessUnlessGranted() übergebene Rolle/Voter-Attribut, geprüft je Request (nicht pro Zeile/Entity) | null |
voterAttributes | Voter-Attribut je Operation (Keys: Operation::DISPLAY/EDIT/DELETE), geprüft nach dem Laden der Entity via denyAccessUnlessGranted($attribut, $entity) — für entity-scoped Autorisierung | [] |
templates | Template-Overrides je Operation (Keys: Operation::*), z. B. [Operation::LIST => 'admin/host/index.html.twig'] — überschreibt nur die genannten Operationen, der Rest bleibt beim Bundle-Default | [] |
routeNames | Routennamen-Suffix-Overrides je Operation, z. B. [Operation::LIST => 'index', Operation::DISPLAY => 'show'] | Operationsname selbst |
redirectAfterEdit | Ziel-Operation für den Redirect nach erfolgreichem Speichern im Edit-/Neu-Formular: Operation::LIST oder Operation::DISPLAY | Operation::LIST |
Alle Parameter, die Operationen referenzieren (operations, voterAttributes-Keys, templates-Keys,
routeNames-Keys, redirectAfterEdit), sollten die Konstanten aus Lindesbs\UiBuilder\Metadata\Operation
statt Rohstrings nutzen — siehe Operation-Konstanten.
#[UiField]
| Parameter | Beschreibung | Default |
|---|---|---|
label | Spalten-/Formularbeschriftung — Anzeigetext oder Übersetzungsschlüssel (siehe Übersetzung) | – |
type | Feldtyp-Key (siehe docs/field-types.md) | – |
visibleOn | Auf welchen Views das Feld erscheint, als Operation::*-Konstanten | [Operation::LIST, Operation::DISPLAY, Operation::EDIT] |
order | Sortierung (aufsteigend) | 0 |
options | Feldtyp-spezifische Optionen | [] |
Attribute funktionieren auf Properties und öffentlichen Methoden (z. B. für berechnete Werte).
Operation-Konstanten
Lindesbs\UiBuilder\Metadata\Operation bündelt alle Operation-Identifier als String-Konstanten
(LIST, DISPLAY, NEW, EDIT, DELETE, BULK_DELETE). Eine bewusst einfache Klasse mit
public const string-Werten statt eines nativen PHP-Enums, damit Operation-Identifier weiterhin
plain Strings bleiben (attribut-kompatibel, erweiterbar um eigene Operationsnamen). Der Zweck ist
Tippfehler-Sicherheit: ein falsch geschriebener Roh-String ('lsit' statt 'list') deaktiviert eine
Operation ohne Fehlermeldung — mit den Konstanten fängt das die IDE/der Compiler ab.
use Lindesbs\UiBuilder\Metadata\Operation;
#[UiResource(
name: 'setting',
label: 'Einstellung',
operations: [Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::NEW],
)]
class Setting { /* ... */ }
Übersetzung
label auf #[UiField] und #[UiResource] kann ein Anzeigetext oder ein Übersetzungsschlüssel
sein — beides wird identisch behandelt, da alle Templates das Label über den Twig-trans-Filter
ausgeben (Standard-Domain messages, aktuelle Locale). Ohne passenden Katalog-Eintrag gibt Symfonys
Übersetzer den Wert unverändert zurück (Identity-Fallback) — bestehender Code mit reinen
Anzeigetexten als Label funktioniert daher unverändert weiter.
# translations/messages.de.yaml
Besitzer: Eigentümer
#[UiField(label: 'Besitzer', type: 'many_to_one', order: 10)]
private ?User $owner = null;
Flash-Messages im generischen Controller ("%s wurde gespeichert." etc.) übersetzen resource.label
ebenfalls, bevor sie interpoliert werden.
Rendering
GenericCrudController (und damit auch die von UiBuilderRouteLoader generierten Routen) rendert
ausschließlich Content-Fragmente — @LindesbsUiBuilder/crud/{list,display,new,edit}.html.twig
enthalten kein <!DOCTYPE>, kein <html>, kein Layout, kein Flash-Message-Rendering. UiBuilder
erzeugt bewusst keine vollständige Webseite. Eine App, die eine dieser Routen direkt aufruft,
bekommt daher nur den rohen Inhalt zurück — für den produktiven Einsatz bindet man die View
stattdessen ins eigene Template ein.
Views ins eigene Template einbinden
Vier Twig-Funktionen rendern die jeweilige View als Fragment per internem Sub-Request (Symfonys
Fragment-Mechanismus, Renderer inline — voller Zugriff auf Request/Session/Security wie bei einer
normalen Controller-Action):
{# templates/admin/settings.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="card">
<div class="card-body">
{{ uibuilder_list('setting') }}
</div>
</div>
{% endblock %}
| Funktion | Entspricht |
|---|---|
uibuilder_list(resource) | GenericCrudController::list() |
uibuilder_display(resource, id) | GenericCrudController::display() |
uibuilder_new(resource) | GenericCrudController::new() |
uibuilder_edit(resource, id) | GenericCrudController::edit() |
Die App entscheidet komplett selbst, unter welcher eigenen Route/welchem eigenen Controller sie das
einbindet — die von UiBuilder generierten Routen ({routeNamePrefix}{name}_list etc.) müssen dafür
nicht einmal aufgerufen werden, sie existieren unabhängig davon weiter (z. B. für Formular-Aktionen
wie Speichern/Löschen, deren path()-Ziele innerhalb der Fragmente liegen).
Eigenes Aussehen
Für volle Kontrolle über Markup/CSS-Framework bleibt #[UiResource(templates: [...])] (siehe
Tabelle oben) der empfohlene Weg — eigene Templates, die z. B. direkt {% extends 'base.html.twig'
%} nutzen (so wie es dieses Projekt für Host macht, siehe templates: [...] in dessen
#[UiResource]). Ein Bundle-weiter Template-Override
(docs/extending.md) funktioniert ebenfalls weiterhin.
CRUD-Controller minimieren
Da routePrefix, routeNamePrefix und operations frei pro Resource konfigurierbar sind, muss
für Standard-CRUD (list/display/edit/delete) kein eigener Controller geschrieben werden — die
generierten Routen und GenericCrudController decken das vollständig ab. Ein eigener Controller
wird dann nur noch für Aktionen gebraucht, die über reines CRUD hinausgehen (Bulk-Aktionen,
Sonder-Workflows etc.) und kann als schlanker, separater Controller neben den generierten Routen
existieren:
#[UiResource(
name: 'host',
label: 'Hosts',
routePrefix: '/studio/hosts',
routeNamePrefix: 'app_admin_hosts_',
operations: [Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::DELETE],
)]
class Host { /* ... */ }
// Nur noch die Sonderaktion, kein list/display/edit/delete mehr nötig:
final class HostBulkActionsController extends AbstractController
{
#[Route('/studio/hosts/bulk-delete', name: 'app_admin_hosts_bulk_delete', methods: ['POST'])]
public function bulkDelete(Request $request): Response { /* ... */ }
}
display/edit/delete nutzen intern {id} mit dem Muster [^/]+ (ein einzelnes Pfad-Segment,
keine Slashes). Eigene zusätzliche Pfad-Segmente unter demselben routePrefix auf gleicher Ebene
(z. B. /new neben /{id}) kollidieren dadurch nicht mit {id}/edit oder {id}/delete, könnten
aber — je nach Registrierungsreihenfolge — weiterhin mit der {id}-Route selbst kollidieren (z. B.
würde /new sonst als id=new interpretiert). Eigene Routen auf derselben Ebene wie {id} daher
entweder vor dem UiBuilder-Routenimport importieren, oder die Resource unter einem eigenen,
nicht überlappenden routePrefix mounten (z. B. /studio/ui/host statt /studio/hosts).
Soll die bestehende Optik (eigene Templates, eigenes CSS) erhalten bleiben, obwohl list/display/edit
über GenericCrudController laufen, kombiniert man das mit #[UiResource(templates: [...])] und ggf.
redirectAfterEdit: Operation::DISPLAY, wenn nach dem Speichern auf die Detailseite statt auf die
Liste verlinkt werden soll.
Neue Einträge anlegen (new)
#[UiResource(name: 'setting', label: 'Einstellung', operations: [Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::DELETE, Operation::NEW])]
class Setting { /* parameterloser Konstruktor */ }
Erzeugt zusätzlich GET|POST {routePrefix}/{name}/new. Die Entity wird per new $class() erzeugt
und mit dem Formular aus den für edit sichtbaren Feldern befüllt (Felder, die kein eigenes new
in visibleOn haben, erben automatisch die Sichtbarkeit von edit). Passt für Entities mit
parameterlosem Konstruktor und ohne Pflicht-Defaulting (UUID-Generierung, aktueller Nutzer als
Owner etc.) — für solche Fälle bleibt ein schlanker eigener Controller (siehe
CRUD-Controller minimieren) die bessere Wahl.
Bulk-Delete (bulk_delete)
#[UiResource(name: 'setting', label: 'Einstellung', operations: [Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::DELETE, Operation::BULK_DELETE])]
class Setting { /* ... */ }
Erzeugt POST {routePrefix}/{name}/bulk-delete (CSRF-Token-ID: bulk_delete_{name}) und blendet in
der Standard-Listenansicht automatisch Checkboxen samt "Ausgewählte löschen"-Button ein. Jeder
Eintrag wird einzeln über voterAttributes[Operation::DELETE] geprüft, falls konfiguriert.
Listen: Pagination, Sortierung, Suche
GenericCrudController::list() liest folgende Query-Parameter und reicht sie als ListCriteria an
DataProviderInterface::list() durch: page, per_page (max. 200), sort, dir (asc/desc),
q (Freitextsuche). DoctrineOrmDataProvider sucht dabei über alle text-Feldtypen, die auf
list sichtbar sind, und sortiert nur nach Feldern, die tatsächlich als #[UiField] auf list
sichtbar sind (Schutz vor beliebigen Sortier-Pfaden über den Query-Parameter). Das Standard-Template
zeigt eine Suchleiste, sortierbare Spaltenköpfe und einen Pager an.
ListResult (Rückgabe von list()) bietet items, totalCount, page, perPage,
totalPages(), hasPrevious(), hasNext(). Eigene Datenquellen (siehe
docs/extending.md) können createListQueryBuilder(),
applySearch() und applySort() aus DoctrineOrmDataProvider überschreiben statt alles neu zu
implementieren.
Resource-Dashboard
# config/packages/lindesbs_ui_builder.yaml
lindesbs_ui_builder:
dashboard_path: /studio/ui
Registriert GET {dashboard_path} (Routenname uibuilder_dashboard), das alle über resources
registrierten Resourcen mit Label und Link zur jeweiligen Liste auflistet. Ohne dashboard_path
(Default null) wird keine Route registriert.
Weiterführende Docs
- docs/field-types.md — eingebaute Feldtypen und ihre Optionen
- docs/configuration.md — Bundle-Konfiguration im Detail
- docs/extending.md — eigene Feldtypen und Datenquellen
Bekannte Einschränkungen
- Die von
UiBuilderRouteLoadergenerierten Routen liefern nur das Content-Fragment, kein vollständiges HTML-Dokument — direkter Aufruf im Browser zeigt unstyled/chromeless Inhalt. Für ein vollwertiges Nutzererlebnis muss die App die View peruibuilder_*()-Twig-Funktion in ein eigenes Template einbetten (siehe Rendering). collection-Feldtyp ist bewusst simpel (komma-separierter String, keine Objekt-Arrays/echten Collections).many_to_one:formatValue()zeigt in List/Display den Klassennamen, falls wederchoice_labelnoch__toString()verfügbar sind.- Suche/Sortierung berücksichtigen nur einfache Property-Pfade — methodenbasierte Felder (
getX()) sind davon ausgenommen. voterAttributesdeckt nur display/edit/delete/bulk_delete ab; Owner-Scoping fürlist()(welche Zeilen überhaupt auftauchen) bleibt Aufgabe einer eigenenDataProviderInterface-Implementierung.- Resourcen müssen explizit konfiguriert werden, kein Auto-Discovery über Doctrine-Metadaten.
Entwicklung
# Tests (aus dem App-Root heraus, nutzt deren vendor/)
ddev exec bash -c "cd lib/lindesbs/uibuilder && ../../../vendor/bin/phpunit"
# Coding Standard prüfen / fixen
ddev exec bash -c "cd lib/lindesbs/uibuilder && ../../../vendor/bin/ecs check"
ddev exec bash -c "cd lib/lindesbs/uibuilder && ../../../vendor/bin/ecs check --fix"
Entwicklungsweise & Transparenz
Dieses Projekt entsteht in enger menschlich geführter Entwicklung mit Unterstützung durch LLMs (z. B. Cursor). Die KI wird bewusst eingesetzt für:
- Debugging und Fehleranalyse
- Mockups und UI-/Ablauf-Skizzen und natürlich CSS (!!!)
- Durchspielen von Abläufen vor der Implementierung
- Code-Review und Konsistenzprüfungen
- Planung (Roadmap, Architektur, Wishlist)
Architektur-Entscheidungen, Review und Freigabe liegen beim Autor. Automatisierte Checks (ECS, PHPStan, PHPUnit) sichern den Code zusätzlich ab.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-08