cboxdk / laravel-engage-client
Cbox Engage client for Laravel: product events in, transactional messages out, and an `engage` mail driver with a circuit breaker and a fallback mailer.
Requires
- php: ^8.4
- illuminate/bus: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/mail: ^12.0 || ^13.0
- illuminate/queue: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- symfony/mailer: ^7.2 || ^8.0
- symfony/mime: ^7.2 || ^8.0
Requires (Dev)
- larastan/larastan: ^3.12
- laravel/pint: ^1.32
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.0 || ^5.0
- pestphp/pest-plugin-laravel: ^4.0 || ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The Cbox Engage client for Laravel. Three things, all against the workspace's ingest key:
- Product events in.
ProductEvents::record()queues one event to Engage'sPOST /v1/events; nothing that goes wrong reaches the caller. - Transactional messages out.
Messages::send()andMessages::status()overPOST /v1/messagesandGET /v1/messages/{id}, with typed receipts and exceptions. - The
engagemail driver.MAIL_MAILER=engagesends every Mailable through Engage's send gate, with a circuit breaker and a fallback mailer for when Engage is down, so a password reset always leaves.
Documentation: docs/index.md. Start with the quickstart.
Install
composer require cboxdk/laravel-engage-client
Two environment keys, the same ones every Cbox app already uses:
ENGAGE_INGEST_URL=https://app.cboxengage.com ENGAGE_INGEST_KEY=ek_…
With either empty, product events go nowhere, Messages throws EngageNotConfigured
on use, and the engage mailer refuses to be built. Publish the config with
php artisan vendor:publish --tag=engage-config for the rest (config/engage.php).
Product events
use Cbox\Engage\Contracts\ProductEvents; use Cbox\Engage\Events\ProductEvent; public function __construct(private ProductEvents $events) {} $this->events->record(ProductEvent::byContact( name: 'project.created', eventId: "cbox-id:project.created:{$project->id}", // deterministic: a retry is one event userExternalId: $user->cbox_id, companyExternalId: $organization->cbox_id, props: ['project_id' => $project->id], ));
Messages
use Cbox\Engage\Contracts\Messages; use Cbox\Engage\Messages\{InlineContent, OutgoingMessage, Recipient}; $receipt = app(Messages::class)->send(new OutgoingMessage( idempotencyKey: "otp:{$user->id}:{$slot}", to: new Recipient(externalId: $user->cbox_id, email: $user->email), brand: 'br_01…', content: new InlineContent('Your code', "<p>{$code}</p>", $code), )); $receipt->queued(); // or ->gatedOut() with ->reasons app(Messages::class)->status($receipt->id)->latestEvent()?->status; // 'delivered', 'bounced', …
The mail driver
// config/mail.php 'default' => env('MAIL_MAILER', 'engage'), 'mailers' => [ 'engage' => ['transport' => 'engage', 'fallback' => 'resend'], 'resend' => ['transport' => 'resend'], ], // A Mailable use Cbox\Engage\Mail\EngageOptions; class PasswordReset extends Mailable { use EngageOptions; /* … */ } Mail::to($user)->send((new PasswordReset($token))->engage(externalId: $user->cbox_id, idempotencyKey: "reset:{$token->id}"));
When Engage is unavailable five times in a minute, the breaker opens for two minutes and
mail goes straight through resend; one probe every two minutes brings Engage back. A
suppressed address throws Cbox\Engage\Mail\MailRefused instead of being sent elsewhere.
Testing
use Cbox\Engage\Testing\InteractsWithEngage; // in your TestCase $engage = $this->fakeEngage(); $engage->assertRecorded('project.created', fn ($e) => $e->props['project_id'] === $project->id); $engage->assertSent(fn ($m) => $m->idempotencyKey === "reset:{$token->id}");
Development
composer qa # pint, phpstan (max), pest, licenses, audit, sbom
MIT. See LICENSE.md and SECURITY.md.