Files
breadcrumb-the-shire/docs/howto-modul-erstellen.md
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

10 KiB

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) 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

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:

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

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

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

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:

<h1><?php e(t('Mein Modul')); ?></h1>
<p><?php e(t('Hier kommt der Inhalt.')); ?></p>

Schritt 6: Runtime synchronisieren

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

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

Prueft: Manifest-Syntax, Klassen-Existenz, Namespace-Konventionen, Verzeichnisstruktur.

Schritt 8: Testen

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):

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

Erweitern: Haeufige Erweiterungspunkte

UI-Slot: Sidebar-Tab

'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

'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):

'asset_groups' => [
    'mein-modul' => [
        'modules/mein-modul/css/pages/mein-modul.css',
    ],
],

Im View mit style_groups laden:

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:

'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 geprueft).

Layout-Kontext

LayoutContextProvider implementieren — liefert Daten fuer Templates bei jedem Page-Render:

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

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

Key-Konvention: Top-Level-Keys muessen namespaced sein: mein-modul.nav, nicht nav.

Event-Listener

'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 fuer Beispiele.

Modul-eigene Datenbank

'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

'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

'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 --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

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>\*