Skip to content

Settings Form

This is a documentation for adding a settings form in a civicrm extension.

Generate Settings Form

To start, you need to go to the main extension folder and open terminal, commandline or git bash. After that, we need to run the civix generate:form Settings civicrm/admin/extensionname/settings to create our settings form with the form/page URL in the extension. Example if you have an extension that called com.joineryhq.extensionname. This will generate a:

  • Settings.php file in the CRM folder for the backend functions of your settings
  • Settings.tpl file in templates folder for the frontend view (html and js)
  • extensionname.xml file in the xml folder for defining request-routing rules and associates controller of the Settings.php class with the web path civicrm/admin/extensionname/settings.
- CRM
  - Extensionname
    - Form
      - Settings.php
- templates
  - Extensionname
    - Form
      - Settings.tpl
- xml
  - Menu
    - extensionname.xml

Creating Settings Folder and Fields

Before adding some functions and views on Settings.php and Settings.tpl, you need to add first the settings folder which let you create a form fields (input, select, texarea, etc.,) for your settings.

- settings
  - Extensionname.setting.php

Here is a code example for creating 4 settings: a radio field; textarea field; and select field with a callback option; and a setting that is not shown on the settings form.

<?php

use CRM_Extensionname_ExtensionUtil as E;

return array(
  'com.joineryhq.extensionname' => array(
    'group_name' => 'Extensionname Settings',
    'group' => 'extensionname',
    'name' => 'extensionname_show',
    'add' => '5.0',
    'is_domain' => 1,
    'is_contact' => 0,
    'description' => '',
    'title' => E::ts('Show Extensionname on User Dashboard?'),
    'type' => 'Boolean',
    'quick_form_type' => 'YesNo',
    'default' => 0,
    'html_type' => 'radio',
  ),
  'com.joineryhq.extensionname' => array(
    'group_name' => 'Extensionname Settings',
    'group' => 'extensionname',
    'name' => 'extensionname_description',
    'add' => '5.0',
    'is_domain' => 1,
    'is_contact' => 0,
    'description' => '',
    'title' => E::ts('Extensionname Description'),
    'type' => 'Text',
    'default' => FALSE,
    'html_type' => 'textarea',
    'quick_form_type' => 'Element',
  ),
  'com.joineryhq.extensionname' => array(
    'group_name' => 'Extensionname Settings',
    'group' => 'extensionname',
    'name' => 'extensionname_contact',
    'add' => '5.0',
    'is_domain' => 1,
    'is_contact' => 0,
    'description' => '',
    'title' => E::ts('Select Contact for Extensionname'),
    'type' => 'Int',
    'quick_form_type' => 'Element',
    'html_type' => 'Select',
    'html_attributes' => array(
      'class' => 'crm-select2',
      'style' => "width:auto;",
    ),
    'X_options_callback' => 'CRM_Extensionname_Form_Settings::getContactList',
  ),
  'com.joineryhq.extensionname' => array(
    'group_name' => 'Extensionname Settings',
    'group' => 'extensionname',
    'name' => 'extensionname_internal_id',
    'add' => '5.0',
    'is_domain' => 1,
    'is_contact' => 0,
    'description' => '',
    'title' => E::ts('Extensionname Internal ID'),
    'type' => 'Text',
    'default' => FALSE,
    'html_type' => 'textarea',
    // Omit 'quick_form_type' property to hide from settings form.
    'quick_form_type' => NULL,
  ),
);

Setup Functions in Settings.php

After creating the settings folder and fields. You can now setup the functions in Settings.php by copying the example code below.

<?php

require_once 'CRM/Core/Form.php';
use CRM_Extensionname_ExtensionUtil as E;

/**
 * Form controller class for extension Settings form.
 * Borrowed heavily from
 * https://github.com/eileenmcnaughton/nz.co.fuzion.civixero/blob/master/CRM/Civixero/Form/XeroSettings.php
 *
 * @see http://wiki.civicrm.org/confluence/display/CRMDOC43/QuickForm+Reference
 */
class CRM_Extensionname_Form_Settings extends CRM_Core_Form {

  // Group must be the same as the group in your Extension.setting.php
  public static $settingFilter = array('group' => 'extensionname');
  public static $extensionName = 'com.joineryhq.extensionname';
  private $_submittedValues = array();
  private $_settings = array();

  public function __construct(
    $state = NULL,
    $action = CRM_Core_Action::NONE,
    $method = 'post',
    $name = NULL
  ) {

    $this->setSettings();

    parent::__construct(
      $state = NULL,
      $action = CRM_Core_Action::NONE,
      $method = 'post',
      $name = NULL
    );
  }

  public function buildQuickForm() {
    $settings = $this->_settings;
    foreach ($settings as $name => $setting) {
      $element = NULL;
      if (isset($setting['quick_form_type'])) {
        switch ($setting['html_type']) {
          case 'Select':
            $element = $this->add(
              // field type
              $setting['html_type'],
              // field name
              $setting['name'],
              // field label
              $setting['title'],
              $this->getSettingOptions($setting),
              NULL,
              $setting['html_attributes']
            );
            break;

          case 'CheckBox':
            $element = $this->addCheckBox(
              // field name
              $setting['name'],
              // field label
              $setting['title'],
              array_flip($this->getSettingOptions($setting))
            );
            break;

          case 'Radio':
            $element = $this->addRadio(
              // field name
              $setting['name'],
              // field label
              $setting['title'],
              $this->getSettingOptions($setting)
            );
            break;

          default:
            $add = 'add' . $setting['quick_form_type'];
            if ($add == 'addElement') {
              $element = $this->$add($setting['html_type'], $name, E::ts($setting['title']), CRM_Utils_Array::value('html_attributes', $setting, array()));
            }
            else {
              $element = $this->$add($name, E::ts($setting['title']));
            }
            break;
        }
        if ($element && ($setting['html_attributes']['readonly'] ?? FALSE)) {
          $element->freeze();
        }
      }
      $descriptions[$setting['name']] = E::ts($setting['description']);

      if (!empty($setting['X_form_rules_args'])) {
        $rules_args = (array) $setting['X_form_rules_args'];
        foreach ($rules_args as $rule_args) {
          array_unshift($rule_args, $setting['name']);
          call_user_func_array(array($this, 'addRule'), $rule_args);
        }
      }
    }
    $this->assign("descriptions", $descriptions);

    $this->addButtons(array(
      array(
        'type' => 'submit',
        'name' => E::ts('Submit'),
        'isDefault' => TRUE,
      ),
      array(
        'type' => 'cancel',
        'name' => E::ts('Cancel'),
      ),
    ));

    // Add styles path if you have custom styles for the form in your extension
    $style_path = CRM_Core_Resources::singleton()->getPath(self::$extensionName, 'css/extension.css');
    if ($style_path) {
      CRM_Core_Resources::singleton()->addStyleFile(self::$extensionName, 'css/extension.css');
    }

    // export form elements
    $this->assign('elementNames', $this->getRenderableElementNames());

    $session = CRM_Core_Session::singleton();
    $session->pushUserContext(CRM_Utils_System::url('civicrm/admin/extensionname/settings', 'reset=1', TRUE));
    parent::buildQuickForm();
  }

  /**
   * You need to write custom code for this function to validate the data in your settings fields
   */
  public function validate() {
    $error = parent::validate();
    $values = $this->exportValues();

    if (!$values['extensionname_show']) {
      $this->setElementError('extensionname_show', E::ts('This is a required field'));
    }

    if (!$values['extensionname_description']) {
      $this->setElementError('extensionname_description', E::ts('This is a required field'));
    }

    if (!$values['extensionname_contact']) {
      $this->setElementError('extensionname_description', E::ts('This is a required field'));
    }

    return (0 == count($this->_errors));
  }

  /* 
   * You need to write custom code for this function to save the data in your settings fields
   *
   */
  public function postProcess() {
    $this->_submittedValues = $this->exportValues();
    $this->saveSettings();
    parent::postProcess();
  }

  /**
   * Get the fields/elements defined in this form.
   *
   * @return array (string)
   */
  public function getRenderableElementNames() {
    // The _elements list includes some items which should not be
    // auto-rendered in the loop -- such as "qfKey" and "buttons". These
    // items don't have labels. We'll identify renderable by filtering on
    // the 'label'.
    $elementNames = array();
    foreach ($this->_elements as $element) {
      $label = $element->getLabel();
      if (!empty($label)) {
        $elementNames[] = $element->getName();
      }
    }
    return $elementNames;
  }

  /**
   * Define the list of settings we are going to allow to be set on this form.
   *
   */
  public function setSettings() {
    if (empty($this->_settings)) {
      $this->_settings = self::getSettings();
    }
  }

  public static function getSettings() {
    $settings = _extensionname_civicrmapi('setting', 'getfields', array('filters' => self::$settingFilter));
    return $settings['values'];
  }

  /**
   * Get the settings we are going to allow to be set on this form.
   *
   */
  public function saveSettings() {
    $settings = $this->_settings;
    $values = array_intersect_key($this->_submittedValues, $settings);
    _extensionname_civicrmapi('setting', 'create', $values);

    // Save any that are not submitted, as well (e.g., checkboxes that aren't checked).
    $settingsEditable = $this->filterEditableSettings();
    $unsettings = array_fill_keys(array_keys(array_diff_key($settingsEditable, $this->_submittedValues)), NULL);
    _extensionname_civicrmapi('setting', 'create', $unsettings);

    CRM_Core_Session::setStatus(" ", E::ts('Settings saved.'), "success");
  }

  /**
   * From all settings, get only the ones that are editable in the form.
   * (E.g. settings are not shown in the form if 'quick_form_type' is NULL;
   * settings are not editable in the form if ['html_attributes']['readonly'] is set.)
   */
  private function filterEditableSettings() {
    $ret = [];
    foreach ($this->_settings as $name => $setting) {
      if (
        !isset($setting['quick_form_type'])
        || ($setting['html_attributes']['readonly'] ?? NULL)
      ) {
        continue;
      }
      $ret[$name] = $setting;
    }
    return $ret;
  }

  /**
   * Set defaults for form.
   *
   * @see CRM_Core_Form::setDefaultValues()
   */
  public function setDefaultValues() {
    $result = _extensionname_civicrmapi('setting', 'get', array('return' => array_keys($this->_settings)));
    $domainID = CRM_Core_Config::domainID();
    $ret = CRM_Utils_Array::value($domainID, $result['values']);
    return $ret;
  }

  public function getSettingOptions($setting) {
    if (!empty($setting['X_options_callback']) && is_callable($setting['X_options_callback'])) {
      return call_user_func($setting['X_options_callback']);
    }
    else {
      return CRM_Utils_Array::value('X_options', $setting, array());
    }
  }

  /* 
   * Example code for the callback option
   *
   */
  public function getContactList() {
    // Select placeholder
    $options = [
      '' => '-' . E::ts('none') . '-',
    ];

    // Get all contact using api
    $contacts = \Civi\Api4\Contact::get()
      ->setCheckPermissions(FALSE)
      ->execute();
    foreach ($contacts as $contact) {
      $options[$contacts['id']] = $contacts['display_name'];
    }

    return $options;
  }

}

Don't forget to add this code in the extensions main php file (extensionname.php) for the setup to be complete.

/**
 * Log CiviCRM API errors to CiviCRM log.
 */
function _extensionname_log_api_error(API_Exception $e, string $entity, string $action, array $params) {
  $message = "CiviCRM API Error '{$entity}.{$action}': " . $e->getMessage() . '; ';
  $message .= "API parameters when this error happened: " . json_encode($params) . '; ';
  $bt = debug_backtrace();
  $error_location = "{$bt[1]['file']}::{$bt[1]['line']}";
  $message .= "Error API called from: $error_location";
  CRM_Core_Error::debug_log_message($message);
}

/**
 * CiviCRM API wrapper. Wraps with try/catch, redirects errors to log, saves
 * typing.
 */
function _extensionname_civicrmapi(string $entity, string $action, array $params, bool $silence_errors = TRUE) {
  try {
    $result = civicrm_api3($entity, $action, $params);
  }
  catch (API_Exception $e) {
    _extensionname_log_api_error($e, $entity, $action, $params);
    if (!$silence_errors) {
      throw $e;
    }
  }

  return $result;
}

Setup View Fields in Settings.tpl

To setup Settings.tpl, you just need to copy the code below and the fields you created will be shown on the settings pages.

{* HEADER *}
{* Display top submit button only if there are more than three elements on the page *}
{if ($elementNames|@count) gt 3}
  <div class="crm-submit-buttons">
  {include file="CRM/common/formButtons.tpl" location="top"}
  </div>
{/if}

{* FIELDS (AUTOMATIC LAYOUT) *}

{foreach from=$elementNames item=elementName}
  <div class="crm-section">
    <div class="label">{$form.$elementName.label}</div>
    <div class="content">{$form.$elementName.html}<div class="description">{$descriptions.$elementName}</div></div>
    <div class="clear"></div>
  </div>
{/foreach}

{* FOOTER *}
<div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="bottom"}
</div>

Conclusion

After all of the setup, you can now check if the settings form you created is available in this link: https://yoursite/civicrm/admin/extensionname/settings. Be sure to used cv flush (or reinstall the extension if it is newly created) if the settings form page doesn't exist.

  • Civix - Link for creating a civicrm extension
  • Summary Fields extension - You can check a bunch of examples of fields in the settings of this extension