# Neues Modul erstellen

Letzte Aktualisierung: 2026-03-26

## Ziel

Schritt-für-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) | `core/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
  core/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 (überschreibt 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-Prüfung für 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 führt 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-Änderung erneut ausführen.

## Schritt 7: Validieren

```bash
docker compose exec php php bin/console module:validate mein-modul
```

Prüft: 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 (prüfen automatisch Namespace-Isolation, Session-Key-Prefix, Manifest-Contract):

```bash
docker compose exec php vendor/bin/phpunit tests/Architecture/
```

---

## Erweitern: Häufige 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: `core/App/Module/Contracts/SessionProvider.php` (Methoden: `populate()`, `clear()`).

**Session-Key-Konvention:** `module.mein-modul.*` (wird per Architektur-Test geprüft).

### Layout-Kontext

`LayoutContextProvider` implementieren — liefert Daten für Templates bei jedem Page-Render:

```php
'layout_context_providers' => [
    \MintyPHP\Module\MeinModul\Providers\MeinModulLayoutProvider::class,
],
```

Interface: `core/App/Module/Contracts/LayoutContextProvider.php` (Methode: `provide()`).

**Key-Konvention:** Top-Level-Keys müssen namespaced sein: `mein-modul.nav`, nicht `nav`.

### Event-Listener

```php
'event_listeners' => [
    'user.created' => [
        \MintyPHP\Module\MeinModul\Listeners\UserCreatedListener::class,
    ],
],
```

Interface: `core/App/Module/Contracts/EventListener.php` (Methode: `handle(string $event, array $payload)`).

### Scheduler-Job

Siehe `/docs/howto-geplante-aufgaben.md` und `modules/audit/module.php` für Beispiele.

### Modul-eigene Datenbank

```php
'migrations_path' => 'db/updates',
```

SQL-Dateien unter `modules/mein-modul/db/updates/` — idempotent (IF NOT EXISTS, ON DUPLICATE KEY).
Ausgeführt über `php bin/console module:migrate`.

### Übersetzungen

```php
'i18n_path' => 'i18n',
```

Dateien: `modules/mein-modul/i18n/default_de.json`, `modules/mein-modul/i18n/default_en.json`.
Schlüssel 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 geprüft.

---

## 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 --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --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

## Häufige Fehler

| Problem | Ursache | Lösung |
|---|---|---|
| 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 ausgeführt | `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-Änderung | `php bin/console module:sync` |
| Namespace-Fehler in Tests | Modul-Klasse in Core-Namespace | Alle Klassen unter `MintyPHP\Module\<Name>\*` |
