CommunitySchreiben & Editierengithub.com

shaxzodbek-uzb/laravel-guardrails

🛡️ Schutzgeländer-Fähigkeiten für KI-gestütztes Laravel – stoppt mandantenübergreifende Lecks, N+1-Probleme und zerstörerische Migrationen, bevor Ihr KI-Agent sie ausliefert. Für Claude Code, Cursor und 40+ Agenten.

Was ist laravel-guardrails?

Diese Fähigkeit setzt eine strenge Testdisziplin mit Pest 3/4 in Laravel-Projekten durch, die von KI-Agenten wie Claude Code oder Cursor unterstützt werden. Der Fokus liegt darauf, dass Tests tatsächliches Verhalten überprüfen, unangenehme Pfade und Autorisierung abdecken und Refaktorierungen überstehen – statt auf übermocked, zerbrechliche Tests, die nur falsche Sicherheit geben. Die Skill-Datei bietet 14 klare Regeln, darunter das Schreiben von Tests parallel zur Funktion, die Verwendung echter Datenbank-Assertions anstelle von Methodenaufrufen, das Faken externer Grenzen und das Testen von Autorisierungspfaden (Gast, falscher Benutzer, fehlende Ressource) als nicht verhandelbar. Sie enthält zahlreiche PHP-Codebeispiele, die gute und schlechte Praktiken gegenüberstellen, sowie eine Checkliste mit grep-Befehlen, um häufige Anti-Patterns wie Over-Mocking oder handgeschriebene Datenbank-Inserts zu erkennen. Die Fähigkeit unterstützt Claude Code, Cursor und über 40 andere KI-Agenten und ist unverzichtbar, um mandantenübergreifende Lecks, N+1-Probleme oder zerstörerische Migrationen zu verhindern, die ein Agent versehentlich ausliefern könnte.

Funktioniert mit✓Claude Code~Codex CLI✓Cursor
npx skills add shaxzodbek-uzb/laravel-guardrails

Installed? Explore more Schreiben & Editieren skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Pest-Testdisziplin

Funktionen, die ungetestet ausgeliefert werden – oder "getestet" mit übermocked Suiten, die Implementierungsdetails behaupten – brechen in der Produktion still und verrotten bei jeder Refaktorierung. Diese Fähigkeit zwingt Sie dazu, Pest 3/4-Tests zu schreiben, die Verhalten beweisen, den unglücklichen Pfad und die Autorisierung abdecken und Refaktorierungen überstehen.

Die Gefahrenquelle

Zwei Fehlermodi, beide teuer:

  1. Keine Tests / Nur-Happy-Path-Tests. Die Funktion funktioniert in der Demo, aber dann erreicht in der Produktion ein Gast den Endpunkt, ein Mandant liest die Daten eines anderen Mandanten, eine Validierungslücke lässt Müll herein oder ein 404 wird zu einem 500. Autorisierungslücken (403/404-Pfade, die nie behauptet wurden) sind der Grund, warum Datenlecks in die Produktion gelangen.
  2. Spröde, übermocked Tests. Sie mocken Ihr eigenes UserRepository/Ihren Service mit shouldReceive('save')->once(), also besteht der Test, selbst wenn die echte Abfrage falsch ist, und explodiert, sobald jemand eine Methode umbenennt. Die Suite ist grün, gibt falsche Sicherheit und wird bei der nächsten Refaktorierung frustriert gelöscht – zurück zu Modus 1.

Die Lösung ist dieselbe Disziplin in beiden Fällen: Faken Sie die Grenzen, die Sie nicht besitzen, führen Sie den Code aus, den Sie besitzen, und behaupten Sie beobachtbare Ergebnisse (DB-Zeilen, HTTP-Status, ausgelöste Jobs/Mails) – einschließlich der Fehler- und Autorisierungsergebnisse.

Regeln

  1. Schreiben Sie den Test mit der Funktion, nicht "später". Eine Controller/Job/Befehl/Richtlinienänderung ist erst abgeschlossen, wenn sie einen Pest-Test hat. Melden Sie eine Funktion niemals als erledigt ohne einen entsprechenden Test, der ihr Verhalten behauptet.
  2. Verwenden Sie Pest-Idiome. Definieren Sie Fälle mit it('...') oder test('...'); behaupten Sie mit dem fließenden expect(...); teilen Sie die Einrichtung mit beforeEach(). Schreiben Sie kein PHPUnit class FooTest extends TestCase, es sei denn, die Datei verwendet bereits diesen Stil.
  3. Wählen Sie den richtigen Typ. HTTP/End-to-End-Verhalten → Feature-Test (startet das Framework, trifft Routen via $this->get/post/...). Reine Logik ohne Framework/DB → Unit-Test. Standardmäßig einen Feature-Test wählen, wenn Sie unsicher sind – er fängt mehr ab.
  4. Setzen Sie die Datenbank mit einem Trait zurück, via uses(). Wenden Sie RefreshDatabase (transaktionsgekapselt) oder LazilyRefreshDatabase (migriert nur, wenn ein Test die DB berührt) in tests/Pest.php für das gesamte Feature-Verzeichnis an, oder uses(RefreshDatabase::class) am Anfang einer Datei. Lassen Sie DB-Tests NIEMALS von übriggebliebenem Zustand abhängen.
  5. Bauen Sie Daten mit Fabriken, niemals handgerollten Inserts. Verwenden Sie User::factory()->create() / ->make() und Beziehungsfabriken. Handgebaute DB::table()->insert([...]) oder new Model([...])->save() in Tests ist spröde und überspringt Casts/Defaults – verbieten Sie es.
  6. Entdeduplizieren Sie mit Datensätzen, nicht mit Copy-Paste. Wenn dieselbe Behauptung über viele Eingaben läuft (gültige/ungültige Payloads, Rollenmatrizen), verwenden Sie ->with([...]) (inline oder einen benannten Datensatz in tests/Datasets/) anstatt N nahezu identischer Tests.
  7. Faken Sie die Grenzen, die Sie nicht besitzen. Verwenden Sie für alles Externe oder Asynchrone die Fassaden-Fakes vor der Aktion: Http::fake(), Queue::fake(), Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). Für Zeit verwenden Sie $this->travel(...), $this->travelTo(...) oder $this->freezeTime() – behaupten Sie niemals gegen ein nicht eingefrorenes now().
  8. Mocken Sie NICHT Ihre eigenen Modelle/Services in Bedeutungslosigkeit. Vermeiden Sie Mockery::mock(MyService::class)->shouldReceive(...) und Model::shouldReceive(...) für zu testenden Code. Lassen Sie den echten Code gegen die Test-DB und die gefakten Grenzen laufen. Mocken Sie Ihre eigene Klasse nur an einer echten Nahtstelle, die Sie nicht ausführen (z. B. einen langsamen Drittanbieter-SDK-Wrapper).
  9. Behaupten Sie Ergebnisse, nicht Interna. Bevorzugen Sie assertDatabaseHas / assertDatabaseMissing / assertModelExists, assertStatus / assertOk / assertCreated, assertJson / assertJsonPath, assertRedirect, Mail::assertSent, Notification::assertSentTo, Queue::assertPushed, Bus::assertDispatched, Storage::disk()->assertExists. Behaupten Sie nicht "Methode X wurde einmal aufgerufen" als Proxy für Verhalten.
  10. Testen Sie IMMER den unglücklichen Pfad UND die Autorisierung. Jeder Endpunkt/Aktion benötigt mindestens: den glücklichen Pfad, einen Validierungs-/Fehlerpfad und die Auth-Pfade – Gast → 401/Weiterleitung, falscher Benutzer/Rolle → 403, fehlende/fremde Ressource → 404. Fügen Sie für jede sicherheitskritische Funktion einen expliziten "Ein anderer Benutzer kann nicht darauf zugreifen/dies ändern"-Test hinzu. Dies ist nicht verhandelbar für alles, was Eigentum oder Mandantenfähigkeit betrifft.
  11. Fügen Sie Architekturtests als billige, globale Schutzgeländer hinzu. Halten Sie eine tests/Arch.php (oder Arch-Fälle in Pest.php) mit den Voreinstellungen, die passen: arch()->preset()->php() (verbietet die/var_dump/dd-artige Debug-Ausgaben und veraltete PHP-Funktionen), arch()->preset()->security() (verbietet eval, extract, unserialize, schwaches Hashing/md5/sha1, unsichere Zufälligkeit usw.) und arch()->preset()->laravel() (erzwingt Laravel-Konventionen – einschließlich kein env() außerhalb der Konfiguration). Fügen Sie Projektinvarianten hinzu wie arch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller').
  12. Ein Verhalten pro Test, beschreibender Name. it('blockiert Gäste vom Dashboard'), nicht it('funktioniert'). Wenn ein Testname "und" benötigt, teilen Sie ihn auf.
  13. Erzwingen Sie Abdeckung in CI. Führen Sie ./vendor/bin/pest --coverage --min=NN aus, um ein Minimum zu erzwingen, und ./vendor/bin/pest --type-coverage (erfordert das Type-Coverage-Plugin), um typisierte Signaturen zu erzwingen. Behandeln Sie neue ungetestete Zeilen als Fehler.
  14. Browser/E2E sparsam (Pest 4). Pest 4 liefert Browser-Tests via visit('/')->...->assertSee(...) (Playwright-gestützt). Reservieren Sie es für echte Full-Stack/JS-Flows; führen Sie HTTP-Layer-Assertions nicht als langsame Browser-Tests neu ein.

Gut vs. schlecht

// tests/Feature/UpdatePostTest.php

use App\Models\Post;
use App\Models\User;

// ❌ übermocked: testet, dass eine Methode aufgerufen wurde, nicht was passiert ist.
//    Besteht, selbst wenn der falsche Post aktualisiert wird oder die Auth fehlt.
it('aktualisiert einen Beitrag', function () {
    $service = Mockery::mock(App\Services\PostService::class);
    $service->shouldReceive('update')->once()->andReturnTrue();
    $this->app->instance(App\Services\PostService::class, $service);

    $this->put('/posts/1', ['title' => 'Neu'])->assertOk();
});
// ✅ führt echten Code gegen die Test-DB aus; behauptet das Ergebnis UND die Autorisierung.
use App\Models\Post;
use App\Models\User;

it('erlaubt dem Besitzer, seinen Beitrag zu aktualisieren', function () {
    $owner = User::factory()->create();
    $post  = Post::factory()->for($owner)->create(['title' => 'Alt']);

    $this->actingAs($owner)
        ->put("/posts/{$post->id}", ['title' => 'Neu'])
        ->assertRedirect();

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Neu']);
});

it('verbietet einem Nicht-Besitzer, den Beitrag zu aktualisieren', function () {
    $post     = Post::factory()->create(['title' => 'Alt']);
    $attacker = User::factory()->create();

    $this->actingAs($attacker)
        ->put("/posts/{$post->id}", ['title' => 'Gehackt'])
        ->assertForbidden(); // 403

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Alt']);
});

it('blockiert Gäste', function () {
    $post = Post::factory()->create();

    $this->put("/posts/{$post->id}", ['title' => 'Neu'])
        ->assertRedirect('/login'); // oder assertUnauthorized() für eine API
});
// ❌ trifft das echte Netzwerk (flakelig, langsam, kann Schlüssel preisgeben); behauptet nichts Nützliches.
it('benachrichtigt Slack', function () {
    $resp = (new App\Services\Slack)->ping('Deploy ok');
    expect($resp)->toBeArray();
});
// ✅ fake die Grenze, die Sie nicht besitzen; behaupten Sie die gesendete Anfrage.
use Illuminate\Support\Facades\Http;

it('postet die Deploy-Nachricht an Slack', function () {
    Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);

    (new App\Services\Slack)->ping('Deploy ok');

    Http::assertSent(fn ($request) =>
        $request->url() === 'https://hooks.slack.com/services/T/B/x'
        && $request['text'] === 'Deploy ok'
    );
});
// ❌ kopierte nahezu identische Validierungstests.
it('lehnt leere E-Mail ab', function () {
    $this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('lehnt schlechte E-Mail ab', function () {
    $this->post('/register', ['email' => 'nein'])->assertSessionHasErrors('email');
});
// ✅ ein Test, tabellengetrieben mit einem Datensatz.
it('lehnt ungültige E-Mails ab', function (string $email) {
    $this->post('/register', ['email' => $email])
        ->assertSessionHasErrors('email');
})->with([
    'leer'       => '',
    'kein Klammeraffe'  => 'nein',
    'keine Domain'   => 'a@',
]);
// ✅ asynchrone + Zeitgrenzen gefaked; behaupten Sie den Dispatch und den Nebeneffekt.
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

it('stellt eine Erinnerung in die Warteschlange und begrüßt den Benutzer bei der Registrierung', function () {
    Queue::fake();
    Mail::fake();
    $this->freezeTime();

    $this->post('/register', [
        'name' => 'Ada', 'email' => '[email protected]', 'password' => 'geheimes-passwort',
    ])->assertRedirect();

    Queue::assertPushed(SendReminder::class);
    Mail::assertSent(WelcomeMail::class, fn ($m) => $m->hasTo('[email protected]'));
    $this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// tests/Arch.php — ✅ billige globale Schutzgeländer
arch()->preset()->php();        // verbietet die/var_dump/dd-artige Debug-Ausgabe + veraltete PHP-Funktionen
arch()->preset()->security();   // verbietet eval, extract, unserialize, md5/sha1, schwache Zufälligkeit usw.
arch()->preset()->laravel();    // erzwingt Laravel-Konventionen, inkl. kein env() außerhalb der Konfiguration

arch('Controller erweitern den Basis-Controller')
    ->expect('App\Http\Controllers')
    ->toExtend('App\Http\Controllers\Controller');

arch('Modelle leben im richtigen Namensraum')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

So überprüfen Sie

Führen Sie diese aus, bevor Sie eine Funktion und ihre Tests als erledigt melden:

# 1. Die gesamte Suite muss grün sein.
./vendor/bin/pest

# 2. Führen Sie nur die neue(n)/geänderte(n) Datei(en) während der Iteration aus.
./vendor/bin/pest tests/Feature/UpdatePostTest.php

# 3. Erzwingen Sie eine Abdeckungsuntergrenze (erfordert Xdebug oder PCOV).
./vendor/bin/pest --coverage --min=80

# 4. Typabdeckung (erfordert pestphp/pest-plugin-type-coverage).
./vendor/bin/pest --type-coverage --min=100

# 5. Stil bleibt sauber.
./vendor/bin/pint --test

Prüfen Sie dann, dass Sie die Gefahrenquelle NICHT wieder eingeführt haben:

# Authz-Abdeckung: jeder geschützte Feature-Test sollte einen 401/403/404-Pfad behaupten.
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\((401|403|404)\)" tests/

# Over-Mocking-Geruch: Das Mocken Ihrer EIGENEN App-Klassen ist eine rote Flagge – überprüfen Sie jeden Treffer.
grep -rEn "Mockery::|->shouldReceive\(|::partialMock\(|::spy\\(" tests/

# Handgerollte Inserts anstelle von Fabriken – ersetzen Sie durch ->factory().
grep -rEn "DB::table\(.*\)->insert|new App\\\\Models" tests/

# Externe Grenzen müssen gefaked werden – bestätigen Sie, dass Fakes vorhanden sind, wo Code nach außen ruft.
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/

# Die Security + PHP Arch-Voreinstellungen (die billigen globalen Schutzgeländer) müssen irgendwo vorhanden sein.
grep -rEn "preset\(\)->(security|php|laravel)\(\)" tests/

Behaupten Sie in Tests, nicht nur im Code: bestätigen Sie für jeden neuen Endpunkt, dass (a) eine Happy-Path-Ergebnisbehauptung (assertDatabaseHas/assertOk/assertJsonPath), (b) mindestens eine Fehler-/Validierungsbehauptung und (c) die Gast- + Falscher-Benutzer-Autorisierungsbehauptungen vorhanden sind. Wenn etwas fehlt, ist die Funktion nicht erledigt.

Wann es in Ordnung ist, die Regel zu biegen

  • Mocken Ihres eigenen Codes an einer echten externen Nahtstelle. Ein dünner Wrapper um ein kostenpflichtiges Drittanbieter-SDK (SMS, Zahlungsgateway), den Sie nicht auf HTTP-Ebene faken können, ist ein legitimes Mockery-Ziel – Sie isolieren deren Abhängigkeit, nicht das zu testende Verhalten.
  • Unit-Testen reiner Logik ohne DB. Wertobjekte, Formatierer und Rechner benötigen kein RefreshDatabase und keine Fabriken – instanziieren Sie sie direkt und expect() das Ergebnis; das ist schneller und korrekt.
  • make() über create() wenn ein Test nie persistiert – User::factory()->make() vermeidet einen DB-Schreibvorgang für reine Logikbehauptungen.
  • Niedrigere Abdeckungsschwellen für Legacy-Code. Setzen Sie ein realistisches --min und erhöhen Sie es im Laufe der Zeit, anstatt die gesamte Arbeit zu blockieren; lassen Sie niemals zu, dass "100 % oder nichts" eine Ausrede wird, um null Tests auszuliefern.
  • Browser-Tests für echt JS-gesteuerte Flows (Livewire/Inertia/Vue-Interaktionen, Dateiauswähler) – dort ist visit() das richtige Werkzeug, kein Overreach.

Referenzen

Verwandte Skills