public function getTargetEntities(): array {
$targetEntities = [];
$bao = CRM_Core_DAO_AllCoreTables::getClassForTable($this->refTable);
- $targetTables = (array) $bao::buildOptions($this->refTypeColumn);
+ $targetTables = $bao::buildOptions($this->refTypeColumn) ?: [];
foreach ($targetTables as $table => $label) {
$targetEntities[$table] = CRM_Core_DAO_AllCoreTables::getEntityNameForTable($table);
}
public function fields() {
$fields = parent::fields();
+ $fields[] = [
+ 'name' => 'dfk_entities',
+ 'description' => 'List of possible entity types this field could be referencing.',
+ 'data_type' => 'Array',
+ ];
$fields[] = [
'name' => 'help_pre',
'data_type' => 'String',
/**
* @param array $data
- * @param string $entity
+ * @param string $entityName
*
* @return FieldSpec
*/
- public static function arrayToField(array $data, $entity) {
+ public static function arrayToField(array $data, string $entityName): FieldSpec {
$dataTypeName = self::getDataType($data);
$hasDefault = isset($data['default']) && $data['default'] !== '';
// Custom field
if (!empty($data['custom_group_id'])) {
- $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName);
- if (strpos($entity, 'Custom_') !== 0) {
+ $field = new CustomFieldSpec($data['name'], $entityName, $dataTypeName);
+ if (strpos($entityName, 'Custom_') !== 0) {
$field->setName($data['custom_group_id.name'] . '.' . $data['name']);
}
else {
// Core field
else {
$name = $data['name'] ?? NULL;
- $field = new FieldSpec($name, $entity, $dataTypeName);
+ $field = new FieldSpec($name, $entityName, $dataTypeName);
$field->setType('Field');
$field->setColumnName($name);
$field->setNullable(empty($data['required']));
$field->setTitle($data['title'] ?? NULL);
$field->setLabel($data['html']['label'] ?? NULL);
$field->setLocalizable($data['localizable'] ?? FALSE);
+ if (!empty($data['DFKEntities'])) {
+ $field->setDfkEntities(array_values($data['DFKEntities']));
+ }
if (!empty($data['pseudoconstant'])) {
// Do not load options if 'prefetch' is disabled
if (($data['pseudoconstant']['prefetch'] ?? NULL) !== 'disabled') {
$specification = new RequestSpec($entity, $action, $values);
// Real entities
- if (strpos($entity, 'Custom_') !== 0) {
+ if (!str_starts_with($entity, 'Custom_')) {
$this->addDAOFields($entity, $action, $specification, $values);
if ($includeCustom) {
$this->addCustomFields($entity, $specification, $checkPermissions);
/**
* @param \Civi\Api4\Service\Spec\Provider\Generic\SpecProviderInterface $provider
*/
- public function addSpecProvider(SpecProviderInterface $provider) {
+ public function addSpecProvider(SpecProviderInterface $provider): void {
$this->specProviders[] = $provider;
}
/**
- * @param string $entity
+ * @param string $entityName
* @param string $action
* @param \Civi\Api4\Service\Spec\RequestSpec $spec
* @param array $values
*/
- private function addDAOFields($entity, $action, RequestSpec $spec, array $values) {
- $DAOFields = $this->getDAOFields($entity);
+ private function addDAOFields(string $entityName, string $action, RequestSpec $spec, array $values) {
+ $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 ($DAOField['name'] == 'is_active' && empty($DAOField['default'])) {
$DAOField['default'] = '1';
}
- $this->setDynamicFk($DAOField, $entity, $values);
- $field = SpecFormatter::arrayToField($DAOField, $entity);
+ $this->setDynamicFk($DAOField, $values);
+ $field = SpecFormatter::arrayToField($DAOField, $entityName);
$spec->addFieldSpec($field);
}
}
/**
- * Cleverly enables getFields to report dynamic FKs if a value is supplied for the entity type.
+ * Adds metadata about dynamic foreign key fields.
+ *
+ * E.g. some tables have a DFK with a pair of columns named `entity_table` and `entity_id`.
+ * This will gather the list of 'dfk_entities' to add as metadata to the e.g. `entity_id` column.
*
- * E.g. many tables have a DFK with a pair of `entity_table` and `entity_id` columns.
- * If you supply a value for `entity_table`, then getFields will output the correct `fk_entity` for the `entity_id` field.
+ * Additionally, if $values contains a value for e.g. `entity_table`,
+ * then getFields will also output the corresponding `fk_entity` for the `entity_id` field.
*
* @param array $DAOField
- * @param string $entityName
* @param array $values
*/
- private function setDynamicFk(array &$DAOField, string $entityName, array $values): void {
- if (empty($field['FKClassName']) && $values) {
- $bao = CoreUtil::getBAOFromApiName($entityName);
- // Check all dynamic FKs for entity for a match with this field and a supplied value
- foreach ($bao::getReferenceColumns() ?? [] as $reference) {
- if ($reference instanceof \CRM_Core_Reference_Dynamic
- && $reference->getReferenceKey() === $DAOField['name']
- && array_key_exists($reference->getTypeColumn(), $values)
- ) {
- $DAOField['FKClassName'] = \CRM_Core_DAO_AllCoreTables::getClassForTable($values[$reference->getTypeColumn()]);
+ private function setDynamicFk(array &$DAOField, array $values): void {
+ if (empty($DAOField['FKClassName']) && !empty($DAOField['bao']) && $DAOField['type'] == \CRM_Utils_Type::T_INT) {
+ // Check if this field is a key for a dynamic FK
+ foreach ($DAOField['bao']::getReferenceColumns() ?? [] as $reference) {
+ if ($reference instanceof \CRM_Core_Reference_Dynamic && $reference->getReferenceKey() === $DAOField['name']) {
+ $entityTableColumn = $reference->getTypeColumn();
+ $DAOField['DFKEntities'] = $reference->getTargetEntities();
+ $DAOField['html']['controlField'] = $entityTableColumn;
+ // If we have a value for entity_table then this field can pretend to be a single FK too.
+ if (array_key_exists($entityTableColumn, $values)) {
+ $DAOField['FKClassName'] = \CRM_Core_DAO_AllCoreTables::getClassForTable($values[$entityTableColumn]);
+ }
break;
}
}
*/
public $fkEntity;
+ /**
+ * @var string
+ */
+ public $dfkEntities;
+
/**
* Aliases for the valid data types
*
return $this;
}
+ /**
+ * @return string
+ */
+ public function getDfkEntities() {
+ return $this->dfkEntities;
+ }
+
+ /**
+ * @param string $dfkEntities
+ *
+ * @return $this
+ */
+ public function setDfkEntities($dfkEntities) {
+ $this->dfkEntities = $dfkEntities;
+ return $this;
+ }
+
/**
* @return int
*/
!isset($values[$fieldName]) &&
($field['required'] || AbstractAction::evaluateCondition($field['required_if'], $values + $extraValues))
) {
- $extraValues[$fieldName] = $this->getRequiredValue($field);
+ $extraValues[$fieldName] = $this->getRequiredValue($field, $requiredFields);
}
}
if (!empty($field['fk_entity'])) {
return $this->getFkID($field['fk_entity']);
}
+ if (!empty($field['dfk_entities'])) {
+ return $this->getFkID($field['dfk_entities'][0]);
+ }
if (isset($field['default_value'])) {
return $field['default_value'];
}
- if ($field['name'] === 'contact_id') {
+ if ($field['name'] === 'contact_id' || $field['name'] === 'entity_id') {
+ // Obviously an FK field, but if we get here it's missing FK metadata :(
+ // FIXME: This is what we SHOULD do here...
+ // throw new \CRM_Core_Exception($field['name'] . ' should have foreign key information defined.');
+ // ... instead this is how it was done, so we're stuck with it until FK metadata for every field gets fixed
return $this->getFkID('Contact');
}
- if ($field['name'] === 'entity_id') {
- // What could possibly go wrong with this?
- switch ($field['table_name'] ?? NULL) {
- case 'civicrm_financial_item':
- return $this->getFkID(FinancialItemCreationSpecProvider::DEFAULT_ENTITY);
-
- default:
- return $this->getFkID('Contact');
- }
- }
// If there are no options but the field is supposed to have them, we may need to
// create a new option
if (!empty($field['suffixes']) && !empty($field['table_name'])) {
*/
class FinancialItemCreationSpecProvider extends \Civi\Core\Service\AutoService implements Generic\SpecProviderInterface {
- // I'm not sure it makes sense to have a default `entity_table`... actually, I don't even know if it makes
- // sense to expose `FinancialItem` as a public API, for what that's worth. But it's there, so clearly it does.
- // And the ConformanceTests require that you be able to create (and read-back) a record using metadata.
-
- const DEFAULT_TABLE = 'civicrm_line_item';
- const DEFAULT_ENTITY = 'LineItem';
-
/**
* @param \Civi\Api4\Service\Spec\RequestSpec $spec
*/
public function modifySpec(RequestSpec $spec) {
+ // TODO: These fields ought to be required in the schema.
$spec->getFieldByName('entity_table')->setRequired(TRUE);
$spec->getFieldByName('entity_id')->setRequired(TRUE);
- $spec->getFieldByName('entity_table')->setDefaultValue(self::DEFAULT_TABLE);
}
/**
$tagFields = EntityTag::getFields(FALSE)
->execute()->indexBy('name');
$this->assertEmpty($tagFields['entity_id']['fk_entity']);
+ $this->assertContains('Activity', $tagFields['entity_id']['dfk_entities']);
+ $this->assertEquals('entity_table', $tagFields['entity_id']['input_attrs']['control_field']);
$tagFields = EntityTag::getFields(FALSE)
->addValue('entity_table', 'civicrm_activity')
->execute()->indexBy('name');
+ // fk_entity should be specific to specified entity_table, but dfk_entities should still contain all values
$this->assertEquals('Activity', $tagFields['entity_id']['fk_entity']);
+ $this->assertContains('Contact', $tagFields['entity_id']['dfk_entities']);
$tagFields = EntityTag::getFields(FALSE)
->addValue('entity_table:name', 'Contact')
->execute()->indexBy('name');
$this->assertEquals('Contact', $tagFields['entity_id']['fk_entity']);
+ $this->assertContains('SavedSearch', $tagFields['entity_id']['dfk_entities']);
}
public function testFiltersAreReturned(): void {