Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Elasticemail Php Laravel Package

elasticemail/elasticemail-php

ElasticEmail PHP SDK for managing email via the REST API: send campaigns, transactional email, contacts, and templates using a generated Guzzle-based client with API key auth. Requires PHP 7.4+.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture Fit

  • RESTful API Alignment: Still aligns well with Laravel’s HTTP conventions. The package now exposes an optional Idempotency-Key header on the three send endpoints (POST /emails/transactional, POST /emails, POST /emails/mergefile), which is a contract-level guarantee we can lean on rather than hand-rolling dedupe logic in App\Services.
  • Service-Oriented Design: Modular API endpoints (CampaignsApi, ContactsApi, EmailsApi) still map cleanly to a service layer. The new capability concentrates in EmailsApi, so the send path becomes the single place where idempotency policy lives — a good seam for a SendEmailService / EmailDispatcher abstraction.
  • Event-Driven Potential: Unchanged for EventsApi/StatisticsApi. One new signal worth emitting: a EmailSendReplayed event when the response carries Idempotency-Replayed: true, so analytics can distinguish “new send” from “retry confirmed” without double-counting volume.
  • Architectural shift: Idempotency is now a client-supplied concern. The design question moves from “can we safely retry?” to “who owns the key lifecycle in our domain model?” The natural owner is the queued job / persisted email record, not the controller.

Integration Feasibility

  • Laravel Ecosystem Compatibility: Unchanged — Guzzle-based client, .env-stored X-ElasticEmail-ApiKey, Http facade compatibility, middleware for rate limits and retries.
  • Retry semantics now first-class: The package’s documented behavior (24-hour replay window, Idempotency-Replayed: true, 409 while in flight, 422 on body mismatch, 503 + Retry-After, key released on 4xx/5xx failure) maps directly onto Laravel queue retry semantics — with the important caveat that Laravel’s $tries/$backoff window must stay inside 24 hours.
  • Key generation is trivial in PHP: The release ships a random_bytes UUIDv4 recipe, but for correctness we should not generate the key inside the job attempt. Generate once at dispatch time, persist it, and pass it through every retry.
  • Breaking-change surface: The new $idempotency_key argument sits before the optional $contentType argument on emailsTransactionalPost, emailsPost, emailsMergefilePost and their ...WithHttpInfo, ...Async, ...AsyncWithHttpInfo, ...Request variants. Any code calling these positionally with a content type will silently misbehave (content type becomes the key). Payload-only calls are unaffected.

Technical Risk

  • Positional-argument break (highest immediate risk): Not a semver-major, but a real signature break for positional $contentType callers. Silent failure mode (invalid key → 400, or worse, a valid-but-wrong key string) rather than a compile error. Requires a codebase grep before upgrade.
  • Key reuse anti-pattern: Reusing one key across different sends returns 422; reusing across environments/accounts is safe (keys are account-scoped, subaccounts separate) but confusing. Needs a documented invariant: one key per logical send, reused only for retries of that send.
  • Key/retry window mismatch: Keys live 24 hours from first request. A Laravel job with long backoff or retryUntil() beyond 24h will retry with an expired key and send a duplicate. Retry policy must be bounded to <24h, with a hard failure/alert path beyond that.
  • 409 in-flight handling: Concurrent retries (e.g., overlapping queue workers or a manual re-dispatch) will get 409. Treating 409 as a job failure would be wrong; it needs a delayed release, not a failure.
  • 503 + Retry-After: Must be honored explicitly; default Laravel exponential backoff may retry sooner than instructed.
  • Replay detection is not free: A replayed response returns the original TransactionID/MessageID. Downstream code that assumes MessageID uniqueness per response will silently collapse duplicate records — good in principle, but must be tested.
  • Scope gaps: Idempotency covers the three send endpoints only. Bulk/merge campaigns, contact imports, and campaign state changes remain non-idempotent and still need application-level safeguards.
  • Async variants: The key is threaded through ...Async variants, but async + idempotency means the 409/503 responses surface later; error handling must be attached to the promise resolution path, not just the sync path.

Key Questions

  1. Which callers pass $contentType positionally today? Inventory before bumping to ^4.4; migrate to named arguments (idempotencyKey:) to make future signature changes safe.
  2. Where does the key live in our domain model? Proposal: a uuid column on the persisted email_messages / outbound_emails table, generated on record creation, immutable thereafter.
  3. What is our max retry horizon? Must be <24h and enforced in job config, with an explicit “give up + alert” terminal state rather than an eventual duplicate send.
  4. Do we need idempotency on bulk/campaign sends? If bulk volume is material, the absence of keys there is a residual double-send exposure we should quantify.
  5. How do we reconcile replayed responses with analytics? Do we count Idempotency-Replayed: true as a send, a suppressed duplicate, or neither?
  6. Subaccount mapping: Keys are scoped per account with subaccounts separate — does our multi-tenant model map to ElasticEmail subaccounts, and is the key derivation tenant-aware?
  7. 503 without Retry-After or with an unparseable value — what is our default backoff, and who owns that decision?
  8. Observability: Are we capturing Idempotency-Replayed, 409, 422, 503 as first-class metrics, distinct from generic HTTP errors?

Integration Approach

Stack Fit

  • Service Providers: Singleton registration approach unchanged; the send path is where the key is injected:
    $this->app->singleton(EmailsApi::class, function ($app) {
        $config = ElasticEmail\Configuration::getDefaultConfiguration()
            ->setApiKey('X-ElasticEmail-ApiKey', config('services.elasticemail.key'));
        return new EmailsApi(new GuzzleHttp\Client(), $config);
    });
    
  • Queue System: SendTransactionalEmailJob should read $this->emailMessage->idempotency_key (persisted at record creation) and pass it as a named argument:
    $emails->emailsTransactionalPost(
        email_transactional_message_data: $payload,
        idempotency_key: $this->emailMessage->idempotency_key,
    );
    
  • Job retry policy: public $tries = 5;, public $backoff = [5, 30, 120, 600];, and a retryUntil() capped well under 24h. Add a failed() handler that flags the record as undeliverable — never re-dispatches with a fresh key automatically.
  • 409 handling: Catch the API exception for 409, release($delay) with backoff, do not count as a failure attempt against $tries if practical (or budget for it explicitly).
  • 503 handling: Parse Retry-After and honor it; fall back to configured backoff only when the header is absent/unparseable.
  • Events: Emit EmailSendReplayed (from the Idempotency-Replayed: true response header) alongside EmailSent so downstream consumers can dedupe correctly.
  • Facade: A thin ElasticEmail facade remains useful, but the key must be a required, explicit parameter on the send helper — no defaulting to null inside the service layer, or the protection silently evaporates.

Migration Path

  1. Phase 0 — Signature audit (do this first, before any deploy)
    • grep -rn "emailsTransactionalPost\|emailsPost\|emailsMergefilePost" across app, packages, and tests.
    • Identify every call passing a second positional argument. Rewrite to named arguments.
    • Add a static-analysis rule (PHPStan/Psalm baseline or a custom PHPCS sniff) forbidding positional calls to these three methods.
  2. Phase 1 — Key plumbing (no behavior change)
    • Add idempotency_key (UUID, unique index) to the outbound email table via migration; backfill existing rows.
    • Generate at record creation. Ship with the key passed but retries still disabled — verify keys flow end-to-end via logs.
  3. Phase 2 — Enable retry-with-same-key
    • Turn on queue retries with the bounded backoff/retryUntil config.
    • Replace any existing application-level dedupe (e.g., “sent_at” checks, unique constraints on message hash) that now overlaps with the API guarantee — keep one mechanism, not two.
  4. Phase 3 — Observe and tune
    • Instrument Idempotency-Replayed, 409, 422, 503 counters.
    • Run fault injection (kill connection mid-send) in staging to prove a retry returns the stored response and does not produce a second MessageID.
  5. Phase 4 — Roll out
    • Non-production first, then a low-volume production segment behind a feature flag, then full.
    • Keep the null-key code path (original behavior) available as a kill switch.

Compatibility

  • Package constraint: Bump to elasticemail/elasticemail-php: ^4.4. Do not widen to ^4.0 if we rely on the $idempotency_key argument — the argument does not exist below 4.4 and PHP will throw on the named argument (or, worse, on positional calls, not at all).
  • Semver caveat: This is a minor version with a non-additive-at-the-end signature change. Treat as behaviorally breaking; pin exactly and review 4.5.0 notes before bumping.
  • GuzzleHttp: ^7.0 still holds.
  • Laravel: Http::fake() still works, but fakes must now assert the Idempotency-Key header is present and stable across retries — that assertion is the regression test that matters most.
  • PHP: Unchanged; random_bytes and named arguments both require PHP 7.4+/8.0+ respectively — named arguments effectively require PHP 8.0+, which should be confirmed against our floor.
  • Client-side validation: 1–255 printable ASCII. A UUIDv4 always satisfies this; do not substitute free-text correlation IDs (e.g., email subject lines) that could exceed 255 or contain non-ASCII.

Sequencing

  1. Audit & pin — signature grep, composer require elasticemail/elasticemail-php:^4.4, lock file review.
  2. Schema — idempotency_key column, unique index, backfill.
  3. Service layer — named-argument sends, key required (not optional) in our wrapper.
  4. Queue layer — bounded retries, 409 release semantics, 503 Retry-After handling, terminal failure path.
  5. Observability — headers and status codes as metrics; structured logs include the key (it is safe to log — not a secret — and essential for correlation).
  6. Testing — feature tests for replay, 422 mismatch, 409 in-flight, 400 invalid, 503 retry; a fault-injection test that simulates a timeout after the request was accepted.
  7. Deploy — staged rollout with a flag; monitor duplicate-send rate and replay rate for one full 24h window before full rollout.

Operational Impact

Maintenance

  • Dependency updates: elasticemail/elasticemail-php is now on a path of evolving send-endpoint signatures. Subscribe to release notes for any endpoint we use; re-run the positional-argument audit on every minor bump. Add a composer.json constraint ^4.4 with a documented policy that minor bumps require the audit script to pass.
  • Key lifecycle: The key is generated once and persisted forever (it is only meaningful for 24h server-side, but keeping it is harmless and useful for audit). No rotation needed — it is not a credential. Do not build key-rotation jobs.
  • Retention: Keys and their request bodies are stored server-side for 24h. Confirm this against any data-retention or DPA commitments if payloads contain PII (they do — recipient addresses, message bodies).
  • Schema management: One migration and one column; low ongoing cost. Keep the unique index to catch accidental key reuse in our own code before it reaches the API as a 422.

Support

  • Troubleshooting: Status codes are now diagnostically meaningful and should be surfaced in the runbook:
    • 400 → our key generator or validation is wrong (code bug, not an API problem).
    • 409 → concurrent processing of the same logical send (queue misconfiguration or manual re-dispatch).
    • 422 → key reused with a different body/endpoint (code bug: key not scoped to one logical send).
    • 503 → upstream could not confirm the key; retry with the same key, honor Retry-After.
  • Documentation: Add an internal “Idempotency contract” page covering the one-key-per-logical-send invariant, the 24h window, the bounded retry policy, and the fact that a released key (after 4xx/5xx) means a corrected retry runs as a new send — not a replay.
  • Support script: Extend the existing API-key/quota validation script to verify key generation (ASCII/length) and to replay-check a known TransactionID within 24h.

Scaling

  • Horizontal scaling: Idempotency makes sharded queue workers safe for send retries — previously, worker redelivery was a duplicate-send risk. This is a genuine scaling unlock for bulk transactional volume.
  • Throughput note: 409 is more likely at higher concurrency when the same logical send is processed by overlapping workers. Ensure the queue does not double-dispatch the same email record (ShouldBeUnique on the job, keyed by message ID) — idempotency protects correctness, but 409-driven retries cost latency and API quota.
  • Caching: Unchanged for read-heavy endpoints; sends must never be cached at the HTTP layer.
  • Load testing: Add an idempotent-retry scenario to the k6/Dusk suite — N concurrent workers retrying the same payload must yield exactly one TransactionID and one charge. This is the single most valuable load test to add.

Failure Modes

Failure Scenario Mitigation Strategy
Duplicate send after timeout Persist key at record creation; retry with the same key; expect Idempotency-Replayed: true.
Key generated per attempt (bug) Key sourced only from the persisted record; static analysis / unit test asserts stability across retry() invocations.
Retry beyond the 24h key window Cap retryUntil() well under 24h; terminal failure + alert rather than a fresh-key auto-resend.
422 — same key, different body Treat as a code bug (payload mutated between retries); fail the job and page. Do not auto-generate a new key.
409 — request still in flight Release the job with delay; do not count as a hard failure; alert if it recurs for the same message.
400 — invalid key Fail fast, log the key, fix the generator; do not retry.
503 + Retry-After Honor the header exactly; retry with the same key; only fall back to configured backoff if absent.
First attempt 5xx → key released Corrected retry runs as a new send (documented behavior); ensure our failure classification doesn’t assume replay safety here.
$contentType positional call after upgrade Pre-upgrade audit + named arguments + static-analysis rule; covered by Phase 0.
Replay response double-counted in analytics Emit EmailSendReplayed; exclude from volume metrics; reconcile against TransactionID uniqueness.
ElasticEmail outage Queue locally (database driver) and retry within the 24h window with the persisted key.
Subaccount key leakage across tenants Verify key derivation is tenant-scoped; keys are account/subaccount-scoped server-side, so a cross-tenant retry surfaces as a mismatch or a new send.

Ramp-Up

  • Onboarding: Quickstart gains a mandatory “Idempotent sends” section: key generation at record creation, named arguments in the wrapper, bounded retry config, and the status-code table above. Include the caution that idempotency applies to sends only — campaign, contact, and bulk operations still require application-level safeguards.
  • Training: Short session on distributed-systems retry semantics (why “retry with the same key” ≠ “retry”) plus the specific 409/422/503 behaviors. Engineers should be able to explain why generating the key inside the job attempt is wrong.
  • Code review checklist: Named arguments on all send calls; key from persisted record; no new key on retry; 409 released not failed; 503 honors Retry-After; retry horizon <24h.
  • Adoption metrics: Replay rate (proxy for how much duplicate sending we’re now preventing), 409 rate, 422 rate (should be zero), terminal-failure rate, and the delta in duplicate TransactionID/MessageID counts against pre-4.4 baseline.
  • Success criteria: Zero duplicate sends observed under fault injection; 422 rate at 0; replay rate stable and explained by known retry volume; retry horizon confirmed <24h in production config.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk