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

Hermes Bundle Laravel Package

2lenet/hermes-bundle

HermesBundle is a simple mail manager for CruditBundle that lets you create and send templated emails through Symfony Mailer. It supports consent-management links and optional statistics for tracking consent and link clicks, with setup via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. 2lenet/hermes-bundle is a Symfony bundle built for CruditBundle admin panels, wired on top of Symfony Mailer. It is not a Laravel package: there is no service provider, no facade, no config/hermes.php, and no Blade support. So the honest starting position for a Laravel developer is a decision: are you bolting this on for its persistence model + templated-send service, or are you better served by Mail + Notifications + a mail_templates table of your own? Adopt only if (a) you need admin-managed, DB-stored email templates with recipient tracking/subscriptions, and (b) you accept running Symfony Mailer alongside — or instead of — Laravel's mailer.

Minimal install path.

composer require 2lenet/hermes-bundle

Then:

  1. Read docs/index.md in vendor/2lenet/hermes-bundle/ first — the README is a stub that just points there. That file plus docs/img/dashboard.png is the entire onboarding surface.
  2. Inspect src/ before writing code. The public surface you actually care about is small: the Mail entity (subject, body/template, status, attachments), the Recipient entity (address, status, error message), and the mailer/manager service that renders and sends. Class names and constructor signatures are undocumented — read them, don't guess.
  3. Decide the persistence bridge. The bundle ships Doctrine ORM entities mapped to Mail / Recipient / subscription tables. In an Eloquent app you either (a) run Doctrine alongside Eloquent for these three tables, or (b) copy the migration schema into Laravel migrations and write a thin Eloquent model over it, losing the bundle's repositories and admin CRUD. Option (b) is usually the pragmatic one.
  4. Wire a transport. If you go the Symfony Mailer route, the cleanest Laravel-side bridge is spatie/laravel-symfony-mailer, or configure symfony/mailer directly with a DSN from config/mail.php values so you don't end up with two divergent mail configs.
  5. Add a single indirection point — e.g. app(MailDispatcher::class)->send($templateKey, $recipients, $vars) — and hide everything Hermes-specific behind it. Given 2 stars and 0 dependents, you want one file to change if you swap it out later.

First use case to prove out: a transactional notification (order confirmation / password reset) where the subject and HTML body live in a Mail row rather than a Blade file, and where you can see per-recipient delivery status. If that works end-to-end, the admin-driven campaign flow (the bundle's actual reason to exist) is a short hop; if it doesn't, you've spent an hour instead of a sprint.

Implementation Patterns

Template-per-mail-type. The core value is that a Mail record is the template: subject + body stored in the DB, rendered at send time with variables. Model one row per logical email (order.confirmation, user.welcome), key senders by a stable slug rather than by primary key, and treat the row as content that marketing/support can edit without a deploy. Keep variables explicit — a documented array of expected keys per template — because there is no schema on the body and a missing Twig variable is a runtime error, not a compile-time one.

// app/Mail/TemplateMailer.php  — the one file that knows about Hermes
public function send(string $slug, array $to, array $vars = [], array $attachments = []): void
{
    $mail = $this->templates->findBySlug($slug);      // your lookup, not the bundle's
    // delegate to the bundle's mailer/manager service here
}

Transactional sends from application code. Resolve the bundle's mailer service out of the container once, keep it out of controllers, and call it from actions/listeners. Because it renders from stored content, template changes ship without releases — which is the actual day-to-day win versus hand-editing Blade files.

Queue everything. Symfony Mailer sends synchronously and the bundle's send() is annotated to throw (release 1.8.2 explicitly documented the exceptions). Wrap the call in a Laravel job with retries and failed() logging:

final class SendTemplatedMail implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function handle(TemplateMailer $mailer): void
    {
        $mailer->send($this->slug, $this->to, $this->vars);
    }
}

Admin-driven campaigns. This is the bundle's home turf: a Crudit-backed dashboard where an editor creates a mail, picks a recipient sublist, previews, schedules/sends, and can cancel or resend. If you have the Crudit panel, use it as-is. If you're Laravel-only, this flow is the part you'd be reimplementing in Filament/Nova — factor that reimplementation cost into the adoption decision, because it's where most of the bundle's ~130 releases of features live (CC sublists in 1.10.6, resend-recipient in 1.8.2, cancel sending in 1.10.0, attachment deletion in 1.9.3, CC-aware mail status in 1.14.5).

Recipient sublists and CC. Since 1.10.6 a mail can carry a CC sublist independent of the primary recipients. Model this deliberately: one recipient row per address per send — CC addresses included — so status and error text are per-address. Don't fan out a single row to multiple addresses — you lose the per-recipient error tracking added in 1.7.16, which is the best debugging affordance the bundle has. Since 1.14.5 the mail status aggregates CC recipients too (Crudit #668), so a mail is only fully "sent" once CC deliveries resolve; any status computation you write yourself must mirror that, or your UI and the bundle's will disagree.

Attachments. Supported since 1.9.2, deletable since 1.9.3. Store files via Laravel's filesystem and only hand the bundle paths/contents — don't let two abstractions both decide where bytes live, or attachment cleanup becomes orphan-row archaeology.

Localization. 1.11.0 added translatable emails. If you're multilingual, decide now whether that means one Mail row per locale (the safer read of "translatable email") or a translated body field. Mixed conventions mid-project are painful to unwind, and the unsubscribe-link translations (fixed in 1.11.1) live in the same area.

Unsubscribe flow. Treat subscription state as a first-class part of the send path, not an afterthought: generate absolute URLs from a configured base (the 1.8.6 fix was exactly this), route them through a signed URL, and verify they work behind a proxy before any real campaign goes out.

Gotchas and Tips

  • No Laravel integration exists. No service provider, no Mail::fake() interception, no Blade. You will write the container binding, the config mapping, and the testing strategy yourself. Budget for that; it's the single biggest hidden cost.
  • Two ORMs is a smell that compounds. Doctrine entities inside an Eloquent app work until you need transactions across both, or a join between recipients and users. Prefer copying the schema into Laravel migrations and writing Eloquent models over those tables — you keep the data, lose the bundle's repositories and admin, and gain a codebase your team can actually read.
  • Two mail configs will bite you. config/mail.php (Laravel) and Symfony Mailer's DSN are separate sources of truth. If mail silently vanishes, check the Symfony transport/spool first — MAIL_MAILER=log in Laravel does nothing for Symfony Mailer, and vice versa. Configure one from the other at boot.
  • Sending is synchronous and throws. Wrap every call site in try/catch, persist the failure into the recipient's error field, and only then re-throw to the queue. An uncaught transport exception in a loop aborts the whole campaign.
  • Per-recipient status is your debugger. When "the email didn't arrive," the answer is almost always in the recipient row (error message) rather than in a log. Build a small admin/debug view over recipients early — one query saves hours. Remember CC addresses are rows too as of 1.14.5's status change.
  • MJML is configured, not automatic. The bundle integrates MJML + CKEditor on personalized templates (1.10.1, and the 1.8.1 change moved rich-text config out to Crudit). If MJML isn't rendering, look at the template's content-type config and whether the external MJML renderer is actually reachable — not at the mail body.
  • Twig in a Blade app. Either add a Twig bridge (extra dependency, extra footgun with helpers/filters) or render the body yourself and pass final HTML in. If you're storing user-edited HTML/MJML anyway, the second option is less machinery.
  • Pin the version and vendor the fork. 2 stars, 0 dependents, but a genuinely busy release cadence, now at 1.14.x (Symfony 8 compatibility in 1.9.0, Crudit compatibility shims in 1.10.6 and 1.14.5). That combination means fast upstream movement and no ecosystem safety net — composer require 2lenet/hermes-bundle:1.14.* with a committed lockfile, and be ready to fork.
  • Extension points, in order of usefulness: (1) decorate/alias the mailer service — cheapest override; (2) override Twig templates/partials via the bundle's resource path in templates/bundles/... — this now includes the status partial introduced in 1.14.5; (3) custom Crudit field/brick if you're in the admin; (4) fork. Anything below (3) is fork territory, so keep the adapter thin enough that you never need it.
  • Cancellation and resend are status transitions, not deletions. Cancelling a send (1.10.0) leaves rows behind; resending (1.8.2) mutates recipient state. Make sure your reporting queries filter on status rather than assuming one row per address equals one delivery, or your metrics will double-count — the CC-participates-in-status behaviour added in 1.14.5 makes naive counting wrong in a new way too.
  • Test with Symfony's test transport, not Mail::fake(). Point the Symfony DSN at null:// or the test transport in CI, and assert on the resulting Mail/Recipient rows — that's the contract the bundle actually honours.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator