Skip to content

Example of Email Preferences form

As the combination of the Form Processor extension and the Action Provider extension really is a framework it is relatively easy to develop your own actions.

This could be necessary because:

  • the current set of actions is limited to what we needed initially and it could well be that you need an action that is not there but will be very useful to others as well. This probably means you want to develop a new action in the Action Provider extension (with a pull request).

  • you need to develop a very specific action which is only meaningful for the organisation/project you are working with. In this case you will probably create your specific action in your own extension.

Note

The Action Provider is a separate extension rather than part of the Form Processor because it can also be used by other extensions (at the moment the Data Processor extension).

This example includes developing specific action and retrieval criteria.

What result do I want?

I would like to have a form on my website where a contact can specify his or her email preferences.

The idea is that we include a link in each mailing or mail we send out with a checksum (see the wonderful AGH Strategies Email Link Guide). This link should lead to the form (prefilled if we know the person) which should look like this:

Mockup Form

Once the person has ticked all their boxes the result should be processed into CiviCRM.

In CiviCRM I will have a few groups:

  • a group called Monthly Newsletter
  • a group called Monthy Actions
  • a group called Yearly Summary

Groups in CiviCRM

The email preferences should reflect this group membership:

  • if someone ticks the box I do not want any general mails the contact should be removed from all three groups
  • if someone ticks the box I would only like to receive the yearly summary the contact should be removed (if a member) from the groups Monthly Newsletter and Monthly Actions and added (if not a member already) to the group Yearly Summary.
  • if someone ticks the box I would only like to receive the monthy newsletter the contact should be removed (if a member) from the groups Monthly Actions and Yearly Summary and added (if not a member already) to Monthly Newsletter
  • if someone ticks the box I would like to receive the monthly newsletter and the monthly action summary the contact should be removed (if a member) from the group Yearly Summary and added to (if not a member already) to the groups Monthly Actions and Monthly Newsletter

The same rules apply when retrieving the current preferences.

Note

The assumption is that the form will be developed in such a way that I can not receive conflicting ticks!

What do I need to do?

Linking the tick boxes on the form to the group memberships in CiviCRM is a very specific thing. It is unlikely that exactly the same set up will be used by more organizations, so in this case I have elected to create a new specific extension for these actions, which can then be used by the Form Processor on this specific CiviCRM installation.

In this extension I will develop:

  1. An action to retrieve the current group membership and translate that into defaults for my yes/no fields on my form processor
  2. An action to process the submitted yes/no fields on the form processor into group membership in CiviCRM.

I can then specify a form processor for this form using my new actions, and create a form.

Create the extension, folders and info.xml

I have created the extension myemailprefs. In this extension I will now add a folder Civi and within that folder the folder Action. This is not required, but it reflects the structure of CiviCRM and the structure of the Action Provider extension and I like to adhere to those :-)

In the Action folder I can add my actions if I only expect to have a couple, or add a folder for each entity and then add my actions in there. Because this extension for now will only hold 2 actions I will put them in the Action folder.

Extension folders

Next I have updated my info.xml, most of the information is not too important for this example but the <classloader> tag is! This is required for an extension that creates actions for the Action Provider.

On top of that the <required> tag also makes sense in this context as the extension will not work without the Action Provider extension and the relevant classes that we are using will not be present.

<?xml version="1.0"?>
<extension key="myemailprefs" type="module">
  <file>myemailprefs</file>
  <name>My Email Preferences - CiviCRM specific actions (Action Provider)</name>
  <description>CiviCRM extension for documentation purposes - example actions</description>
  <license>AGPL-3.0</license>
  <maintainer>
    <author>Erik Hommel (CiviCooP)</author>
    <email>erik.hommel@civicoop.org</email>
  </maintainer>
  <urls>
    <url desc="Main Extension Page">https://lab.civicrm.org/partners/civicoop/myemailprefs</url>
    <url desc="Documentation">https://lab.civicrm.org/partners/civicoop/myemailprefs</url>
    <url desc="Support">https://civicoop.org</url>
    <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
  </urls>
  <releaseDate>2020-01-08</releaseDate>
  <version>1.0</version>
  <develStage>beta</develStage>
  <compatibility>
    <ver>5.19</ver>
  </compatibility>
  <requires>
    <ext>action-provider</ext>
  </requires>
  <classloader>
    <psr4 prefix="Civi\" path="Civi" />
  </classloader>
  <comments>Extension is for documentation purposes</comments>
  <civix>
    <namespace>CRM/Myemailprefs</namespace>
  </civix>
</extension>

Develop my action to retrieve the current email preferences

Next step is to develop my action that will retrieve the current email preferences. In this action I expect to receive a contact ID, and I will then collect the group membership for the contact and set the yes/no fields accordingly.

The first step is create a class in my extension extending the AbstractAction class from the Action Provider. This will initially have to look like this:

<?php

  namespace Civi\Myemailprefs\Actions;

  use \Civi\ActionProvider\Action\AbstractAction;
  use \Civi\ActionProvider\Parameter\ParameterBagInterface;
  use \Civi\ActionProvider\Parameter\SpecificationBag;
  use \Civi\ActionProvider\Parameter\Specification;

  use CRM_Myemailprefs_ExtensionUtil as E;

  class GetEmailPrefs extends AbstractAction {
  }

Next step is to specify the required methods getParameterSpecification, getConfigurationSpecification and doAction.

The method getParameterSpecification determines the parameters I expect to receive in my class. This is the data that will be sent to my action from my form processor or from one of the previous actions. In this example I expect to get a contact ID as a parameter:

/**
 * @return SpecificationBag
 */
public function getParameterSpecification() {
  return new SpecificationBag([
    new Specification('contact_id', 'Integer', E::ts('Contact ID'), TRUE)
  ]);
}
The method getConfigurationSpecification is not really needed here because I have no configuration to set. But I do need this function in my class (as specified in the abstract class) so I make it 'void':

/**
 * @return SpecificationBag|void
 */
public function getConfigurationSpecification() {
  return new SpecificationBag();
}

The method getOutputSpecification determines what data I will output in my action. In this case this will be the flags that correspond to my group memberships and the fields on my form processor:

/**
 * @return SpecificationBag
 */
public function getOutputSpecification() {
  return new SpecificationBag([
    new Specification('newsletter_and_action', 'Boolean', E::ts('Monthly Newsletter and Monthly Action'), FALSE),
    new Specification('newsletter_only', 'Boolean', E::ts('Monthly Newsletter Only'), FALSE),
    new Specification('yearly_summary', 'Boolean', E::ts('Yearly Summary Only'), FALSE),
    new Specification('no_general_mail', 'Boolean', E::ts('No General Mails'), FALSE),
  ]);
}
The method doAction actually performs my action, so in this case use the contact_id to retrieve the group memberships and set my booleans reflecting this:

/**
 * @param ParameterBagInterface $parameters
 * @param ParameterBagInterface $output
 * @throws InvalidParameterException
 */
public function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) {
  $contactId = (int) $parameters->getParameter('contact_id');
  if ($contactId) {
    $contactGroups = $this->retrieveContactGroups($contactId);
    $outputValues = $this->calculateOutputValues($contactGroups);
    foreach ($outputValues as $key => $value) {
      $output->setParameter($key, $value);
    }
  }
  else {
    throw new InvalidParameterException(E::ts("Could not find mandatory parameter contact_id"));
  }
}
This is all that is needed for my action to get the email preferences.

I want the rest too

If you want to also see what the methods retrieveContactGroups and calculateOutputValues do, check the code on My Email Prefs Code on Gitlab

Develop my action to save the new email preferences

In this action I will receive the output of the form and process that into the desired group memberships. I expect a contact_id parameter, and the tickboxes from the form.

The first step is create a class in my extension extending the AbstractAction class from the Action Provider. This will initially have to look like this:

namespace Civi\Myemailprefs\Actions;

use \Civi\ActionProvider\Action\AbstractAction;
use Civi\ActionProvider\Exception\InvalidParameterException;
use \Civi\ActionProvider\Parameter\ParameterBagInterface;
use \Civi\ActionProvider\Parameter\SpecificationBag;
use \Civi\ActionProvider\Parameter\Specification;

use Civi\FormProcessor\API\Exception;
use CRM_Myemailprefs_ExtensionUtil as E;

/**
 * Class SetEmailPrefs - set the email preferences of a contact from form data
 */
class SetEmailPrefs extends AbstractAction {
Next step is to specify the required methods getParameterSpecification, getConfigurationSpecification and doAction.

The method getParameterSpecification determines the parameters I expect to receive in my class. This is the data that will be sent to my action from my form processor or from one of the previous actions. In this example I expect to get a contact ID and all the tick boxes as parameters:

/**
 * @return SpecificationBag
 */
public function getParameterSpecification() {
  $specs = new SpecificationBag();
  $specs->addSpecification(new Specification('contact_id', 'Integer', E::ts('Contact ID'), TRUE, NULL));
  $specs->addSpecification(new Specification('newsletter_and_action', 'Boolean', E::ts('Monthly Newsletter and Monthly Action'), FALSE, FALSE));
  $specs->addSpecification(new Specification('newsletter_only', 'Boolean', E::ts('Monthly Newsletter Only'), FALSE, FALSE));
  $specs->addSpecification(new Specification('yearly_summary', 'Boolean', E::ts('Yearly Summary Only'), FALSE, FALSE));
  $specs->addSpecification(new Specification('no_general_mail', 'Boolean', E::ts('No General Mails'), FALSE, FALSE));
  return $specs;
}

The method getConfigurationSpecification is not really needed here because I have no configuration to set. But I do need this function in my class (as specified in the abstract class) so I make it 'void':

/**
 * @return SpecificationBag|void
 */
public function getConfigurationSpecification() {
  return new SpecificationBag();
}

The doAction method performs my actual actions, so will translate the tick boxes into group memberships and add/remove the contact from the relevant groups.

/**
 * @param ParameterBagInterface $parameters
 * @param ParameterBagInterface $output
 * @throws InvalidParameterException
 */
public function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) {
  $contactId = (int) $parameters->getParameter('contact_id');
  if ($contactId) {
    $this->setGroupMemberships($contactId, $parameters);
  }
  else {
    throw new InvalidParameterException(E::ts("Could not find mandatory parameter contact_id"));
  }
}

This is all that is needed for my action to set the email preferences.

I want the rest too

If you want to also see what the method setGroupMemberships does, check the code on My Email Prefs Code on Gitlab

Add my actions to the Action Provider

As a last step I need to tell CiviCRM about my actions.

First I need to add a CompilerPass Class to my extension:

namespace Civi\Myemailprefs;

/**
 * Compiler Class for action provider
 */

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use CRM_Myemailprefs_ExtensionUtil as E;

class CompilerPass implements CompilerPassInterface {

  public function process(ContainerBuilder $container) {
    if ($container->hasDefinition('action_provider')) {
      $actionProviderDefinition = $container->getDefinition('action_provider');
      $actionProviderDefinition->addMethodCall('addAction',
        ['GetEmailPrefs', 'Civi\Myemailprefs\Actions\GetEmailPrefs', E::ts('Get Email Preferences'), []]);
      $actionProviderDefinition->addMethodCall('addAction',
        ['SetEmailPrefs', 'Civi\Myemailprefs\Actions\SetEmailPrefs', E::ts('Set Email Preferences'), []]);
    }
  }
}

And then I need to add my CompilerPass Class to the CiviCRM container using the container hook in my main extension file myemailprefs.php:

/**
 * Implements hook_civicrm_container().
 *
 * @param ContainerBuilder $container
 */
function myemailprefs_civicrm_container(ContainerBuilder $container) {
  $container->addCompilerPass(new Civi\Myemailprefs\CompilerPass());
}

Form

I have completed the code for my actions, tested them with the API Explorer with entities FormProcessor and FormProcesserDefaults.

Now I could set up a form on my website to work with the actions. This is outside the scope of this section, the ambition here was to explain how to create your own actions. If you want to see an example of how to set up a form check Newsletter Drupal7 Example or Newsletter Wordpress Example.