composer run check # lint → analyse → test (preferred local workflow)
composer run test # PHPUnit with Clover + HTML coverage → coverage/
composer run analyse # PHPStan level 8, --memory-limit=1G
composer run format # Pint — modifies files in-place
composer run lint # Pint --test — checks formatting without modifyingCoverage gotcha: composer run test and composer run check both require a coverage driver (Xdebug or pcov). Without one, PHPUnit exits with error. For local runs:
vendor/bin/phpunit --no-coverageRun a single test:
vendor/bin/phpunit --filter MethodName --no-coverageCI (.github/workflows/php.yml) runs in this order: composer validate --strict → test → lint → analyse. Different from composer run check (lint → analyse → test). CI matrix covers PHP 8.3/8.4/8.5 × Laravel 10.x-13.x, excluding Laravel 10 from newer PHP versions.
Five source files in src/, all under namespace Ronald2Wing\LaravelMailtrap\ (PSR-4):
| File | Role |
|---|---|
MailtrapServiceProvider.php |
Registers the mailtrap transport via MailManager::extend(). Auto-discovered via extra.laravel.providers in composer.json. Reads from config('mailtrap') by default; each mailer may override via 'config' => 'mailtrap.marketing' (dot-notation sub-key). Publishes config/mailtrap.php with tag mailtrap-config. |
MailtrapTransport.php |
Symfony AbstractTransport. Uses Laravel HTTP client (Illuminate\Http\Client\Factory) to POST JSON from MessagePayloadFactory to apiUrl(). __toString() includes the host (e.g. mailtrap+api://send.api.mailtrap.io). firstMessageId() extracts the message ID from the response. |
MessagePayloadFactory.php |
Translates Symfony Email + Envelope into Mailtrap's JSON payload. Pure, no I/O. Entry point: build(). DIRECTIVES constant maps Mailtrap headers to payload keys. |
MailtrapConfig.php |
final readonly value object — token, endpoint enum, optional inbox id, optional base URL override, HTTP options. Factory: fromArray(). Missing optional keys use constructor defaults (e.g. endpoint → Transactional, HTTP → connect_timeout:10/timeout:30). |
MailtrapEndpoint.php |
String-backed enum: Transactional, Bulk, Sandbox. Has baseUrl() and requiresInboxId() methods. |
Exceptions live in src/Exceptions/:
MailtrapException(base, extendsRuntimeException)InvalidConfigurationException extends MailtrapException— token empty, bad URL, missing/invalid inbox_id, non-integer inbox_id, non-array HTTP configMailtrapApiException extends MailtrapException— non-2xx HTTP responses. Has$statusCode(int) and$body(string) readonly properties. Named constructor:fromResponse(Response $response).MailtrapTransportException extends MailtrapException— connection failures (DNS, timeout, TLS). Named constructor:fromConnectionError(ConnectionException $e). Constructor is private.
PHPStan runs at level 8 with no ignored errors on both src/ and tests/. Never add baseline entries or @phpstan-ignore comments.
- Base classes:
Orchestra\Testbench\TestCasefor container-aware tests;PHPUnit\Framework\TestCasefor pure unit tests. - HTTP mocking:
Http::fake()+Http::assertSent(fn (Request $r) => ...)instead of GuzzleMockHandler/HandlerStack. firstMessageId(private) is tested indirectly throughsend()results (checking debug output).getEnvironmentSetUp()configuresmailtrap.token,mail.default,mail.mailers.mailtrap, andmail.fromfor integration tests.- Test trait
BuildsTestEmailsprovides shared constants (SENDER_EMAIL,RECIPIENT_EMAIL,SENDER_NAME,TEST_TOKEN,API_URL) andbasicEmail().
declare(strict_types=1)in every file.composer.jsonhassort-packages: true— Composer auto-sorts aftercomposer require. Keep it valid for CI.- PHPDoc with
@paramarray shapes on every public/protected method.
- Add an entry to
MessagePayloadFactory::DIRECTIVES: specify thekey(payload field name) and whether the value needsjson_decode('json' => true). - Done.
extractDirectives()picks it up automatically, andreservedHeaderNames()strips it from the forwarded headers block.