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.
Confirm the framework line before anything else. This package targets Laravel 5.x only. Run composer require graham-campbell/contact and, if it conflicts, composer why-not graham-campbell/contact — a conflict against illuminate/support means you are on Laravel 6+ and this is not a candidate. Do not force it with --ignore-platform-reqs; the failure mode is runtime breakage in container bindings and mail/event signatures, not a clean install error.
composer require graham-campbell/contact
Laravel 5.0–5.4 predates auto-discovery, so register the provider explicitly:
// config/app.php
'providers' => [
// ...
GrahamCampbell\Contact\ContactServiceProvider::class,
],
Publish only what you intend to own:
php artisan vendor:publish --provider="GrahamCampbell\Contact\ContactServiceProvider"
Where to look first, in this order:
vendor/graham-campbell/contact/src/ — read the service provider first. It tells you the route prefix, route names, view namespace, config key, and whether anything is bound into the container. This is a few-hundred-line package; read all of it before configuring anything.config/contact.php after publishing — recipient address, subject, view, redirect target.old() usage, error display, CSRF token.First use case. Set MAIL_DRIVER=log (or Mailtrap), set the recipient in config/contact.php, then visit the published route (typically /contact). Submit; confirm the entry lands in storage/logs/laravel.log or your inbox. That one round trip proves provider registration, route registration, view resolution, and mail configuration at once. Do it before you touch styling.
Sanity checklist before showing it to anyone:
php artisan route:list | grep -i contact # route exists and is named
php artisan config:clear # if you changed config/contact.php
Override the view, not the package. Publish views and edit your copy under resources/views/vendor/contact/ (Laravel's standard package view override path). Wrap the form in your site layout by editing the published Blade file:
@extends('layouts.marketing')
@section('content')
<div class="max-w-xl mx-auto">
@include('vendor.contact.form')
</div>
@endsection
If the package view already extends a layout name you don't have, add that layout — cheaper than rewriting the view.
Reuse the form partial outside the package route. Include it on a landing page or in a modal, but point action at the named route so you never hardcode a URI:
<form method="POST" action="{{ route('contact.store') }}">
{{ csrf_field() }}
...
</form>
Keep field names exactly as the validation rules expect (name, email, message); renaming an input silently breaks validation and surfaces as confusing "required" errors rather than a crash.
Configure by environment, not by committed file. Recipients belong in .env:
// config/contact.php
'recipient' => env('CONTACT_RECIPIENT', '[email protected]'),
Then [email protected] per environment. Keeps real addresses out of version control and lets staging send to a sink.
Take over the route when you need custom behaviour. Point the URI at your own controller extending the package controller:
class ContactController extends \GrahamCampbell\Contact\Http\Controllers\ContactController
{
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:100',
'email' => 'required|email|max:255',
'message' => 'required|string|max:5000',
'website' => 'prohibited', // honeypot
]);
// persist + queue here, then reuse the parent's response shape
return parent::store($request);
}
}
Extending gets you the package's redirect/success UX while you own validation and side effects.
Add persistence to close the silent-failure gap. A form that only sends mail leaves no record when transport fails. Write to a contact_messages table in the same request, before dispatch:
ContactMessage::create($request->only('name', 'email', 'message'));
Mail::to(config('contact.recipient'))->queue(new ContactMessageReceived($message));
Queue the send. The package sends synchronously, so request latency tracks your SMTP round trip. Swapping Mail::send for a queued Mailable is the highest-value change you can make — a two-line edit in a forked controller.
Localisation. To change copy without editing views, publish and override the language file; to add a locale, copy resources/lang/en/contact.php to resources/lang/{locale}/contact.php.
Test the three states that matter. A happy-path feature test (Mail::fake(), assert redirect + assertSent), a validation-failure test (assertSessionHasErrors(['email'])), and a honeypot test (assert no mail sent when the trap is filled). Fifteen minutes, and they catch every regression a future edit will introduce.
The framework constraint is a wall, not a slope. Do not try constraint aliasing or --ignore-platform-reqs to install on Laravel 6+. Container bindings and mail/event signatures break at runtime in ways far more expensive to diagnose than to avoid. If the gate fails, extract the requirements and build natively — a FormRequest, a Mailable, a migration, a rate limiter: 1–3 days.
Config cache bites on the second deploy. php artisan config:cache freezes config/contact.php; changing .env afterwards has no effect until you re-run config:clear or re-cache. Classic symptom: "the recipient is still the old address on staging."
Publishing overwrites. vendor:publish --force will clobber your restyled view and local config. Publish once, commit the results, and never run --force in a loop — or publish only the tags you need (--tag=views, --tag=config).
Route name and URI collisions. If you already have a contact route or a ContactController in App\Http\Controllers, the package registration will either shadow yours or throw on duplicate route names depending on load order. php artisan route:list is the first command when "the form goes to the wrong place."
CSRF and the token. The package view includes csrf_field(), but reusing the partial in a JS-injected modal makes it easy to drop — you get a 419 that looks like a validation problem. Keep the token inside the partial itself.
Header injection is on you. If any user-supplied value flows into the subject or a Reply-To header, strip newlines and validate strictly. A form that lets \r\n into Reply-To becomes an open relay and a domain-reputation incident. Use a Mailable with ->replyTo($validatedEmail) and let Symfony handle encoding rather than assembling header strings.
No rate limiting, no honeypot, no throttling — by default. This is the package's biggest day-to-day gap. Add both, even internally:
Route::post('contact', 'ContactController@store')
->middleware('throttle:5,1'); // Laravel 5.2+; otherwise a hand-rolled limiter
Pair with a prohibited honeypot and, on a public marketing page, a CAPTCHA behind a feature flag.
Mail::send failures are silent to the user. The submitter sees the success redirect whether or not delivery succeeded. Log the send result, and if you queue, add failed-job alerting — otherwise lost inquiries surface only as customer complaints weeks later.
Deliverability is infrastructure, not code. Set a verified From: on your own domain with SPF and DKIM; sending "from" the submitter's address is the fastest route to a spam folder. Use the submitter's address in Reply-To only.
No extension points means no listening. There are no events, repository interfaces, or bindable contracts here — "integration" means binding your own controller or editing a fork. Isolate it behind a first-party interface (ContactSender) so the dependency never leaks into application code, and pin the fork by commit SHA in a private VCS if you keep it.
Debugging order for a form that "doesn't work": route:list (routing) → config:clear (recipient/subject) → MAIL_DRIVER=log (delivery, isolate mailer from SMTP/DNS) → storage/logs/laravel.log (exceptions, including validation). Ninety percent of issues are one of those four.
Treat it as a spec, not a dependency. Zero dependents and no maintainer signal — adopting this is vendoring. Read the source once and document the behavioural contract (fields, rules, recipients, copy, success/failure UX). That document is what you reimplement natively at the next framework upgrade, which is a question of when, not if.
How can I help you explore Laravel packages today?