graham-campbell/contact
A Laravel package for adding a contact form and managing submitted enquiries. It provides the model, migrations, validation, and email notifications needed to capture and store contact messages in your application.
Architecture fit. graham-campbell/contact is a single-purpose Laravel 5 package that wires four things together: a route pair (show form / accept POST), a controller with hardcoded validation (name, email, message), a Blade view, and a synchronous Mail::send to a configured recipient. There is no domain layer, no persistence, no events, no contracts, and no repository interface. For a TPM this matters less as a code-quality critique and more as an ownership statement: the package is a pre-built controller plus a form, not a platform component. It fits only where the requirement is literally "accept a message and email it to an address" — marketing landing pages, support inboxes, internal notification forms. It fits nowhere the requirement involves routing rules, ticketing/CRM handoff, audit trails, PII retention policy, or multi-tenant recipient logic.
Integration feasibility. Mechanically trivial if and only if the framework gate passes. composer require resolves only against illuminate/* 5.x; on Laravel 6+ the constraint conflicts and forcing it with --ignore-platform-reqs or aliasing produces runtime breakage in container bindings and mail/event signatures — the expensive, hard-to-diagnose kind. On a genuine 5.x app the integration is: register ContactServiceProvider in config/app.php (no auto-discovery before 5.5), vendor:publish, set a recipient, set MAIL_DRIVER=log, submit the form, confirm in storage/logs/laravel.log. A one-hour spike validates provider registration, routing, view resolution, and mail config simultaneously. That is the acceptance test for the feasibility question, and it should be run before any planning commitment is made.
Technical risk. Ranked by what actually hurts:
| Risk | Severity | Why |
|---|---|---|
| Laravel 5 EOL (both the app and the package) | High | Security patching is your problem, not the maintainer's. The framework line itself is unsupported. |
| No extension points | Medium-High | No events, no contracts, no bindables. Any customisation means binding your own controller, editing a fork, or both. The dependency leaks into application code by default. |
| No rate limiting / no honeypot / no bot mitigation | Medium-High | Public form = abuse vector on day one. This is your requirement to add, not a package feature. |
Synchronous Mail::send |
Medium | Request latency tracks SMTP round-trip; a slow or failing mailer degrades the user-facing response path. |
| Silent delivery failure | Medium | Submitter sees the success redirect whether or not mail left the building. Lost inquiries surface as customer complaints weeks later. |
Header injection via subject/Reply-To |
Medium | User input into headers without strict validation turns the form into an open relay and a domain-reputation incident. |
| Zero stars, zero dependents, no evident CI or disclosure channel | Medium | This is vendoring with extra steps. Score 0.3 and zero dependents are the signal, not noise. |
Key questions (answers close the evaluation, roughly in this order):
composer why-not graham-campbell/contact settles it in minutes. Laravel 6+ → stop, extract requirements, build natively.FormRequest, a queued Mailable, a migration, and throttle:5,1 is the whole feature. The package saves hours, not weeks.contact route name or ContactController? Collisions either shadow yours or throw on duplicate route names depending on load order.Stack fit. Narrow by design. It assumes Laravel 5.x, Blade, the framework's Mail facade, and the framework's translation system. It plays acceptably with a conventional Laravel app layout (config/, resources/views/vendor/, app/Http/Controllers/) and badly with anything that has its own abstraction over notifications, its own form-handling conventions, or a non-Laravel mail path. There is no adapter surface, so fit is evaluated once, at adoption, and does not improve.
Migration path. Two viable paths, and the choice should be made explicitly at adoption rather than discovered later:
CONTACT_RECIPIENT via .env, include the published partial in your layout. The package owns the route, controller, validation, and send. Ship in hours. Accepted debt: the package is on your request path with no seam.ContactController extending the package controller (or replacing it), and put everything behind a first-party interface — ContactSender — so the dependency never appears in application code. Pin the fork by commit SHA in a private VCS. This is the only path that survives the eventual framework upgrade without a rewrite scramble, and it costs perhaps half a day more.Whichever path is chosen, write the behavioural contract down: field names, validation rules, recipient resolution, subject/copy, success and failure UX, redirect target. That document is the artefact you reimplement natively later; it is also the acceptance criteria for the spike.
Compatibility. Three constraints to verify rather than assume:
php artisan route:list | grep -i contact after install.throttle middleware. Below that, a hand-rolled limiter. Verify before promising the abuse-control checkbox.php artisan config:cache freezes config/contact.php; subsequent .env changes silently do nothing until re-cache. This must be in the deploy runbook, not learned on staging.Sequencing. Do not parallelise this; each step gates the next.
composer why-not. Kill or continue.MAIL_DRIVER=log, one round-trip submission, confirm the log entry.contact_messages for the silent-failure gap), queued send, deliverability (verified From: with SPF/DKIM on your own domain; submitter address in Reply-To only), retention policy. Size each. This list is usually larger than the adoption itself and belongs in the same estimate.Mailable replacing synchronous Mail::send; persistence write before dispatch; throttle:5,1; prohibited honeypot; header-injection defence by using ->replyTo($validatedEmail) rather than string-assembled headers.Mail::fake + assertSent), validation failure (assertSessionHasErrors), honeypot (assert no mail sent). Fifteen minutes of work that catches every future regression.vendor:publish --force in a loop.Maintenance. The honest framing is build for free, maintain forever. Zero stars, zero dependents, no visible CI, no disclosure channel means there is no upstream to depend on — you are the maintainer the moment it merges. Concrete tail: reading the source once at adoption (it is a few hundred lines; budget the afternoon), patching for any PHP or framework bump on the host app, and responding yourself to any security issue in the form path. Put this in capacity planning as a line item, not a footnote. The mitigating move is isolation behind ContactSender so the eventual native reimplementation is a swap, not an excavation.
Support. No vendor, no SLA, no issue-triage expectation. Internal support means whoever adopted it. Two mitigations that cost little: the behavioural contract document (so a second engineer can pick it up cold) and the three feature tests (so a future edit cannot silently break validation or the honeypot). Debugging order should be documented in the runbook because it is non-obvious and highly repeatable: route:list (routing) → config:clear (recipient/subject) → MAIL_DRIVER=log (isolate mailer from SMTP/DNS) → storage/logs/laravel.log (exceptions, including validation). Roughly ninety percent of "the form doesn't work" resolves in those four steps.
Scaling. Not a scaling concern in the throughput sense — a contact form is low-volume by nature, and the package imposes no meaningful per-request cost beyond one synchronous SMTP round-trip. Two scaling axes do matter:
throttle:5,1 plus a prohibited honeypot field is the floor; a public marketing page warrants a CAPTCHA behind a feature flag.Mailable is a two-line change in the forked controller and should be treated as the first hardening task, not a later optimisation.Failure modes. Enumerate these in the runbook, because several are silent:
| Failure | Symptom | Detection |
|---|---|---|
| Mail transport fails | Submitter sees success redirect anyway | None by default — requires logging send results or a contact_messages table |
| Cached config after deploy | Recipient still the old address on staging | config:clear / re-cache in deploy script |
| Route name or URI collision | Form posts to the wrong place | route:list at boot |
| Missing CSRF in a JS-injected modal | 419 that reads like a validation error | Keep csrf_field() inside the partial |
| Validation field rename | Confusing "required" errors, not a crash | Pin field names in the contract doc |
Header injection via Reply-To |
Spam-folder delivery, domain-reputation damage | Strict validation + ->replyTo() + newline stripping |
| Queued send exhausts retries | Inquiries lost with no alert | Failed-job alerting if you move to a queue |
The single most important line in that table is the first: a contact form that only sends mail has no record when transport fails. Persisting to contact_messages before dispatch is the minimum-viable close of that gap and should be scoped as a required addition, not a nice-to-have.
Ramp-up. Low individual ramp, moderate organisational ramp. An engineer familiar with Laravel reads the source and ships in a day. The organisational cost is the framing: this must be recorded as adoption plus fork ownership, never as vendor selection, so the maintenance tail shows up in future capacity planning rather than as a surprise in someone's sprint. Realistic total for a competent team on a Laravel 5.x app: one day to adopt and spike, two to three days to harden (persistence, queue, throttle, honeypot, deliverability, three tests) — against a one-to-three day native build that you own outright with no fork tail. That comparison, not the package's feature list, is the decision worth putting in front of stakeholders.
How can I help you explore Laravel packages today?