Skip to content

Create a custom entity in your extension.

This step by step guide will show you how you can create a new entity in your extension. It includes the API functionality and forms for editing the entity.

Alternative: ECK

The Entity Construction Kit Extension provides a user interface based on this step by step guide. So that you create your own entity with clicking rather than programming. You still have to do hard work yourself which is the thinking.

1. Introduction

This guide assumes you have successfully set up a CiviCRM development environment and that you have civix working.

2. Create the extension

On the command line go the extension directory of your development environment.

Note

In a Drupal installation the extension directory is usually under sites/default/files/civicrm/ext. In a WordPress installation this is usually wp-content/uploads/civicrm/ext

Run the following commands to create an extension with the name myentity

cd sites/default/files/civicrm/ext
civix generate:module myentity
cd myentity

When asked whether to enable the extension answer "No" by pressing N. We will enable the extension later.

Now it is time to change info.xml and set the name, description and author of the extension.

3. Add a new entity

Run the following command to add the new entity, in the directory of your extension:

civix generate:entity MyEntity

This will add the following files:

  • Civi\Api4\MyEntity.php the api v4 file
  • xml/schema/CRM/Myentity/MyEntity.entityType.php a description of the entity
  • xml/schema/CRM/Myentity/MyEntity.xml the schema.xml file describing the structure of the entity (such as the fields, primary keys, etc.)

Open xml/schema/CRM/Myentity/MyEntity.xml and change it to your needs. This file contains the schema description, in our example we want to have the following fields: * ID * Contact ID * Title

See Schema Definition for an explanation of the XML file.

Our xml/schema/CRM/Myentity/MyEntity.xml will initially look like this, however you can delete unwanted fields (e.g. contact_id) and add others:

<?xml version="1.0" encoding="iso-8859-1" ?>
<table>
  <base>CRM/Myentity</base>
  <class>MyEntity</class>
  <name>civicrm_my_entity</name>
  <comment>A test entity for the documenation</comment>
  <log>true</log>

  <field>
    <name>id</name>
    <type>int unsigned</type>
    <required>true</required>
    <comment>Unique MyEntity ID</comment>
  </field>
  <primaryKey>
    <name>id</name>
    <autoincrement>true</autoincrement>
  </primaryKey>

  <field>
    <name>contact_id</name>
    <type>int unsigned</type>
    <html>
      <type>EntityRef</type>
    </html>
    <comment>FK to Contact</comment>
  </field>
  <foreignKey>
    <name>contact_id</name>
    <table>civicrm_contact</table>
    <key>id</key>
    <onDelete>CASCADE</onDelete>
  </foreignKey>

  <field>
    <name>title</name>
    <type>varchar</type>
    <length>255</length>
    <html>
      <type>Text</type>
    </html>
    <required>false</required>
  </field>

</table>

4. Generate SQL, DAO and BAO files

Once you have your desired fields defined in the .xml file, generate the SQL, DAO and BAO files using the following Civix command:

civix generate:entity-boilerplate

Our sql/auto_install.sql should look like this:

-- ...

-- /*******************************************************
-- *
-- * civicrm_my_entity
-- *
-- * A test entity for the documenation
-- *
-- *******************************************************/
CREATE TABLE `civicrm_my_entity` (


     `id` int unsigned NOT NULL AUTO_INCREMENT  COMMENT 'Unique MyEntity ID',
     `contact_id` int unsigned    COMMENT 'FK to Contact',
     `title` varchar(255) NULL
,
        PRIMARY KEY (`id`)


,          CONSTRAINT FK_civicrm_my_entity_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE
) ENGINE=InnoDb   ;

-- ...

Note

You have to repeat this step everytime you make changes to the schema in MyEntity.xml.

5. Add upgrader class

We need an upgrader class so that upon installation and uninstallation of our extension the files auto_install.sql and auto_unsinstall.sql are executed.

civix generate:upgrader

6. Install the extension

Now it is time to install the extension. In CiviCRM navigate to Administer > System settings > Extensions and click on Install next to our extension, or from the command line type:

cv en myentity

7. Add a form

Recommended alternative: Form Builder/Afform

CiviCRM is migrating to using Afform instead of Quickform. Consider that alternative first. Expose an entity in Form Builder

Add a form to add new entities, we will use the same form for editing existing entities and for deleting an existing entity.

 civix generate:form MyEntity civicrm/myentity/form

The above command will generate a skeleton page at CRM/Myentity/Form/MyEntity.php with the url civicrm/myentity/form.

Change the code of CRM/Myentity/Form/MyEntity.php to:

<?php

use CRM_Myentity_ExtensionUtil as E;

/**
 * Form controller class
 *
 * @see https://docs.civicrm.org/dev/en/latest/framework/quickform/
 */
class CRM_Myentity_Form_MyEntity extends CRM_Core_Form {

  protected $_id;

  protected $_myentity;

  public function getDefaultEntity() {
    return 'MyEntity';
  }

  public function getDefaultEntityTable() {
    return 'civicrm_my_entity';
  }

  public function getEntityId() {
    return $this->_id;
  }

  /**
   * Preprocess form.
   *
   * This is called before buildForm. Any pre-processing that
   * needs to be done for buildForm should be done here.
   *
   * This is a virtual function and should be redefined if needed.
   */
  public function preProcess() {
    parent::preProcess();

    $this->_action = CRM_Utils_Request::retrieve('action', 'String', $this);
    $this->assign('action', $this->_action);

    $this->_id = CRM_Utils_Request::retrieve('id', 'Positive', $this, FALSE);
    CRM_Utils_System::setTitle('Add Entity');
    if ($this->_id) {
      CRM_Utils_System::setTitle('Edit Entity');
      $entities = civicrm_api4('MyEntity', 'get', ['where' => [['id', '=', $this->_id]], 'limit' => 1]);
      $this->_myentity = reset($entities);
      $this->assign('myentity', $this->_myentity);

      $session = CRM_Core_Session::singleton();
      $session->replaceUserContext(CRM_Utils_System::url('civicrm/myentity/form', ['id' => $this->getEntityId(), 'action' => 'update']));
    }
  }


  public function buildQuickForm() {
    $this->assign('id', $this->getEntityId());
    $this->add('hidden', 'id');
    if ($this->_action != CRM_Core_Action::DELETE) {
      $this->addEntityRef('contact_id', E::ts('Contact'), [], TRUE);
      $this->add('text', 'title', E::ts('Title'), ['class' => 'huge'], FALSE);

      $this->addButtons([
        [
          'type' => 'upload',
          'name' => E::ts('Submit'),
          'isDefault' => TRUE,
        ],
      ]);
    } else {
      $this->addButtons([
        ['type' => 'submit', 'name' => E::ts('Delete'), 'isDefault' => TRUE],
        ['type' => 'cancel', 'name' => E::ts('Cancel')]
      ]);
    }
    parent::buildQuickForm();
  }

  /**
   * This virtual function is used to set the default values of various form
   * elements.
   *
   * @return array|NULL
   *   reference to the array of default values
   */
  public function setDefaultValues() {
    if ($this->_myentity) {
      $defaults = $this->_myentity;
    }
    return $defaults;
  }

  public function postProcess() {
    if ($this->_action == CRM_Core_Action::DELETE) {
      civicrm_api4('MyEntity', 'delete', ['where' => [['id', '=', $this->_id]]]);
      CRM_Core_Session::setStatus(E::ts('Removed My Entity'), E::ts('My Entity'), 'success');
    } else {
      $values = $this->controller->exportValues();
      $action = 'create';
      if ($this->getEntityId()) {
        $params['id'] = $this->getEntityId();
        $action = 'update';
      }
      $params['title'] = $values['title'];
      $params['contact_id'] = $values['contact_id'];
      civicrm_api4('MyEntity', $action, ['values' => $params]);
    }
    parent::postProcess();
  }

}

In the function preProcess we check whether an id is passed in the url and if so we retrieve the entity with that id. In the function buildQuickForm we add a hidden field for the id, a text field for the title, and a contact reference field for the contact_id field. In the function setDefaultValues we return the current entity. In the function postProcess we check whether we need to delete the entity of whether we need to create/update the entity.

Now it is time to update the template in templates/CRM/Myentity/Form/MyEntity.tpl:

{crmScope extensionKey='myentity'}
{if $action eq 8}
  {* Are you sure to delete form *}
  <h3>{ts}Delete Entity{/ts}</h3>
  <div class="crm-block crm-form-block">
    <div class="crm-section">{ts 1=$myentity.title}Are you sure you wish to delete the entity with title: %1?{/ts}</div>
  </div>

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

  <div class="crm-block crm-form-block">

    <div class="crm-section">
      <div class="label">{$form.title.label}</div>
      <div class="content">{$form.title.html}</div>
      <div class="clear"></div>
    </div>

    <div class="crm-section">
      <div class="label">{$form.contact_id.label}</div>
      <div class="content">{$form.contact_id.html}</div>
      <div class="clear"></div>
    </div>

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

  </div>
{/if}
{/crmScope}

8. Integration with Search Kit

When we generated the entity it also generated an API version 4. This automatically made the entity available in Search Kit. What it did not do is include links to add, edit or delete the entity. (See Search Kit - Links for an explanation).

To do this we need to add the links to Schema.xml. Open schema/CRM/Myentity/MyEntity.xml and add the following lines:

  <paths>
    <add>civicrm/myentity/form?reset=1&amp;action=add</add>
    <view>civicrm/myentity/form?reset=1&amp;action=view&amp;id=[id]</view>
    <update>civicrm/myentity/form?reset=1&amp;action=update&amp;id=[id]</update>
    <delete>civicrm/myentity/form?reset=1&amp;action=delete&amp;id=[id]</delete>
  </paths>

Run the following command to update the SQL, DAO files so that those paths are available in Search Kit:

civix generate:entity-boilerplate

Use Search Kit!

The preferred way to add search capability is via Search Kit. You can find information on how to Add Saved Search to Your Extension.

Add a page which will show all entities in the database. Later on we will add a form to add new entities or edit them.

 civix generate:form Search civicrm/myentity/search

The above command will generate a skeleton form which we will use as the base search page at CRM/Myentity/Form/Search.php with the url civicrm/myentity/search.

Open CRM/Myentity/Page/Search.php and change it as follows:

<?php

use CRM_Myentity_ExtensionUtil as E;

/**
 * Form controller class
 *
 * @see https://docs.civicrm.org/dev/en/latest/framework/quickform/
 */
class CRM_Myentity_Form_Search extends CRM_Core_Form {

  protected $formValues;

  protected $pageId = false;

  protected $offset = 0;

  protected $limit = false;

  public $count = 0;

  public $rows = [];


  public function preProcess() {
    parent::preProcess();


    $this->formValues = $this->getSubmitValues();
    $this->setTitle(E::ts('Search My Entities'));

    $this->limit = CRM_Utils_Request::retrieveValue('crmRowCount', 'Positive', 50);
    $this->pageId = CRM_Utils_Request::retrieveValue('crmPID', 'Positive', 1);
    if ($this->limit !== false) {
      $this->offset = ($this->pageId - 1) * $this->limit;
    }
    $this->query();
    $this->assign('entities', $this->rows);

    $pagerParams = [];
    $pagerParams['total'] = 0;
    $pagerParams['status'] =E::ts('%%StatusMessage%%');
    $pagerParams['csvString'] = NULL;
    $pagerParams['rowCount'] =  50;
    $pagerParams['buttonTop'] = 'PagerTopButton';
    $pagerParams['buttonBottom'] = 'PagerBottomButton';
    $pagerParams['total'] = $this->count;
    $pagerParams['pageID'] = $this->pageId;
    $this->pager = new CRM_Utils_Pager($pagerParams);
    $this->assign('pager', $this->pager);
  }


  public function buildQuickForm() {
    parent::buildQuickForm();

    $this->add('text', 'title', E::ts('Title'), array('class' => 'huge'));
    $this->addEntityRef('contact_id', E::ts('Contact'), ['create' => false, 'multiple' => true], false, array('class' => 'huge'));

    $this->addButtons(array(
      array(
        'type' => 'refresh',
        'name' => E::ts('Search'),
        'isDefault' => TRUE,
      ),
    ));
  }

  public function postProcess() {
    parent::postProcess();
  }

  /**
   * Runs the query
   *
   * @throws \CRM_Core_Exception
   */
  protected function query() {
    $sql = "
    SELECT SQL_CALC_FOUND_ROWS
      `civicrm_my_entity`.`id`,
      `civicrm_my_entity`.`title`,
      `civicrm_my_entity`.`contact_id`
    FROM `civicrm_my_entity`
    WHERE 1";
    if (isset($this->formValues['title']) && !empty($this->formValues['title'])) {
      $sql .= " AND `civicrm_my_entity`.`title` LIKE '%".$this->formValues['title']."%'";
    }
    if (isset($this->formValues['contact_id']) && is_array($this->formValues['contact_id']) && count($this->formValues['contact_id'])) {
      $sql  .= " AND `civicrm_my_entity`.`contact_id` IN (".implode(", ", $this->formValues['contact_id']).")";
    }

    if ($this->limit !== false) {
      $sql .= " LIMIT {$this->offset}, {$this->limit}";
    }
    $dao = CRM_Core_DAO::executeQuery($sql);
    $this->count = CRM_Core_DAO::singleValueQuery("SELECT FOUND_ROWS()");
    $this->rows = array();
    while($dao->fetch()) {
      $row = [
        'id' => $dao->id,
        'contact_id' => $dao->contact_id,
        'title' => $dao->title,
      ];
      if (!empty($row['contact_id'])) {
        $row['contact'] = '<a href="'.CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $dao->contact_id]).'">'.CRM_Contact_BAO_Contact::displayName($dao->contact_id).'</a>';
      }
      $this->rows[] = $row;
    }
  }
}

In the code above the function buildQuickForm adds the fields and operators to the search form. We allow the user to search on the Contact ID and on the Title.

In the code the function Query is used to query the database and build the result set which is stored in $this->rows.

We call the Query function in the preProcess function. We also add a pager in the preProcess function.

The template of this form can be found in templates/CRM/Myentity/Form/Search.tpl and looks like:

{crmScope extensionKey='myentity'}
  <div class="crm-content-block">

    <div class="crm-block crm-form-block crm-basic-criteria-form-block">
      <div class="crm-accordion-wrapper crm-expenses_search-accordion collapsed">
        <div class="crm-accordion-header crm-master-accordion-header">{ts}Search My Entities{/ts}</div><!-- /.crm-accordion-header -->
        <div class="crm-accordion-body">
          <table class="form-layout">
            <tbody>
            <tr>
              <td class="label">{$form.contact_id.label}</td>
              <td>{$form.contact_id.html}</td>
            </tr>
            <tr>
              <td class="label">{$form.title.label}</td>
              <td>{$form.title.html}</td>
            </tr>
            </tbody>
          </table>
          <div class="crm-submit-buttons">
            {include file="CRM/common/formButtons.tpl"}
          </div>
        </div><!- /.crm-accordion-body -->
      </div><!-- /.crm-accordion-wrapper -->
    </div><!-- /.crm-form-block -->


    <div class="action-link">
      <a class="button" href="{crmURL p="civicrm/myentity/form" q="reset=1&action=add" }">
        <i class="crm-i fa-plus-circle">&nbsp;</i>
        {ts}Add my entity{/ts}
      </a>
    </div>

    <div class="clear"></div>

    <div class="crm-results-block">
      {include file="CRM/common/pager.tpl" location="top"}

      <div class="crm-search-results">
        <table class="selector row-highlight">
          <thead class="sticky">
          <tr>
            <th scope="col">
              {ts}ID{/ts}
            </th>
            <th scope="col">
              {ts}Contact{/ts}
            </th>
            <th scope="col">
              {ts}Title{/ts}
            </th>
            <th>&nbsp;</th>
          </tr>
          </thead>
          {foreach from=$entities item=row}
            <tr>
              <td>{$row.id}</td>
              <td>{$row.contact}</td>
              <td>{$row.title}</td>
              <td class="right nowrap">
                  <span>
                    <a class="action-item crm-hover-button" href="{crmURL p='civicrm/myentity/form' q="id=$row.id&action=update"}"><i class="crm-i fa-pencil"></i>&nbsp;{ts}Edit{/ts}</a>
                    <a class="action-item crm-hover-button" href="{crmURL p='civicrm/myentity/form' q="id=$row.id&action=delete"}"><i class="crm-i fa-trash"></i>&nbsp;{ts}Delete{/ts}</a>
                  </span>
              </td>
            </tr>
          {/foreach}
        </table>

      </div>

      {include file="CRM/common/pager.tpl" location="bottom"}
    </div>
  </div>
{/crmScope}

The template also contains a link to add a new entity to the system.

9.1. Add navigation

Open myentity.php and add the following code:

/**
 * Implements hook_civicrm_navigationMenu().
 *
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_navigationMenu
 */
function myentity_civicrm_navigationMenu(&$menu) {
  _myentity_civix_insert_navigation_menu($menu, 'Search', array(
    'label' => E::ts('Search My Entities'),
    'name' => 'search_my_entity',
    'url' => 'civicrm/myentity/search',
    'permission' => 'access CiviCRM',
    'operator' => 'OR',
    'separator' => 0,
  ));
  _myentity_civix_navigationMenu($menu);
}

The code above adds a navigation item with a label, name, url and permission to the civicrm navigation.

10. Add a tab on contact summary

The tab on the contact summary screen will list all entities linked to the contact.

You can create a tab by first creating a page with civix.

civix generate:page ContactTab civicrm/myentity/contacttab

Edit the file CRM/Page/ContactTab.php and make sure it looks as follows:

<?php
use CRM_Myentity_ExtensionUtil as E;

class CRM_Myentity_Page_ContactTab extends CRM_Core_Page {

  public function run() {
    // Example: Set the page-title dynamically; alternatively, declare a static title in xml/Menu/*.xml
    CRM_Utils_System::setTitle(E::ts('My Entity'));
    $contactId = CRM_Utils_Request::retrieve('cid', 'Positive', $this, TRUE);
    $myEntities = \Civi\Api4\MyEntity::get()
      ->select('*')
      ->addWhere('contact_id', '=', $contactId)
      ->execute();
    $rows = array();
    foreach($myEntities as $myEntity) {
      $row = $myEntity;
      if (!empty($row['contact_id'])) {
        $row['contact'] = '<a href="'.CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $row['contact_id']]).'">'.CRM_Contact_BAO_Contact::displayName($row['contact_id']).'</a>';
      }
      $rows[] = $row;
    }
    $this->assign('contactId', $contactId);
    $this->assign('rows', $rows);

    // Set the user context
    $session = CRM_Core_Session::singleton();
    $userContext = CRM_Utils_System::url('civicrm/contact/view', 'cid='.$contactId.'&selectedChild=contact_my_entity&reset=1');
    $session->pushUserContext($userContext);

    parent::run();
  }

}

Edit the template templates/CRM/Myentity/Page/ContactTab.tpl and make sure it looks like:

{crmScope extensionKey='myentity'}
  <div class="crm-content-block">
    <div class="action-link">
      <a class="button" href="{crmURL p="civicrm/myentity/form" q="reset=1&action=add" }">
        <i class="crm-i fa-plus-circle">&nbsp;</i>
        {ts}Add my entity{/ts}
      </a>
    </div>

    <div class="clear"></div>

    <div class="crm-results-block">
      {include file="CRM/common/pager.tpl" location="top"}

      <div class="crm-search-results">
        <table class="selector row-highlight">
          <thead class="sticky">
          <tr>
            <th scope="col">
              {ts}ID{/ts}
            </th>
            <th scope="col">
              {ts}Contact{/ts}
            </th>
            <th scope="col">
              {ts}Title{/ts}
            </th>
            <th>&nbsp;</th>
          </tr>
          </thead>
          {foreach from=$rows item=row}
            <tr>
              <td>{$row.id}</td>
              <td>{$row.contact}</td>
              <td>{$row.title}</td>
              <td class="right nowrap">
                  <span>
                    <a class="action-item crm-hover-button" href="{crmURL p='civicrm/myentity/form' q="id=`$row.id`&action=update"}"><i class="crm-i fa-pencil"></i>&nbsp;{ts}Edit{/ts}</a>
                    <a class="action-item crm-hover-button" href="{crmURL p='civicrm/myentity/form' q="id=`$row.id`&action=delete"}"><i class="crm-i fa-trash"></i>&nbsp;{ts}Delete{/ts}</a>
                  </span>
              </td>
            </tr>
          {/foreach}
        </table>

      </div>

      {include file="CRM/common/pager.tpl" location="bottom"}
    </div>
  </div>
{/crmScope}

Add the hook hook_civicrm_tabset to myentity.php:

/**
 * Implementation of hook_civicrm_tabset
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tabset
 */
function myentity_civicrm_tabset($path, &$tabs, $context) {
  if ($path === 'civicrm/contact/view') {
    // add a tab to the contact summary screen
    $contactId = $context['contact_id'];
    $url = CRM_Utils_System::url('civicrm/myentity/contacttab', ['cid' => $contactId]);

    $myEntities = \Civi\Api4\MyEntity::get()
      ->selectRowCount()
      ->addWhere('contact_id', '=', $contactId)
      ->execute();

    $tabs[] = array(
      'id' => 'contact_my_entity',
      'url' => $url,
      'count' => $myEntities->count(),
      'title' => E::ts('My Entity'),
      'weight' => 1,
      'icon' => 'crm-i fa-envelope-open',
    );
  }
}

11. Add Subtype field (optional)

If you wish for your entity to be segmented by subtype, similar to many core entities (e.g. Activies are segmented by activity_type_id, Cases by case_type_id, etc.), you must add a column to store that value, plus an option group to hold the possible subtypes. In the following section you'll also see how to allow custom data to be segmented according to this subtype.

Open schema/CRM/Myentity/MyEntity.xml and add the field named something like type_id. This field is linked to an option group with the name my_entity_type.

    <!--
    ...
    -->
    <primaryKey>
        <name>id</name>
        <autoincrement>true</autoincrement>
    </primaryKey>

    <!-- Below the new field for type_id -->
    <field>
        <name>type_id</name>
        <title>Type</title>
        <type>int</type>
        <length>3</length>
        <default>NULL</default>
        <pseudoconstant>
            <optionGroupName>my_entity_type</optionGroupName>
        </pseudoconstant>
        <html>
            <type>Select</type>
        </html>
    </field>

    <field>
    <name>contact_id</name>
    <type>int unsigned</type>
    <comment>FK to Contact</comment>
    </field>
    <!-- ... -->

Run the following command to update the SQL, DAO files:

civix generate:entity-boilerplate

Open sql/auto_install.sql and make sure the create table statement has the ENGINE=InnoDB and the statement looks like this:

CREATE TABLE `civicrm_my_entity` (
    `id` int unsigned NOT NULL AUTO_INCREMENT  COMMENT 'Unique MyEntity ID',
    `type_id` int   DEFAULT NULL ,
    `contact_id` int unsigned    COMMENT 'FK to Contact',
    `title` varchar(255) NULL,
    PRIMARY KEY (`id`),
    CONSTRAINT FK_civicrm_my_entity_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB;

To create the option group, add a .mgd.php file (example from CiviGrant):

<?php
use CRM_Grant_ExtensionUtil as E;

return [
  [
    'name' => 'OptionGroup_grant_type',
    'entity' => 'OptionGroup',
    'cleanup' => 'always',
    'update' => 'unmodified',
    'params' => [
      'version' => 4,
      'values' => [
        'name' => 'grant_type',
        'title' => 'Grant Type',
        'description' => NULL,
        'data_type' => NULL,
        'is_reserved' => TRUE,
        'is_active' => TRUE,
        'is_locked' => FALSE,
      ],
      'match' => ['name'],
    ],
  ],
];

Change grant_type to whatever you used for my_entity_type above.

Enable Managed Entities

If your extension doesn't already support managed entities you need to enable that mixin. Type civix mixin --enable=mgd-php@1.

Open CRM/Myentity/Form/MyEntity.php and change the following:

public function buildQuickForm() {
    // ...
    if ($this->_action != CRM_Core_Action::DELETE) {
      // Add the two lines below:
      $types = CRM_Core_OptionGroup::values('my_entity_type');
      $this->add('select', 'type_id', E::ts('Type'), $types, TRUE, ['class' => 'huge crm-select2', 'data-option-edit-path' => 'civicrm/admin/options/my_entity_type']);

      $this->addEntityRef('contact_id', E::ts('Contact'), [], TRUE);
      // ...
    }
    // ...
}
// ...
public function setDefaultValues() {
  // ...
  if (empty($defaults['type_id'])) {
    $defaults['type_id'] = CRM_Core_OptionGroup::getDefaultValue('expense_type');
  }
  return $defaults;
}
// ...
public function postProcess() {
  // ...
  $params['title'] = $values['title'];
  $params['contact_id'] = $values['contact_id'];
  // Add the line below:
  $params['type_id'] = $values['type_id'];
  // ...
  $result = civicrm_api4('MyEntity', $action, ['values' => $params]);
}

Open templates/CRM/Myentity/Form/MyEntity.tpl and add type id field to the template with the following code:

<div class="crm-section">
  <div class="label">{$form.type_id.label}</div>
  <div class="content">{$form.type_id.html}</div>
  <div class="clear"></div>
</div>

Add a new file under CRM/Myentity/PseudoConstant.php with the following contents:

<?php

class CRM_Myentity_PseudoConstant extends CRM_Core_PseudoConstant {

  public static function myEntityType() {
    $types = CRM_Core_OptionGroup::values('my_entity_type');
    return $types;
  }

}

Now when you uninstall the extension and reinstall it again you have should be able to configure type's for our entity.

The next step is to make it possible to connect custom fields to our entity or to a certain types of our entity.

12. Enabling Custom Data

In this step we make it possible to create custom fields for our entity.

12.1 Making our entity available for custom data

We will make our entity available for custom data by inserting an option value via the Managed Entity system:

The value goes in the custom group cg_extend_objects with the following properties:

  • label: user-readable name of our entity
  • value: the API name of our entity
  • name: the database table name of our entity
  • grouping: (optional) The "subtype" field if this entity supports sub-types (see previous section).

Here is an example from the CiviGrant Extension. This belongs in a file named ./managed/OptionValue_cg_extends_objects.mgd.php (the exact name of the file isn't important as long as it ends in .mgd.php). You should substitute label, value & name as appropriate for your entity:

Supporting multiple entities

If your extension creates multiple entities, you must add one option value per entity that needs Custom Data. You can place all of them in the same .mgd.php file, or break each one to its own file.

<?php
use CRM_Grant_ExtensionUtil as E;

// This enables custom fields for Grant entities
return [
  [
    'name' => 'cg_extend_objects:Grant',
    'entity' => 'OptionValue',
    'cleanup' => 'always',
    'update' => 'always',
    'params' => [
      'version' => 4,
      'values' => [
        'option_group_id.name' => 'cg_extend_objects',
        'label' => E::ts('Grants'),
        'value' => 'Grant',
        'name' => 'civicrm_grant',
        'is_reserved' => TRUE,
        'is_active' => TRUE,
        'grouping' => 'grant_type_id',
      ],
    ],
  ],

Enable Managed Entities

If your extension doesn't already support managed entities you need to enable that mixin. Type civix mixin --enable=mgd-php@1.

12.2 Add custom data to the form

Open CRM/Myentity/Form/MyEntity.php and change the following functions:

public function buildQuickForm(): void {
  if ($this->isSubmitted()) {
      // The custom data fields are added to the form by an ajax form.
      // However, if they are not present in the element index they will
      // not be available from `$this->getSubmittedValue()` in post process.
      // We do not have to set defaults or otherwise render - just add to the element index.
      $this->addCustomDataFieldsToForm('MyEntity', array_filter([
        'id' => $this->_id,
        // potentially pass in another field if your custom data can be filtered by it - e.g
        'type_id' => $this->getSubmittedValue('type_id'),
      ]));
    }

    // If your custom data is filtered by another field on the form then add js to get it to update when that field
    // is updated.

    $this->addSelect('type_id', ['placeholder' => ts('- select type -'), 'onChange' => "CRM.buildCustomData('MyEntity', this.value );"], TRUE);
    // Assign the type_id loaded from the existing record or from the url to ensure that on initial load that custom data
    // subtype is loaded.
    $this->assign('customDataSubType', $typeID ?? NULL);

}
// ...
public function postProcess() {
  // ...
  $params['type_id'] = $values['type_id'];

  // Add the line below:
  $params['custom'] = \CRM_Core_BAO_CustomField::postProcess($this->getSubmittedValues(), $this->getEntityId(), 'MyEntity');

  $result = civicrm_api4('MyEntity', $action, ['values' => $params]);
  // ...

Note you may see this legacy code in use - unless you set a lot of unnecessary properties it will cause notices with PHP 8.2.

public function preProcess() {
  // ...OLD CODE - for reference only
  if (!empty($_POST['hidden_custom'])) {
    $type_id = $this->getSubmitValue('type_id');
    CRM_Custom_Form_CustomData::preProcess($this, NULL, $type_id, 1, 'MyEntity', $this->getEntityId());
    CRM_Custom_Form_CustomData::buildQuickForm($this);
    CRM_Custom_Form_CustomData::setDefaultValues($this);
  }
}

12.3 Ensure the BAO uses standard functions

Open the BAO file for your entity and ensure that it doesn't contain a create or add function, as those would interfere with automatic handling of custom data.

12.4 Add the custom fields to the template

Open templates/CRM/Myentity/Form/MyEntity,tpl and add the following lines:

{* ... *}
</div>

{* Add the line below: *}
{include file="CRM/common/customDataBlock.tpl" customDataType='MyEntity' entityID=$id cid='' groupID=''}

<div class="crm-submit-buttons">
{* ... *}
{* If your custom data is filterable by subType you probably also want a line to referesh the data
if that subype changes. If you added the js through the php later (per the example) you can leave out the lines below *}
{* At the bottom of the file add the following lines: *}
{literal}
<script type="text/javascript">
CRM.$(function($) {
  function updateCustomData() {
    var subType = '{/literal}{$type_id}{literal}';
    if ($('#type_id').length) {
      subType = $('#type_id').val();
    }
    CRM.buildCustomData('MyEntity', subType, false, false, false, false, false, {/literal}{$cid}{literal});
  }
  if ($('#type_id').length) {
    $('#type_id').on('change', updateCustomData);
  }
  updateCustomData();
});
</script>
{/literal}

The javascript code above loads the custom data when the type of our entity is changed.

13. Add a custom permission for our entity

When we want to have a custom permission for our entity we have to declare the permission first, we do that with the hook_civicrm_permission.

Open myentity.php and the following code:

/**
 * Implementation of hook_civicrm_permission
 * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_permission/
 */
function myentity_civicrm_permission(&$permissions) {
  $permissions['manage my entity'] = E::ts('CiviCRM My Entity: Manage my entity');
}

Change the function myentity_civicrm_tabset in myentity.php from:

function myentity_civicrm_tabset($path, &$tabs, $context) {
  if ($path === 'civicrm/contact/view') {

to:

function myentity_civicrm_tabset($path, &$tabs, $context) {
  if ($path === 'civicrm/contact/view' && CRM_Core_Permission::check('manage my entity')) {

Change the function myentity_civicrm_navigationMenu in myentity.php from:

function myentity_civicrm_navigationMenu(&$menu) {
    _myentity_civix_insert_navigation_menu($menu, 'Search', array(
        'label' => E::ts('Search My Entities'),
        'name' => 'search_my_entity',
        'url' => 'civicrm/myentity/search',
        'permission' => 'access CiviCRM', // This line is changed
        'operator' => 'OR',
        'separator' => 0,
    ));
    _myentity_civix_navigationMenu($menu);
}

to:

function myentity_civicrm_navigationMenu(&$menu) {
    _myentity_civix_insert_navigation_menu($menu, 'Search', array(
        'label' => E::ts('Search My Entities'),
        'name' => 'search_my_entity',
        'url' => 'civicrm/myentity/search',
        'permission' => 'manage my entity', // This line is changed
        'operator' => 'OR',
        'separator' => 0,
    ));
    _myentity_civix_navigationMenu($menu);
}

Open xml/Menu/myentity.xml and change all <access_arguments>access CiviCRM</access_arguments> into <access_arguments>manage my entity</access_arguments>.

Open the APIv4 file and add a permissions() function, following this example from CiviGrant:

  public static function permissions() {
    return [
      'get' => [
        'access CiviGrant',
      ],
      'delete' => [
        'delete in CiviGrant',
      ],
      'create' => [
        'edit grants',
      ],
      'update' => [
        'edit grants',
      ],
    ];
  }

Note

Unless otherwise set, the APIv4 permissions will default to 'administer CiviCRM'.

See also