Developing new Form Handlers

Form Reform Developer provides a growing suite of resources to assist those developing blocks, handlers and more complex forms for Form Reform.

Form Handler Pipeline

All processing of forms in Form Reform is managed through a handler pipeline as illustrated below. This pipeline is a configurable list of handlers that are applied to a form when it is submitted.

Functionality can be extended by adding plugin classes for additional Form Handlers. Form Handler plugins are simple classes that provide the functionality to perform a single form handling action.

The overall mechanism is a derivative from that described at Plugin System.

What kinds of custom form handlers can be added?

The kind of form handlers you can add is really up to your requirements and imagination. Some custom handlers we have implemented include:

  • Forwarding completed form data to a SOAP API.
  • Forwarding completed form data to a REST API.
  • Create, Read and Update integration with Microsoft Dynamics.
  • Using zip codes to lookup an area sales office from a database table.
  • Saving completed form data to a custom database table.
  • Retrieving customer account details from a third party system.

Plugin Recognition

A new Form Handler Plugin class should inherit from FormHandlersPluginBase.

Plugins can be added by placing the plugin classes at one of

  • packages/anyPackageName/src/FormReform/FormHandlers/Plugins/PluginName.
  • application/src/FormReform/FormHandlers/Plugins/PluginName.

Plugins can also be similarly placed beneath the plugin type's namespace declared in a package controller's AutoloaderRegistries.

For a plugin to be recognised by Form Reform, If in a package, the install() or update() method can call:

$this->app->make('JtF\FormReform\FormHandlers\FormHandlersController')
    ->listAll(true);

Otherwise, simply visiting the dashboard page at Dashboard > System & Settings > Form Reform > Form Handler Plugins will also refresh the plugin list.

Start from an existing handler

You probably don't need to code a form handler from scratch. Pick an existing handler close to what you want to achieve and copy/paste and adapt it. Below we have the None form handler as an overview of the general structure, though you will likely be starting from something a bit more involved.

 

        
<?php
namespace JtF\FormReform\FormHandlers\Plugins;
use JtF\FormReform\FormHandlers\FormHandlersPluginBase;

class None extends FormHandlersPluginBase
{
    public $preferered_default = true;  // Delete this line for any other form handler

    public function getName()
    {
        return t('None');
    }

    public function getDescription()
    {
        return t('A null handler.');
    }

    public function getHelp()
    {
        return t("Do absolutely nothing.") . '<br>' . parent::getHelp();
    }

    public function edit($controller, $data)
    {
        /*
         * Extract plugin specific data from request data
         * Always need both of these, in this order, or import fails
         */
        extract($controller->getSets());
        $data = $this->synthesizeData($data);
        $form = $this->form(true);
        ob_start();
        
        View::element('form_reform/handlers/handler_header',
            [
              'controller'     => $controller,
              'plugin'         => $this,
              'form'           => $form,
              'data'           => $data,
              'header_classes' => 'reform_none',
              'emphasis_class' => 'text-secondary text-muted',
              'icons'          => '<i class="fa fas fa-star"></i>',
           ],
           'jl_form_reform'
        );

        /*
         * Other form-groups for the edit dialogue go here.
         * Wrap and field name in 
         * $this->prefixNamespace('input_name')
         * The corresponding value will be in $data['input_name']
         */
        ?>

        <?php
        $opts_HTML = ob_get_contents();
        ob_end_clean();
        return $opts_HTML;
    }

    /*
     * The None form handler does nothing, so this is not actually needed. It could be inherited
     * from FormHandlersPluginBase.
     *
     * We have the method here because this can also be used as a starting point for other handlers.
     *
     * $pipeline_manager - the manager for the pipeline
     * $data - current plugin parameters
     * $form_data - data from the form
    */
    public function handler_action($pipeline_manager, $data, $form_data)
    {
        return t('Handler for %s at step %d.', $this->clientClassName(), $pipeline_manager->getCurrrentStepIndex());
    }
}
    

Handlers for an external API

If you are developing form handlers for a CRUD interface to an external API, consider starting from the handlers in Form Reform Dynamics

Required methods

A form handler plugin needs to provide some essential methods. You could potentially leave any of these out, but if you do you are effectively removing the entire purpose of creating a new form handler. 

getName()

This method simply returns the name of the handler. This is usually the same as the class name, but with spaces inserted.

getDescription()

The description is as short as possible while adding a bit of detail.

getHelp()

The getHelp() method returns the complete built in user documentation for the handler. If you look at some of the existing handlers, you will see this is usually assembled from a mixture of smaller getSomethingHelp() methods and lists compiled from methods that return arrays of edit dialogue options.

edit($controller, $data)

This method provides the edit dialogue for the handler. We won't go through it in micro detail here, just some salient points that will help you understand what is going on when you read some of the existing code and help you to create a new dialogue based on existing code.

Edit input parameters

$controller

Is the block controller object. A controller passes parameters to an edit or view using $this->set('parameter_name', $value). To use those parameters in the edit dialogue we do extract($controller->getSets()); That is exactly what the core does when preparing an edit or view. Alternatively you can get individual parameters with $controller->get('parameter_name').

$data

Is an associative array of the configuration settings for the form handler. This could be just a plain array, or it could be any mixture of serialized, prefixed or encrypted. $data = $this->synthesizeData($data); undoes all that so you can be sure to have an associative array.

The handler edit form

$form = $this->form(true); provides a form helper, for use in creating dialogue controls.

Every dialogue input name should be wrapped in $this->prefixNamespace('input_name'). That adds prefixes to uniquely associate the input with the current handler so it doesn't get mixed up with any other handlers in the dialogue.

The entire set of dialogue inputs should be wrapped in an output buffer, so their output is captured and then returned as an $html string at the end of the handler.

Edit for header form-group

The entire form is then a sequence of <div class="form-group"> elements, many of which are shared between  handlers as elements.

The first of these form-groups should follow the content of existing handlers to provide a header for the form handler dialogue. You need this even if your new Form Handler Plugin has no actual dialogue inputs. The classes are keyed on to collapse and expand the handler and manage the expander in the Submit block dialogues.

This group also provide an icon for the handler, which should provide icon classes suitable for both v8 (Font Awesome 3) and v9 (Font Awesome 5).

As a general convention:

  • Logging and diagnostic handlers use text-warning to show they are not part of the main form flow.
  • OnSuccess uses text-success,
  • OnErrorFail and anything that creates an error use text-danger,
  • ConditionIf and other control flow use text-primary.
  • Next Form State use text-info.
  • Our Null handler uses text-muted.
  • Handlers interfacing to an external API should use text-indigo.
  • Other handlers should generally leave the colour scheme as the default.

From version 9.3.1, Form Reform introduces a range of extra text colors for use in the pipeline edit dialogues and particularly for color coding groups of associated or types of handlers, such as the convention of text-indigo for handlers interfacing to an external API. See the file jl_form_reform/elements/form_reform/pipeline/handler_ui.php

Unless the built in help from getHelp() is particularly short, the help shown in the banner will usually be a just a key part of the help.

Edit form input form-groups

The rest of the edit method will be various form-groups with one or more input fields, inline help and sections of JavaScript.

The important thing to remember is that all a Form Handler's parameters are in $data and all field names must be wrapped in $this->prefix_namespace() to ensure they don't conflict with any other Form Hadlers in the same dialogue.

With php8 in mind, you should also confirm the data item exists and assign a default before using it.

        
<div class="form-group">
<?php
    echo $form->label(
        $this->prefixNamespace('field_name'),
        t('Label for this input')
    );
    echo $form->text(
        $this->prefixNamespace('field_name'),
        $data['field_name']
    );
?>
</div>    

Edit returns the <html>

The edit() method should always return the html for the edit form. This is usually achieved by wrapping the entire content of the edit method in an output buffer and returning the content of the output buffer at the end of the method.

Where there are common fragments of edit dialogue, many handlers also make use of Concrete CMS elements and of classes providing input widgets and package elements to build the edit form. 

The important thing to remember is that it doesn't output directly. Output needs to be captured and then returned.

This is necessary because edit() methods are use both directly and via ajax. If the edit method were to output directly, something will break!

        
ob_start();
/*
 * ... body of edit form ...
 */
$html = ob_get_contents();
ob_end_clean();
return $html;    

handler_action($pipeline_manager, $data, $form_data)

The handler_action() method is where a handler does its processing of a submitted form. These are called in sequence by the handler pipeline.

Handler input parameters

The parameters passed are:

$pipeline_manager

This is the instance of the HandlerPipeline class that is running through the individual handlers in sequence. As such it can provide information about the handlers in the pipeline, the blocks in the form, the submitted form data and the outcome of handlers already executed.

$data

This is the array of configuration data set in the edit() method.

$form_data

This is the raw form data as an associative array, as arriving from the form POST request. If you need the data extracted from the that by the form blocks, you can interrogate the $pipeline_manager.

Processing in the handler

The processing is whatever you need to do with the form data in your new handler. There is such a variety of what can be done that you won't get any more guidance than these few words.

Study some existing handler_action() methods close to what you need to do to get some ideas. Maybe even use one of them as a starting point.

The processing should never depart the handler sequence. For example, by redirecting to another page. The one thing every handler_action() must always do is actually return.

Making new data available

When your form handler does something with data from the form, that action may also generate new data that you would like to make available for further processing. For example, suppose your new form handler forwards form data to a third party system. The outcome may be success or failure, and the the third party system may also return some information.

If its purely a success or fail, the solution will usually be to add any failure reason to the $errors list and return it (see below). It can then be managed by On Success / On Error.

You can also make data returned by the third party system available to subsequent form handlers as {{place_holders}} by adding them to the DataValues class as a static category.

DataValues::addStaticCategory('my_category',$response_data);

Where $response_data is an associative array of keys and values.

If the response could be multi-dimensional, you should flatten it first, such as using Arr::dot($response_data), or whatever data flattening strategy you prefer.

DataValues::addStaticCategory('my_category',Arr::dot($response_data));

Subsequent handlers such as Condition If, Message and Email can then use your returned data with a placeholder such as {{my_category:response.data.key}}.

If you need to save a value returned with form data, you can then use an Extend Form Data handler to incorporate it into the data available for saving.

As a development assist, add one of the the placeholders {{my_category:all}} or {{my_category:all_formatted}} to a Message form handler to see all of the data in 'my_category' serialized as JSON, so you can see everything available from your custom handler response.

Returning from the Form Handler

We have a few options on what a form handler can return.

return false;

Returning the boolean false results in an immediate end to the pipeline with nothing returned from the form. The form has disappeared into a black hole. For example, the Fail handler always returns false and the Spam Detect handler can be configured to return false instead of an $errors list.

return $errors;

Any errors should be reported by returning an $errors object, as assembled using the core helper/validation/error.

This should only be returned this if there are any errors to return.

return t('text message');

Most PipelineHandlers will return a simple text message on successful completion. This is used by the $pipeline_manager for reporting status, diagnostics and logging. If you look at a few of the packaged handlers, this includes the handler name and current step. return t("%s successful message at step %d", $this->getName(), $pipeline_manager->getCurrrentStepIndex()).

return new PipelineAction();

Some actions can only be actually implemented at the end of the pipeline, after all handlers have completed. For example, a typical pipeline may need to return a message, set a new form state and then redirect to another page. If any handler did such directly and unilaterally it would prevent subsequent handlers from being run as they should. We have a chicken and egg problem.

The solution is that these handlers return a PipelineAction object. At the end of the pipeline the $pipeline_manager then consolidates all the actions that have been returned by the various PipelineHandlers.

Have a look at some of the existing handlers for some examples.

return parent::handler_action(...);

If you get to the end of a handler_action() and don't really have anything to return, then return return parent::handler_action($pipeline_manager, $data, $form_data). This will return some default status information that may be used by other handlers and more importantly by any logging or diagnostics.

PipelineAction

Pipeline Action object is just one of the things a Form Handler can return. These are generally used to note an action that needs to be taken after the Form Handlers in a pipeline have all completed.

Having said that, your new Form Handler is unlikely to need to return a PipelineAction because you could just use an existing Form Handler in your pipeline. If you are considering a new Pipeline Action, its best to Contact Me to discuss.

The nature of a Pipeline Action can be one of:

MESSAGE

Notes a message that will be returned to the browser. When multiple messages are specified, all are returned to the browser as a list of messages. Each message implies a new form state based on the message level, such as success, warning, danger. That form state can be overridden by a subsequent STATE Pipeline Action.

A MESSAGE action is returned by the Message handler.

REDIRECT

Prepares for a redirect to another page. The actual redirect is then deferred to the end of the pipeline because if it happened somewhere in the middle other things like messages wouldn't happen!

For AJAX form submissions, a redirect can also be delayed.

When multiple REDIRECT Pipeline Actions are returned by Form Handlers, only the last returned REDIRECT is applied.

A REDIRECT action is returned by the Redirect handler.

STATE

Specifies one or more new form states. This overrides any state implied by a message. When multiple STATE Pipeline Actions are specified by Form Handlers, only the last STATE is applied. This action can set multiple states together such as Success and Step 4.

A STATE action is returned by the New State handler and by the Message handler.

QUARANTINE_FILES_CLEARED

This action is specific to form inputs that upload files to quarantine and form handlers that then move or remove those files from quarantine. It provides a list of file IDs for return to the browser to synchronize the repeater thumbnails shown by associated inputs.

NULL

A NULL action, not currently used

// ERROR

There is not actually an ERROR action. Form Handlers simply return a core $errors object to record an error then use a MESSAGE action containing the error text.

$params

A pipeline action also has an array of parameters specific to the action. Search the existing Form Handlers for return new PipelineAction(...) for some examples.

HandlerPipeline

A bit more about the HandlerPipeline class. The $pipeline_manager is an instance of this the class. It runs through the configured form handlers, keeps track of errors and actions, and can be manipulated to some extent by the handlers to manipulate the pipeline, such as the consequences of error handling or conditional handlers.

getSubmittedData()

Each form block will extract its own submitted data and the pipeline_manager takes care of collating that data from all the form blocks. If you need it all together, such as when saving a form submission, $pipeline_manager->getSubmittedData() returns an associative array of the data extracted by all the block controllers.

recordPipelineData()

Most handlers record a simple status flag such as $pipeline_manager->recordPipelineData($this->getName(), 'status_word') before returning. This is used primarily for traceing, but may also be used for related handlers to communicate status.

advanceToNextStepByName()

You probably shouldn't be using this, but we will explain what it does anyway. $pipeline_manager->advanceToNextStepByName() takes another handler name or list of names and is used to jump forward in the processing pipeline by handlers such as OnSuccess, OnError and ConditionIf

All such handlers need to work in cooperation, so messing with the next step shouldn't be done lightly. Best to Contact Me if you think you are considering doing anything like this in your own handler code.

setFlag(), clearFlag()

This pair of methods set and clear named flags in the $pipeline_manager. If you have a pair or sequence of handlers that need to communicate, you can use these methods. There is no namespacing on the flags, so you need to check thoroughly you are not messing with a flag used by other handlers such as such as OnSuccess, OnError and ConditionIf

Save Handlers

There are some special considerations for handlers that save form data. If you are developing a custom form handler plugin, there is a good chance it will be to save to a new or application specific location.

A form save handler may also need to make previously saved form submissions available for populating subseqent form controls.

Any handler providing such functionality will inherit from FormSaveHandlersPluginBase. This provides some extra flags and signifies to the form block controllers that previously saved inputs are available.

For example, the SavetoDefault, SaveToSession and SaveToCookie handlers provide this functionality. The SaveToCSV handler does not, because trawling through a CSV file for every request could be far too expensive.

static public function getPreviousResponsePriority()

Any handler providing the extended functionality then provides the static method getPreviousResponsePriority().

This method simply returns a constant defining the priority that Form Reform should use when pre-populating or persisting form inputs from previously saved data. This priority is based on both proximity of the data to the user and cost of searching and retrieval. 

  • SAVED_IN_BROWSER (as a cookie)
  • SAVED_IN_SESSION
  • SAVED_IN_LOCAL_DB
  • SAVED_IN_LOCAL_FILE
  • SAVED_IN_REMOTE_DB
  • SAVED_IN_REMOTE_FILE
  • SAVED_IN_NOT_SUPPORTED

When priorities of multiple Form Handlers are equal, alphabetical sequence is used. When a form input is populated according to persistence, all installed Form Handlers are interrogated in sequence until a previous submission that matches the persistence criteria is returned. This is not limited to only the handlers used by that form's Submit buttons.

The parent in FormSaveHandlersPluginBase returns SAVED_IN_NOT_SUPPORTED to show that it does not support previous response inquires from form block controllers.

static public function getPreviousResponse()

Any form save handler that supports retrieving a previous response does so with this method. 

Parameters are:

$persistence

The persistence option from a block edit dialogue.

$form_name

The form name from a block edit dialogue.

$cid

The id of the page.

$key

Typically the input name, but could be a compound such as for an array/list input.

These parameters enable the Form Handler to find for the most recent previous input that matches the persistence conditions.

When such a match is found, the handler returns the value. Otherwise it returns null.

If badly coded this could be an enormous overhead, so it is critical to search efficiently and, if it can't be done efficiently, think twice before supporting this interface from a form save handler. That is why the Save to CSV handler does not support this interface, because it couldn't do so efficiently!

Handlers in application/src

Personally I like to work in packages and recommend others to also work in packages. Hence the above is predominantly about packages. However, some developers like to work in /application, so here are some special considerations for /application.

Your class in /application/src needs to be loaded

Any class in /application/src needs to be loaded. To facilitate this, you will usually need to add an auto loader in application/bootstrap/autoload.php.

See https://documentation.concretecms.org/developers/framework/extending-concrete5-with-custom-code/creating-custom-code-in-the-application-directory.

(The documentation is out of date in that it refers to a deprecated Psr4ClassLoader. Nevertheless, it still works!)

        $classLoader = new \Symfony\Component\ClassLoader\Psr4ClassLoader();
$classLoader->addPrefix('Application\\Src', DIR_APPLICATION . '/src');
$classLoader->register();    

Your custom handler needs to be in that name space

We can now create a handler called NewHandler in the application\Src name space.

for example, a application/src/FormReform/FormHandlers/Plugins/NewHandler.php

The character case of filename and name space is critical here. A misplaced character can lead to the handler plugin not being found or to a Whoops screen for an auto loading error.

        <?php
namespace Application\Src\FormReform\FormHandlers\Plugins;
use JtF\FormReform\FormHandlers\FormHandlersPluginBase;
defined('C5_EXECUTE') or die('Access Denied.');

class NewHandler extends FormHandlersPluginBase
{
    public function getName()
    {
        return t('New Handler');
    }
    //...
    //...
}    

Concrete CMS Events

An alternative way to integrate further functionality is to use Concrete CMS events. 

Form Reform provides the handler Fire Event which can be configured to fire an event with a name beginning on_form_reform_... . By default, the event name is on_form_reform_event, but any event name can be configured. Just pick a name suited to your use!

The data passed with the event and handled when the event returns is configurable to be any of:

  • The submitted form data as 'submission_data'
  • The errors list object as 'errors'

For testing, the special event on_form_reform_test is handled by the Form Reform controller and will append some test text to the submission_data and an error message to the errors list.

Multiple Fire Event form handlers can be inserted into the pipeline. We strongly recommend you configure each for a unique event name !

Form Reform Developer provides a growing suite of resources to assist those developing blocks, handlers and more complex forms for Form Reform.

Additional Pages

Reform the way you add new input controls

If you need a specialized template or a custom input element, you can design new templates  or new block types for form elements as you would any block type.

Blocks are easy for third party addition or extension. Block templates and are the first thing any Concrete CMS developer learns to code. They are one of the easiest things to code. The underlying mechanisms are well established and reliable.

Reform what you can do with form data

Form handlers are built about the same extensible plugin system as many of my other addons (Universal Content Puller, Omni Gallery, Extreme Clean ...).

The whole system is aimed at easy extension within Form Reform, by third party addons, by agencies and by site building developers.

Handlers can be easily added to do whatever you want with the form data.

Reform where you can save form data

Saving form data with Form Reform is simply a handler in the processing pipeline. You can save to multiple locations or just one location.

If you need to save data elsewhere, such as to a dedicated table, a table provided through another addon, to another database, send it to an API, forward it to another server, or anywhere you can imagine, you can adapt or develop a form handler to do so.

The complexity of the code depends on where you are saving or sending the data, but wrapping that into a form handler plugin for Form Reform is straight forward.

The Form Reform handler plugin system is designed for easy extension.

Form Reform

Reform the way forms are built. Build a form out of blocks. Take control of how form submissions are processed and how the submitted data is stored. Easy to extend. Easy to reconfigure. Tangible data. Easy to add your own integrations.

Form Reform Display

List and display form submissions from Form Reform.

Form Reform UTM

Not just Form Reform and not just UTM! Capture and hold incoming UTM (or other) tags and make the tag values available to Form Reform and/or Conditional Redirect as {{place_holders}}. You don't need Form Reform to use this.

Form Reform Dynamics

Form handlers for querying Microsoft Dynamics, forwarding and updating form data to Microsoft Dynamics.

Snapshot

A suite of advanced image capture and upload tools. Enhanced drag and drop file uploading. Make screengrabs from within Concrete CMS. Capture images directly from device webcams. Edit images before uploading.

Form Reform Attributes, Express and Users

Save submitted forms to Express objects and user attributes. Add and remove users from groups.

Form Reform Image Picker

Form Reform Image Picker provides an image picking input block for Form Reform. The Image Picker Input is preconfigured to connect to most Omni Gallery gallery and slider display widgets, the core gallery block, and thumbnail showing templates for the core page list block. Advanced settings allow the Image Picker Input to be configured to pick images from other galleries and sliders.

Form Reform Data Picker

Form Reform Data Picker provides data picking input blocks for Form Reform. The Table Picker Input is preconfigured to connect to Universal Content Puller table display widgets. Advanced settings allow the Table Picker Input to be configured to pick data from other HTML tables.

Form Reform Developer

A growing suite of resources to assist those developing blocks, handlers and more complex forms for Form Reform.

Learn with a simple form

While you may have plans to implement some much more complex forms using Form Reform, we strongly recommend you start with a simple form such as our contact form example in order to review the basic principles of using Form Reform before you move onto anything bigger.

  1. Start by submitting the form at Getting Started - Your First Form a few times, even making some deliberate mistakes.
  2. Watch our Getting Started with Form Reform video to see how the form is built.
  3. Read through the rest of Getting Started - Your First Form for more details of how this form is built.
  4. Create a test page on your site to build your own version of Getting Started - Your First Form and experiment.
  5. Develop your test page with some of the concepts introduced by our further examples and experiment with some of the other form inputs.