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.

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 --api-version=3,4 MyEntity

This will add the following files:

  • api/v3/MyEntity.php the api v3 file
  • 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 should look like this:

<?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>
    <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>
    <required>false</required>
  </field>

</table>

4. Generate SQL, DAO and BAO files

In this step we will generate the SQL, DAO and BAO files. Those will be generated for you by Civix. we only need to change the SQL file afterwards.

Run the following command to generate the SQL and DAO files.

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.

7. Add a form

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}

Note that the preferred way to add search capability is via search kit

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.

8.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.

9. 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',
    );
  }
}

10. Add Type ID field

In this step we are going to add functionality to have certain types of our entity. We will also make it possible to attach custom fields to our entity.

Open schema/CRM/Myentity/MyEntity.xml and add the field for 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;

Open CRM/Myentity/Upgrader.php and add the following to the install and uninstall functions:

<?php
use CRM_Myentity_ExtensionUtil as E;

/**
 * Collection of upgrade steps.
 */
class CRM_Myentity_Upgrader extends CRM_Myentity_Upgrader_Base {


  public function install() {
    // Create the my_entity_type option group
    $typeOptionGroupId = civicrm_api3('OptionGroup', 'create', ['name' => 'my_entity_type', 'title' => E::ts('My Entity Type')]);
    $typeOptionGroupId = $typeOptionGroupId['id'];
    civicrm_api3('OptionValue', 'create', ['value' => 1, 'is_default' => '1', 'name' => 'General', 'label' => E::ts('General'), 'option_group_id' => $typeOptionGroupId]);
  }

  public function uninstall() {
    try {
      $optionGroupId = civicrm_api3('OptionGroup', 'getvalue', ['return' => 'id', 'name' => 'my_entity_type']);
      $optionValues = civicrm_api3('OptionValue', 'get', ['option_group_id' => $optionGroupId, 'options' => ['limit' => 0]]);
      foreach ($optionValues['values'] as $optionValue) {
        civicrm_api3('OptionValue', 'delete', ['id' => $optionValue['id']]);
      }
      civicrm_api3('OptionGroup', 'delete', ['id' => $optionGroupId]);
    } catch (\CiviCRM_API3_Exception $ex) {
      // Ignore exception.
    }
  }

}

This will create the option group for My Entity Type and it will add one option: General.

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.

11. Attaching Custom Data functionality

In this step we are going to attach the custom data functionality to our entity making it possible to create custom fields for our entity.

11.1 Making our entity available for custom data

We will make our entity available for custom data in the installer function of the upgrader class. What is needed is that we add our entity to the custom group cg_extend_objects: * Name: the database table name of our entity * Value: the name of our entity * Description: a callback to return the subtypes. We will also remove our entity upon uninstallation.

Open CRM/Myentity/Upgrader.php and add the following to the install and uninstall function:

public function install() {
    civicrm_api3('OptionValue', 'create', [
      'option_group_id' => "cg_extend_objects",
      'label' => E::ts('My Entity'),
      'value' => 'MyEntity',
      'name' => 'civicrm_my_entity',
      'description' => 'CRM_Myentity_PseudoConstant::myEntityType;',
    ]);
    // ....
}

public function uninstall() {
    // ...
    // Remove all custom data attached to our entity.
    try {
      $customGroups = civicrm_api3('CustomGroup', 'get', [
        'extends' => 'MyEntity',
        'options' => ['limit' => 0],
      ]);
      foreach($customGroups['values'] as $customGroup) {
        $customFields = civicrm_api3('CustomField', 'get', [
          'custom_group_id' => $customGroup['id'],
          'options' => ['limit' => 0],
        ]);
        foreach($customFields['values'] as $customField) {
          civicrm_api3('CustomField', 'delete', ['id' => $customField['id']]);
        }
        civicrm_api3('CustomGroup', 'delete', ['id' => $customGroup['id']]);
      }
      // Remove our entity from the cg_extend_objects option group.
      $cgExtendOptionId = civicrm_api3('OptionValue', 'getvalue', [
        'option_group_id' => "cg_extend_objects",
        'value' => 'MyEntity',
        'return' => 'id',
      ]);
      civicrm_api3('OptionValue', 'delete', ['id' => $cgExtendOptionId]);

    } catch (\CiviCRM_API3_Exception $ex) {
      // Ignore exception
    }
}

11.2 Add custom data to the form

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

public function preProcess() {
  // ...
  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);
  }
}
// ...
public function postProcess() {
  // ...
  $params['type_id'] = $values['type_id'];

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

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

11.3 Add a create function to the BAO

Now we have to create our own create function in the BAO in this function we add the handling of the custom fields.

Open CRM/Myentity/BAO/MyEntity.php and make sure it looks as follows:

<?php
use CRM_Myentity_ExtensionUtil as E;

class CRM_Myentity_BAO_MyEntity extends CRM_Myentity_DAO_MyEntity {

  /**
   * Create a new MyEntity based on array-data
   *
   * @param array $params key-value pairs
   * @return CRM_Myentity_DAO_MyEntity|NULL
   */
  public static function create($params) {
    $className = 'CRM_Myentity_DAO_MyEntity';
    $entityName = 'MyEntity';
    $hook = empty($params['id']) ? 'create' : 'edit';

    CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
    $instance = new $className();
    $instance->copyValues($params);
    $instance->save();

    if (!empty($params['custom']) &&
      is_array($params['custom'])
    ) {
      CRM_Core_BAO_CustomValueTable::store($params['custom'], self::$_tableName, $instance->id);
    }

    CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);

    return $instance;
  }

}

11.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}

<div class="crm-submit-buttons">
{* ... *} 

{* 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.

12. 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>.

See also