Download the PHP package esanj/notification-client without Composer
On this page you can find all versions of the php package esanj/notification-client. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download esanj/notification-client
More information about esanj/notification-client
Files in esanj/notification-client
Package notification-client
Short Description Laravel client package for Esanj Notification Microservice
License MIT
Informations about the package notification-client
Esanj Notification Client
Laravel client package for the Esanj Notification Microservice. Handles OAuth 2.0 token acquisition, automatic caching, token refresh, and retry logic out of the box.
Supports: Laravel 12 & 13 · PHP 8.2+
Installation
Publish the config file:
Configuration
Add the following variables to your .env file:
The first three are required. If any is missing when the client is resolved, you get a
ConfigurationException naming the variable — not a TypeError from inside the package. NOTIFICATION_SERVICE_URL
must be a full URL, and must use https:// when the app runs in production: the client-credentials call carries
your client_secret, and plain HTTP hands it to anyone on the network path.
Full config reference (config/esanj/notification.php):
Token Management
Token handling is fully automatic:
- On the first request the package fetches a token via the OAuth 2.0 client-credentials flow (
POST /api/v1/oauth/token). - The response is validated before anything is cached — it must carry a usable
access_tokenand a numericexpires_inlonger thanbuffer_seconds. A response missing either is rejected with anAuthenticationExceptionrather than cached as a token that is already expired. - The token is stored in your configured cache store with a TTL equal to
expires_in - buffer_seconds. SetNOTIFICATION_TOKEN_ENCRYPT=trueto encrypt that cache entry with yourAPP_KEY— worth doing when the store is shared with anything you don't fully trust. - A fast in-memory copy avoids cache I/O on subsequent calls within the same process.
- Fetching happens behind a cache lock. When the cache is cold — a deploy, a Redis restart, an invalidation — one process fetches the token while the others wait and then read its result, instead of twenty workers hitting the throttled token endpoint at once.
- If a request receives an
HTTP 401, the package invalidates the cached token, fetches a fresh one, and replays the request once. A second401means the credentials themselves are wrong, so it throws instead of hammering the token endpoint. - An
HTTP 403is left alone: the token is valid, the client simply has no permission for that endpoint. Refreshing would drop a healthy token for nothing — see$e->isForbidden(). - If all retries fail, an
ApiException(orAuthenticationException) is thrown and the error is logged.
Retry & Idempotency
A lost response does not mean the service ignored the request. The service creates the notification and queues the
job before it answers, so replaying a POST /api/v1/send that timed out sends the message a second time. The
client therefore retries by method:
| Situation | Retried? |
|---|---|
GET on a 5xx or connection error |
Yes, up to retry.attempts. |
401 on any method |
Once — the token is refreshed and the call replayed immediately (a rejected token proves the request was never processed). A second 401 throws. |
429 on any method |
Yes — waits for the server's Retry-After (capped at 30s) and replays. The throttle rejects before any processing happens, so this is safe for sends too. |
send / sendBatch on a 5xx or timeout |
Only when idempotency.enabled is true. |
400, 403, and every other 4xx |
Never — an ApiException is thrown at once. |
Waits grow exponentially and carry jitter: retry.sleep_ms doubles per attempt (1s, 2s, 4s… capped at 10s) with
half of each delay randomised, so clients that fail at the same moment don't all retry at the same moment. A 429
keeps the server's Retry-After as the floor and adds up to a second of spread on top.
With idempotency.enabled = false (the default) a failed send throws after a single attempt. Handle it yourself —
usually by letting the queued job retry with a key of your own, see below.
Set NOTIFICATION_IDEMPOTENCY=true only if the service honours the Idempotency-Key header and replays the
original response for a repeated key. The client then sends a fresh key with every send and reuses it across that
call's retries, so a duplicate never reaches your users.
For a send that your application itself can issue twice — a queued job that gets retried, a form the user double-submits — pass a stable key so both attempts collapse into one notification:
The same parameter exists on SendBatchNotificationData.
Usage
Dependency Injection (recommended)
Facade
Sending Notifications
SMS — plain message
SMS — pattern (template code)
Push Notification
Using a Template (any channel)
Targeting a Specific Provider
Adding Tags
Batch Notifications
Querying Notifications
List with filters
Walk every page
eachNotification() pulls one page at a time and yields the items lazily, so the whole result set never has to fit
in memory:
Driving the loop by hand works too — advance the filter, otherwise you keep re-fetching page 1:
Get single notification
Batches
Providers & Tags
Error Handling
All exceptions extend Esanj\NotificationClient\Exceptions\NotificationClientException.
| Exception | When thrown |
|---|---|
AuthenticationException |
Cannot fetch/refresh OAuth token (bad credentials, service unreachable) |
RateLimitException |
The token endpoint returned 429. Credentials are valid — you're just asking for tokens too often |
ApiException |
Non-retriable HTTP error (4xx, persistent 5xx, or a 429 that survived the back-off) |
ConfigurationException |
The package isn't configured — a missing NOTIFICATION_* env var, a base_url that isn't a URL, or plain HTTP in production. Thrown when the client is resolved, and the message names the variable to set |
UnexpectedResponseException |
HTTP 200, but the body isn't usable JSON (a proxy or WAF page), or the payload is missing a field the contract guarantees. The message quotes the body or names the field |
NotificationClientException |
Base class — all exceptions above extend this |
Fields the service may legitimately leave empty are typed nullable rather than blowing up: providerName and
providerChannel (null when the underlying provider record is gone), and updatedAt on notifications, batches and
tags (null instead of being parsed into a fake "now").
RateLimitException and ApiException::isRateLimited() both carry $e->retryAfter — the server's Retry-After
in seconds, or null when it didn't send one. It's exactly what $job->release() wants.
What the status codes mean
ApiException has a helper for each one, so you never have to compare $e->statusCode by hand:
| Status | Helper | What happened |
|---|---|---|
0 |
isConnectionError() |
No response at all — timeout or refused connection |
400 |
isBadRequest() |
The request is well-formed but unusable: no active provider for this channel, or the chosen provider doesn't support it. Not a field error, so getErrors() is empty — read $e->getMessage() |
401 |
isUnauthorized() |
The token was rejected. The client already refreshed and retried once |
403 |
isForbidden() / isPermissionDenied() |
The token is fine; this client lacks the service permission for that endpoint. Grant it on the service |
404 |
isNotFound() |
No notification, batch, tag or provider with that identifier |
422 |
isValidationError() |
Field-level validation failed — getErrors() returns the map |
429 |
isRateLimited() |
Throttled, and still throttled after the client backed off. $e->retryAfter holds the server's hint |
5xx |
isServerError() |
The service failed, and every allowed retry was used |
isClientInputError() covers 400 and 422 — the two cases where the fix is in what you sent, not in
retrying. Checking only isValidationError() silently misses the "no active provider" case.
Testing
The package integrates cleanly with Guzzle's MockHandler. In your feature tests:
Available Payload Classes
| Class | Channel | Factory |
|---|---|---|
SmsPayload |
SMS | SmsPayload::fromMessage('text') |
SmsPatternPayload |
SMS | SmsPatternPayload::make('key', ['var' => 'val']) |
EmailPayload |
EmailPayload::make()->subject(...)->html(...) |
|
PushPayload |
Push | PushPayload::make()->title(...)->body(...) |
TemplatePayload |
Any | TemplatePayload::make('key')->variables([...])->language('fa') |
Resource Properties
NotificationResource
| Property | Type | Description |
|---|---|---|
uuid |
string |
Unique notification identifier |
status |
string |
pending | queued | processing | sent | failed | delivered | undelivered |
channel |
string |
sms | email | push |
recipient |
string |
Recipient address / token |
batchUuid |
string\|null |
Parent batch UUID if sent as part of a batch |
sentAt |
CarbonImmutable\|null |
When the message was sent |
createdAt |
CarbonImmutable |
|
updatedAt |
CarbonImmutable\|null |
Null when the service didn't send one |
BatchResource
| Property | Type | Description |
|---|---|---|
uuid |
string |
Unique batch identifier |
status |
string |
pending | processing | canceled | completed |
totalNotifications |
int |
Number of notifications in the batch |
processedNotifications |
int |
Notifications processed so far |
createdAt |
CarbonImmutable |
|
updatedAt |
CarbonImmutable\|null |
Null when the service didn't send one |
progressPercentage() |
float |
Computed progress 0–100 |
ProviderResource
| Property | Type | Description |
|---|---|---|
id |
int |
Client-provider row id |
providerName |
string\|null |
Null if the provider record no longer exists |
providerChannel |
string\|null |
Null for the same reason |
providerId |
int |
The provider this row points at |
orderColumn |
int |
Selection order |
createdAt |
CarbonImmutable |
TagResource
| Property | Type | Description |
|---|---|---|
id |
int |
|
name |
string |
|
description |
string\|null |
|
color |
string\|null |
|
usedCount |
int |
How many notifications carry the tag |
createdAt |
CarbonImmutable |
|
updatedAt |
CarbonImmutable\|null |
Null when the service didn't send one |
Documentation
For a complete, beginner-friendly, step-by-step walkthrough — installing, sending your first notification, building custom payloads, swapping the client implementation, testing, and troubleshooting — see docs/GUIDE.md.
Changelog
See CHANGELOG.md for release history.
License
MIT — © Esanj
All versions of notification-client with dependencies
ext-mbstring Version *
ext-json Version *
illuminate/support Version ^12.0|^13.0
illuminate/contracts Version ^12.0|^13.0
illuminate/cache Version ^12.0|^13.0
illuminate/log Version ^12.0|^13.0
guzzlehttp/guzzle Version ^7.5
nesbot/carbon Version ^2.72|^3.0
psr/http-message Version ^1.1|^2.0
psr/log Version ^1.0|^2.0|^3.0