Zum Hauptinhalt springen
Schneespur
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:

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.