4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
13 namespace Civi\Api4\Generic\Traits
;
15 use Civi\Api4\CustomField
;
16 use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable
;
17 use Civi\Api4\Utils\FormattingUtil
;
18 use Civi\Api4\Utils\CoreUtil
;
21 * @method string getLanguage()
22 * @method $this setLanguage(string $language)
24 trait DAOActionTrait
{
27 * Specify the language to use if this is a multi-lingual environment.
29 * E.g. "en_US" or "fr_CA"
36 * @return \CRM_Core_DAO|string
38 protected function getBaoName() {
39 return CoreUtil
::getBAOFromApiName($this->getEntityName());
43 * Convert saved object to array
45 * Used by create, update & save actions
47 * @param \CRM_Core_DAO $bao
51 public function baoToArray($bao, $input) {
52 $allFields = array_column($bao->fields(), 'name');
53 if (!empty($this->reload
)) {
54 $inputFields = $allFields;
58 $inputFields = array_keys($input);
59 // Convert 'null' input to true null
60 foreach ($input as $key => $val) {
61 if ($val === 'null') {
67 foreach ($allFields as $field) {
68 if (isset($bao->$field) ||
in_array($field, $inputFields)) {
69 $values[$field] = $bao->$field ??
NULL;
76 * Fill field defaults which were declared by the api.
78 * Note: default values from core are ignored because the BAO or database layer will supply them.
80 * @param array $params
82 protected function fillDefaults(&$params) {
83 $fields = $this->entityFields();
84 $bao = $this->getBaoName();
85 $coreFields = array_column($bao::fields(), NULL, 'name');
87 foreach ($fields as $name => $field) {
88 // If a default value in the api field is different than in core, the api should override it.
89 if (!isset($params[$name]) && !empty($field['default_value']) && $field['default_value'] != \CRM_Utils_Array
::pathGet($coreFields, [$name, 'default'])) {
90 $params[$name] = $field['default_value'];
96 * Write bao objects as part of a create/update action.
99 * The records to write to the DB.
102 * The records after being written to the DB (e.g. including newly assigned "id").
103 * @throws \API_Exception
104 * @throws \CRM_Core_Exception
106 protected function writeObjects(&$items) {
107 $baoName = $this->getBaoName();
109 // Some BAOs are weird and don't support a straightforward "create" method.
112 'EntityTag' => 'add',
113 'GroupContact' => 'add',
115 $method = $oddballs[$this->getEntityName()] ??
'create';
116 if (!method_exists($baoName, $method)) {
117 $method = method_exists($baoName, 'add') ?
'add' : FALSE;
122 foreach ($items as &$item) {
123 $entityId = $item['id'] ??
NULL;
124 FormattingUtil
::formatWriteParams($item, $this->entityFields());
125 $this->formatCustomParams($item, $entityId);
127 // Skip to writeRecords if not using legacy method
131 $item['check_permissions'] = $this->getCheckPermissions();
133 // For some reason the contact bao requires this
134 if ($entityId && $this->getEntityName() === 'Contact') {
135 $item['contact_id'] = $entityId;
138 if ($this->getCheckPermissions()) {
139 $this->checkContactPermissions($baoName, $item);
142 if ($this->getEntityName() === 'Address') {
143 $createResult = $baoName::$method($item, $this->fixAddress
);
146 $createResult = $baoName::$method($item);
149 if (!$createResult) {
150 $errMessage = sprintf('%s write operation failed', $this->getEntityName());
151 throw new \
API_Exception($errMessage);
154 $result[] = $this->baoToArray($createResult, $item);
157 // Use bulk `writeRecords` method if the BAO doesn't have a create or add method
158 // TODO: reverse this from opt-in to opt-out and default to using `writeRecords` for all BAOs
160 $items = array_values($items);
161 foreach ($baoName::writeRecords($items) as $i => $createResult) {
162 $result[] = $this->baoToArray($createResult, $items[$i]);
166 FormattingUtil
::formatOutputValues($result, $this->entityFields(), $this->getEntityName());
171 * @param array $params
172 * @param int $entityId
174 * @throws \API_Exception
175 * @throws \CRM_Core_Exception
177 protected function formatCustomParams(&$params, $entityId) {
180 // $customValueID is the ID of the custom value in the custom table for this
181 // entity (i guess this assumes it's not a multi value entity)
182 foreach ($params as $name => $value) {
183 $field = $this->getCustomFieldInfo($name);
188 // todo are we sure we don't want to allow setting to NULL? need to test
189 if (NULL !== $value) {
191 if ($field['suffix']) {
192 $options = FormattingUtil
::getPseudoconstantList($field, $field['suffix'], $params, $this->getActionName());
193 $value = FormattingUtil
::replacePseudoconstant($options, $value, TRUE);
196 if ($field['html_type'] === 'CheckBox') {
197 // this function should be part of a class
198 formatCheckBoxField($value, 'custom_' . $field['id'], $this->getEntityName());
201 if ($field['data_type'] === 'ContactReference' && !is_numeric($value)) {
202 require_once 'api/v3/utils.php';
203 $value = \
_civicrm_api3_resolve_contactID($value);
204 if ('unknown-user' === $value) {
205 throw new \
API_Exception("\"{$field['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $field['name'], "type" => "integer"]);
209 \CRM_Core_BAO_CustomField
::formatCustomField(
213 $field['custom_group.extends'],
214 // todo check when this is needed
225 $params['custom'] = $customParams;
230 * Gets field info needed to save custom data
232 * @param string $fieldExpr
233 * Field identifier with possible suffix, e.g. MyCustomGroup.MyField1:label
236 protected function getCustomFieldInfo(string $fieldExpr) {
237 if (strpos($fieldExpr, '.') === FALSE) {
240 list($groupName, $fieldName) = explode('.', $fieldExpr);
241 list($fieldName, $suffix) = array_pad(explode(':', $fieldName), 2, NULL);
242 $cacheKey = "APIv4_Custom_Fields-$groupName";
243 $info = \Civi
::cache('metadata')->get($cacheKey);
244 if (!isset($info[$fieldName])) {
246 $fields = CustomField
::get(FALSE)
247 ->addSelect('id', 'name', 'html_type', 'data_type', 'custom_group.extends')
248 ->addWhere('custom_group.name', '=', $groupName)
249 ->execute()->indexBy('name');
250 foreach ($fields as $name => $field) {
251 $field['custom_field_id'] = $field['id'];
252 $field['name'] = $groupName . '.' . $name;
253 $field['entity'] = CustomGroupJoinable
::getEntityFromExtends($field['custom_group.extends']);
254 $info[$name] = $field;
256 \Civi
::cache('metadata')->set($cacheKey, $info);
258 return isset($info[$fieldName]) ?
['suffix' => $suffix] +
$info[$fieldName] : NULL;
262 * Check edit/delete permissions for contacts and related entities.
264 * @param string $baoName
267 * @throws \Civi\API\Exception\UnauthorizedException
269 protected function checkContactPermissions($baoName, $item) {
270 if ($baoName === 'CRM_Contact_BAO_Contact' && !empty($item['id'])) {
271 $permission = $this->getActionName() === 'delete' ? \CRM_Core_Permission
::DELETE
: \CRM_Core_Permission
::EDIT
;
272 if (!\CRM_Contact_BAO_Contact_Permission
::allow($item['id'], $permission)) {
273 throw new \Civi\API\Exception\
UnauthorizedException('Permission denied to modify contact record');
277 // Fixme: decouple from v3
278 require_once 'api/v3/utils.php';
279 _civicrm_api3_check_edit_permissions($baoName, $item);