docs: add module creation tutorial and improve make:module guidance
Add howto-modul-erstellen.md covering the full module creation workflow: scaffold, manifest, container registration, first page, sync, validate, and test. Includes 9 extension point examples, merge checklist, and troubleshooting table. Documents the AuthorizationPolicy container registration requirement (policies with constructor params must be explicitly registered). Update make:module command output to reference the tutorial, manifest contract, and notifications as reference module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
363
docs/howto-modul-erstellen.md
Normal file
363
docs/howto-modul-erstellen.md
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
# Neues Modul erstellen
|
||||||
|
|
||||||
|
Letzte Aktualisierung: 2026-03-26
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Schritt-fuer-Schritt Anleitung: vom leeren Verzeichnis zum lauffaehigen Modul mit Route, Permission, UI-Slot und Test.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Laufende Dev-Umgebung (`docker compose up`)
|
||||||
|
- Vertraut mit dem Schichtmodell (Service, Repository, Pages) — siehe `/docs/tutorial-04-architektur-request-flow.md`
|
||||||
|
- Vertraut mit Permissions — siehe `/docs/howto-rbac-permissions-playbook.md`
|
||||||
|
|
||||||
|
## Referenzdokumentation
|
||||||
|
|
||||||
|
| Was | Wo |
|
||||||
|
|---|---|
|
||||||
|
| Manifest-Contract (verbindlich) | `/docs/reference-module-manifest-contract.md` |
|
||||||
|
| Manifest-JSON-Schema | `/.agents/contracts/module-manifest.schema.json` |
|
||||||
|
| Platform-Contracts (Interfaces) | `lib/App/Module/Contracts/` (5 Interfaces) |
|
||||||
|
| CLI-Kommandos | `/docs/reference-cli-commands.md` |
|
||||||
|
| Referenzmodul (vollstaendig) | `modules/notifications/` |
|
||||||
|
| Einfaches Modul | `modules/bookmarks/` |
|
||||||
|
|
||||||
|
## Schritt 1: Scaffold erzeugen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec php php bin/console make:module mein-modul
|
||||||
|
```
|
||||||
|
|
||||||
|
Erzeugt:
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/mein-modul/
|
||||||
|
module.php ← Manifest
|
||||||
|
lib/Module/MeinModul/
|
||||||
|
MeinModulContainerRegistrar.php ← DI-Registrierung
|
||||||
|
MeinModulAuthorizationPolicy.php ← Berechtigungspruefung
|
||||||
|
pages/ ← Seiten (file-based routing)
|
||||||
|
templates/ ← Modul-spezifische Templates
|
||||||
|
web/css/ ← Stylesheets
|
||||||
|
web/js/ ← JavaScript
|
||||||
|
tests/Module/MeinModul/ ← PHPUnit-Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schritt 2: Modul aktivieren
|
||||||
|
|
||||||
|
`config/modules.php` bearbeiten — Modul-ID in die Liste aufnehmen:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
'enabled_modules' => ['audit', 'addressbook', 'bookmarks', 'notifications', 'mein-modul'],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternative: `APP_ENABLED_MODULES` Umgebungsvariable (ueberschreibt die Datei).
|
||||||
|
|
||||||
|
## Schritt 3: Manifest konfigurieren
|
||||||
|
|
||||||
|
Die Datei `modules/mein-modul/module.php` enthaelt alle Deklarationen. Hier ein Minimal-Beispiel mit einer Route und einer Permission:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => 'mein-modul',
|
||||||
|
'version' => '0.1.0',
|
||||||
|
'enabled_by_default' => false,
|
||||||
|
'load_order' => 100,
|
||||||
|
'requires' => [],
|
||||||
|
|
||||||
|
'routes' => [
|
||||||
|
['path' => 'mein-modul', 'target' => 'mein-modul'],
|
||||||
|
],
|
||||||
|
'public_paths' => [],
|
||||||
|
'container_registrars' => [
|
||||||
|
\MintyPHP\Module\MeinModul\MeinModulContainerRegistrar::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'ui_slots' => [],
|
||||||
|
|
||||||
|
'authorization_policies' => [
|
||||||
|
\MintyPHP\Module\MeinModul\MeinModulAuthorizationPolicy::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'search_resources' => [],
|
||||||
|
'asset_groups' => [],
|
||||||
|
'scheduler_jobs' => [],
|
||||||
|
'layout_context_providers' => [],
|
||||||
|
'session_providers' => [],
|
||||||
|
|
||||||
|
'permissions' => [
|
||||||
|
[
|
||||||
|
'key' => 'mein_modul.view',
|
||||||
|
'description' => 'Can view mein-modul',
|
||||||
|
'active' => true,
|
||||||
|
'is_system' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'event_listeners' => [],
|
||||||
|
'deactivation_handler' => null,
|
||||||
|
'migrations_path' => null,
|
||||||
|
'i18n_path' => null,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Vollstaendige Felddokumentation: `/docs/reference-module-manifest-contract.md`.
|
||||||
|
|
||||||
|
## Schritt 4: Authorization Policy im Container registrieren
|
||||||
|
|
||||||
|
**Wichtig:** Die generierte `AuthorizationPolicy` hat einen Konstruktor-Parameter (`PermissionService`). Damit die Plattform sie instanziieren kann, muss sie im Container registriert werden.
|
||||||
|
|
||||||
|
In `MeinModulContainerRegistrar.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\MeinModul;
|
||||||
|
|
||||||
|
use MintyPHP\App\AppContainer;
|
||||||
|
use MintyPHP\App\Container\ContainerRegistrar;
|
||||||
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
|
||||||
|
final class MeinModulContainerRegistrar implements ContainerRegistrar
|
||||||
|
{
|
||||||
|
public function register(AppContainer $container): void
|
||||||
|
{
|
||||||
|
$container->set(MeinModulAuthorizationPolicy::class,
|
||||||
|
static fn (AppContainer $c): MeinModulAuthorizationPolicy => new MeinModulAuthorizationPolicy(
|
||||||
|
$c->get(PermissionService::class)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Weitere Services hier registrieren.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne diese Registrierung wird die Policy von `ModuleClassResolver` ignoriert und die Permission-Pruefung fuer UI-Slots schlaegt fehl (Slot wird nicht angezeigt).
|
||||||
|
|
||||||
|
## Schritt 5: Erste Seite anlegen
|
||||||
|
|
||||||
|
Datei `modules/mein-modul/pages/mein-modul/index().php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Buffer;
|
||||||
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
|
Guard::requireLogin();
|
||||||
|
Guard::requireAbilityOrForbidden(
|
||||||
|
\MintyPHP\Module\MeinModul\MeinModulAuthorizationPolicy::ABILITY_VIEW
|
||||||
|
);
|
||||||
|
|
||||||
|
Buffer::set('title', t('Mein Modul'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Datei `modules/mein-modul/pages/mein-modul/index(default).phtml`:
|
||||||
|
|
||||||
|
```phtml
|
||||||
|
<h1><?php e(t('Mein Modul')); ?></h1>
|
||||||
|
<p><?php e(t('Hier kommt der Inhalt.')); ?></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schritt 6: Runtime synchronisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec php php bin/console module:sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Befehl fuehrt automatisch aus:
|
||||||
|
|
||||||
|
1. `module:migrate` — SQL-Migrationen anwenden
|
||||||
|
2. `module:permissions-sync` — Permissions registrieren
|
||||||
|
3. `module:build` — Seiten-Symlinks erstellen
|
||||||
|
4. `module:assets-sync` — CSS/JS-Assets veroeffentlichen
|
||||||
|
|
||||||
|
Nach jedem Manifest-Aenderung erneut ausfuehren.
|
||||||
|
|
||||||
|
## Schritt 7: Validieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec php php bin/console module:validate mein-modul
|
||||||
|
```
|
||||||
|
|
||||||
|
Prueft: Manifest-Syntax, Klassen-Existenz, Namespace-Konventionen, Verzeichnisstruktur.
|
||||||
|
|
||||||
|
## Schritt 8: Testen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec php vendor/bin/phpunit --filter=MeinModul
|
||||||
|
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon modules/mein-modul/
|
||||||
|
```
|
||||||
|
|
||||||
|
Architektur-Tests (pruefen automatisch Namespace-Isolation, Session-Key-Prefix, Manifest-Contract):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec php vendor/bin/phpunit tests/Architecture/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erweitern: Haeufige Erweiterungspunkte
|
||||||
|
|
||||||
|
### UI-Slot: Sidebar-Tab
|
||||||
|
|
||||||
|
```php
|
||||||
|
'ui_slots' => [
|
||||||
|
'aside.tab_panel' => [
|
||||||
|
[
|
||||||
|
'key' => 'mein-modul',
|
||||||
|
'label' => 'Mein Modul',
|
||||||
|
'icon' => 'bi-puzzle',
|
||||||
|
'href' => 'mein-modul',
|
||||||
|
'permission' => 'mein_modul.view',
|
||||||
|
'panel_template' => 'templates/aside-panel.phtml',
|
||||||
|
'order' => 200,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Der `permission`-Wert muss dem Ability-String der AuthorizationPolicy entsprechen (hier: `mein_modul.view`).
|
||||||
|
|
||||||
|
### UI-Slot: Topbar-Button
|
||||||
|
|
||||||
|
```php
|
||||||
|
'ui_slots' => [
|
||||||
|
'topbar.right_item' => [
|
||||||
|
[
|
||||||
|
'key' => 'mein-modul-button',
|
||||||
|
'template' => 'templates/topbar-button.phtml',
|
||||||
|
'permission' => 'mein_modul.view',
|
||||||
|
'order' => 200,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS/JS-Assets
|
||||||
|
|
||||||
|
Dateien unter `modules/mein-modul/web/` werden per Symlink nach `web/modules/mein-modul/` veroeffentlicht.
|
||||||
|
|
||||||
|
Fuer seitenspezifische Assets (`asset_groups`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
'asset_groups' => [
|
||||||
|
'mein-modul' => [
|
||||||
|
'modules/mein-modul/css/pages/mein-modul.css',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Im View mit `style_groups` laden:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Buffer::set('style_groups', ['mein-modul']);
|
||||||
|
```
|
||||||
|
|
||||||
|
Fuer globale Assets (auf jeder Seite) stattdessen `layout.head_style` oder `runtime.component` verwenden.
|
||||||
|
|
||||||
|
### Globale Suche
|
||||||
|
|
||||||
|
`SearchResourceProvider` implementieren — siehe `/docs/howto-globale-suche.md` und `modules/addressbook/lib/Module/AddressBook/Providers/AddressBookSearchProvider.php`.
|
||||||
|
|
||||||
|
### Session-Daten bei Login
|
||||||
|
|
||||||
|
`SessionProvider` implementieren — populiert Modul-spezifische Session-Daten nach Login:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'session_providers' => [
|
||||||
|
\MintyPHP\Module\MeinModul\Providers\MeinModulSessionProvider::class,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Interface: `lib/App/Module/Contracts/SessionProvider.php` (Methoden: `populate()`, `clear()`).
|
||||||
|
|
||||||
|
**Session-Key-Konvention:** `module.mein-modul.*` (wird per Architektur-Test geprueft).
|
||||||
|
|
||||||
|
### Layout-Kontext
|
||||||
|
|
||||||
|
`LayoutContextProvider` implementieren — liefert Daten fuer Templates bei jedem Page-Render:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'layout_context_providers' => [
|
||||||
|
\MintyPHP\Module\MeinModul\Providers\MeinModulLayoutProvider::class,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Interface: `lib/App/Module/Contracts/LayoutContextProvider.php` (Methode: `provide()`).
|
||||||
|
|
||||||
|
**Key-Konvention:** Top-Level-Keys muessen namespaced sein: `mein-modul.nav`, nicht `nav`.
|
||||||
|
|
||||||
|
### Event-Listener
|
||||||
|
|
||||||
|
```php
|
||||||
|
'event_listeners' => [
|
||||||
|
'user.created' => [
|
||||||
|
\MintyPHP\Module\MeinModul\Listeners\UserCreatedListener::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Interface: `lib/App/Module/Contracts/EventListener.php` (Methode: `handle(string $event, array $payload)`).
|
||||||
|
|
||||||
|
### Scheduler-Job
|
||||||
|
|
||||||
|
Siehe `/docs/howto-geplante-aufgaben.md` und `modules/audit/module.php` fuer Beispiele.
|
||||||
|
|
||||||
|
### Modul-eigene Datenbank
|
||||||
|
|
||||||
|
```php
|
||||||
|
'migrations_path' => 'db/updates',
|
||||||
|
```
|
||||||
|
|
||||||
|
SQL-Dateien unter `modules/mein-modul/db/updates/` — idempotent (IF NOT EXISTS, ON DUPLICATE KEY).
|
||||||
|
Ausgefuehrt ueber `php bin/console module:migrate`.
|
||||||
|
|
||||||
|
### Uebersetzungen
|
||||||
|
|
||||||
|
```php
|
||||||
|
'i18n_path' => 'i18n',
|
||||||
|
```
|
||||||
|
|
||||||
|
Dateien: `modules/mein-modul/i18n/default_de.json`, `modules/mein-modul/i18n/default_en.json`.
|
||||||
|
Schluessel werden per `t('key')` aufgeloest — identisch zu Core-Translations.
|
||||||
|
|
||||||
|
### Abhaengigkeit von anderem Modul
|
||||||
|
|
||||||
|
```php
|
||||||
|
'requires' => ['notifications'],
|
||||||
|
```
|
||||||
|
|
||||||
|
Nicht-deklarierte Cross-Module-Imports sind verboten und werden per Architektur-Test geprueft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checkliste vor Merge
|
||||||
|
|
||||||
|
- [ ] `module:validate` ist fehlerfrei
|
||||||
|
- [ ] `module:sync` laeuft ohne Fehler
|
||||||
|
- [ ] PHPUnit gruent (`vendor/bin/phpunit`)
|
||||||
|
- [ ] PHPStan Level 5 gruent (`vendor/bin/phpstan analyse -c phpstan.neon`)
|
||||||
|
- [ ] Architektur-Tests gruent (`tests/Architecture/`)
|
||||||
|
- [ ] PHP-CS-Fixer gruent (`vendor/bin/php-cs-fixer fix --dry-run`)
|
||||||
|
- [ ] AuthorizationPolicy ist im Container registriert (falls Konstruktor-Parameter vorhanden)
|
||||||
|
- [ ] Alle sichtbaren Texte nutzen `t('...')`
|
||||||
|
- [ ] Session-Keys verwenden `module.<id>.*` Prefix
|
||||||
|
- [ ] Keine Core-Namespaces (`MintyPHP\Service\*`, `MintyPHP\Repository\*`) im Modul-Code
|
||||||
|
|
||||||
|
## Haeufige Fehler
|
||||||
|
|
||||||
|
| Problem | Ursache | Loesung |
|
||||||
|
|---|---|---|
|
||||||
|
| Sidebar-Tab/UI-Slot wird nicht angezeigt | AuthorizationPolicy nicht im Container registriert | Policy in ContainerRegistrar mit `PermissionService` registrieren |
|
||||||
|
| 404 auf Modul-Route | `module:sync` nicht ausgefuehrt | `php bin/console module:sync` |
|
||||||
|
| Permission existiert nicht | Permission nicht in Manifest deklariert | `permissions`-Array in `module.php` ergaenzen, dann `module:sync` |
|
||||||
|
| CSS/JS werden nicht geladen | Assets nicht veroeffentlicht | `php bin/console module:assets-sync` oder `module:sync` |
|
||||||
|
| RuntimeException: fingerprint mismatch | Build veraltet nach Manifest-Aenderung | `php bin/console module:sync` |
|
||||||
|
| Namespace-Fehler in Tests | Modul-Klasse in Core-Namespace | Alle Klassen unter `MintyPHP\Module\<Name>\*` |
|
||||||
@@ -55,6 +55,8 @@ Diese Dokumentation ist nach dem Diataxis-Modell in vier Quadranten gegliedert:
|
|||||||
- Haeufige Fehler und schnelle Loesungen.
|
- Haeufige Fehler und schnelle Loesungen.
|
||||||
- `/docs/howto-lokale-entwicklung.md`
|
- `/docs/howto-lokale-entwicklung.md`
|
||||||
- Tages-Workflow fuer Entwicklung und Checks.
|
- Tages-Workflow fuer Entwicklung und Checks.
|
||||||
|
- `/docs/howto-modul-erstellen.md`
|
||||||
|
- Neues Modul erstellen: Scaffold, Manifest, Route, Permission, UI-Slot, Test.
|
||||||
- `/docs/howto-dokumentation-erweitern.md`
|
- `/docs/howto-dokumentation-erweitern.md`
|
||||||
- Regeln fuer konsistente, wachsende Doku.
|
- Regeln fuer konsistente, wachsende Doku.
|
||||||
|
|
||||||
|
|||||||
@@ -127,8 +127,13 @@ final class ModuleCommand extends Command
|
|||||||
fwrite(STDOUT, " modules/{$moduleId}/tests/Module/{$pascalName}/\n");
|
fwrite(STDOUT, " modules/{$moduleId}/tests/Module/{$pascalName}/\n");
|
||||||
fwrite(STDOUT, "\nNext steps:\n");
|
fwrite(STDOUT, "\nNext steps:\n");
|
||||||
fwrite(STDOUT, " 1. Add '{$moduleId}' to config/modules.php enabled_modules\n");
|
fwrite(STDOUT, " 1. Add '{$moduleId}' to config/modules.php enabled_modules\n");
|
||||||
fwrite(STDOUT, " 2. Edit module.php to define routes, permissions, and UI slots\n");
|
fwrite(STDOUT, " 2. Register the AuthorizationPolicy in {$pascalName}ContainerRegistrar (required for UI slot visibility)\n");
|
||||||
fwrite(STDOUT, " 3. Run: php bin/console module:sync\n");
|
fwrite(STDOUT, " 3. Edit module.php to define routes, permissions, and UI slots\n");
|
||||||
|
fwrite(STDOUT, " 4. Run: php bin/console module:sync\n");
|
||||||
|
fwrite(STDOUT, "\nGuides:\n");
|
||||||
|
fwrite(STDOUT, " Tutorial: docs/howto-modul-erstellen.md\n");
|
||||||
|
fwrite(STDOUT, " Manifest contract: docs/reference-module-manifest-contract.md\n");
|
||||||
|
fwrite(STDOUT, " Reference module: modules/notifications/ (full-featured example)\n");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user