Hooks reference
Every hook below is actually emitted by the code, except the one flagged at
the end of the page. They are published by EventDispatcher, which prefixes the
names with solis_forms_: they are ordinary WordPress hooks, consumable with
add_action() / add_filter().
The actions receive a single object rather than a list of positional arguments: adding a datum to a payload then breaks no existing subscriber.
Actions
solis_forms_register_field_types
Add field types. Emitted once per request, at the registry’s first consultation.
- Payload:
SolisForms\Domain\Field\FieldTypeRegistry - Emitted in:
FieldTypeRegistry::notify_extensions()
add_action( 'solis_forms_register_field_types', function ( $registry ) {
$registry->register( new My_Signature_Field() );
} );
solis_forms_register_addons
Register an add-on. Emitted on init at priority 5, before the processing hooked
at the default priority.
- Payload:
SolisForms\AddOns\AddOnManager - Emitted in:
AddOnsServiceProvider::boot()
add_action( 'solis_forms_register_addons', function ( $manager ) {
$manager->register( new My_AddOn() );
} );
solis_forms_entry_created
A submission has just been recorded. This is the hooking point for external integrations (CRM, webhook, logging).
- Payload:
Events\Payload\EntryCreatedEvent—entry,form,form_id - Emitted in:
SubmissionHandler::handle() - Note:
entry->valuescontains the values actually recorded.
add_action( 'solis_forms_entry_created', function ( $event ) {
error_log( sprintf( 'Submission %d on form %d', $event->entry->id, $event->form_id ) );
} );
solis_forms_form_saved
A form has been created or updated through the REST API.
- Payload:
Events\Payload\FormSavedEvent—form,is_new - Emitted in:
FormsController::create()and::update()
solis_forms_captcha_verification_failed
The captcha verification service is unreachable or answers unusably — never when it legitimately rejects a token. The submission is refused out of caution; without a subscriber to this hook, an expired key would block all the traffic leaving no trace.
- Payload:
Events\Payload\CaptchaVerificationFailedEvent—provider,form_id,reason - Emitted in:
AbstractCaptchaProvider
solis_forms_stats_page_rendered
The statistics page has finished rendering. An add-on can add its own reading there — that is what the friction analytics does.
- Payload:
Events\Payload\StatsPageRenderedEvent—form_id,range,from,to - Emitted in:
StatsPage::render() - Note: the form has already been checked as accessible to the user, and the date bounds are already computed. Recomputing them yourself would end up diverging by a day depending on the timezone chosen.
add_action( 'solis_forms_stats_page_rendered', function ( $event ) {
printf(
'<p>Form %d, from %s to %s</p>',
$event->form_id,
esc_html( $event->from ),
esc_html( $event->to )
);
} );
solis_forms_addon_boot_failed
An add-on threw during its boot. The exception having been caught so as not to bring the site down, this hook is the only way to be informed of it.
- Payload:
Events\Payload\AddOnBootFailedEvent—addon_id,addon_name,exception - Emitted in:
AddOnManager::boot_all()
Filters
solis_forms_settings_sections
Add a section to the global settings screen. An API key does not file itself per form: this is where an integration puts its own.
The section gets storage, rendering, sanitisation and its default value in one
go; there is no interface to write. It is read back afterwards with
PluginSettings::section( 'my-section' ).
- Value:
array<string, array{title: string, description: string|callable, fields: array<string, array{label: string, type: string, default: string, choices?: array, help?: string}>}> - Emitted in:
SettingsRegistry::sections() - Field types rendered:
text,password,number,select,checkbox
add_filter( 'solis_forms_settings_sections', function ( array $sections ) {
$sections['my-crm'] = array(
'title' => 'My CRM',
'description' => 'Account credentials.',
'fields' => array(
'api_key' => array(
'label' => 'API key',
'type' => 'password',
'default' => '',
),
),
);
return $sections;
} );
slf:step-changed (JavaScript event)
Emitted on the form at every step change, including at the first render. It lets
a front-end module know where the visitor has got to without watching the pages’
hidden attribute — that is, without inferring an intent from a display detail,
which breaks at the first rendering change.
- Detail:
{ step, total }, both starting from 1 - Emitted in: the core’s
multistepmodule - Bubbles: yes (
bubbles: true)
form.addEventListener( 'slf:step-changed', ( event ) => {
console.log( `Step ${ event.detail.step } of ${ event.detail.total }` );
} );
solisForms.formSettingsPanels (JavaScript filter)
Add a panel to the form’s settings, in the builder. It is the interface-side counterpart to the previous filter: one carries the account credentials, the other what is set form by form.
The component receives settings — the entirety of the form’s settings — and
onChange, which applies a partial change. File your values under a key of your
own, read back on the PHP side by FormSettings::section().
- Value:
Array<{ name: string, render: Function }> - Emitted in:
components/FormSettingsPanel.js
import { addFilter } from '@wordpress/hooks';
addFilter( 'solisForms.formSettingsPanels', 'my-addon/crm', ( panels ) => [
...panels,
{ name: 'my-crm', render: MyPanel },
] );
solis_forms_settings_tabs
Rename a tab of the settings screen, or add one.
The array is indexed by tab key; the value is the label displayed. An unknown key
creates a tab, and the sections declared by solis_forms_settings_sections file
themselves there by naming it.
Removing a key does not erase its sections: they fall back into the “Other” tab, rather than disappearing from a screen without anybody noticing.
- Arguments:
array<string, string> $titles - Emitted in:
Settings\SettingsTabs::titles()
add_filter(
'solis_forms_settings_tabs',
static function ( array $titles ): array {
$titles['my_module'] = 'My module';
return $titles;
}
);
solis_forms_payment_gateways
Add a payment gateway.
- Value:
array<string, PaymentGatewayInterface> - Emitted in:
GatewayRegistry::notify_extensions()
add_filter( 'solis_forms_payment_gateways', function ( array $gateways ) {
$gateways['mollie'] = new My_Mollie_Gateway();
return $gateways;
} );
solis_forms_form_templates
Offer additional form templates. Each entry is indexed by id and supplies
label, description and path (absolute path to a file in the export
envelope’s format).
- Value:
array<string, array{label: string, description: string, path: string}> - Emitted in:
TemplateRegistry::notify_extensions()
add_filter( 'solis_forms_form_templates', function ( array $templates ) {
$templates['my-template'] = array(
'label' => 'My template',
'description' => 'Preconfigured form.',
'path' => plugin_dir_path( __FILE__ ) . 'templates/my-template.json',
);
return $templates;
} );
solis_forms_rest_controllers
Serve your own REST routes. The filter receives the instances of the core’s
controllers: an add-on builds its own with its own dependencies, which the core’s
container does not know about. Any value that does not extend
AbstractController is set aside silently — a badly configured add-on does not
deprive the site of its API.
- Value:
array<int, SolisForms\Rest\AbstractController> - Emitted in:
RestServiceProvider::controllers(), onrest_api_init
add_filter( 'solis_forms_rest_controllers', function ( array $controllers ) {
$controllers[] = new My_Controller();
return $controllers;
} );
solis_forms_notifications
Choose which notifications go out. The core keeps only one — the first active — and the filter receives that choice, the form and the submission. It can substitute whatever list it likes: that is how the paid add-on restores multiple notifications and conditional sending, which cannot be decided without the submission.
A value that is not an array is ignored.
- Value:
array<int, array<string, mixed>> - Context:
SolisForms\Domain\Form,SolisForms\Domain\Entry - Emitted in:
NotificationManager::notifications(), on sending and on resending
add_filter( 'solis_forms_notifications', function ( array $notifications, $form, $entry ) {
$notifications[] = array(
'id' => 'accounts-copy',
'enabled' => true,
'to' => 'accounts@example.com',
'subject' => 'New order',
'message' => 'See submission {entry_id}.',
);
return $notifications;
}, 10, 3 );
solis_forms_mail_args
Retouch an email just before it is dispatched: copy, blind copy, reply address, attachments, subject, body. Each filtered key is checked against the expected type and falls back to the core’s value if it does not match.
The delivery log records the filtered values: what remains consultable on a submission’s detail really is what was dispatched.
- Value:
array{to: string[], subject: string, message: string, is_html: bool, cc: string[], bcc: string[], reply_to: string, attachments: string[], text_alternative: string} - Context: the notification,
Form,Entry - Emitted in:
NotificationManager::mail_args()
add_filter( 'solis_forms_mail_args', function ( array $args ) {
$args['bcc'][] = 'archive@example.com';
return $args;
} );
solis_forms_form_availability
Revisit a form’s availability: waiting list, quota by role, early opening for a
member. The filter can close an open form as well as reopen a closed one —
availability is a business rule, not a security check. Anything that is not an
AvailabilityStatus is ignored.
The check being redone at submission time, a closure pronounced here withstands a forged request.
- Value:
SolisForms\Domain\Availability\AvailabilityStatus - Context:
SolisForms\Domain\Form - Emitted in:
FormAvailability::filtered()
use SolisForms\Domain\Availability\AvailabilityStatus;
add_filter( 'solis_forms_form_availability', function ( $status, $form ) {
return my_list_is_full( $form->id ) ? AvailabilityStatus::QuotaReached : $status;
}, 10, 2 );
solis_forms_can_submit
Refuse a submission the core was admitting: login required, your own quota, a blacklist.
The filter only goes one way. It is consulted only on submissions already admitted, and a value that would admit them again is ignored: an add-on can tighten the admission, never loosen it. Without that rule, an add-on would reopen in one line what the nonce and the anti-spam checks have just refused.
The submitted values are not passed: a refusal based on the contents is a matter
for an anti-spam check, which SpamCheckManager knows how to host and which runs
at the right moment within the rate quota.
- Value:
SolisForms\Frontend\AuthorizationOutcome - Context:
FormSettings,int $form_id - Emitted in:
SubmissionAuthorization::hardened()
use SolisForms\Frontend\AuthorizationOutcome;
add_filter( 'solis_forms_can_submit', function ( $outcome, $settings, $form_id ) {
return is_user_logged_in() ? $outcome : AuthorizationOutcome::RejectedExtension;
}, 10, 3 );
solis_forms_pro_active
Silence what the core announces about the paid add-on. Answering true removes
the builder’s badges, the locked panels and the “SolisForms Pro” menu entry:
somebody who has paid has no use for an advertisement.
The check goes through a filter rather than through a class’s presence: the core thus needs to know nothing of the add-on’s names, and any distribution — Freemius, in-house server, lifetime licence — answers the same way.
- Value:
bool(false by default) - Consulted in:
Support\ProFeatures::active()
add_filter( 'solis_forms_pro_active', '__return_true' );
The fields do not need this filter: a field type is only advertised if it is
absent from the registry. A third-party add-on declaring rating therefore makes
its badge disappear without having to say anything.
solis_forms_pro_url
Change the badges’ destination. By default, an internal screen of the plugin — no external address is hard-coded, and the plugin directs nowhere without somebody having decided it.
- Value:
string - Consulted in:
Support\ProFeatures::url()
add_filter( 'solis_forms_pro_url', fn (): string => 'https://my-site.test/pro' );
solis_forms_pro_allowed_registration_roles
Open up the list of roles a form can assign at registration. By default,
subscriber alone.
Going through code is deliberate: a form must not be able to raise the privileges of whoever fills it in, from a menu in the admin interface.
A second check applies after this filter and does not lift: a role carrying a
capability that takes over the site — manage_options, edit_users,
install_plugins, unfiltered_html, solis_forms_manage_settings… — stays set
aside, and administrator is refused by name. The error to fear is not the
administrator’s malice, it is their mistake: a role created by a third-party
add-on can carry edit_users without its name giving it away.
- Value:
array<int, string>(['subscriber']by default) - Consulted in:
SolisFormsPro\UserAccounts\AccountRoles::allowed()
add_filter(
'solis_forms_pro_allowed_registration_roles',
static fn ( array $roles ): array => array_merge( $roles, array( 'member' ) )
);
solis_forms_submission_errors
Refuse a submission after the fields have been validated, just before it is recorded. It is by this path that a paid form’s payment is checked: the transaction must exist, be marked paid, be attached to no other submission and carry exactly the expected amount.
The filter can only refuse: the errors already found are not submitted to it, so it could not erase them. An extension point able to make what the core refuses acceptable would end up doing so by mistake.
- Value:
array<string, array<int, string>>— messages indexed by field id - Context:
Form,array $values(the values kept) - Emitted in:
SubmissionHandler::guarded()
add_filter( 'solis_forms_submission_errors', function ( array $errors, $form, array $values ) {
if ( ! my_payment_is_compliant( $form, $values ) ) {
$errors['payment'] = array( 'The payment does not match.' );
}
return $errors;
}, 10, 3 );
The counterpart to this guard is solis_forms_entry_created: that is where what
the guard verified gets attached to the submission.
solis_forms_draft_saved
Published after a draft has been saved.
The core records, cleans and signs; an add-on wanting to enrich a draft — with a consent, with a scheduled reminder — does not have to reproduce those checks for all that. It listens, and works on what the core has already kept.
created distinguishes the first save from the following ones: an automatic save
renewed every two minutes must not be treated as the discovery of an entry.
- Payload:
Events\Payload\DraftSavedEvent—entry_id,form,values,created - Published in:
Frontend\SubmissionHandler::save_draft()
add_action(
'solis_forms_draft_saved',
static function ( SolisForms\Events\Payload\DraftSavedEvent $event ): void {
if ( $event->created ) {
// First save of this entry.
}
}
);
solis_forms_submission_values
The last word on the submitted values, before they are validated.
Used for what the server decides and the browser cannot choose: the value of a locked field, for instance, which a forged request would have replaced.
The filter applies before validation: a value substituted here takes exactly the same path as a visitor’s, and therefore gets round no field check.
- Value:
array<string, mixed> - Context: the
Formsubmitted - Consulted in:
Frontend\SubmissionHandler::handle()
add_filter(
'solis_forms_submission_values',
static function ( array $values ): array {
$values['source'] = 'internal';
return $values;
}
);
solis_forms_pro_prefill_value
Paid add-on. Supplies the value of a prefill from a business source.
It is the only place provided for hooking up a source of the site’s own: it lives in code, under the responsibility of whoever writes it. What the filter returns then goes through the target field type’s sanitisation.
- Value:
string(empty by default) - Context: the declared key, the field’s id, the
Form - Consulted in:
Prefill\PrefillResolver
add_filter(
'solis_forms_pro_prefill_value',
static function ( string $value, string $key ): string {
return 'case_number' === $key ? my_case_number() : $value;
},
10,
2
);
solis_forms_pro_ai_providers
Paid add-on. Add a completion provider to the admin assistant.
The module ships one. Shipping several would mean maintaining several shapes of request, response and error, for a feature that has to stay optional — so this filter opens the door to whoever wants their own, as the plugin already does for payment gateways and field types.
A value that does not implement AiProviderInterface is set aside silently: a
badly configured third-party module does not deprive the site of its assistant.
The contract is deliberately narrow — a provider receives a text and returns a
text. It knows nothing of forms or fields: composing the request belongs to
PromptBuilder, judging the answer to AiSuggestionValidator. A third-party
provider therefore has no way to bring in anything but a text, which is rebuilt
key by key before it reaches a screen.
- Value:
array<string, AiProviderInterface> - Emitted in:
Ai\AiProviderRegistry::all()
add_filter(
'solis_forms_pro_ai_providers',
static function ( array $providers ): array {
$providers['my-service'] = new My_Provider();
return $providers;
}
);
solis_forms_pro_personal_data_providers
Paid add-on. Declare a module that holds data tied to a person, so that it enters the privacy centre’s export and erasure.
The centre does not sweep the tables. It could — they are all prefixed slf_ —
but it would have to know which column carries the address in each: twenty
copies of the same knowledge in the wrong place, and a module added tomorrow
would be forgotten without the omission showing. An incomplete export looks
complete.
Each module therefore declares what it holds. One that does not declare appears neither in the export nor in the erasure — and that is observable, since the registry is listed on screen.
erase is not necessarily a deletion: a module that has to keep a trace — an
accounting entry, a payment — returns what it anonymised rather than deleted,
and says so. The centre does not decide in its place; it reports.
Everything is idempotent: a request is replayed after a partial failure, and a module with nothing left returns an empty, successful result. Without that, the second run would fail on what the first had correctly erased.
- Value:
array<int, PersonalDataProviderInterface> - Emitted in:
Privacy\PersonalDataRegistry::all()
add_filter(
'solis_forms_pro_personal_data_providers',
static function ( array $providers ): array {
$providers[] = new My_Provider();
return $providers;
}
);
solis_forms_form_pages
Recompose a form’s pages before they are rendered.
The core’s splitting follows the page breaks, and nothing else. A module wanting to present one question per screen had, without this filter, only one recourse: redoing the markup in JavaScript afterwards — and therefore rewriting the navigation, the progress and the focus handling the core already holds, and letting them diverge.
A page is an array {title, fields}. Returning anything other than a list of
valid pages falls back to the core’s splitting: a form must display even if an
add-on gets it wrong.
- Value:
array<int, array{title: string, fields: array}> - Context: the
Formrendered - Consulted in:
Frontend\FormRenderer::pages()
add_filter(
'solis_forms_form_pages',
static function ( array $pages, SolisForms\Domain\Form $form ): array {
// One question per screen.
return array_merge( ...array_map(
static fn ( array $page ): array => array_map(
static fn ( array $field ): array => array(
'title' => '',
'fields' => array( $field ),
),
$page['fields']
),
$pages
) );
},
10,
2
);
solis_forms_form_markup
Wrap a rendered form’s markup.
Used for what surrounds the form without belonging to it: a shell, a dedicated page’s header, a container carrying colours. The form’s own markup stays the core’s.
Returning anything other than a string, or an empty string, falls back to the original markup: an add-on that gets it wrong must not make the form disappear from the page, because the breakage would be total and silent.
- Value:
string - Context: the
Formrendered - Consulted in:
Frontend\FormRenderer::render_form()
add_filter(
'solis_forms_form_markup',
static fn ( string $markup ): string => '<div class="my-shell">' . $markup . '</div>'
);
solis_forms_entry_action
Handle an action posted from a submission’s detail — refund, stop a subscription.
The generic checks are done before publication: the capability to view submissions, and access to the form. What is specific to the action remains your responsibility: its nonce, and its particular capability. Refunding commits the site’s account, stopping a recurring commitment cannot be caught up; neither is checked the way a consultation is.
- Payload:
Events\Payload\EntryActionEvent—action,entry,form - Emitted in:
EntryActionHandler::handle()
solis_forms_entry_detail_sections
Add a section to a submission’s detail screen, between the answers and the status. Each section receives the submission and its form, and returns its markup — escaping it is its own business.
- Value:
array<string, callable>—fn ( Entry $entry, Form $form ): string - Context:
Entry,Form - Emitted in:
EntryDetailPage::render_extension_sections()
solis_forms_entry_status_changed
A submission’s status has just changed: trash, spam, restoration.
Published from the repository and not from the screens, of which several already change a status — the list, the detail, the bulk action, the REST API. A guarantee depending on each caller remembering it is not one.
And only if the value changes. Setting a status that is already written publishes nothing: announcing it would force every subscriber to re-read the row to find out whether anything had moved, which is to say to redo the work the event exists to avoid.
The state left travels with the new one, because it cannot be inferred: a submission can go from the trash to spam without passing back through active.
- Payload:
Events\Payload\EntryStatusChangedEvent—entry_id,from,to - Emitted in:
Database\Repository\EntryRepository::update_status()
add_action(
'solis_forms_entry_status_changed',
static function ( \SolisForms\Events\Payload\EntryStatusChangedEvent $event ): void {
if ( \SolisForms\Domain\EntryStatus::Active !== $event->to ) {
my_module_releases( $event->entry_id );
}
}
);
solis_forms_entry_deleting
Purge what an add-on files per submission. Published before the row disappears: the subscriber can therefore still read what it has to erase.
Any add-on storing data per submission must subscribe to it. A deletion under GDPR must leave nothing behind it, and the core does not know about other people’s tables.
- Payload:
Events\Payload\EntryDeletingEvent—entry_id - Emitted in:
EntryRepository::delete()
solis_forms_entry_columns
Add a column to the submissions table. A filtered list that had lost the cb key
is rejected wholesale: without a checkbox, the screen would lose its bulk
actions.
- Value:
array<string, string>— column id, header - Context:
Form|null - Emitted in:
EntriesListTable::get_columns()
solis_forms_entry_column_value
Fill in an added column. The value returned is displayed as it is: escaping it is the add-on’s business.
- Value:
string - Context:
string $column,Entry,Form|null - Emitted in:
EntriesListTable::column_default()
add_filter( 'solis_forms_entry_columns', function ( array $columns ) {
$columns['amount'] = 'Amount';
return $columns;
} );
add_filter( 'solis_forms_entry_column_value', function ( $value, $column, $entry ) {
return 'amount' === $column ? esc_html( my_amount( $entry->id ) ) : $value;
}, 10, 3 );
solis_forms_entry_row_actions
Add a row action. The core’s actions are already reduced to the user’s rights: an add-on adding its own does not have to redo that check, and does not reintroduce one either.
- Value:
array<string, string>— id, HTML link - Context:
Entry,Form|null - Emitted in:
EntriesListTable::build_row_actions()
solis_forms_pro_workflow_transitioned
Paid add-on. A processing case has just changed state.
Published after the write, and only if it took place: a refused transition, or one lost in a revision conflict, emits nothing. What listens can therefore trust the event without re-reading the row to find out whether it moved.
The state left may be a key the configuration no longer declares — a state removed from the settings stays written on the cases that were in it.
- Arguments:
int $entry_id,string $from,string $to - Emitted in:
Workflows\WorkflowService::update()
add_action(
'solis_forms_pro_workflow_transitioned',
static function ( int $entry_id, string $from, string $to ): void {
if ( 'resolved' === $to ) {
my_dashboard_records( $entry_id );
}
},
10,
3
);
solis_forms_pro_workflow_assigned
Paid add-on. A processing case has just been assigned to somebody.
Published before the “notify the assignee” setting, and independently of it: a team may want the announcement in its channel without wanting the individual message. Tying them together would have forced you to switch one on in order to get the other.
The deadline is the case’s at the moment of assignment, or null if it carries
none.
- Arguments:
int $entry_id,int $form_id,int $assignee_id,string|null $due_at - Emitted in:
Workflows\WorkflowService::assign()
add_action(
'solis_forms_pro_workflow_assigned',
static function ( int $entry_id, int $form_id, int $assignee_id, ?string $due_at ): void {
my_channel_announces( $entry_id, $assignee_id, $due_at );
},
10,
4
);
solis_forms_pro_workflow_overdue
Paid add-on. A case has passed its deadline.
Published after the reminder has been claimed, and therefore only once per deadline passed: the hourly check runs on every site in the network and the row claim lets only one process through.
It exists so that a module can give notice somewhere other than by email without going and reading the cases table, which would have tied two modules that nothing guarantees are active together.
- Arguments:
int $entry_id,int $form_id,int $assignee_id,string|null $due_at - Emitted in:
Workflows\WorkflowService::remind_overdue()
add_action(
'solis_forms_pro_workflow_overdue',
static function ( int $entry_id, int $form_id, int $assignee_id, ?string $due_at ): void {
my_channel_alerts( $entry_id, $due_at );
},
10,
4
);
solis_forms_pro_booking_reserved
Paid add-on. A slot has just been booked.
Published after the booking and its notification, and therefore only once per
place obtained: the claim returns null when the place is already taken, and the
module stops before getting that far.
It exists so that a module can react — the first to use it sends a confirmation SMS — without going and reading the bookings table, which would have tied two modules that nothing guarantees are active together.
The slot is passed as it was submitted, and not reformatted: what listens decides its own display.
- Arguments:
int $entry_id,int $form_id,string $slot - Emitted in:
Bookings\BookingsAddOn::on_entry_created()
add_action(
'solis_forms_pro_booking_reserved',
static function ( int $entry_id, int $form_id, string $slot ): void {
my_module_confirms( $entry_id, $slot );
},
10,
3
);
solis_forms_pro_recovery_reminded
Paid add-on. An abandoned draft has just been followed up by email.
Emitted only if the email went out: a module doubling the reminder with an SMS must not send it when the reminder itself failed, failing which the person receives a reminder without the link it announces.
Emitted after the module’s consent check: a reminder only takes place if the person explicitly accepted it, and what grafts itself onto it inherits that agreement.
- Arguments:
int $entry_id,int $form_id - Emitted in:
Abandonment\RecoveryService::remind()
add_action(
'solis_forms_pro_recovery_reminded',
static function ( int $entry_id, int $form_id ): void {
my_module_doubles_the_reminder( $entry_id );
},
10,
2
);
solis_forms_pro_booking_cancelled
Paid add-on. A slot booking has just been cancelled.
Emitted from the repository and not at the callers’, of which there are already two — the public link and the admin screen. A guarantee depending on each caller remembering it is not one: the first one you forgot would leave a cancelled appointment in somebody’s calendar.
A replayed cancellation does not emit it twice: the condition bears on the number of rows actually modified.
- Arguments:
int $booking_id,int $entry_id,int $form_id - Emitted in:
Database\Repository\BookingRepository::cancel()
add_action(
'solis_forms_pro_booking_cancelled',
static function ( int $booking_id, int $entry_id, int $form_id ): void {
my_calendar_removes( $booking_id );
},
10,
3
);
solis_forms_pro_booking_promoted
Paid add-on. A booking on the waiting list has just obtained a place.
A promoted place is an appointment like any other: it deserves to enter a calendar, which a waiting place did not. That is this event’s reason for being, distinct from that of a booking confirmed straight away.
- Arguments:
int $booking_id,int $entry_id,int $form_id - Emitted in:
Database\Repository\BookingRepository::promote()
add_action(
'solis_forms_pro_booking_promoted',
static function ( int $booking_id, int $entry_id, int $form_id ): void {
my_calendar_places( $booking_id );
},
10,
3
);
solis_forms_process_entries (meta capability)
Paid add-on. The right to process a form’s submissions.
Used with the form’s id as an argument. It is translated by map_meta_cap into
two conditions the site already grants — viewing submissions, and having
authority over that form — and is therefore assigned to nobody: an updated site
loses no access.
It is on it, and not on the assignment, that access rests: being designated on a case never gives the right to read it. The list of assignable people is derived from it.
A site wanting a distinct “processor” role rehooks it with a higher-priority filter, without touching the code.
- Declared in:
Workflows\WorkflowAccess::register()
add_filter(
'map_meta_cap',
static function ( array $caps, string $cap, int $user_id, array $args ): array {
if ( 'solis_forms_process_entries' !== $cap ) {
return $caps;
}
return user_can( $user_id, 'my_processor_role' ) ? array( 'read' ) : $caps;
},
20,
4
);
solis_forms_pro_portal_decided
Paid add-on. A submission has just changed publication state in a portal.
Published after the write, and only if it took place: a refused transition, or one lost in a revision conflict, emits nothing. What listens can therefore trust the event without re-reading the decision to find out whether it moved.
The decision bears on the portal/submission pair: the same entry can be approved here and refused there, and the event is emitted once per portal.
- Arguments:
int $entry_id,int $view_id,string $from,string $to - Emitted in:
Views\PortalModerationService::decide()
add_action(
'solis_forms_pro_portal_decided',
static function ( int $entry_id, int $view_id, string $from, string $to ): void {
if ( 'approved' === $to ) {
my_directory_refreshes( $view_id );
}
},
10,
4
);
solis_forms_moderate_form (meta capability)
Paid add-on. The right to decide what appears in a form’s portals.
Used with the form’s id as an argument. It first requires the
solis_forms_moderate_entries capability, then the scope: all forms for whoever
manages them, their own for whoever only manages their own.
The order matters. Requiring the scope first would have let any forms manager moderate without it having been granted to them, when that is precisely what this capability serves to separate: moderating is neither modifying the form, nor refunding a payment, nor cancelling a subscription.
- Declared in:
Support\FormAccess::register()
if ( current_user_can( 'solis_forms_moderate_form', $form_id ) ) {
// …
}
Zapier subscription routes (REST API)
Paid add-on. Four routes under solis-forms/v1, used by the Zapier client and
by nothing else.
They are documented here because they are public in the API’s sense: a third-party add-on can hook onto them, and an integrator may want to build their own client rather than going through the marketplace’s.
| Route | Role |
|---|---|
GET /zapier/forms | Forms whose trigger is switched on, for a menu |
POST /zapier/subscriptions | {form_id, target_url} → {id, secret} |
DELETE /zapier/subscriptions/<id> | Switches a Zap off |
GET /zapier/sample?form_id=<id> | A sample item, manufactured |
Each requires two checks: the solis_forms_view_entries capability and access to
the form targeted. The first alone would let a manager of their own forms
subscribe a Zap to somebody else’s, by changing a number.
The sample is never a real submission. Zapier keeps it in the Zap’s configuration, where it would outlive the erasure of the data.
The secret returned at subscription is stored nowhere: it is rederived from the site’s salt and the subscription’s row number at every signature.
window.solisForms.registerModule() (JavaScript registry)
Hook front-end behaviour onto every form on the page. The core’s entry point goes through this same registry: a field whose rendering calls for JavaScript therefore has nothing special to obtain.
bind receives the form and its context — context.schema describes the fields,
context.refresh() replays the declared re-evaluations. A module returning a
function sees it added to those re-evaluations. The priority orders the hooking
(10 by default, the submission interception being at 100). A module that throws
is logged and ignored: it does not deprive the form of its submission.
Declare your script with solis-forms-frontend as a dependency, so that it runs
after the registry.
context.hold( () => message | null ) holds the submission back: the payment
module uses it to prevent sending until the payment is confirmed. The core
consults the holds before sending and displays the first message returned — it
therefore does not have to know what is holding the send back.
window.solisForms.collectFormValues( form ) goes with it: a module reacting to
input needs to read the form, and that reading knows the plugin’s naming
conventions — repeatable row indices, multiple checkboxes. Copying it would make
two readings of the same structure live on.
pro/assets/src/frontend/core.js shows how it is borrowed.
window.solisForms.registerModule( 'my-addon/signature', ( form, context ) => {
const field = form.querySelector( '[data-signature]' );
if ( ! field ) {
return;
}
// Returning a function has context.refresh() replay it.
return () => update( field, context.schema );
} );
solisForms.formSettingsSections (JavaScript filter)
Replace a section of the form settings panel, where
solisForms.formSettingsPanels adds one. An add-on enriching an existing
feature needs it: without that, the settings of one and the same object would end
up split between two panels — one carrying the email’s subject, the other its
blind copy.
The filter receives an empty object and returns the replacements indexed by
section name. The substituted component receives the same properties as an added
panel: settings and onChange.
- Value:
Record<string, Function> - Core sections:
notifications,confirmation,anti-spam,save-resume,availability - Emitted in:
components/FormSettingsPanel.js
import { addFilter } from '@wordpress/hooks';
addFilter(
'solisForms.formSettingsSections',
'my-addon/notifications',
( sections ) => ( { ...sections, notifications: MyEditor } )
);
solisForms.fieldInspectorControls (JavaScript filter)
Add a setting specific to a field type, in the builder’s inspector.
The schema declared by get_settings_schema() generates most of that column, and
is enough for a simple control. It cannot describe a control that needs the rest
of the form — a calculation formula, which offers the neighbouring fields’ ids.
That is what this filter allows.
Each control receives the selected field and applies a partial change; it is its own business to return nothing for a type that does not concern it.
- Value:
Array<{ name: string, render: Function }> - Emitted in:
components/Inspector.js
addFilter(
'solisForms.fieldInspectorControls',
'my-addon/signature',
( controls ) => [ ...controls, { name: 'signature', render: MyEditor } ]
);
function MyEditor( { field, onChange } ) {
if ( 'signature' !== field.type ) {
return null;
}
return <MyControl onChange={ ( style ) => onChange( { style } ) } />;
}
window.solisForms.builder (builder API)
What the builder publishes for add-ons to use, when its bundle loads:
| Key | Contents |
|---|---|
ConditionalLogicEditor | The core’s conditions editor, as the form’s panels use it |
MERGE_TAGS | Merge tokens recognised by the server, to remind a message’s author of them |
STORE_NAME | Name of the builder’s @wordpress/data store, to read the current form’s fields |
An add-on panel needs it as soon as it offers conditional sending: copying the
editor would make two implementations of the same rules format live on. Declare
solis-forms-builder as a dependency of your script, and resolve the component
at render time — an absent core must deprive the panel of its editor, never
take the builder with it.
- Published in:
assets/src/builder/expose.js, called bybuilder/index.js - Complete example:
pro/assets/src/builder/core.js
function ConditionalLogicEditor( props ) {
const Editor = window.solisForms?.builder?.ConditionalLogicEditor;
return Editor ? <Editor { ...props } /> : null;
}
Declared but not emitted
EventNames::PAYMENT_COMPLETED (solis_forms_payment_completed) exists among the
constants but no code publishes it to this day. Do not subscribe to it: it
will not fire. A payment’s state is observed today through
solis_forms_entry_created, the submission only being recorded once the
transaction has been verified.
