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?}:
- Builds the validation rules: for built forms from the form's fields through
Concerns\HandlesFormData, for typed forms fromFormType::rules()(buildTypedValidationRules(), which addsaddKeywordRules()over the type's text fields). Both getuser_identificationwithRules\Honeypot(encrypted render time, must be at leastevolve.honeypot-thresholdseconds old;fax_numbermust be empty),g-recaptcha-responsewithRules\Recaptchawhenservices.google.recaptcha.secretis set (verifies at Google, requiresscorefor v3 tokens, rejects when Google is unreachable), andRules\Keywordsoverevolve.forms.blocked-keywordsandblocked-patterns. - Builds the lead attributes. Built forms: fillable request keys into their columns, the rest into
payload(builderLeadAttributes()). Typed forms: the result ofFormType::prepare(), fillable keys into their columns, the rest intopayload,pathfrom the request (typedLeadAttributes()).applyConsent()reads the consent field (Form::consentField():accepts_marketing, orsync.consent_fieldset by the email module's Marketing card). When it is ticked,Lead::recordConsent()stores its label (its name when the label is empty) asconsent_textwithconsent_given_atandconsent_hash(SHA-256 of the text) and setsaccepts_marketing. Under double opt in (Form::usesDoubleOptIn())accepts_marketingstaysfalseandconsent_given_atempty until the email module records the confirmation. The consent and sync columns (synced_at,sync_error,contact_id) are not fillable. - Stores the
Leadwith its field data and attachments (limits fromevolve.forms), downloadable throughLeadAttachmentControllerbehind auth. - Fires
Events\LeadCreated(2.17), then notifies:Notifications\NewLeadReceivedto the form's recipient (Recipients\FormRecipient),Notifications\LeadReceivedConfirmationto the sender when the form has a confirmation subject and body. Since 2.19 both notifications return a rendered mail template fromtoMail()when the form hasconfirmation_template_idornotification_template_id, and the Markdown mail otherwise. The core bindsContracts\LeadMailTemplatestoMailTemplates\FormMailTemplatesitself since 2.22 (bindIf, a project may bind its own), see Mail templates below and Mail Templates for Form Mails. The lead'slocaleis set on creation from the session locale.Notifications\UnreadLeadsexists for a digest but is not scheduled by the package. - Records
TrackingEvent::generateLead()throughServerSideTrackingwith a freshSupport\EventId, see Tracking pipeline. - Redirects to
_redirectfrom the request, else the form'sredirect_url, else back to the page, withtrackingEventIdin 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).