Skip to content

Forms internals ​

The path from a form built in the panel to a stored lead, two mails and a conversion event. The user side is on Forms and leads and Form Spam Protection.

Definition ​

Models\Form (evolve_forms): label, a unique name derived from the label (createUniqueFormName() adds a numeric suffix on collision), is_active, data (the fields as JSON) and the mail settings (admin_address, replyto_address, sender_name, sender_address, email_subject, email_body, redirect_url). Http\Livewire\FormBuilder edits data: text, email, textarea, checkbox and file fields with label, placeholder, required, width; file fields are posted as name[].

Form types (2.17): evolve_forms.type holds the key of a registered Contracts\FormType, builder for built forms. Evolve::registerFormType() stores the class and resolves it through the container on first use; getFormTypes() and getFormType($key) read the registry. On the model, Form::BUILDER, formType() (the instance, null for built forms and unknown keys) and isTyped(). The data accessor returns FormType::fields() for a registered type and the stored JSON otherwise, so the theme, validation messages, file and carbon copy handling read typed and built forms the same way. FormManager offers the type only on create (Rule::in over the registered keys) and keeps it on save; FormBuilder::ensureEditable() answers 403 for every write on a typed form. Developer guide: Form Types.

Submission ​

Http\Controllers\Frontend\FormController handles POST api/form/{form:name?}:

  1. Builds the validation rules: for built forms from the form's fields through Concerns\HandlesFormData, for typed forms from FormType::rules() (buildTypedValidationRules(), which adds addKeywordRules() over the type's text fields). Both get user_identification with Rules\Honeypot (encrypted render time, must be at least evolve.honeypot-threshold seconds old; fax_number must be empty), g-recaptcha-response with Rules\Recaptcha when services.google.recaptcha.secret is set (verifies at Google, requires score for v3 tokens, rejects when Google is unreachable), and Rules\Keywords over evolve.forms.blocked-keywords and blocked-patterns.
  2. Builds the lead attributes. Built forms: fillable request keys into their columns, the rest into payload (builderLeadAttributes()). Typed forms: the result of FormType::prepare(), fillable keys into their columns, the rest into payload, path from the request (typedLeadAttributes()). applyConsent() reads the consent field (Form::consentField(): accepts_marketing, or sync.consent_field set by the email module's Marketing card). When it is ticked, Lead::recordConsent() stores its label (its name when the label is empty) as consent_text with consent_given_at and consent_hash (SHA-256 of the text) and sets accepts_marketing. Under double opt in (Form::usesDoubleOptIn()) accepts_marketing stays false and consent_given_at empty until the email module records the confirmation. The consent and sync columns (synced_at, sync_error, contact_id) are not fillable.
  3. Stores the Lead with its field data and attachments (limits from evolve.forms), downloadable through LeadAttachmentController behind auth.
  4. Fires Events\LeadCreated (2.17), then notifies: Notifications\NewLeadReceived to the form's recipient (Recipients\FormRecipient), Notifications\LeadReceivedConfirmation to the sender when the form has a confirmation subject and body. Since 2.19 both notifications return a rendered mail template from toMail() when the form has confirmation_template_id or notification_template_id, and the Markdown mail otherwise. The core binds Contracts\LeadMailTemplates to MailTemplates\FormMailTemplates itself since 2.22 (bindIf, a project may bind its own), see Mail templates below and Mail Templates for Form Mails. The lead's locale is set on creation from the session locale. Notifications\UnreadLeads exists for a digest but is not scheduled by the package.
  5. Records TrackingEvent::generateLead() through ServerSideTracking with a fresh Support\EventId, see Tracking pipeline.
  6. Redirects to _redirect from the request, else the form's redirect_url, else back to the page, with trackingEventId in the session so the theme pushes the browser event with the same ID.

Validation failures redirect back with errors; the spam fields have no visible input, so the theme's form component shows a generic message when any of them fails.

Mail templates ​

Part of the core since 2.22, before in the email module (#338). Models\MailTemplate (email_templates, the table name stays) carries blocks through HasBlocks and implements BlockContainer (blockEditUrl() points at evolve.mailtemplate.edit) and Selectable. The rendering lives under core/src/MailTemplates/: TemplateRenderer renders a template for one lead in the lead's language (blocks and subjects without that translation fall back to the default locale) and returns a RenderedMail (subject, HTML, plain text through HtmlToText, locale, unknown placeholders); Placeholders fills {{ lead.* }}, {{ form.* }}, {{ site.* }} and FormType::placeholders(); LeadSummary builds the rows of the lead summary block; SampleLead is the unsaved lead of the preview and the test send; TemplatedMail wraps a rendered mail for the mailer; FormMailTemplates implements LeadMailTemplates. Services\EmailDocumentRenderer turns any block carrier into the table layout evolve::email.document with the block views evolve::email.blocks.*; the email module's campaigns render through the same class with email::render.campaign as the wrapper. The 16 email.* blueprints live in core/src/MailTemplates/Blueprints and are registered as a package path in evolve.blueprints.paths, with selectable: false so the page editor never offers them. Panel: Http\Livewire\MailTemplateManager, Http\Controllers\Backend\MailTemplateController, MailTemplatePreviewController, MailTemplateTestSendController, views evolve::mailtemplate.*, routes evolve.mailtemplate.* behind the forms' role middleware, translations evolve::mail-templates.*. The look comes from evolve.mail (theme, dialect, merge-tags, asset-base); a published config/email.php of the module with those keys wins, see EmailDocumentRenderer::theme() and mergeTags().

Stored names did not change. Blocks (evolve_blocks.blockable_type), activity rows (subject_type) and block props (__model) written by the module name the template Racerfish\Email\Models\MailTemplate. That string is the morph alias of the core model (MailTemplate::MORPH_ALIAS, registered with Relation::morphMap() in EvolveServiceProvider::bootMailTemplates()), so old rows resolve and new rows are written with the same name; no migration touches the data. The module keeps Racerfish\Email\Models\MailTemplate, Racerfish\Email\Templates\* and Racerfish\Email\Services\EmailDocumentRenderer as deprecated empty subclasses until 3.0; an instance of the old model class reports the same morph class and reads the same rows. Tests: core/tests/Feature/MailTemplates/, all with the email module switched off (Tests\Support\SwitchesModules), MailTemplateMorphAliasTest for the stored names, EmailModuleDefaultTest for the default and the binding; the module tests extend email/tests/EmailTestCase.php, which switches the module on.

Cached pages ​

The honeypot timestamp and the CSRF token would go stale inside a page-cache hit. The theme refreshes both on load through /api/csrf-token, which the page cache never writes.

Events ​

Event registrations (event/src/Http/Controllers/Frontend/EventController::register()) reuse the honeypot and reCAPTCHA rules (2.16) and the form's recipient for their notification, but store Participant rows instead of leads.

Tests ​

core/tests/Feature/*Form*, *Honeypot*, *Recaptcha*; the 2.16 changes are covered in the tests added by #274, form types, LeadCreated and the consent snapshot in FormTypeTest (2.17, with the workbench type CallbackRequestFormType).