hermesihq / laravel
Laravel integration for Hermesi: a facade, a test fake and a queued job on top of hermesihq/hermesi.
Requires
- php: ^8.1
- hermesihq/hermesi: ^0.1
- illuminate/contracts: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/queue: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.9
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-05 13:05:38 UTC
README
Hermesi for Laravel: a facade, a test fake and a queued job, on top of
hermesihq/hermesi. It decides nothing about notifications; it makes the SDK feel native.
- A facade and a container binding, configured from
config/hermesi.phpand your.env. Hermesi::dispatch(): publish an event from a queue worker, so a request does not wait for Hermesi and a Hermesi outage does not fail it. Its idempotency key is fixed when you dispatch, so a retried or redelivered job cannot send a notification twice.Hermesi::fake()with assertions, in the style ofNotification::fake().- Laravel 10, 11, 12 and 13, on PHP 8.1 and later.
Install
composer require hermesihq/laravel
The service provider is discovered. Set your secret key and your Hermesi address in .env:
HERMESI_SECRET_KEY=hm_sk_...
HERMESI_BASE_URL=https://your-hermesi-host
HERMESI_ENVIRONMENT_ID=env_01...
HERMESI_ENVIRONMENT_ID (the env_... id shown in the dashboard) is only needed to mint subscriber tokens. To change the defaults
(timeout, retries, queue), publish the config:
php artisan vendor:publish --tag=hermesi-config
The package needs a PSR-18 HTTP client. A Laravel application already has Guzzle, which is used.
Publish an event
use Hermesi\Laravel\Facades\Hermesi; Hermesi::trigger('order.shipped', $user->id, ['order_id' => $order->id], idempotencyKey: "order-{$order->id}-shipped");
That waits for Hermesi's answer. 202 means the event is recorded and queued; nothing has been delivered yet, so watch the Activity Log in the
dashboard. The arguments are the SDK's: see hermesihq/hermesi for recipients, Subscriber, delay,
sendAt, override, tenant, and for what is refused before anything is sent.
The client is also in the container, so you can type-hint it:
public function __construct(private \Hermesi\Hermesi $hermesi) {}
Hermesi::events(), Hermesi::subscribers() and Hermesi::tokens() give the SDK's resources.
From a queue: Hermesi::dispatch()
Hermesi::dispatch('order.shipped', $user->id, ['order_id' => $order->id]);
The arguments are the same. What differs:
- The request does not wait, and it does not fail if Hermesi is down. A worker publishes the event and retries with a backoff.
- It is validated now. A payload that cannot be sent (
NAN, an object that is not JSON, a list where an object is needed) throws where you dispatch, not in a worker an hour later. - It cannot send twice. The idempotency key is generated when you dispatch and travels in the job. Every attempt of the job, and every
redelivery after a worker died, sends the same key, and Hermesi answers a repeat with the original event instead of creating a second one.
If the thing that happened has an identity, pass your own key (
order-4821-shipped): then even two dispatches collapse into one. - Errors that waiting cannot fix fail the job at once instead of being retried five times: a wrong key, an unknown subscriber, a malformed
event. A
429with a longRetry-Afterreleases the job for exactly that long. A dropped connection or a5xxis retried with the backoff inconfig/hermesi.php(10 s, 1 min, 5 min, 15 min, then the job is failed).
Two layers of retry, on purpose: the SDK retries a few times inside the same attempt for a blip, and the queue retries for an outage.
Set the queue in config/hermesi.php (HERMESI_QUEUE_CONNECTION, HERMESI_QUEUE). Hermesi::dispatch() returns a PendingDispatch, so
->delay(...), ->onQueue(...) and the rest of Laravel's job API work.
Subscriber tokens
A browser or an app talks to Hermesi's client API as one subscriber, with a token minted on your server:
Route::get('/hermesi-token', fn () => ['token' => Hermesi::token(auth()->id())])->middleware('auth');
Valid for an hour at most; minted locally, with no request. The environment comes from HERMESI_ENVIRONMENT_ID unless you pass one:
Hermesi::token($id, 'env_01...'). Hand the token to your frontend (@hermesihq/js, @hermesihq/react).
Testing
use Hermesi\Laravel\Facades\Hermesi; use Hermesi\SimulatedEvent; public function test_shipping_an_order_notifies_the_customer(): void { Hermesi::fake(); $this->post('/orders/1/ship')->assertOk(); Hermesi::assertTriggered('order.shipped', fn (SimulatedEvent $e) => $e->payload['order_id'] === '1'); Hermesi::assertNotTriggered('order.cancelled'); Hermesi::assertTriggeredTimes('order.shipped', 1); Hermesi::assertNothingTriggered(); // when it should not have }
fake() replaces the client everywhere, including where you type-hinted it, and sends nothing. A payload that could not be sent fails in
your test exactly as it would in production. An event published with Hermesi::dispatch() is recorded when its job runs: with the sync
queue driver that is at once, and with Queue::fake() assert on the job instead:
Queue::fake(); Hermesi::dispatch('order.shipped', 'user_1'); Queue::assertPushed(\Hermesi\Laravel\Jobs\TriggerHermesiEvent::class);
The key never shows
The key is read from .env by the config and held by the SDK, which never prints it (var_dump, print_r, var_export, dd() and
json_encode show nothing) and refuses to be serialised, so it cannot end up in a cache or a session. Do not log config('hermesi'): that array
holds it.
Not included
No Laravel notification channel ($user->notify(...)) yet. If your application is built around notifications and you want one, say so.
License
MIT