Dokumentation durchblättern
Module entwickeln
Schnelleinstieg: ein Modul in 15 Minuten
Schritt für Schritt ein lauffähiges Modul von Grund auf bauen: Verzeichnis, Manifest, ServiceProvider, Controller, Views, Migration und Übersetzungen — bis zum aktivierten Modul.
Diese Anleitung baut ein vollständiges, lauffähiges Modul von Grund auf. Sie ist der beste Einstieg, wenn Sie die Konzepte aus den vorigen Kapiteln einmal in Aktion sehen wollen — am Ende haben Sie ein Modul mit Menüpunkt, Einstellung, Dashboard-Widget, eigener Tabelle und Übersetzungen.
Was wir bauen
Ein einfaches „Notes”-Modul, das
- eine Einstellungs-Seite in die Admin-Seitenleiste einhängt,
- eine konfigurierbare Willkommensnachricht speichert,
- ein Dashboard-Widget anzeigt,
- eine eigene Datenbanktabelle mitbringt
- und deutsche wie englische Übersetzungen enthält.
Schritt 1: Verzeichnisstruktur anlegen
mkdir -p modules/notes/src/Http/Controllers
mkdir -p modules/notes/resources/views/widgets
mkdir -p modules/notes/database/migrations
mkdir -p modules/notes/lang/de
mkdir -p modules/notes/lang/en
Schritt 2: module.json schreiben
{
"name": {
"de": "Notizen",
"en": "Notes"
},
"version": "1.0.0",
"namespace": "Schneespur\\Module\\Notes",
"service_provider": "Schneespur\\Module\\Notes\\NotesServiceProvider",
"description": {
"de": "Einfaches Notiz-System für Einsätze.",
"en": "Simple notes system for service jobs."
},
"min_schneespur_version": "1.0.0",
"requires_permissions": [],
"default_enabled": true,
"requires": {},
"conflicts": []
}
Die Felder im Detail erklärt das Kapitel Manifest. Hier
steht default_enabled auf true, damit das Beispiel direkt nach dem Migrieren läuft;
produktive Module lassen das in der Regel auf false.
Schritt 3: Den ServiceProvider schreiben
modules/notes/src/NotesServiceProvider.php:
<?php
namespace Schneespur\Module\Notes;
use App\Services\Extension\DashboardWidgetRegistry;
use App\Services\Extension\NavigationRegistry;
use App\Services\ModuleManager;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class NotesServiceProvider extends ServiceProvider
{
public function register(): void {}
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'notes');
// Standard-Einstellungen
app(ModuleManager::class)->registerSettings('notes', [
'welcome_message' => 'Willkommen im Notiz-System!',
]);
// Navigation
app(NavigationRegistry::class)->addItem(
group: 'system',
slug: 'notes-settings',
label: __('notes::messages.nav_label'),
route: 'admin.notes.settings',
icon: 'heroicon-o-document-text',
order: 195,
);
// Dashboard-Widget
app(DashboardWidgetRegistry::class)->registerWidget('notes-count', [
'label' => __('notes::messages.widget_label'),
'view' => 'notes::widgets.count',
'dataCallback' => function () {
return [
'count' => \DB::table('mod_notes_entries')->count(),
];
},
'order' => 180,
'size' => 'half',
]);
// Routen
Route::middleware(['web', 'auth'])
->prefix('admin/notes')
->name('admin.notes.')
->group(function () {
Route::get('settings', [Http\Controllers\NotesSettingsController::class, 'index'])
->name('settings');
Route::post('settings', [Http\Controllers\NotesSettingsController::class, 'update'])
->name('settings.update');
});
}
}
Alles passiert in boot() — das ist die Phase, in der die Registries sicher verfügbar sind
(siehe ServiceProvider).
Schritt 4: Den Controller schreiben
modules/notes/src/Http/Controllers/NotesSettingsController.php:
<?php
namespace Schneespur\Module\Notes\Http\Controllers;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class NotesSettingsController
{
public function index()
{
Gate::authorize('settings.view');
return view('notes::settings', [
'welcomeMessage' => Setting::get('notes.welcome_message', ''),
]);
}
public function update(Request $request)
{
Gate::authorize('settings.edit');
$validated = $request->validate([
'welcome_message' => 'required|string|max:500',
]);
Setting::set('notes.welcome_message', $validated['welcome_message']);
return redirect()->route('admin.notes.settings')
->with('success', __('notes::messages.settings_saved'));
}
}
Der Gate::authorize()-Aufruf nutzt die Core-Berechtigungen settings.view und
settings.edit — so respektiert das Modul von Anfang an die bestehende Rechtevergabe.
Schritt 5: Die Views schreiben
modules/notes/resources/views/settings.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">
{{ __('notes::messages.nav_label') }}
</h2>
</x-slot>
<div class="max-w-2xl mx-auto py-6">
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-800">
{{ session('success') }}
</div>
@endif
<form method="POST" action="{{ route('admin.notes.settings.update') }}"
class="bg-white shadow rounded-lg p-6">
@csrf
<div class="mb-4">
<label for="welcome_message" class="block text-sm font-medium text-gray-700 mb-1">
{{ __('notes::messages.welcome_message_label') }}
</label>
<textarea id="welcome_message" name="welcome_message" rows="3"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>{{ old('welcome_message', $welcomeMessage) }}</textarea>
@error('welcome_message')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md">
{{ __('notes::messages.save') }}
</button>
</form>
</div>
</x-app-layout>
modules/notes/resources/views/widgets/count.blade.php:
<div class="bg-white rounded-lg shadow p-4">
<h3 class="text-sm font-medium text-gray-500">{{ $label }}</h3>
<p class="mt-2 text-2xl font-bold text-gray-900">{{ $data['count'] ?? 0 }}</p>
<p class="text-xs text-gray-400 mt-1">{{ __('notes::messages.total_notes') }}</p>
</div>
Schritt 6: Die Migration schreiben
modules/notes/database/migrations/2026_06_01_000001_create_mod_notes_entries_table.php:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('mod_notes_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('job_id')->nullable()->constrained('service_jobs')->nullOnDelete();
$table->foreignId('user_id')->constrained('users');
$table->text('content');
$table->timestamps();
$table->index('job_id');
});
}
public function down(): void
{
Schema::dropIfExists('mod_notes_entries');
}
};
Der Tabellen-Präfix mod_ macht auf einen Blick erkennbar, dass die Tabelle aus einem Modul
stammt und nicht zum Core gehört.
Schritt 7: Übersetzungen schreiben
modules/notes/lang/de/messages.php:
<?php
return [
'nav_label' => 'Notizen',
'widget_label' => 'Notizen',
'total_notes' => 'Notizen gesamt',
'welcome_message_label' => 'Willkommensnachricht',
'settings_saved' => 'Einstellungen gespeichert.',
'save' => 'Speichern',
];
modules/notes/lang/en/messages.php:
<?php
return [
'nav_label' => 'Notes',
'widget_label' => 'Notes',
'total_notes' => 'Total notes',
'welcome_message_label' => 'Welcome message',
'settings_saved' => 'Settings saved.',
'save' => 'Save',
];
Schritt 8: Migrieren und aktivieren
php artisan migrate --path=modules/notes/database/migrations
Aktivieren Sie das Modul anschließend in der Oberfläche unter Einstellungen → Module.
Der fertige Dateibaum
modules/notes/
├── module.json
├── src/
│ ├── NotesServiceProvider.php
│ └── Http/Controllers/
│ └── NotesSettingsController.php
├── resources/views/
│ ├── settings.blade.php
│ └── widgets/
│ └── count.blade.php
├── database/migrations/
│ └── 2026_06_01_000001_create_mod_notes_entries_table.php
└── lang/
├── de/messages.php
└── en/messages.php
Wie es weitergeht
Von hier aus lässt sich das Modul erweitern — die zugrundeliegenden Mechaniken kennen Sie schon aus den vorigen Kapiteln:
- weitere Routen — Notizen auflisten, anlegen, bearbeiten
- Event-Listener — Notizen automatisch bei Einsatz-Abschluss anlegen
- Berechtigungen — eigene
notes.view,notes.editüber diePermissionRegistry - Benachrichtigungs-Kanal — Notizen per Telegram versenden
- API-Endpunkt — für externe Anbindungen
- Template-Slots — Notizen auf der Einsatz-Detailseite einblenden
- Filter-Hooks — die Notiz-Anzahl ins KPI-Dashboard einhängen
- geplante Aufgabe — eine tägliche Notiz-Zusammenfassung
Die API der dafür nötigen Registries steht in der
Registries-Referenz. Als ausführliches Vorbild liegt der
Anwendung außerdem ein Referenzmodul unter modules/example/ bei.