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.
If you have not yet created your new extension, refer to the civix generate:module
command documentation.
2. Pick a name¶
-
By convention, every entity is named with
CamelCaseName
, starting with a capital letter. Underscores are allowed within the name. -
Also consider that all entity names (including yours) should be unique across all core entities as well as all extension entities (for all installed extensions). Thus in many cases it's best to prefix your entity name with the short name of your extension.
-
For the remainder of this tutorial, we will use
MyEntity
as the name of the entity.
3. Generate the entity files¶
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:
schema/MyEntity.entityType.php
entity declaration and schema definition file.Civi/Api4/MyEntity.php
the APIv4 file.
4. Write your schema¶
- Edit the
MyEntity.entityType.php
file to define all of your entity's fields and other metadata. See documentation on CiviCRM Entity Definition for details.
5. Reinstall extension¶
cv ext:uninstall myextension
cv ext:enable myextension
This will drop and recreate database tables for your entity. Now your entity should be ready to use.
Try it out with cv api4 MyEntity.create
and cv api4 MyEntity.get
. Then add some tests.
Note
You have to repeat this step every time you make changes to the schema in MyEntity.entityType.php
.
6. Add upgrader class¶
You can skip this step for now, but once your extension is released, it's no longer practical to drop and reinstall the tables, you'll need to write an upgrader class to handle future schema changes without deleting data.
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 APIv4 class. This automatically made the entity available in Search Kit.
SearchKit Placement
By default, SearchKit places most entities in the secondary list (behind the "Other..." expander). To promote your entity to the primary list,
or to hide it altogether from SearchKit, adjust the @searchable
annotation in the APIv4 class.
What it did not do is include links to add, edit or delete the entity.
To do this we need to add the links metadata to the entity. Open schema/MyEntity.entityType.php
and add a getPaths
callback
if it's not already there, and populate it with the paths at which your forms can be found.
See Search Kit - Links for more details.
9. Add a search¶
You can create listings and other displays for your entity using Search Kit. Follow documentation to Add a Saved Search in Your Own Extension.
10. Add a tab on contact summary¶
If your entity is linked to contacts, either through a direct foreign key, a dynamic foreign key, or a bridge table, you can use SearchKit to create a display & then expose it to an Afform as a tab on the contact summary screen.
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/MyEntity.entityType.php
and add the field named something like my_entity_type_id
. This field is linked to an option group with the name my_entity_type
.
'my_entity_type_id' => [
'title' => ts('My Type ID'),
'sql_type' => 'int unsigned',
'input_type' => 'Select',
'required' => TRUE,
'description' => ts('Type of my entity.'),
'pseudoconstant' => [
'option_group_name' => 'my_entity_type',
],
],
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>
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 entityvalue
: the API name of our entityname
: the database table name of our entitygrouping
: (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 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');
}
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',
],
];
}
Permission Default
Unless otherwise set, the APIv4 permissions will default to 'administer CiviCRM'
.
See also¶
- Civix
- Schema Definition for an explanation of the schema
- hook_civicrm_tabset for adding a new tab on the contact summary screen.
- hook_civicrm_permission
- The example extension
- Entity Construction Kit extension which provides a user interface for creating an entity.