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

Contact Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. The package's routes file — the exact URI and route name you will link to from your nav.
  3. config/contact.php after publishing — recipient address, subject, view, redirect target.
  4. The Blade view (then your published copy) — field names, 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

Implementation Patterns

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.

Gotchas and Tips

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.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect