Laravel y PHP

El paquete honk-me/honk-me funciona en cualquier app con PHP 8.3 y se integra a fondo con Laravel 13: una facade, un canal de notificaciones, informe de excepciones, hooks del programador de tareas y un fake para las pruebas.

Paquete
honk-me/honk-me Publicado
Código fuente
github.com/honk-me/honk-php · Licencia MIT
Requisitos
PHP 8.3 o posterior con ext-curl (o cualquier cliente PSR-18). La integración con Laravel es para Laravel 13.

Instalación

Añade el paquete, publica la configuración y pon en .env la URL del servidor y la clave de ingesta de un proyecto (nunca la subas al repositorio). honk:test envía un Bocinazo suave para comprobar la conexión.

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

Quien tenga una clave de ingesta (honk_…) puede enviar mensajes a su proyecto. Guárdala en servidores, tareas y secretos de CI, nunca en un navegador ni en una app móvil o de escritorio.

Enviar un evento

La facade tiene un helper para cada nivel de la escala Honk. source toma por defecto tu APP_NAME, y environment, tu 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']);

El canal de notificaciones

Usa el canal honk como mail o database. Con notificaciones en cola, la respuesta al cliente no se retrasa, y la clave de idempotencia es el id de la notificación, así que los reintentos de la cola nunca avisan dos veces. Haz que toHonk() sea determinista: un reintento debe enviar el mismo contenido.

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

Ahora, después de la respuesta o desde la cola

Honk::defer() envía después de la respuesta HTTP, en el mismo proceso. Honk::queue() envía desde un worker y sobrevive a los reinicios. Los helpers normales envían al momento, con reintentos incluidos. Tanto defer() como queue() validan el mensaje de inmediato, así que los errores aparecen en tu código.

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

Avisos de excepciones

Una línea en bootstrap/app.php convierte en un Bocinazo largo cada excepción que reporta Laravel. Las repeticiones de un mismo fallo comparten grupo (exceptions/<Class>@<file>:<line>), y cada grupo avisa como mucho una vez cada 5 minutos. No hace nada mientras falten HONK_URL o HONK_KEY, y nunca lanza excepciones.

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

Tareas programadas

honkOnFailure() envía un problema con el código de salida y el final de la salida, y una recuperación la próxima vez que la tarea termina bien, pero solo después de un fallo, así que una tarea que funciona nunca te molesta.

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

Pruebas con Honk::fake()

El fake registra lo que se enviaría o se pondría en cola y nunca toca la red. Valida igual que el cliente real, así que un mensaje no válido sigue haciendo fallar tu prueba.

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 sin Laravel

Sin Laravel, crea un Client y llama a los mismos helpers. problem y recovery abren y cierran un incidente para una clave de grupo.

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

Errores

Todas las excepciones extienden HonkException y te dicen si tiene sentido reintentar. Una ValidationException es un error que hay que corregir; una reintentable se puede volver a poner en cola con la misma clave de idempotencia.

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
}

Reintentos que nunca envían dos veces

  • Cada envío lleva una Idempotency-Key: la tuya o un UUIDv7 nuevo. La misma clave se reutiliza en cada reintento y, durante 24 horas, Honk responde a una repetición con el id original y duplicate: true, así que una respuesta perdida nunca crea un segundo mensaje.
  • Solo se reintentan los errores de red, los tiempos de espera agotados, 429 y 5xx, con backoff exponencial y jitter completo, y nunca antes del Retry-After del servidor. Las demás respuestas 4xx no se reintentan nunca: hay que corregir la solicitud.
  • Cada intento se corta a los 5 segundos y todo se detiene a los 30. Una espera que superaría ese plazo, como una cuota diaria que se reinicia a medianoche, falla al momento y te dice cuándo reintentar.
  • Los campos se validan antes de enviar, y todos los errores de validación se devuelven juntos.