Repository navigation
[6.x] Forms 2: Connections - #15063
Open
duncanmcclean wants to merge 137 commits into
Open
duncanmcclean wants to merge 137 commits into
duncanmcclean wants to merge 137 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…need to use antlers Using Antlers in email address fields is still supported, but this is a slightly easier approach for end-users.
these two tests were deleted on purpose in 29d7e78 when the `DeleteTemporaryFiles` job moved out of `SendEmails` and into the job chain built by `Submission::finalize()`. they were only added back by a bad merge conflict resolution when merging `forms-2` into `forms-2-connections` (72b9e33). the behaviour is already covered by `SubmissionTest` and `DeleteTemporaryFilesTest`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qe1g6yd25kh7oTxtWfzB95
Co-authored-by: Duncan McClean <duncan@duncanmcclean.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # resources/js/bootstrap/fieldtypes.js # src/Providers/ExtensionServiceProvider.php
# Conflicts: # resources/js/bootstrap/components.js # src/Forms/Form.php # src/Providers/AddonServiceProvider.php # tests/Forms/FormTest.php
CreateAssetsFromFileUploads still implemented ShouldQueue after it was taken out of the connection chain, so dispatchSync ran it against a serialized copy of the submission and the converted asset paths never reached the connection jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single Bus::chain meant one failing connection stopped every connection after it, and on the sync queue gave the visitor a 500 after the submission was saved. Each connection job is now dispatched on its own. Temporary files are deleted once every job has succeeded, tracked by a per-submission countdown in the cache (configurable via statamic.forms.connections_cache_store), so a failed job's files survive for a later retry. The Email connection now returns one SendEmail job per email, and SendEmails is deprecated. Connection jobs must implement ShouldQueue and use the Queueable trait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he Queueable trait The countdown only relies on the job's public $middleware property, which Laravel runs for queued jobs. Checking for it directly accepts jobs that define the property themselves and catches ones where it isn't public. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base Connection::preProcess() returned rows untouched, so addon connections without their own preProcess() gave the CP rows with no conditions or enabled keys, and duplicating a row threw. The defaults now live in a shared preProcessRow() that Email and Webhook use too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Send if" rule with a field but an empty value was dropped on save, so on reload it reset to "Always send" and the connection fired every time. Rules are now kept as long as they have a field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l views The preview endpoint rendered whatever config it was sent without the validation saving uses. It now validates with the connection's rules first. Those rules now also restrict the html and text views to what the template field offers: no namespaced views, and only views inside statamic.forms.email_view_folder when it's set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a connection returned the processed config and the editor ignored it, keeping client-only ids and incomplete conditions the server had stripped, which were then sent back on the next save. The endpoint now returns the pre-processed values and the editor replaces its state with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Making it extend Support\VueComponent added parameter and return types, so addon widgets overriding render() or toArray() with the released signatures would fatal on upgrade. It's a standalone deprecated class again with the 6.x signatures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside the local environment, webhook URLs are now validated with RemoteUrlValidator before sending, so they can't target loopback, private, link-local or reserved addresses (e.g. cloud metadata), and the request is pinned to the validated IPs so DNS can't be rebound in between. Credentials in the URL are allowed. Sending requires the curl extension for pinning. Redirects are no longer followed and any 3xx response counts as a failed delivery. The local environment skips validation so webhooks to local sites keep working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside the local environment, saving a webhook connection with a URL that resolves to an internal address now fails validation, so it's caught when configuring it rather than only failing at send time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request implements the concept of "Connections" for forms.
Connections let a form talk to the outside world when submissions come in — starting with Emails and Webhooks, and paving the way for third-party integrations.
Forms now have a "Connect" area in the Control Panel, listing the available connections along with how many of each are configured.
Emails
Emails mostly work like they did before, they've just moved to the "Connect" area. We have added a few niceities though:
pagesandsectionsvariables alongsidefields, so multi-page forms can be rendered with their structure — each section carries the same renderable field data asfields.CleanShot.2026-08-17.at.10.59.30.mp4
Existing
emailconfigs in form YAML are automatically converted to email connections, saved under the newconnectionskey.Form::email()has been deprecated in favour ofForm::connections().Webhooks
Upon submission, forms can now send webhooks — a POST request containing the
formhandle andsubmissiondata, sent to a URL of your choice.SSL verification can be disabled per webhook, useful for local development or when sending requests to internal services.
Like emails, webhooks can be triggered based on conditions.
Registering custom connections
Apps and addons can register their own connections, which will show up alongside the built-in ones in the "Connect" area.
Registering a connection
A connection is a class that extends
Statamic\Forms\Connections\Connection. It provides a title, description and icon for the Connect index, an optionalcount()for the badge on the index table, and returns a Vue component fromrender():$iconis shown on the Connect index.$smallIconis optional and used in small spaces like the breadcrumb on the edit page, falling back to$icon. Both accept an icon name or inline SVG markup. Bake any colours into the SVG itself, usinglight-dark()to support dark mode.Connections in the
FormConnectionsdirectory of apps and addons are registered automatically. Addons can also register them explicitly via the$formConnectionsproperty in their service provider.Saving
Connections don't need any routes or controllers for saving — Statamic owns the save process.
The config makes a round trip through the connection class:
preProcess()method and handed to its Vue component asmodelValue.update:modelValueas the user makes changes.rules(), passed through itsprocess()method, and saved to the form under the connection's handle.Validation errors are passed to the component via the
errorsprop, keyed by row index — like0.channel.Both
preProcess()andprocess()return their input untouched by default, so simple connections only need to implement what they use.Connections can register their own routes (eg. OAuth callbacks) via the
routes()method — they're automatically wrapped in authorization.Connections needing credentials can override
isConfigured()— when it returnsfalse, the edit page hides the save button so the component can render setup instructions instead.Frontend
The
render()method determines which Vue component gets rendered, along with its props. The edit page also passes itform,modelValue(the pre-processed value) anderrorsautomatically.If your connection supports multiple "rows" (eg. multiple emails per form), you can use the
<ConnectionRows>component to get a head start.Pass it your array of rows via
v-model, your validation errors viaerrors, and a header slot and a body slot for each row. It takes care of the collapsible row UI, the expand and collapse all button, and the add/duplicate/remove actions. New rows are seeded fromdefaults.values, and each row is given anid,enabledstate and emptyconditionsfor you.The default slot hands each row its own validation errors, grouped by field handle, ready to pass along to your fields.
Logic
If you want your connection to support conditional logic, the
<ConnectionRules>component renders the logic builder. Simply bind your conditions withv-model:conditionsand put whatever the conditions control inside itsthenslot.On the PHP side, the
Statamic\Forms\Connections\ConnectionLogicclass handles the rest:ConnectionLogic::preProcess($conditions)gives each condition the row ID the logic builder needs — call it from your connection'spreProcess()method.ConnectionLogic::process($conditions)strips out the row IDs and any incomplete conditions, and returnsnullwhen there's nothing to save — call it from yourprocess()method.ConnectionLogic::passes($config, $submission)evaluates the conditions against the submission, so you can decide whether to send anything or not.Statamic also exports the
<ConnectionRowSummary>component, which describes a row's conditions in its header when collapsed — like "If Enquiry Type equals Sales". When a row has no conditions, it shows whatever you pass asfallback(the row's subject or heading, say), or "Always".Sending notifications
When a submission is finalized, Statamic dispatches a single job chain: file uploads are converted to assets, then each of the connection jobs run and finally temporary file uploads are deleted.
Before
finalized()is called, Statamic resolves the connection's config — the form's own, or an entry's override when unique instances is enabled (#15255) — and makes it available via$this->config().To hook into this process, you should return a job (or array of jobs) from the
finalized()method:Because we're using Laravel's job chaining feature, if you need to dispatch additional jobs within one of your jobs, call
$this->prependToChain($job)(from Laravel'sQueueabletrait) so they stay part of the chain.Closes statamic/ideas#1176
Closes statamic/ideas#1434
Closes statamic/ideas#1263
Related: https://github.com/statamic/forms-pro/pull/17