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.
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:
composer require hanfrey/sparkpost-bundle plus the underlying SparkPost client it wraps.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.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.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.
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.
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.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.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.services.yaml (same id, your class) is cleaner than forking.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.How can I help you explore Laravel packages today?