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

Email Engine Laravel Package

sfcod/email-engine

Email Engine is a Laravel/PHP package for building reusable email templates and sending transactional or bulk mail through a clean, configurable API. It helps organize layouts, content, and delivery settings so email workflows stay consistent and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install the package and publish its config:
    composer require sfcod/email-engine
    php artisan vendor:publish --provider="Sfcod\EmailEngine\EmailEngineServiceProvider"
    
  • Open config/email-engine.php first. That file is the single source of truth for driver credentials, the default driver, and any queue settings. Everything else in the package is driven from here.
  • Before configuring real SMTP/API credentials, set the default driver to a local/log driver (or a mailhog-style sink) so you can exercise the send path without touching a live provider.
  • Compose and send your first message using the package's fluent message object rather than Laravel's Mail facade. That is the whole point of adopting the package — you get one API across providers.
  • Then prove the queued path works end-to-end: stop the queue worker, dispatch a send, confirm the job lands on the queue (php artisan queue:work --once or your Horizon dashboard), and confirm delivery. Slow provider calls inside the request lifecycle are exactly what this package is meant to avoid.
  • Order of things to look at on day one: (1) the published config, (2) the driver interface/contract, (3) the message object API, (4) the service provider bindings. Those four files tell you everything about how to extend and swap behaviour.
  • First real use case: transactional email (password reset, receipts, notifications) where you want the provider to differ per environment — log in local, a sandbox provider in staging, a paid provider in production — without changing a line of application code.

Implementation Patterns

  • Keep provider choice in config, not in code. Resolve the driver through the container and read the selected driver from config('email-engine.driver'). Switching from one provider to another, or splitting drivers per environment, should be a .env change only.
  • Wrap the package behind one app-level service. Create an App\Mail\Mailer (or NotificationSender) that depends on the package's contract and expose your own send(Message $message) method. Application code depends on your service, not on the vendor's message object. This contains the blast radius if the package goes unmaintained and you need to replace it.
  • Send through the queue, always. Use the package's queued sending for anything that talks to a provider over HTTP. It gives the slow, flaky, rate-limited provider call a job wrapper you can retry and backoff — the single biggest day-to-day win of the package.
  • Configure retries and backoff deliberately. Provider timeouts and 429s are normal. Set tries, backoff, and a failed() handler on the job that logs the provider, recipient, and provider error payload — not just the exception message.
  • Fallback driver pattern. Where the package supports multiple drivers, wire a primary and a fallback: try the primary, and on a provider-specific failure signal (outage class, not a validation 4xx) re-dispatch against the fallback. Never blindly retry every error against the fallback — auth and payload errors will fail identically everywhere.
  • Compose messages fluently and keep the object immutable-ish. Build the message, hand it to the queue, do not mutate it after dispatch. Any per-recipient customisation (unsubscribe links, tenant from-address, merge fields) should be finalised before the job is pushed, because the queue serialises the state you pushed.
  • Per-tenant or per-domain driver selection. If you send on behalf of customers, resolve the driver at dispatch time from tenant config rather than a single global default. Pass the resolved driver key into the job as a scalar so the job is reconstructable after a deploy.
  • Use a fake/in-memory driver in tests. Assert on the message object (recipient, subject, template, attachments) rather than on HTTP. Reserve one thin integration test per provider against its sandbox for smoke coverage. This keeps the fast suite fast and provider credentials out of CI.
  • Instrument delivery. Emit an app event on dispatch and on success/failure. The package gives you sending, not deliverability analytics — pair it with your own logging or a webhook consumer if you need bounce/complaint tracking.

Gotchas and Tips

  • Staleness is the headline risk. Last release was 2022 and the repo has single-digit stars. Pin the version in composer.json instead of relying on a floating range, and plan for the possibility that you own future compatibility work.
  • The 1.2.0 change is a Symfony 5.4 floor, not a feature. That has two consequences: it aligns with Laravel 8.x, and it likely conflicts with Laravel 9+/Symfony 6 stacks. Run composer why-not sfcod/email-engine 1.2.0 before committing to it, and check whether the constraint is ^5.4 (floor) or ~5.4 (ceiling) — the difference changes your upgrade runway.
  • config:cache will silently swallow env changes. Credentials must be read via env() inside the config file, and you must re-run php artisan config:cache after changing them. A stale cached config pointing at the old provider is a classic "why is it still sending from the wrong domain" bug.
  • Restart queue workers after config or code changes. php artisan queue:restart. Workers hold the old config in memory; long-lived workers are the usual culprit for "it works locally but sends from the wrong provider."
  • Provider-specific limits leak through the abstraction. Sending quotas, per-second rate limits, verified-sender-domain rules, and attachment size caps all differ. The package unifies the API, not the policies. Rate-limit your job middleware to the strictest provider you might use, otherwise a driver swap surprises you in production.
  • Retries can duplicate email. A timeout after the provider accepted the message looks identical to a failure. Keep retries low, use a provider-side idempotency key or message reference if supported, and never let a "retry all failures" admin button run unattended.
  • Debugging order: (1) switch the driver to the log driver and confirm the message object is correct, (2) run the job synchronously (dispatchSync) to surface the real exception, (3) inspect the raw provider response in logs — the package's normalised exception may drop the provider's status code and error text. Log the original payload in your job's failed().
  • Extension points to prefer, in order: custom driver implementing the package's contract → container binding override → published config → events. Do not patch vendor code. A custom driver is cheap here and is the intended seam; forking is not.
  • Clear caches after publishing or upgrading:
    php artisan package:discover
    php artisan config:clear
    php artisan cache:clear
    
    Missing package:discover after install gives you a "class not found" far from the actual cause.
  • Symfony 5.4 deprecation noise can turn into test failures if your suite converts deprecations to exceptions. Do the first upgrade in a branch and check the first CI run for E_DEPRECATED output before merging.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource