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

Simple Contact Form Laravel Package

solution-forest/simple-contact-form

A simple Laravel contact form package that handles form rendering, validation, email delivery, and spam protection out of the box. Drop it into any Laravel app to add a working contact page with minimal setup and configurable recipients.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, single-maintainer package (12 stars, 0 dependents), so treat the source as the documentation. Install it, publish everything, and read src/ before you build on top of it.

composer require solution-forest/simple-contact-form

Then publish and migrate in one pass:

# If the provider isn't auto-discovered, or you want everything at once:
php artisan vendor:publish --provider="SolutionForest\SimpleContactForm\SimpleContactFormServiceProvider"

# Otherwise publish by tag, one concern at a time:
php artisan vendor:publish --tag=simple-contact-form-config
php artisan vendor:publish --tag=simple-contact-form-migrations
php artisan vendor:publish --tag=simple-contact-form-views

Run php artisan vendor:publish with no arguments first — it prints the available tags and provider FQCN, which saves you guessing at names.

php artisan migrate
php artisan route:list --path=contact

That route:list call is your single most useful first step: it tells you whether the package registers routes at all, what URI it claims, what middleware it applies, and the controller/action names. If nothing shows up, the package expects you to wire the route yourself.

Where to look first, in order:

  1. routes/ (or a Route:: call inside the service provider) — what URL did it just claim?
  2. config/simple-contact-form.php — mail recipient, table name, spam settings.
  3. database/migrations/ — the columns you'll actually be querying.
  4. resources/views/ — the form markup and the field names it posts.
  5. src/Http/Controllers/ — validation rules and the success/failure redirect targets.

First use case: get a submission from browser into the database and into an inbox. Drop the published form partial or component into an existing page, add @csrf, submit against a real route, then verify with php artisan tinker (Contact::latest()->first()) and a local mail catcher (Mailpit/Mailhog at 127.0.0.1:8025). Until you've seen the row and the email, you don't know the package is wired correctly — a green redirect can hide a silent Mail::send failure.

Implementation Patterns

Embedding the form. Two shapes are common: a Blade component (<x-simple-contact-form />) or a plain @include of a published partial. Prefer the component if it exists — it carries the @csrf, old-input repopulation, and error display with it. If you @include a vendor view directly (@include('simple-contact-form::form')), you inherit the markup but own none of the wiring, and you'll duplicate the @csrf token.

Keep the package's controller as the default path. Post the stock form at the stock route, let the package validate and persist. This is the lowest-maintenance configuration and the one that survives composer update cleanly. Reserve customization for the views.

Override views instead of editing vendor files. Publish the views tag and edit the copies in resources/views/vendor/simple-contact-form/. Laravel's view namespace resolution checks the app path first, so your copy wins without touching vendor/. Never edit inside vendor/ — it's wiped on every composer update.

Headless usage — skip the UI, keep the model. When the form lives inside an existing Livewire component, Inertia page, or API endpoint, ignore the package's routes and Blade entirely. Use the published migration's model directly:

$contact = Contact::create($request->validate([
    'name'    => ['required', 'string', 'max:120'],
    'email'   => ['required', 'email'],
    'message' => ['required', 'string', 'max:5000'],
]));

Then fire your own notification. This is usually the right call on a real project: you get the schema and the model for free, and you keep full control of validation, redirects, and email formatting.

Customizing validation. If you need extra rules (phone, subject enum, consent checkbox), don't patch the vendor controller. Either extend it and point a route at your subclass, or replicate the validation in your own controller and only reuse the model. Because the package has no dependents and a single maintainer, the validation rules inside it are the most likely thing to change between releases — avoid depending on their exact shape.

Notifications. If the package ships a Mailable or Notification, the useful extension point is shouldQueue and the to() recipient. Route the recipient through config so it's environment-aware:

// config/simple-contact-form.php
'recipient' => env('CONTACT_FORM_RECIPIENT', '[email protected]'),

That keeps staging from emailing real customers.

Admin listing. There's no admin UI in a package this size, and you shouldn't expect one. Build a resource controller or a Filament resource over the published model — a few lines for a table, filters on created_at, and a read_at / handled_at column you add yourself via a follow-up migration.

Testing. The package is thin; test your integration, not its internals.

use Illuminate\Support\Facades\Mail;

Mail::fake();

$this->post(route('contact.store'), [
    'name' => 'Ada', 'email' => '[email protected]', 'message' => 'Hi',
])->assertRedirect();

$this->assertDatabaseHas('contacts', ['email' => '[email protected]']);
Mail::assertSent(ContactNotification::class);

Note that Mail::fake() will happily pass even if the real mailable throws in production — do one manual end-to-end send against a real mail catcher before shipping.

Gotchas and Tips

Publish the migrations before you migrate. A package migration that lives only in vendor/ still runs, but you cannot amend it. If the table needs an extra column (a subject, an ip_address, a handled_at), publish first, edit the published file, then migrate. Adding a second migration later works too, but you'll be fighting the original schema forever.

Publish-only migrations don't auto-update. This is the biggest long-term trap. On composer update, new columns added by the maintainer will appear in vendor/.../migrations/ but your app is running the published copy from months ago. When you bump the version, diff the vendor migration directory against database/migrations/ and reconcile by hand.

Route collisions are silent until they aren't. The package probably registers something like /contact. If you already have a contact route (or a catch-all page route), you get either a duplicate-name exception at boot or a 404 because the later registration didn't override. Check php artisan route:list after install and again after any route changes in your app.

Views are compiled and cached. After editing a published view, php artisan view:clear if changes don't appear. In production, config:cache means .env changes to the recipient or reCAPTCHA keys do nothing until php artisan config:clear && php artisan config:cache.

Email "works" and nothing arrives. In dev, MAIL_MAILER=log means mail goes to storage/logs/laravel.log, and the form still redirects with a success flash. Check the log or point the app at Mailpit. Also check spam folders when testing real SMTP — contact-form mail frequently fails SPF/DKIM because it's sent From: the submitter's address. Configure a verified From: and a Reply-To of the submitter instead.

Queueing bites twice. If the notification is queued, a worker must be running or mail silently never sends in local dev. And if your queue connection is database but you never migrated jobs, submission itself may 500 after the DB insert — the row is saved but the user sees an error.

You own the anti-spam. Check whether the package ships honeypot or reCAPTCHA support, and read the code if it does — a naive honeypot (a hidden field with a predictable name) is trivially bypassed. In practice add both a rate limit and a honeypot in your own controller:

Route::post('/contact', [ContactController::class, 'store'])
    ->middleware('throttle:5,1');

If reCAPTCHA is supported, the keys must be in both config/services.php and the view, and the verification happens server-side — a client-side-only widget is decoration.

Error display is on you. If the package's controller redirects back with errors, the published form must contain @error('email') blocks and old() calls, or validation failures look like a page that simply reloaded. Verify the failure path by submitting an empty form.

Rate-limit and CSRF on the package route. If the package registers its own route, you may not control middleware. Inspect the controller/route definition; if it lacks throttle, wrap it: disable the package route in config (if supported) and register your own pointing at the same controller.

Maturity risk — read the source. 12 stars and 0 dependents means essentially no public field-testing. Budget an hour to read src/ end to end; it's small. Watch specifically for: hardcoded table names, env() calls outside config/ (these break under config:cache), unescaped output in the Blade view (contact-form content is attacker-controlled — an unescaped {{ }}→{!! !!} in an admin listing is stored XSS), and unbounded message columns.

Have an exit plan. The package is a migration, a model, and a form. If you find yourself customizing three or more things, copy the migration and model into your app and drop the dependency — the surface area is small, and owning it removes the publish-only-migration and abandoned-package risks entirely. At this size, vendoring is a legitimate choice rather than a failure.

Pin the version. "solution-forest/simple-contact-form": "1.2.*" rather than ^1.0 for anything customer-facing, and read the diff before every bump.

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.
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
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo