These behave exactly as the Contact entity but with a pre-set value for contact_type.
Includes tests to ensure joins, ACLs and permissions work.
* @throws \CRM_Core_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
- public static function basicTypes($all = FALSE) {
+ public static function basicTypes($all = FALSE): array {
return array_keys(self::basicTypeInfo($all));
}
return $contactTypes;
}
+ /**
+ * Get contact type by name
+ *
+ * @param string $name
+ * @return array|null
+ */
+ public static function getContactType(string $name): ?array {
+ return self::getAllContactTypes()[$name] ?? NULL;
+ }
+
/**
* @param string $entityName
* @param string $action
*
* Generated from xml/schema/CRM/Contact/Contact.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:4066902548c1bc5ba4358b9e0195a3fa)
+ * (GenCodeChecksum:0470d0df786c3ac33a435555a3d026fb)
*/
/**
],
'where' => 'civicrm_contact.contact_type',
'export' => TRUE,
- 'contactType' => NULL,
'table_name' => 'civicrm_contact',
'entity' => 'Contact',
'bao' => 'CRM_Contact_BAO_Contact',
/**
* List all possible values for `CustomGroup.extends`.
*
- * This includes the fake entities "Individual", "Organization", "Household"
- * but not the extra options from `custom_data_type` used on the form ("ParticipantStatus", etc).
+ * This includes the pseudo-entities "Individual", "Organization", "Household".
*
* Returns a mix of hard-coded array and `cg_extend_objects` OptionValues.
* - 'id' return key (maps to `cg_extend_objects.value`).
*/
protected function getRuleGroupNames() {
$rules = [];
- foreach (\CRM_Contact_BAO_ContactType::basicTypes() as $contactType) {
+ $contactTypes = $this->getEntityName() === 'Contact' ? \CRM_Contact_BAO_ContactType::basicTypes() : [$this->getEntityName()];
+ foreach ($contactTypes as $contactType) {
$rules[] = $contactType . '.Unsupervised';
$rules[] = $contactType . '.Supervised';
}
public static function fields(BasicGetFieldsAction $action) {
$fields = [];
$ignore = ['id', 'contact_id', 'is_primary', 'on_hold', 'location_type_id', 'phone_type_id'];
- foreach (['Contact', 'Email', 'Phone', 'Address', 'IM'] as $entity) {
+ foreach ([$action->getEntityName(), 'Email', 'Phone', 'Address', 'IM'] as $entity) {
$entityFields = (array) civicrm_api4($entity, 'getFields', [
'checkPermissions' => FALSE,
'action' => 'create',
'loadOptions' => $action->getLoadOptions(),
'where' => [['name', 'NOT IN', $ignore], ['type', 'IN', ['Field', 'Custom']]],
]);
- if ($entity !== 'Contact') {
+ if ($entity !== $action->getEntityName()) {
$prefix = strtolower($entity) . '_primary.';
foreach ($entityFields as &$field) {
$field['name'] = $prefix . $field['name'];
* @return Action\Contact\Create
*/
public static function create($checkPermissions = TRUE) {
- return (new Action\Contact\Create(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\Create(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\Update
*/
public static function update($checkPermissions = TRUE) {
- return (new Action\Contact\Update(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\Update(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\Save
*/
public static function save($checkPermissions = TRUE) {
- return (new Action\Contact\Save(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\Save(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\Delete
*/
public static function delete($checkPermissions = TRUE) {
- return (new Action\Contact\Delete(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\Delete(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\GetChecksum
*/
public static function getChecksum($checkPermissions = TRUE) {
- return (new Action\Contact\GetChecksum(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\GetChecksum(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\ValidateChecksum
*/
public static function validateChecksum($checkPermissions = TRUE) {
- return (new Action\Contact\ValidateChecksum(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\ValidateChecksum(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\GetDuplicates
*/
public static function getDuplicates($checkPermissions = TRUE) {
- return (new Action\Contact\GetDuplicates(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\GetDuplicates(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Contact\MergeDuplicates
*/
public static function mergeDuplicates($checkPermissions = TRUE) {
- return (new Action\Contact\MergeDuplicates(__CLASS__, __FUNCTION__))
+ return (new Action\Contact\MergeDuplicates(self::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
+ protected static function getDaoName(): string {
+ // Child classes (Individual, Organization, Household) need this.
+ return 'CRM_Contact_DAO_Contact';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getInfo(): array {
+ $info = parent::getInfo();
+ $contactType = static::getEntityName();
+ // Adjust info for child classes (Individual, Organization, Household)
+ if ($contactType !== 'Contact') {
+ $info['icon'] = \CRM_Contact_BAO_ContactType::getContactType($contactType)['icon'] ?? $info['icon'];
+ $info['type'] = ['DAOEntity', 'ContactType'];
+ // This forces the value into get and create api actions
+ $info['where'] = ['contact_type' => $contactType];
+ }
+ return $info;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function permissions() {
+ $permissions = \CRM_Core_Permission::getEntityActionPermissions();
+ return ($permissions['contact'] ?? []) + $permissions['default'];
+ }
+
}
];
}
+ /**
+ * @return \CRM_Core_DAO|string|null
+ */
+ protected static function getDaoName(): ?string {
+ return 'CRM_Core_BAO_CustomValue';
+ }
+
/**
* @see \Civi\Api4\Generic\AbstractEntity::getInfo()
* @return array
'type' => ['CustomValue', 'DAOEntity'],
'searchable' => 'secondary',
'primary_key' => ['id'],
- 'dao' => 'CRM_Core_BAO_CustomValue',
+ 'dao' => self::getDaoName(),
'see' => [
'https://docs.civicrm.org/user/en/latest/organising-your-data/creating-custom-fields/#multiple-record-fieldsets',
'\Civi\Api4\CustomGroup',
'data_type' => 'Array',
'description' => 'Arguments needed by php action factory functions (used when multiple entities share a class, e.g. CustomValue).',
],
+ [
+ 'name' => 'where',
+ 'data_type' => 'Array',
+ 'description' => 'Constant values which will be force-set when reading/writing this entity (e.g. [contact_type => Individual])',
+ ],
[
'name' => 'bridge',
'data_type' => 'Array',
*/
protected static function getEntityTitle(bool $plural = FALSE): string {
$name = static::getEntityName();
- $dao = \CRM_Core_DAO_AllCoreTables::getFullName($name);
+ $dao = self::getDaoName();
return $dao ? $dao::getEntityTitle($plural) : ($plural ? \CRM_Utils_String::pluralize($name) : $name);
}
return $actionObject;
}
+ /**
+ * @return \CRM_Core_DAO|string|null
+ */
+ protected static function getDaoName(): ?string {
+ return \CRM_Core_DAO_AllCoreTables::getFullName(static::getEntityName());
+ }
+
/**
* Reflection function called by Entity::get()
*
'searchable' => 'secondary',
];
// Add info for entities with a corresponding DAO
- $dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']);
+ $dao = static::getDaoName();
if ($dao) {
$info['paths'] = $dao::getEntityPaths();
$info['primary_key'] = $dao::$_primaryKey;
protected function getRecords() {
$fieldsToGet = $this->_itemsToGet('name');
$typesToGet = $this->_itemsToGet('type');
+ // Force-set values supplied by entity definition
+ // e.g. if this is a ContactType pseudo-entity, set `contact_type` value which is used by the following:
+ // @see \Civi\Api4\Service\Spec\Provider\ContactGetSpecProvider
+ // @see \Civi\Api4\Service\Spec\SpecGatherer::addDAOFields
+ $presetValues = CoreUtil::getInfoItem($this->getEntityName(), 'where') ?? [];
+ foreach ($presetValues as $presetField => $presetValue) {
+ $this->addValue($presetField, $presetValue);
+ }
/** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
$gatherer = \Civi::container()->get('spec_gatherer');
$includeCustom = TRUE;
}
}
+ // Values specified by entity definition (e.g. 'Individual', 'Organization', 'Household' pseudo-entities specify `contact_type`)
+ $presetValues = CoreUtil::getInfoItem($this->getEntityName(), 'where') ?? [];
+
$result = [];
$idField = CoreUtil::getIdFieldName($this->getEntityName());
FormattingUtil::formatWriteParams($item, $this->entityFields());
$this->formatCustomParams($item, $entityId);
+ if (!$entityId) {
+ $item = $presetValues + $item;
+ }
+
// Adjust weights for sortable entities
if ($updateWeights) {
$this->updateWeight($item);
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Api4;
+
+/**
+ * Contacts of type Household.
+ *
+ * @inheritDoc
+ * @searchable secondary
+ * @since 5.67
+ * @package Civi\Api4
+ */
+class Household extends Contact {
+
+ protected static function getEntityTitle(bool $plural = FALSE): string {
+ return $plural ? ts('Households') : ts('Household');
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Api4;
+
+/**
+ * Contacts of type Individual.
+ *
+ * @inheritDoc
+ * @since 5.67
+ * @package Civi\Api4
+ */
+class Individual extends Contact {
+
+ protected static function getEntityTitle(bool $plural = FALSE): string {
+ return $plural ? ts('Individuals') : ts('Individual');
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Api4;
+
+/**
+ * Contacts of type Organization.
+ *
+ * @inheritDoc
+ * @since 5.67
+ * @package Civi\Api4
+ */
+class Organization extends Contact {
+
+ protected static function getEntityTitle(bool $plural = FALSE): string {
+ return $plural ? ts('Organizations') : ts('Organization');
+ }
+
+}
// Add ACLs first to avoid redundant subclauses
$this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $this->getEntity(), [], $this->getWhere()));
+ // Add required conditions if specified by entity
+ $requiredConditions = CoreUtil::getInfoItem($this->getEntity(), 'where') ?? [];
+ foreach ($requiredConditions as $requiredField => $requiredValue) {
+ $this->api->addWhere($requiredField, '=', $requiredValue);
+ }
+
// Add explicit joins. Other joins implied by dot notation may be added later
$this->addExplicitJoins();
}
$side = 'LEFT';
$this->api->addWhere("$alias.id", 'IS NULL');
}
+ // Add required conditions if specified by entity
+ $requiredConditions = CoreUtil::getInfoItem($entity, 'where') ?? [];
+ foreach ($requiredConditions as $requiredField => $requiredValue) {
+ $join[] = [$alias . '.' . $requiredField, '=', "'$requiredValue'"];
+ }
// Add all fields from joined entity to spec
$joinEntityGet = \Civi\API\Request::create($entity, 'get', ['version' => 4, 'checkPermissions' => $this->getCheckPermissions()]);
$joinEntityFields = $joinEntityGet->entityFields();
namespace Civi\Api4\Service\Autocomplete;
+use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;
use Civi\Core\HookInterface;
// If the savedSearch includes a contact join, add it to the output and the sort.
foreach ($e->savedSearch['api_params']['join'] ?? [] as $join) {
[$entity, $contactAlias] = explode(' AS ', $join[0]);
- if ($entity === 'Contact') {
+ if (CoreUtil::isContact($entity)) {
array_unshift($e->display['settings']['sort'], ["$contactAlias.sort_name", 'ASC']);
$e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.sort_name] - [subject]";
$e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.sort_name] (" . ts('no subject') . ')';
namespace Civi\Api4\Service\Autocomplete;
+use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;
use Civi\Core\HookInterface;
// If the savedSearch includes a contact join, add it to the output and the sort.
foreach ($e->savedSearch['api_params']['join'] ?? [] as $join) {
[$entity, $contactAlias] = explode(' AS ', $join[0]);
- if ($entity === 'Contact') {
+ if (CoreUtil::isContact($entity)) {
array_unshift($e->display['settings']['sort'], ["$contactAlias.sort_name", 'ASC']);
$e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.sort_name] - [subject]";
$e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.sort_name] (" . ts('no subject') . ')';
namespace Civi\Api4\Service\Autocomplete;
use Civi\API\Event\PrepareEvent;
+use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;
use Civi\Core\HookInterface;
* @param \Civi\Core\Event\GenericHookEvent $e
*/
public static function on_civi_search_defaultDisplay(GenericHookEvent $e) {
- if ($e->display['settings'] || $e->display['type'] !== 'autocomplete' || $e->savedSearch['api_entity'] !== 'Contact') {
+ if ($e->display['settings'] || $e->display['type'] !== 'autocomplete' || !CoreUtil::isContact($e->savedSearch['api_entity'])) {
return;
}
$e->display['settings'] = [
use Civi\Api4\Query\Api4SelectQuery;
use Civi\Api4\Service\Spec\FieldSpec;
use Civi\Api4\Service\Spec\RequestSpec;
+use Civi\Api4\Utils\CoreUtil;
/**
* @service
*/
public function modifySpec(RequestSpec $spec) {
// Groups field
- $field = new FieldSpec('groups', 'Contact', 'Array');
+ $field = new FieldSpec('groups', $spec->getEntity(), 'Array');
$field->setLabel(ts('In Groups'))
->setTitle(ts('Groups'))
->setColumnName('id')
->setOptionsCallback([__CLASS__, 'getGroupList']);
$spec->addFieldSpec($field);
- // The following fields are specific to Individuals, so omit them if
- // `contact_type` value was passed to `getFields` and is not "Individual"
+ // The following fields are specific to Individuals
if (!$spec->getValue('contact_type') || $spec->getValue('contact_type') === 'Individual') {
// Age field
- $field = new FieldSpec('age_years', 'Contact', 'Integer');
+ $field = new FieldSpec('age_years', $spec->getEntity(), 'Integer');
$field->setLabel(ts('Age (years)'))
->setTitle(ts('Age (years)'))
->setColumnName('birth_date')
$spec->addFieldSpec($field);
// Birthday field
- $field = new FieldSpec('next_birthday', 'Contact', 'Integer');
+ $field = new FieldSpec('next_birthday', $spec->getEntity(), 'Integer');
$field->setLabel(ts('Next Birthday in (days)'))
->setTitle(ts('Next Birthday in (days)'))
->setColumnName('birth_date')
foreach ($entities as $entity => $types) {
foreach ($types as $type => $info) {
$name = strtolower($entity) . '_' . $type;
- $field = new FieldSpec($name, 'Contact', 'Integer');
+ $field = new FieldSpec($name, $spec->getEntity(), 'Integer');
$field->setLabel($info['label'])
->setTitle($info['title'])
->setColumnName('id')
* @return bool
*/
public function applies($entity, $action) {
- return $entity === 'Contact' && $action === 'get';
+ // Applies to 'Contact' plus pseudo-entities 'Individual', 'Organization', 'Household'
+ return CoreUtil::isContact($entity) && $action === 'get';
}
/**
$DAOFields = $this->getDAOFields($entityName);
foreach ($DAOFields as $DAOField) {
- if (array_key_exists('contactType', $DAOField) && $spec->getValue('contact_type') && $DAOField['contactType'] != $spec->getValue('contact_type')) {
+ if (isset($DAOField['contactType']) && $spec->getValue('contact_type') && $DAOField['contactType'] !== $spec->getValue('contact_type')) {
continue;
}
if (!empty($DAOField['component']) && !\CRM_Core_Component::isEnabled($DAOField['component'])) {
* @see \CRM_Core_SelectValues::customGroupExtends
*/
private function addCustomFields(string $entity, RequestSpec $spec, bool $checkPermissions) {
+ $values = $spec->getValues();
+
+ // Handle contact type pseudo-entities
+ $contactTypes = \CRM_Contact_BAO_ContactType::basicTypes();
+ // If contact type is given
+ if ($entity === 'Contact' && !empty($values['contact_type'])) {
+ $entity = $values['contact_type'];
+ }
+
$customInfo = \Civi\Api4\Utils\CoreUtil::getCustomGroupExtends($entity);
if (!$customInfo) {
return;
}
- $values = $spec->getValues();
$extends = $customInfo['extends'];
$grouping = $customInfo['grouping'];
+ if ($entity === 'Contact' || in_array($entity, $contactTypes, TRUE)) {
+ $grouping = 'contact_sub_type';
+ }
$query = CustomField::get(FALSE)
->setSelect(['custom_group_id.name', 'custom_group_id.title', '*'])
$query->addWhere('custom_group_id', 'IN', $allowedGroups);
}
- // Contact custom groups are extra complicated because contact_type can be a value for extends
- if ($entity === 'Contact') {
- if (array_key_exists('contact_type', $values)) {
- $extends = ['Contact'];
- if ($values['contact_type']) {
- $extends[] = $values['contact_type'];
- }
- }
- // Now grouping can be treated normally
- $grouping = 'contact_sub_type';
- }
if (is_string($grouping) && array_key_exists($grouping, $values)) {
if (empty($values[$grouping])) {
$query->addWhere('custom_group_id.extends_entity_column_value', 'IS EMPTY');
* auto-completion of static methods
*/
public static function getBAOFromApiName($entityName) {
+ // TODO: It would be nice to just call self::getInfoItem($entityName, 'dao')
+ // but that currently causes test failures, probably due to early-bootstrap issues.
if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) {
- return 'CRM_Core_BAO_CustomValue';
+ $dao = \Civi\Api4\CustomValue::getInfo()['dao'];
+ }
+ else {
+ $dao = AllCoreTables::getFullName($entityName);
+ }
+ if (!$dao && self::isContact($entityName)) {
+ $dao = 'CRM_Contact_DAO_Contact';
}
- $dao = AllCoreTables::getFullName($entityName);
return $dao ? AllCoreTables::getBAOClassName($dao) : NULL;
}
}
/**
- * @param $entityName
+ * @param string $entityName
* @return string|\Civi\Api4\Generic\AbstractEntity
*/
public static function getApiClass($entityName) {
return self::getInfoItem($entityName, 'class');
}
+ /**
+ * Returns TRUE if `entityName` is 'Contact', 'Individual', 'Organization' or 'Household'
+ */
+ public static function isContact(string $entityName): bool {
+ return $entityName === 'Contact' || in_array($entityName, \CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE);
+ }
+
/**
* Get a piece of metadata about an entity
*
* @return array{extends: array, column: string, grouping: mixed}|null
*/
public static function getCustomGroupExtends(string $entityName) {
+ $contactTypes = \CRM_Contact_BAO_ContactType::basicTypes();
// Custom_group.extends pretty much maps 1-1 with entity names, except for Contact.
+ if (in_array($entityName, $contactTypes, TRUE)) {
+ return [
+ 'extends' => ['Contact', $entityName],
+ 'column' => 'id',
+ 'grouping' => ['contact_type', 'contact_sub_type'],
+ ];
+ }
switch ($entityName) {
case 'Contact':
return [
- 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues::contactType())),
+ 'extends' => array_merge(['Contact'], $contactTypes),
'column' => 'id',
'grouping' => ['contact_type', 'contact_sub_type'],
];
use api\v4\Api4TestBase;
use Civi\Api4\Contact;
use Civi\Api4\Email;
+use Civi\Api4\Individual;
use Civi\Api4\LocationType;
+use Civi\Api4\Organization;
use Civi\Core\HookInterface;
use Civi\Test\TransactionalInterface;
use \Civi\Test\ACLPermissionTrait;
+ public function testPermissionInfo(): void {
+ foreach (['Contact', 'Individual', 'Organization', 'Household'] as $entity) {
+ $apiClass = '\Civi\Api4\\' . $entity;
+ $permissions = $apiClass::permissions();
+ $this->assertEquals([], $permissions['get']);
+ $this->assertContains('add contacts', $permissions['create']);
+ $this->assertContains('delete contacts', $permissions['delete']);
+ $this->assertContains('merge duplicate contacts', $permissions['merge']);
+ }
+ }
+
public function testBasicContactPermissions(): void {
$this->createLoggedInUser();
\CRM_Core_Config::singleton()->userPermissionClass->permissions = [
'view all contacts',
];
- $this->createTestRecord('Contact');
+ $this->createTestRecord('Individual');
$result = Contact::get()->execute();
$this->assertGreaterThan(0, $result->count());
$result = Contact::get()->execute();
$this->assertCount(0, $result);
+
+ $result = Individual::get()->execute();
+ $this->assertCount(0, $result);
+
+ $result = Organization::get()->execute();
+ $this->assertCount(0, $result);
}
- public function testContactAclForRelatedEntity() {
- $cid = $this->saveTestRecords('Contact', ['records' => 4])
+ public function testContactAclForRelatedEntity(): void {
+ $cid = $this->saveTestRecords('Individual', ['records' => 4])
->column('id');
$email = $this->saveTestRecords('Email', [
'records' => [
$this->assertEquals(1, substr_count($allowedEmails->debug['sql'][0], 'civicrm_acl_contact_cache'));
}
- public function testContactAclClauseDedupe() {
- $cid = $this->saveTestRecords('Contact', ['records' => 4])
+ public function testContactAclClauseDedupe(): void {
+ $cid = $this->saveTestRecords('Individual', ['records' => 4])
->column('id');
$locationType = $this->createTestRecord('LocationType');
$email = $this->saveTestRecords('Email', [
\CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view debug output'];
\CRM_Utils_Hook::singleton()->setHook('civicrm_aclWhereClause', [$this, 'aclWhereMultipleContacts']);
+ // Should now have access to 3 contacts
+ $this->assertCount(3, Individual::get()->execute());
+
// Acl clause is added only once and shared by the joined entities
$contactGet = Contact::get()->setDebug(TRUE)
->addSelect('email.id')
// ACL clause should have been inserted once
$this->assertEquals(1, substr_count($contactGet->debug['sql'][0], 'civicrm_acl_contact_cache'));
+ // Same should work with Individual api as Contact
+ $contactGet = Individual::get()->setDebug(TRUE)
+ ->addSelect('email.id')
+ ->addJoin('Email AS email', 'LEFT', ['email.contact_id', '=', 'id'])
+ ->execute();
+ $this->assertCount(3, $contactGet);
+ $this->assertEquals(array_slice($email, 1), $contactGet->column('email.id'));
+ // ACL clause should have been inserted once
+ $this->assertEquals(1, substr_count($contactGet->debug['sql'][0], 'civicrm_acl_contact_cache'));
+
// Joining through another entity does not allow acl bypass
$locationTypeGet = LocationType::get()->setDebug(TRUE)
->addSelect('email.id')
use api\v4\Api4TestBase;
use Civi\Api4\Contact;
+use Civi\Api4\Email;
use Civi\Api4\Relationship;
use Civi\Test\TransactionalInterface;
$this->assertCount(0, $resultCount);
}
+ public function testContactPseudoEntityGet(): void {
+ $allCids = [];
+ $cids = [];
+ // Create a different number of contacts of each type
+ $contactTypes = [
+ 'Individual' => 1,
+ 'Organization' => 2,
+ 'Household' => 3,
+ ];
+ foreach ($contactTypes as $contactType => $count) {
+ $saved = $this->saveTestRecords($contactType, ['records' => $count])->column('id');
+ $allCids = array_merge($allCids, $saved);
+ $cids[$contactType] = $saved;
+ }
+ $getAll = Contact::get(FALSE)
+ ->addWhere('id', 'IN', $allCids)
+ ->execute();
+ $this->assertCount(6, $getAll);
+ // Each pseudo-entity will only return contacts of that type
+ foreach ($contactTypes as $contactType => $count) {
+ $get[$contactType] = civicrm_api4($contactType, 'get', [
+ 'where' => [['id', 'IN', $allCids]],
+ 'debug' => TRUE,
+ ]);
+ $this->assertStringContainsString("`contact_type` = \"$contactType\"", $get[$contactType]->debug['sql'][0]);
+ $this->assertCount($count, $get[$contactType]);
+ }
+ // Ensure fields are returned appropriate to contact type: Individual
+ $this->assertArrayHasKey('first_name', $get['Individual'][0]);
+ $this->assertArrayNotHasKey('organization_name', $get['Individual'][0]);
+ $this->assertArrayNotHasKey('household_name', $get['Individual'][0]);
+ // Ensure fields are returned appropriate to contact type: Organization
+ $this->assertArrayNotHasKey('first_name', $get['Organization'][0]);
+ $this->assertArrayHasKey('organization_name', $get['Organization'][0]);
+ $this->assertArrayNotHasKey('household_name', $get['Organization'][0]);
+ // Ensure fields are returned appropriate to contact type: Household
+ $this->assertArrayNotHasKey('first_name', $get['Household'][0]);
+ $this->assertArrayNotHasKey('organization_name', $get['Household'][0]);
+ $this->assertArrayHasKey('household_name', $get['Household'][0]);
+
+ // Ensure contact type condition is added to the ON clause
+ foreach ($allCids as $cid) {
+ $emails[] = $this->createTestRecord('Email', ['contact_id' => $cid])['id'];
+ }
+ $getAll = Email::get(FALSE)
+ ->addWhere('id', 'IN', $emails)
+ ->addJoin('Contact AS contact', 'INNER', ['contact_id', '=', 'contact.id'])
+ ->execute();
+ $this->assertCount(6, $getAll);
+ foreach ($contactTypes as $contactType => $count) {
+ $get = Email::get(FALSE)
+ ->addWhere('id', 'IN', $emails)
+ ->addJoin("$contactType AS contact", 'INNER', ['contact_id', '=', 'contact.id'])
+ ->setDebug(TRUE)
+ ->execute();
+ $this->assertStringContainsString("`contact`.`contact_type` = \"$contactType\"", $get->debug['sql'][0]);
+ $this->assertCount($count, $get);
+ }
+ }
+
}
*/
public function testIsDeletedPermission(): void {
$contact = $this->createLoggedInUser();
+ $this->createTestRecord('Individual', ['first_name' => 'phoney']);
\CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view all contacts'];
$originalQuery = civicrm_api4('Contact', 'get', [
'checkPermissions' => TRUE,
'select' => ['id', 'display_name', 'is_deleted'],
'where' => [['first_name', '=', 'phoney']],
]);
+ $this->assertGreaterThan(0, $originalQuery->countFetched());
- try {
- $isDeletedQuery = civicrm_api4('Contact', 'get', [
- 'checkPermissions' => TRUE,
- 'select' => ['id', 'display_name'],
- 'where' => [['first_name', '=', 'phoney'], ['is_deleted', '=', 0]],
- ]);
- $this->assertEquals(count($originalQuery), count($isDeletedQuery));
- }
- catch (\CRM_Core_Exception $e) {
- $this->fail('An Exception Should not have been raised');
- }
- try {
- $isDeletedJoinTest = civicrm_api4('Email', 'get', [
- 'checkPermissions' => TRUE,
- 'where' => [['contact_id.first_name', '=', 'phoney'], ['contact_id.is_deleted', '=', 0]],
- ]);
- }
- catch (\CRM_Core_Exception $e) {
- $this->fail('An Exception Should not have been raised');
- }
+ $isDeletedQuery = civicrm_api4('Contact', 'get', [
+ 'checkPermissions' => TRUE,
+ 'select' => ['id', 'display_name'],
+ 'where' => [['first_name', '=', 'phoney'], ['is_deleted', '=', 0]],
+ ]);
+ $this->assertEquals(count($originalQuery), count($isDeletedQuery));
+
+ $isDeletedQuery = civicrm_api4('Individual', 'get', [
+ 'checkPermissions' => TRUE,
+ 'select' => ['id', 'display_name'],
+ 'where' => [['first_name', '=', 'phoney'], ['is_deleted', '=', 0]],
+ ]);
+ $this->assertEquals(count($originalQuery), count($isDeletedQuery));
+
+ civicrm_api4('Email', 'get', [
+ 'checkPermissions' => TRUE,
+ 'where' => [['contact_id.first_name', '=', 'phoney'], ['contact_id.is_deleted', '=', 0]],
+ ]);
}
}
use Civi\Api4\Activity;
use Civi\Api4\Address;
use Civi\Api4\Contact;
+use Civi\Api4\Household;
use Civi\Api4\Tag;
/**
class GetExtraFieldsTest extends Api4TestBase {
public function testGetFieldsByContactType(): void {
- $getFields = Contact::getFields(FALSE)->addSelect('name')->addWhere('type', '=', 'Field');
+ $getFields = Contact::getFields(FALSE)->addWhere('type', '=', 'Field');
$baseFields = array_column(\CRM_Contact_BAO_Contact::fields(), 'name');
$returnedFields = $getFields->execute()->column('name');
// With no contact_type specified, all fields should be returned
$this->assertEmpty($notReturned);
- $individualFields = $getFields->setValues(['contact_type' => 'Individual'])->execute()->column('name');
- $this->assertNotContains('sic_code', $individualFields);
- $this->assertNotContains('contact_type', $individualFields);
- $this->assertContains('first_name', $individualFields);
+ $individualFields = (array) $getFields->setValues(['contact_type' => 'Individual'])->execute()->indexBy('name');
+ $this->assertArrayNotHasKey('sic_code', $individualFields);
+ $this->assertTrue($individualFields['contact_type']['readonly']);
+ $this->assertArrayHasKey('first_name', $individualFields);
$orgId = Contact::create(FALSE)->addValue('contact_type', 'Organization')->execute()->first()['id'];
- $organizationFields = $getFields->setValues(['id' => $orgId])->execute()->column('name');
- $this->assertContains('organization_name', $organizationFields);
- $this->assertContains('sic_code', $organizationFields);
- $this->assertNotContains('contact_type', $organizationFields);
- $this->assertNotContains('first_name', $organizationFields);
- $this->assertNotContains('household_name', $organizationFields);
-
- $hhId = Contact::create(FALSE)->addValue('contact_type', 'Household')->execute()->first()['id'];
- $householdFields = $getFields->setValues(['id' => $hhId])->execute()->column('name');
- $this->assertNotContains('sic_code', $householdFields);
- $this->assertNotContains('contact_type', $householdFields);
- $this->assertNotContains('first_name', $householdFields);
- $this->assertContains('household_name', $householdFields);
+ $organizationFields = (array) $getFields->setValues(['id' => $orgId])->execute()->indexBy('name');
+ $this->assertArrayHasKey('organization_name', $organizationFields);
+ $this->assertArrayHasKey('sic_code', $organizationFields);
+ $this->assertTrue($organizationFields['contact_type']['readonly']);
+ $this->assertArrayNotHasKey('first_name', $organizationFields);
+ $this->assertArrayNotHasKey('household_name', $organizationFields);
+
+ $hhId = Household::create(FALSE)->execute()->first()['id'];
+ $householdFields = (array) $getFields->setValues(['id' => $hhId])->execute()->indexBy('name');
+ $this->assertArrayNotHasKey('sic_code', $householdFields);
+ $this->assertTrue($householdFields['contact_type']['readonly']);
+ $this->assertArrayNotHasKey('first_name', $householdFields);
+ $this->assertArrayHasKey('household_name', $householdFields);
}
public function testGetOptionsAddress(): void {
* @throws \CRM_Core_Exception
*/
public function createLoggedInUser(): int {
- $contactID = $this->createTestRecord('Contact')['id'];
+ $contactID = $this->createTestRecord('Individual')['id'];
UFMatch::delete(FALSE)->addWhere('uf_id', '=', 6)->execute();
$this->createTestRecord('UFMatch', [
'contact_id' => $contactID,
use Civi\Api4\CustomField;
use Civi\Api4\CustomGroup;
use Civi\Api4\Event;
+use Civi\Api4\Individual;
+use Civi\Api4\Organization;
use Civi\Api4\Participant;
/**
->addValue('parent_id:name', 'Individual')
->execute();
- $contact1 = Contact::create(FALSE)
+ $contact1 = Individual::create(FALSE)
->execute()->first();
- $contact2 = Contact::create(FALSE)->addValue('contact_sub_type', [$this->subTypeName])
+ $contact2 = Individual::create(FALSE)->addValue('contact_sub_type', [$this->subTypeName])
->execute()->first();
- $org = Contact::create(FALSE)->addValue('contact_type', 'Organization')
+ $org = Organization::create(FALSE)
->execute()->first();
// Individual sub-type custom group
$this->assertArrayNotHasKey('org_group.sub_field', $fieldsWithSubtype);
$this->assertArrayHasKey('always.on', $fieldsWithSubtype);
+ $fieldsWithSubtype = Individual::getFields(FALSE)
+ ->addValue('id', $contact2['id'])
+ ->execute()->indexBy('name');
+ $this->assertArrayHasKey('contact_sub.sub_field', $fieldsWithSubtype);
+ $this->assertArrayNotHasKey('org_group.sub_field', $fieldsWithSubtype);
+ $this->assertArrayHasKey('always.on', $fieldsWithSubtype);
+
+ $fieldsWithSubtype = Contact::getFields(FALSE)
+ ->addValue('id', $contact2['id'])
+ ->execute()->indexBy('name');
+ $this->assertArrayHasKey('contact_sub.sub_field', $fieldsWithSubtype);
+ $this->assertArrayNotHasKey('org_group.sub_field', $fieldsWithSubtype);
+ $this->assertArrayHasKey('always.on', $fieldsWithSubtype);
+
$fieldsNoSubtype = Contact::getFields(FALSE)
->addValue('id', $contact1['id'])
->execute()->indexBy('name');
$this->assertArrayNotHasKey('org_group.sub_field', $fieldsNoSubtype);
$this->assertArrayHasKey('always.on', $fieldsNoSubtype);
+ $groupFields = Organization::getFields(FALSE)
+ ->addValue('id', $org['id'])
+ ->execute()->indexBy('name');
+ $this->assertArrayNotHasKey('contact_sub.sub_field', $groupFields);
+ $this->assertArrayHasKey('org_group.sub_field', $groupFields);
+ $this->assertArrayHasKey('always.on', $groupFields);
+
$groupFields = Contact::getFields(FALSE)
->addValue('id', $org['id'])
->execute()->indexBy('name');
use Civi\Api4\CustomField;
use Civi\Api4\CustomGroup;
use Civi\Api4\CustomValue;
+use Civi\Api4\Individual;
/**
* @group headless
\CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view all contacts', 'edit all contacts'];
- // Ensure ACLs apply to APIv4 get
+ // Ensure ACLs apply to APIv4 Contact.get
$result = Contact::get()
->addWhere('id', '=', $cid)
->addSelect('custom.*')
$this->assertEquals('456', $result['MyReadOnlySingle.MyField']);
$this->assertArrayNotHasKey('MySuperSecretSingle.MyField', $result);
+ // Ensure ACLs apply to APIv4 Individual.get
+ $result = Individual::get()
+ ->addWhere('id', '=', $cid)
+ ->addSelect('custom.*')
+ ->execute()->single();
+ $this->assertEquals('123', $result['MyReadWriteSingle.MyField']);
+ $this->assertEquals('456', $result['MyReadOnlySingle.MyField']);
+ $this->assertArrayNotHasKey('MySuperSecretSingle.MyField', $result);
+
// Ensure ACLs apply to APIv3 get
$result = civicrm_api3('Contact', 'get', [
'id' => $cid,
$this->assertEquals('456', $result[$v3['single']['readOnly']]);
// Try to update all fields - ACLs will restrict based on write access
- Contact::update()->setValues([
+ Individual::update()->setValues([
'id' => $cid,
'first_name' => 'test1234',
'MyReadWriteSingle.MyField' => '1234',
$this->assertArrayNotHasKey("customGroup.MyField", $row);
}
}
+ // Same check but for Individual entity
+ $result = Individual::get()
+ ->addWhere('id', '=', $cid)
+ ->addJoin("Custom_$groupName AS customGroup")
+ ->addSelect("customGroup.MyField")
+ ->execute();
+ if ($groupName !== 'MySuperSecretMulti') {
+ $this->assertEquals($values, $result->column("customGroup.MyField"));
+ }
+ else {
+ foreach ($result as $row) {
+ $this->assertArrayNotHasKey("customGroup.MyField", $row);
+ }
+ }
try {
CustomValue::create($groupName)
->addValue('MyField', 'new')
use api\v4\Api4TestBase;
use Civi\Api4\ContactType;
use Civi\Api4\Email;
+use Civi\Api4\Individual;
use Civi\Api4\Navigation;
+use Civi\Api4\Organization;
use Civi\Test\TransactionalInterface;
/**
$this->assertEquals('Organization', $result['contact_type']);
}
+ public function testContactTypeWontChange(): void {
+ $hhId = $this->createTestRecord('Household')['id'];
+ $orgId = $this->createTestRecord('Organization')['id'];
+
+ $orgUpdate = Organization::update(FALSE)
+ ->addWhere('id', 'IN', [$hhId, $orgId])
+ ->addValue('organization_name', 'Foo')
+ ->execute();
+ $this->assertCount(1, $orgUpdate);
+
+ $indUpdate = Individual::update(FALSE)
+ ->addWhere('id', 'IN', [$hhId, $orgId])
+ ->addValue('first_name', 'Foo')
+ ->execute();
+ $this->assertCount(0, $indUpdate);
+
+ $orgUpdate = Organization::update(FALSE)
+ ->addWhere('id', '=', $hhId)
+ ->addValue('organization_name', 'Foo')
+ ->execute();
+ // This seems unexpected but is due to the fact that for efficiency the api
+ // will skip lookups and go straight to writeRecord when given a single id.
+ // Commented out assertion doesn't work:
+ // $this->assertCount(0, $orgUpdate);
+
+ $household = Contact::get(FALSE)->addWhere('id', '=', $hhId)->execute()->single();
+
+ $this->assertEquals('Household', $household['contact_type']);
+ $this->assertTrue(empty($household['organization_name']));
+ }
+
}
->indexBy('name');
$this->assertArrayHasKey('Entity', $result, "Entity::get missing itself");
- $this->assertEquals('CRM_Contact_DAO_Contact', $result['Contact']['dao']);
- $this->assertEquals(['DAOEntity'], $result['Contact']['type']);
- $this->assertEquals(['id'], $result['Contact']['primary_key']);
- // Contact icon fields
- $this->assertEquals(['contact_sub_type:icon', 'contact_type:icon'], $result['Contact']['icon_field']);
// Label fields
- $this->assertEquals('display_name', $result['Contact']['label_field']);
$this->assertEquals('title', $result['Event']['label_field']);
// Search fields
$this->assertEquals(['sort_name'], $result['Contact']['search_fields']);
$this->assertEquals(['contact_id.sort_name', 'event_id.title'], $result['Participant']['search_fields']);
}
+ public function testContactPseudoEntityGet(): void {
+ $result = Entity::get(FALSE)
+ ->execute()
+ ->indexBy('name');
+
+ foreach (['Contact', 'Individual', 'Organization', 'Household'] as $contactType) {
+ $this->assertEquals('CRM_Contact_DAO_Contact', $result[$contactType]['dao']);
+ $this->assertContains('DAOEntity', $result[$contactType]['type']);
+ $this->assertEquals('display_name', $result[$contactType]['label_field']);
+ $this->assertEquals(['id'], $result[$contactType]['primary_key']);
+ // Contact icon fields
+ $this->assertEquals(['contact_sub_type:icon', 'contact_type:icon'], $result[$contactType]['icon_field']);
+ }
+
+ foreach (['Individual', 'Organization', 'Household'] as $contactType) {
+ $this->assertContains('ContactType', $result[$contactType]['type']);
+ $this->assertEquals($contactType, $result[$contactType]['where']['contact_type']);
+ }
+
+ $this->assertEquals('Individual', $result['Individual']['title']);
+ $this->assertEquals('Individuals', $result['Individual']['title_plural']);
+ $this->assertEquals('Household', $result['Household']['title']);
+ $this->assertEquals('Households', $result['Household']['title_plural']);
+ $this->assertEquals('Organization', $result['Organization']['title']);
+ $this->assertEquals('Organizations', $result['Organization']['title_plural']);
+ }
+
public function testEntity(): void {
$result = Entity::getActions(FALSE)
->execute()
<readonly>true</readonly>
<add>1.1</add>
<change>3.1</change>
- <contactType>null</contactType>
</field>
<index>
<name>index_contact_type</name>