use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
use Civi\Api4\Utils\FormattingUtil;
use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\ReflectionUtils;
/**
* @method string getLanguage()
*/
protected $language;
+ private $_maxWeights = [];
+
/**
* @return \CRM_Core_DAO|string
*/
}
/**
- * Write bao objects as part of a create/update action.
+ * Write bao objects as part of a create/update/save action.
*
* @param array $items
* The records to write to the DB.
* @throws \API_Exception
* @throws \CRM_Core_Exception
*/
- protected function writeObjects(&$items) {
- $baoName = $this->getBaoName();
-
- // TODO: Opt-in more entities to use the new writeRecords BAO method.
- $functionNames = [
- 'Address' => 'add',
- 'CustomField' => 'writeRecords',
- 'EntityTag' => 'add',
- 'GroupContact' => 'add',
- ];
- $method = $functionNames[$this->getEntityName()] ?? NULL;
- if (!isset($method)) {
- $method = method_exists($baoName, 'create') ? 'create' : (method_exists($baoName, 'add') ? 'add' : 'writeRecords');
+ protected function writeObjects($items) {
+ $updateWeights = FALSE;
+ // Adjust weights for sortable entities
+ if (in_array('SortableEntity', CoreUtil::getInfoItem($this->getEntityName(), 'type'))) {
+ $weightField = CoreUtil::getInfoItem($this->getEntityName(), 'order_by');
+ // Only take action if updating a single record, or if no weights are specified in any record
+ // This avoids messing up a bulk update with multiple recalculations
+ if (count($items) === 1 || !array_filter(array_column($items, $weightField))) {
+ $updateWeights = TRUE;
+ }
}
$result = [];
FormattingUtil::formatWriteParams($item, $this->entityFields());
$this->formatCustomParams($item, $entityId);
- // Skip individual processing if using writeRecords
- if ($method === 'writeRecords') {
- continue;
+ // Adjust weights for sortable entities
+ if ($updateWeights) {
+ $this->updateWeight($item);
}
- $item['check_permissions'] = $this->getCheckPermissions();
- // For some reason the contact bao requires this
- if ($entityId && $this->getEntityName() === 'Contact') {
- $item['contact_id'] = $entityId;
- }
+ $item['check_permissions'] = $this->getCheckPermissions();
+ }
- if ($this->getEntityName() === 'Address') {
- $createResult = $baoName::$method($item, $this->fixAddress);
- }
- else {
- $createResult = $baoName::$method($item);
- }
+ // Ensure array keys start at 0
+ $items = array_values($items);
- if (!$createResult) {
+ foreach ($this->write($items) as $index => $dao) {
+ if (!$dao) {
$errMessage = sprintf('%s write operation failed', $this->getEntityName());
throw new \API_Exception($errMessage);
}
-
- $result[] = $this->baoToArray($createResult, $item);
- \CRM_Utils_API_HTMLInputCoder::singleton()->decodeRows($result);
- }
-
- // Use bulk `writeRecords` method if the BAO doesn't have a create or add method
- // TODO: reverse this from opt-in to opt-out and default to using `writeRecords` for all BAOs
- if ($method === 'writeRecords') {
- $items = array_values($items);
- foreach ($baoName::writeRecords($items) as $i => $createResult) {
- $result[] = $this->baoToArray($createResult, $items[$i]);
- }
+ $result[] = $this->baoToArray($dao, $items[$index]);
}
+ \CRM_Utils_API_HTMLInputCoder::singleton()->decodeRows($result);
FormattingUtil::formatOutputValues($result, $this->entityFields());
return $result;
}
+ /**
+ * Overrideable function to save items using the appropriate BAO function
+ *
+ * @param array[] $items
+ * Items already formatted by self::writeObjects
+ * @return \CRM_Core_DAO[]
+ * Array of saved DAO records
+ */
+ protected function write(array $items) {
+ $saved = [];
+ $baoName = $this->getBaoName();
+
+ $method = method_exists($baoName, 'create') ? 'create' : (method_exists($baoName, 'add') ? 'add' : NULL);
+ // Use BAO create or add method if not deprecated
+ if ($method && !ReflectionUtils::isMethodDeprecated($baoName, $method)) {
+ foreach ($items as $item) {
+ $saved[] = $baoName::$method($item);
+ }
+ }
+ else {
+ $saved = $baoName::writeRecords($items);
+ }
+ return $saved;
+ }
+
/**
* @inheritDoc
*/
* @param array $record
*/
private function resolveFKValues(array &$record): void {
+ // Resolve domain id first
+ uksort($record, function($a, $b) {
+ return substr($a, 0, 9) == 'domain_id' ? -1 : 1;
+ });
foreach ($record as $key => $value) {
- if (substr_count($key, '.') !== 1) {
+ if (!$value || substr_count($key, '.') !== 1) {
continue;
}
[$fieldName, $fkField] = explode('.', $key);
continue;
}
$fkDao = CoreUtil::getBAOFromApiName($field['fk_entity']);
- $record[$fieldName] = \CRM_Core_DAO::getFieldValue($fkDao, $value, 'id', $fkField);
+ // Constrain search to the domain of the current entity
+ $domainConstraint = NULL;
+ if (isset($fkDao::getSupportedFields()['domain_id'])) {
+ if (!empty($record['domain_id'])) {
+ $domainConstraint = $record['domain_id'] === 'current_domain' ? \CRM_Core_Config::domainID() : $record['domain_id'];
+ }
+ elseif (!empty($record['id']) && isset($this->entityFields()['domain_id'])) {
+ $domainConstraint = \CRM_Core_DAO::getFieldValue($this->getBaoName(), $record['id'], 'domain_id');
+ }
+ }
+ if ($domainConstraint) {
+ $fkSearch = new $fkDao();
+ $fkSearch->domain_id = $domainConstraint;
+ $fkSearch->$fkField = $value;
+ $fkSearch->find(TRUE);
+ $record[$fieldName] = $fkSearch->id;
+ }
+ // Simple lookup without all the fuss about domains
+ else {
+ $record[$fieldName] = \CRM_Core_DAO::getFieldValue($fkDao, $value, 'id', $fkField);
+ }
unset($record[$key]);
}
}
continue;
}
- // todo are we sure we don't want to allow setting to NULL? need to test
- if (NULL !== $value) {
+ // Null and empty string are interchangeable as far as the custom bao understands
+ if (NULL === $value) {
+ $value = '';
+ }
- if ($field['suffix']) {
- $options = FormattingUtil::getPseudoconstantList($field, $name, $params, $this->getActionName());
- $value = FormattingUtil::replacePseudoconstant($options, $value, TRUE);
- }
+ if ($field['suffix']) {
+ $options = FormattingUtil::getPseudoconstantList($field, $name, $params, $this->getActionName());
+ $value = FormattingUtil::replacePseudoconstant($options, $value, TRUE);
+ }
- if ($field['html_type'] === 'CheckBox') {
- // this function should be part of a class
- formatCheckBoxField($value, 'custom_' . $field['id'], $this->getEntityName());
- }
+ if ($field['html_type'] === 'CheckBox') {
+ // this function should be part of a class
+ formatCheckBoxField($value, 'custom_' . $field['id'], $this->getEntityName());
+ }
- // Match contact id to strings like "user_contact_id"
- // FIXME handle arrays for multi-value contact reference fields, etc.
- if ($field['data_type'] === 'ContactReference' && is_string($value) && !is_numeric($value)) {
- // FIXME decouple from v3 API
- require_once 'api/v3/utils.php';
- $value = \_civicrm_api3_resolve_contactID($value);
- if ('unknown-user' === $value) {
- throw new \API_Exception("\"{$field['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $field['name'], "type" => "integer"]);
- }
+ // Match contact id to strings like "user_contact_id"
+ // FIXME handle arrays for multi-value contact reference fields, etc.
+ if ($field['data_type'] === 'ContactReference' && is_string($value) && !is_numeric($value)) {
+ // FIXME decouple from v3 API
+ require_once 'api/v3/utils.php';
+ $value = \_civicrm_api3_resolve_contactID($value);
+ if ('unknown-user' === $value) {
+ throw new \API_Exception("\"{$field['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $field['name'], "type" => "integer"]);
}
-
- \CRM_Core_BAO_CustomField::formatCustomField(
- $field['id'],
- $customParams,
- $value,
- $field['custom_group_id.extends'],
- // todo check when this is needed
- NULL,
- $entityId,
- FALSE,
- $this->getCheckPermissions(),
- TRUE
- );
}
+
+ \CRM_Core_BAO_CustomField::formatCustomField(
+ $field['id'],
+ $customParams,
+ $value,
+ $field['custom_group_id.extends'],
+ // todo check when this is needed
+ NULL,
+ $entityId,
+ FALSE,
+ $this->getCheckPermissions(),
+ TRUE
+ );
}
$params['custom'] = $customParams ?: NULL;
return isset($info[$fieldName]) ? ['suffix' => $suffix] + $info[$fieldName] : NULL;
}
+ /**
+ * Update weights when inserting or updating a sortable entity
+ * @param array $record
+ * @see SortableEntity
+ */
+ protected function updateWeight(array &$record) {
+ /** @var \CRM_Core_DAO|string $daoName */
+ $daoName = CoreUtil::getInfoItem($this->getEntityName(), 'dao');
+ $weightField = CoreUtil::getInfoItem($this->getEntityName(), 'order_by');
+ $idField = CoreUtil::getIdFieldName($this->getEntityName());
+ // If updating an existing record without changing weight, do nothing
+ if (!isset($record[$weightField]) && !empty($record[$idField])) {
+ return;
+ }
+ $daoFields = $daoName::getSupportedFields();
+ $newWeight = $record[$weightField] ?? NULL;
+ $oldWeight = empty($record[$idField]) ? NULL : \CRM_Core_DAO::getFieldValue($daoName, $record[$idField], $weightField);
+
+ // FIXME: Need a more metadata-ish approach. For now here's a hardcoded list of the fields sortable entities use for grouping.
+ $guesses = ['option_group_id', 'price_set_id', 'price_field_id', 'premiums_id', 'uf_group_id', 'custom_group_id', 'parent_id', 'domain_id'];
+ $filters = [];
+ foreach (array_intersect($guesses, array_keys($daoFields)) as $filter) {
+ $filters[$filter] = $record[$filter] ?? (empty($record[$idField]) ? NULL : \CRM_Core_DAO::getFieldValue($daoName, $record[$idField], $filter));
+ }
+ // Supply default weight for new record
+ if (!isset($record[$weightField]) && empty($record[$idField])) {
+ $record[$weightField] = $this->getMaxWeight($daoName, $filters, $weightField);
+ }
+ else {
+ $record[$weightField] = \CRM_Utils_Weight::updateOtherWeights($daoName, $oldWeight, $newWeight, $filters, $weightField);
+ }
+ }
+
+ /**
+ * Looks up max weight for a set of sortable entities
+ *
+ * Keeps it in memory in case this operation is writing more than one record
+ *
+ * @param $daoName
+ * @param $filters
+ * @param $weightField
+ * @return int|mixed
+ */
+ private function getMaxWeight($daoName, $filters, $weightField) {
+ $key = $daoName . json_encode($filters);
+ if (!isset($this->_maxWeights[$key])) {
+ $this->_maxWeights[$key] = \CRM_Utils_Weight::getMax($daoName, $filters, $weightField) + 1;
+ }
+ else {
+ ++$this->_maxWeights[$key];
+ }
+ return $this->_maxWeights[$key];
+ }
+
}