This document describes the internals of the lettr/lettr-laravel package, for people working on the package. If you are working with it, read the README and docs.lettr.com/quickstart/laravel instead.
This is a Laravel integration for the lettr/lettr-php SDK. It provides:
- A Laravel Mail driver (
MAIL_MAILER=lettr), so Mailables andMail::to()send through Lettr - A
Lettrfacade that exposes the SDK's services, resolved lazily from the container Mail::lettr()for sending a Lettr template inline, without writing a MailableLettrMailable/InlineLettrMailablebase classes for template-backed mail- Artisan commands for syncing templates and generating typed Blade, DTO and enum artifacts
- A config file and service provider handling API key resolution and auto-discovery
The SDK does the HTTP work. This package owns the Laravel-shaped surface around it, and deliberately keeps a thin pass-through where Laravel adds nothing — Lettr::audience() and Lettr::campaigns(), for example, return the SDK's own services.
lettr-laravel/
├── config/
│ └── lettr.php # Published config: API key + template paths
├── src/
│ ├── Concerns/
│ │ ├── DisplayHelper.php # Shared console output formatting
│ │ └── ThrottlesApiRequests.php # Retry-After aware rate-limit retry for commands
│ ├── Console/
│ │ ├── Enums/
│ │ │ └── Theme.php # Console colour theme
│ │ ├── CheckCommand.php # lettr:check
│ │ ├── GenerateDtosCommand.php # lettr:generate-dtos
│ │ ├── GenerateEnumCommand.php # lettr:generate-enum
│ │ ├── InitCommand.php # lettr:init
│ │ ├── PullCommand.php # lettr:pull
│ │ └── PushCommand.php # lettr:push
│ ├── Exceptions/
│ │ └── ApiKeyIsMissing.php # Thrown when no API key is configured
│ ├── Facades/
│ │ └── Lettr.php # Facade over LettrManager
│ ├── Mail/
│ │ ├── InlineLettrMailable.php # Mailable built by Mail::lettr()
│ │ ├── LettrMailable.php # Base class for template-backed Mailables
│ │ └── LettrPendingMail.php # Fluent chain behind Mail::lettr()
│ ├── Services/
│ │ └── TemplateServiceWrapper.php # Laravel-flavoured wrapper over the SDK templates service
│ ├── Support/
│ │ ├── BladeToSparkpostConverter.php # Blade -> SparkPost syntax (push)
│ │ ├── DtoGenerator.php # Merge tags -> typed DTO classes
│ │ ├── MailerMixin.php # Registers the Mail::lettr() macro
│ │ └── SparkpostToBladeConverter.php # SparkPost -> Blade syntax (pull)
│ ├── Transport/
│ │ └── LettrTransportFactory.php # Symfony Mailer transport
│ ├── LettrManager.php # Lazily resolves the SDK and exposes its services
│ └── LettrServiceProvider.php # Registration, publishing, mail driver, VERSION
├── stubs/ # Templates for generated Mailables, DTOs and enums
├── tests/
│ ├── Feature/ # Console commands + README example tests
│ ├── Unit/ # Manager, transport, mail, converters, SDK surface
│ ├── Pest.php
│ └── TestCase.php
├── .github/workflows/ # ci.yml (PR checks), release.yml (tag -> release)
├── composer.json
├── phpstan.neon # Larastan, level 8
├── phpunit.xml
├── pint.json
└── README.md
- Holds
VERSION, which is sent as thelettr-laravel/<version>User-Agent suffix — bump it on every release - Registers
LettrManageras thelettrsingleton, resolving the API key fromconfig('lettr.api_key')withconfig('services.lettr.key')as a fallback, and throwingApiKeyIsMissingwhen neither is set - Extends Laravel's Mail system with the
lettrtransport - Registers the
Mail::lettr()macro and the six Artisan commands - Publishes
config/lettr.php
Resolves the SDK lazily — nothing hits the network until a service is actually used. Exposes emails(), domains(), projects(), webhooks(), health(), audience(), campaigns() and templates(), each also reachable as a magic property (app('lettr')->audience). All but templates() return the SDK's own service; sdk() returns the underlying client for anything not wrapped.
Implements Symfony Mailer's AbstractTransport. Converts a Symfony Email into the SDK's SendEmailData, carrying template slug/version and substitution data across via X-Lettr-* headers, and rethrows SDK failures as TransportException.
lettr:init scaffolds config and directories, lettr:check verifies the integration end to end, lettr:pull / lettr:push sync templates (converting between Blade and SparkPost syntax), and lettr:generate-enum / lettr:generate-dtos produce typed artifacts from template merge tags.
composer install
composer test # Pest
composer lint # Pint (append -- --test to check without fixing)
composer analyse # PHPStan via Larastan, level 8CI runs all three across the supported PHP and Laravel matrix on every PR. See CONTRIBUTING.md for the workflow and VERSIONING.md for the release process.
| File | Covers |
|---|---|
Unit/LettrServiceProviderTest.php |
Registration, API key resolution, User-Agent |
Unit/LettrTransportTest.php |
Symfony Email -> SendEmailData conversion |
Unit/LettrPendingMailTest.php |
The Mail::lettr() chain |
Unit/AudienceServiceTest.php |
Audience service resolution + bulk contact surface |
Unit/CampaignServiceTest.php |
Campaign service resolution |
Unit/SdkSurfaceTest.php |
SDK builders, filters, DTOs and enums exposed via the facade |
Unit/DtoGeneratorTest.php, Unit/*ConverterTest.php |
Code generation and Blade/SparkPost conversion |
Feature/*CommandTest.php |
Each Artisan command |
Feature/ReadmeDocTest.php |
Every code example currently in the README |
ReadmeDocTest.php is generated by the readme-doc-test skill and must track the README exactly — if an example is removed from the README, its test moves to SdkSurfaceTest.php rather than staying behind.
| Package | Constraint |
|---|---|
php |
^8.4 |
lettr/lettr-php |
^2.5.0 |
illuminate/http, illuminate/support |
^10.0 | ^11.0 | ^12.0 | ^13.0 |
symfony/mailer |
^6.2 | ^7.0 | ^8.0 |
laravel/pint ^1.18 · larastan/larastan ^2.0|^3.0 · mockery/mockery ^1.5 · orchestra/testbench ^8.17|^9.0|^10.8|^11.0 · pestphp/pest ^2.0|^3.7
Constraints drift — composer.json is the source of truth if this table disagrees with it.
- Auto-discovery registers the provider and the
Lettrfacade alias via theextra.laravelblock incomposer.json composer.lockis intentionally gitignored; this is a library, so CI resolves dependencies fresh.ide.phpexists only to give IDEs theMail::lettr()macro signature and is never loaded at runtime