Laravel și PHP

Pachetul honk-me/honk-me funcționează în orice aplicație PHP 8.3 și are o integrare completă cu Laravel 13: un facade, un canal de notificare, raportarea excepțiilor, hookuri pentru scheduler și un fake pentru teste.

Pachet
honk-me/honk-me Publicat
Cod sursă
github.com/honk-me/honk-php · Licență MIT
Cerințe
PHP 8.3 sau mai nou, cu ext-curl (sau orice client PSR-18). Integrarea Laravel este pentru Laravel 13.

Instalare

Adaugă pachetul, publică configurația, apoi pune URL-ul serverului și cheia API a unui proiect în .env (fără să o pui vreodată într-un commit). honk:test trimite un Claxon ușor ca să verifice conexiunea.

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

Oricine are o cheie API (honk_…) poate trimite mesaje în proiectul ei. Ține-o pe servere, în joburi și în secretele din CI, niciodată într-o aplicație de browser, de mobil sau de desktop.

Trimite un eveniment

Facade-ul are câte un helper pentru fiecare nivel al scalei Honk. Implicit, source este APP_NAME, iar environment este 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']);

Canalul de notificare

Folosește canalul honk la fel ca mail sau database. Notificările puse în coadă nu încetinesc cererea clientului, iar cheia de idempotență este id-ul notificării, așa că reîncercările din coadă nu notifică niciodată de două ori. Păstrează toHonk() determinist: o reîncercare trebuie să trimită același conținut.

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

Acum, după răspuns sau din coadă

Honk::defer() trimite după răspunsul HTTP, în același proces. Honk::queue() trimite dintr-un worker și rezistă la reporniri. Helperele simple trimit imediat, cu reîncercări cu tot. Atât defer(), cât și queue() validează mesajul pe loc, ca greșelile să iasă la iveală în codul tău.

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

Raportarea excepțiilor

O singură linie în bootstrap/app.php transformă fiecare excepție raportată într-un Claxon lung. Repetările aceluiași eșec au un grup comun (exceptions/<Class>@<file>:<line>), iar fiecare grup claxonează cel mult o dată la 5 minute. Nu face nimic cât timp lipsește HONK_URL sau HONK_KEY și nu aruncă niciodată excepții.

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));

Sarcini programate

honkOnFailure() trimite o problemă cu codul de ieșire și finalul output-ului, apoi o revenire data viitoare când sarcina reușește, dar doar după un eșec, așa că o sarcină care merge bine nu te deranjează niciodată.

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

Teste cu Honk::fake()

Fake-ul înregistrează ce s-ar fi trimis sau pus în coadă și nu atinge niciodată rețeaua. Validează la fel ca clientul real, așa că un mesaj invalid îți pică în continuare testul.

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()

PHP simplu

Fără Laravel, creează un Client și apelează aceleași helpere. problem și recovery deschid și închid un incident pentru o cheie de grup.

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

Erori

Fiecare excepție extinde HonkException și îți spune dacă are sens o reîncercare. O ValidationException e un bug de corectat; o excepție care se poate reîncerca poate fi pusă din nou în coadă cu aceeași cheie de idempotență.

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
}

Reîncercări care nu trimit nimic de două ori

  • Fiecare trimitere are un Idempotency-Key: al tău sau un UUIDv7 nou. Aceeași cheie e refolosită la fiecare reîncercare, iar timp de 24 de ore Honk răspunde la o repetare cu id-ul original și duplicate: true, așa că un răspuns pierdut nu creează niciodată un al doilea mesaj.
  • Se reîncearcă doar erorile de rețea, cererile care expiră, 429 și 5xx, cu backoff exponențial și jitter complet, niciodată mai devreme decât Retry-After de la server. Celelalte răspunsuri 4xx nu se reîncearcă niciodată: corectează cererea.
  • Fiecare încercare expiră după 5 secunde, iar totul se oprește după 30. Dacă ar trebui să aștepte mai mult, de exemplu până se resetează cota zilnică la miezul nopții, eșuează imediat și îți spune când să reîncerci.
  • Câmpurile sunt verificate înainte de trimitere, iar toate câmpurile invalide sunt raportate deodată.