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>
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:
module:migrate— SQL-Migrationen anwendenmodule:permissions-sync— Permissions registrierenmodule:build— Seiten-Symlinks erstellenmodule: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:validateist fehlerfreimodule:synclaeuft 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>\* |