Laravel und PHP

Das Paket honk-me/honk-me funktioniert in jeder PHP-8.3-App und bringt eine vollwertige Integration für Laravel 13 mit: eine Facade, einen Notification-Kanal, Exception-Reporting, Scheduler-Hooks und ein Fake für Tests.

Paket
honk-me/honk-me Veröffentlicht
Quellcode
github.com/honk-me/honk-php · MIT-Lizenz
Voraussetzungen
PHP 8.3 oder neuer mit ext-curl (oder einem beliebigen PSR-18-Client). Die Laravel-Integration ist für Laravel 13.

Installation

Füge das Paket hinzu, veröffentliche die Konfiguration und trag dann die Server-URL und den Ingest-Schlüssel eines Projekts in .env ein (nie committen). honk:test sendet ein Leichtes Hupen und prüft so die Verbindung.

Terminal
composer require honk-me/honk-me
# publishes config/honk.php and adds HONK_URL and HONK_KEY to .env.example
php artisan honk:install
.env
HONK_URL=https://honk-me.app
HONK_KEY=honk_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Terminal
php artisan honk:test         # sends a light honk and prints the message id
php artisan about --only=honk # URL, key prefix (never the secret), queue, Context metadata

Mit einem Ingest-Schlüssel (honk_…) kann jeder an sein Projekt senden. Er gehört auf Server, in Jobs und in CI-Secrets, nie in einen Browser, eine Mobil- oder Desktop-App.

Ein Ereignis senden

Die Facade hat für jede Stufe der Honk-Skala einen Helfer. source ist standardmäßig dein APP_NAME und environment dein APP_ENV.

use HonkMe\Laravel\Facades\Honk;

Honk::beep('Backup finished', 'nightly pg_dump took 42 s');
Honk::loud('Disk 91%', '/var on app-01', ['groupKey' => 'disk/app-01/var']);

Der Notification-Kanal

Nutze den Kanal honk wie mail oder database. Über die Queue verschickte Notifications halten die Anfrage deiner Kunden schnell, und der Idempotency-Key ist die ID der Notification, sodass Retries aus der Queue nie doppelt benachrichtigen. Halte toHonk() deterministisch: Eine Wiederholung muss dieselben Daten senden.

app/Notifications/CustomerRequested.php
// app/Notifications/CustomerRequested.php
namespace App\Notifications;

use App\Models\CustomerRequest;
use HonkMe\Laravel\Notifications\HonkMessage;
use HonkMe\Laravel\Notifications\ToHonk;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Support\Str;

#[Tries(5)]
#[Backoff(10, 60, 300, 900)]
class CustomerRequested extends Notification implements ShouldQueue, ToHonk
{
    use Queueable;

    public function __construct(public CustomerRequest $request)
    {
        $this->afterCommit();
    }

    public function via(object $notifiable): array
    {
        return ['honk']; // add 'mail', 'database', … as you like
    }

    public function toHonk(object $notifiable): HonkMessage
    {
        $r = $this->request;

        return HonkMessage::create()
            ->title(Str::limit("New request: {$r->subject}", 150))
            ->line("{$r->name} ({$r->company})")
            ->line(Str::limit($r->body, 2000))       // message: ≤ 8192 bytes
            ->light()                                // a light honk (info)
            ->category('customers')
            ->groupKey("requests/{$r->id}")          // one group per request
            ->occurredAt($r->created_at)
            ->url(route('admin.requests.show', $r))  // https only, shown as "Open link"
            // buttons, up to 3 (the first is the main one): https://, mailto:, tel: or sms:
            ->action('Reply by email', "mailto:{$r->email}")
            ->action('Call', "tel:{$r->phone}")
            ->meta('request_id', (string) $r->id);
    }
}
$request = CustomerRequest::create($validated);
$admin->notify(new CustomerRequested($request));            // a User with the Notifiable trait
Notification::route('honk', null)->notify(new CustomerRequested($request)); // or without a user

Sofort, nach der Antwort oder aus der Queue

Honk::defer() sendet nach der HTTP-Antwort im selben Prozess. Honk::queue() sendet aus einem Worker und übersteht Neustarts. Die einfachen Helfer senden sofort, samt Retries. defer() und queue() prüfen die Nachricht sofort, damit Fehler in deinem Code auffallen.

use HonkMe\Laravel\Facades\Honk;
use HonkMe\Message;

Honk::defer()->beep('New order', "{$order->email} paid {$order->total} €");   // after the response
Honk::queue(Message::make('Imported 1 204 rows')->beep()->channel('imports'));   // a queued job
Honk::loud('Disk 91%', '/var on app-01', ['groupKey' => 'disk/app-01/var']);     // right now

Exceptions melden

Eine Zeile in bootstrap/app.php macht aus jeder gemeldeten Exception ein Langes Hupen. Wiederholungen desselben Fehlers teilen sich eine Gruppe (exceptions/<Class>@<file>:<line>), und jede Gruppe hupt höchstens einmal alle 5 Minuten. Solange HONK_URL oder HONK_KEY fehlt, passiert nichts, und es wird nie eine Exception geworfen.

bootstrap/app.php
// bootstrap/app.php
use HonkMe\Laravel\Facades\Honk;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(Honk::reportable());
})
$exceptions->report(Honk::reportable(severity: 'blast', throttleSeconds: 60));

Geplante Tasks

honkOnFailure() sendet ein Problem mit dem Exit-Code und dem Ende der Ausgabe und beim nächsten erfolgreichen Lauf eine Entwarnung, aber nur nach einem Fehlschlag. Ein Task, der sauber läuft, meldet sich also nie.

routes/console.php
// routes/console.php
Schedule::command('backup:run')->daily()->honkOnFailure();          // problem on failure, recovery once it works again
Schedule::command('reports:send')->hourly()->honkOnSuccess();       // a beep after every successful run

Testen mit Honk::fake()

Das Fake zeichnet auf, was gesendet oder in die Queue gestellt würde, und geht nie ins Netzwerk. Es prüft wie der echte Client, sodass eine ungültige Nachricht deinen Test trotzdem fehlschlagen lässt.

use HonkMe\Laravel\Facades\Honk;
use HonkMe\Message;
use HonkMe\Severity;

Honk::fake();

$this->post(route('quote.store'), $data);

Honk::assertSent(fn (Message $m) => $m->groupKey === 'requests/1' && $m->severity === Severity::Light);
Honk::assertSentTimes(1);
Honk::assertNotSent(fn (Message $m) => $m->severity === Severity::Blast);
Honk::assertQueued(fn (Message $m, string $idempotencyKey) => $m->channel === 'imports');
Honk::assertNothingSent(); // also assertNothingQueued(), assertNothingOutgoing()

Reines PHP

Ohne Laravel erstellst du einen Client und rufst dieselben Helfer auf. problem und recovery öffnen und schließen einen Vorfall für einen Gruppenschlüssel.

Terminal
composer require honk-me/honk-me
use HonkMe\Client;

$honk = new Client(url: getenv('HONK_URL'), key: getenv('HONK_KEY'));
$honk->beep('Backup finished', 'nightly pg_dump took 42 s');
$client->problem('db/backup', 'Backup failed', 'pg_dump exited with 1');   // a long honk by default
$client->recovery('db/backup', 'Backup OK', 'pg_dump finished in 41 s');   // a beep by default

Fehler

Jede Exception erweitert HonkException und sagt dir, ob sich ein Retry lohnt. Eine ValidationException ist ein Bug, den du beheben musst; eine wiederholbare kannst du mit demselben Idempotency-Key erneut in die Queue stellen.

use HonkMe\Exception\HonkException;
use HonkMe\Exception\ValidationException;

try {
    $honk->send($message, "order-{$order->id}-failed");
} catch (ValidationException $e) {
    report($e); // a bug: $e->fields says what to fix
} catch (HonkException $e) {
    if (!$e->isRetryable()) {
        throw $e;
    }
    // retry later with $e->idempotencyKey, after $e->retryAfter seconds if set
}

Retries ohne Doppelversand

  • Jedes Senden hat einen Idempotency-Key: deinen oder eine neue UUIDv7. Jeder Retry nutzt denselben Schlüssel, und innerhalb von 24 Stunden beantwortet Honk eine erneut gesendete Anfrage mit der ursprünglichen ID und duplicate: true. Geht eine Antwort verloren, entsteht also nie eine zweite Nachricht.
  • Wiederholt werden nur Netzwerkfehler, Timeouts, 429 und 5xx, mit exponentiellem Backoff und vollem Jitter, nie früher als das Retry-After des Servers. Andere 4xx-Antworten werden nie wiederholt: Korrigiere stattdessen die Anfrage.
  • Jeder Versuch bricht nach 5 Sekunden ab, und nach insgesamt 30 Sekunden ist Schluss. Wäre die nötige Wartezeit länger, etwa bis ein Tageskontingent um Mitternacht zurückgesetzt wird, schlägt der Aufruf sofort fehl und sagt dir, wann du es erneut versuchen kannst.
  • Felder werden vor dem Senden geprüft, und alle ungültigen Felder werden auf einmal gemeldet.