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

Sparkpost Bundle Laravel Package

hanfrey/sparkpost-bundle

hanfrey/sparkpost-bundle provides a Symfony bundle integration for SparkPost, wiring configuration and services so PHP applications can send transactional email via SparkPost's API. Useful for projects that need SparkPost email delivery without manual service setup.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/*.php publish, no facade, and no auto-discovery. If you are in a pure Laravel app, the honest answer is that you should not install this — use sparkpost/sparkpost directly, or Laravel's HTTP client against https://api.sparkpost.com/api/v1/transmissions. Read this assessment as "what to borrow from the bundle's pattern" unless you are actually running Symfony (or a Laravel app that has deliberately adopted Symfony's DI container).

If you are in a Symfony context, the minimal path is:

  1. composer require hanfrey/sparkpost-bundle plus the underlying SparkPost client it wraps.
  2. Register the bundle — Flex does this in config/bundles.php; otherwise add it to bundles.php/Kernel manually. The bundle class name and namespace are the first thing to confirm from the README, since the package name hanfrey/sparkpost-bundle does not map cleanly to a namespace by convention.
  3. Configure the API key in config/packages/*.yaml (typically a api_key: '%env(SPARKPOST_API_KEY)%' entry under the bundle's config root) and put the real key in .env.local or Symfony Secrets — never in the committed YAML.
  4. Inspect the container to find the service id/alias: bin/console debug:container sparkpost (or debug:container | grep -i spark). That alias is your integration point; the bundle's Resources/config/services.xml is the authoritative source for what is registered, whether it is public, and whether the client is lazy.

The first use case worth wiring up is a transactional send through the Transmissions API — a password reset or receipt — because it exercises the whole path (key → client → template → response) in one call and gives you a real response body to log.

Implementation Patterns

Inject the client, not the container. The valuable thing a bundle like this does is hand you a configured SparkPost\SparkPost (or equivalent) instance. Type-hint that in your own mailer service and treat the bundle as bootstrap-only; that keeps your application code portable if the bundle is abandoned (which, at 9 stars and zero dependents, is a realistic outcome).

final class TransactionalMailer
{
    public function __construct(private \SparkPost\SparkPost $client) {}

    public function sendPasswordReset(string $to, string $token): void
    {
        $this->client->transmissions->post([
            'content' => [
                'template_id' => 'password-reset',
                'use_draft_template' => false,
            ],
            'substitution_data' => ['reset_url' => "https://app.test/reset/{$token}"],
            'recipients' => [['address' => ['email' => $to]]],
        ]);
    }
}

Send from a job, never from the request. Wrap the mailer call in a queued job (Symfony Messenger, or a Laravel job if you're bridging). SparkPost round-trips are tens to hundreds of milliseconds and are a failure source you do not want inside a web request. This also gives you retry semantics for 5xx/429 without touching the bundle.

Use templates with substitution_data rather than building HTML in PHP. It keeps content changes out of deploys and lets marketing edit copy without a release. Keep one template id per transactional message and pass the minimum substitution set (tokens, names, URLs) so you never leak internal identifiers.

Isolate the transport behind one interface. Define MailerInterface in your app with one or two methods, implement it with this bundle's client, and add a NullMailer/array-logger implementation for tests. Your controllers and domain services should never see SparkPost\SparkPost. When the bundle dies, you swap one class.

Use sandbox mode in CI. SparkPost accepts a sandbox flag on transmissions which validates the template and payload without delivering. Combine that with a fake client bound in the test container; you get schema validation in CI without burning reputation or quota.

Handle webhooks yourself. The bundle almost certainly does not ship a webhook controller. SparkPost posts delivery/open/bounce events to your endpoint; write a controller that reads the raw request body (not the parsed one), verifies the credentials SparkPost sends, and hands off to a queue for processing. Do not do database writes inline — event bursts are large and bursty.

Gotchas and Tips

  • This is a low-traction package (9 stars, 0 dependents). Before adopting, check the last commit date, the PHP and Symfony version constraints in composer.json, and whether it pins the underlying client to a version you can live with. A bundle that wraps an old client version is worse than a direct dependency, because you cannot upgrade the client independently.
  • No auto-discovery for Laravel. If you are bridging into Laravel, you must write a service provider that binds the SparkPost client into the container with your services.sparkpost key, plus a config file. That is 20 lines and is the whole value of the bundle — at which point just write the 20 lines and skip the bundle entirely.
  • Symfony config is compiled. After changing the bundle config or env vars, run bin/console cache:clear. %env()% values are resolved at container compile time unless you use env() runtime processors; a stale cache is the #1 "my key isn't being picked up" cause.
  • Exceptions surface late. The SparkPost client throws on non-2xx responses, and the exception often contains a per-recipient error array that you must read to know which address bounced. Catch the client's exception type explicitly, log the full response body at warning level, and re-throw only for genuinely retryable conditions.
  • Classify failures before retrying. 4xx (bad template id, invalid recipient, unauthorized key) will never succeed on retry — dead-letter them. 429 and 5xx are retryable with backoff. Don't hand a generic "retry 3 times" policy to the queue without this split.
  • Rate limits are per-account. If you add a second sending path (marketing + transactional), they share the same key and quota. Use separate API keys/subaccounts so you can throttle or revoke them independently.
  • Extension point is the HTTP handler stack, not the bundle. If you need request logging, retry, or correlation IDs, decorate the underlying Guzzle/PSR-18 handler rather than patching the bundle. Overriding the service definition in services.yaml (same id, your class) is cleaner than forking.
  • Don't put the API key anywhere it can be cached or dumped. debug:container and Symfony's profiler will happily show you resolved config; keep the key in Secrets, and confirm it isn't serialized into a container dump in a build artifact.
  • Treat the dependency as replaceable from day one. The interface isolation and template-driven payloads described above are not ceremony — they are what makes walking away from an unmaintained 9-star bundle a one-hour task instead of a two-week one.
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.
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
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource