定制 lindesbs/uibuilder 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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 von DoctrineOrmDataProvider fü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 Twigs trans-Filter (siehe Übersetzung).
  • Operation-Konstantenklasse (Lindesbs\UiBuilder\Metadata\Operation) statt Rohstrings für operations/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]

ParameterBeschreibungDefault
nameSlug der Resource, taucht in Route und Pfad auf (/studio/ui/{name})
labelAnzeigename in der UI — Anzeigetext oder Übersetzungsschlüssel, läuft in Templates durch trans (siehe Übersetzung)
routePrefixBasis-Pfad/studio/ui
routeNamePrefixPräfix für generierte Routennamenuibuilder_
operationsErlaubte Aktionen, als Operation::*-Konstanten (Operation::LIST, DISPLAY, NEW, EDIT, DELETE, BULK_DELETE)[Operation::LIST, Operation::DISPLAY, Operation::EDIT, Operation::DELETE]
securityAn denyAccessUnlessGranted() übergebene Rolle/Voter-Attribut, geprüft je Request (nicht pro Zeile/Entity)null
voterAttributesVoter-Attribut je Operation (Keys: Operation::DISPLAY/EDIT/DELETE), geprüft nach dem Laden der Entity via denyAccessUnlessGranted($attribut, $entity) — für entity-scoped Autorisierung[]
templatesTemplate-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[]
routeNamesRoutennamen-Suffix-Overrides je Operation, z. B. [Operation::LIST => 'index', Operation::DISPLAY => 'show']Operationsname selbst
redirectAfterEditZiel-Operation für den Redirect nach erfolgreichem Speichern im Edit-/Neu-Formular: Operation::LIST oder Operation::DISPLAYOperation::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]

ParameterBeschreibungDefault
labelSpalten-/Formularbeschriftung — Anzeigetext oder Übersetzungsschlüssel (siehe Übersetzung)
typeFeldtyp-Key (siehe docs/field-types.md)
visibleOnAuf welchen Views das Feld erscheint, als Operation::*-Konstanten[Operation::LIST, Operation::DISPLAY, Operation::EDIT]
orderSortierung (aufsteigend)0
optionsFeldtyp-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 %}
FunktionEntspricht
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

Bekannte Einschränkungen

  • Die von UiBuilderRouteLoader generierten 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 per uibuilder_*()-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 weder choice_label noch __toString() verfügbar sind.
  • Suche/Sortierung berücksichtigen nur einfache Property-Pfade — methodenbasierte Felder (getX()) sind davon ausgenommen.
  • voterAttributes deckt nur display/edit/delete/bulk_delete ab; Owner-Scoping für list() (welche Zeilen überhaupt auftauchen) bleibt Aufgabe einer eigenen DataProviderInterface-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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-08

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固