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
;
15 use Civi\Api4\Event\ValidateValuesEvent
;
16 use Civi\API\Exception\UnauthorizedException
;
17 use Civi\Api4\Utils\CoreUtil
;
20 * Create or update one or more $ENTITIES.
22 * Pass an array of one or more $ENTITY to save in the `records` param.
24 * If creating more than one $ENTITY with similar values, use the `defaults` param.
26 * Set `reload` if you need the api to return complete records for each saved $ENTITY
27 * (including values that were unchanged in from updated $ENTITIES).
29 * @method $this setRecords(array $records) Set array of records to be saved.
30 * @method array getRecords()
31 * @method $this setDefaults(array $defaults) Array of defaults.
32 * @method array getDefaults()
33 * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving.
34 * @method bool getReload()
35 * @method $this setMatch(array $match) Specify fields to match for update.
36 * @method bool getMatch()
38 * @package Civi\Api4\Generic
40 abstract class AbstractSaveAction
extends AbstractAction
{
43 * Array of $ENTITIES to save.
45 * Should be in the same format as returned by `Get`.
50 protected $records = [];
53 * Array of default values.
55 * These defaults will be merged into every $ENTITY in `records` before saving.
56 * Values set in `records` will override these defaults if set in both places,
57 * but updating existing $ENTITIES will overwrite current values with these defaults.
61 protected $defaults = [];
64 * Reload $ENTITIES after saving.
66 * By default this action typically returns partial records containing only the fields
67 * that were updated. Set `reload` to `true` to do an additional lookup after saving
68 * to return complete values for every $ENTITY.
72 protected $reload = FALSE;
75 * Specify fields to match for update.
77 * Normally each record is either created or updated based on the presence of an `id`.
78 * Specifying `$match` fields will also perform an update if an existing $ENTITY matches all specified fields.
80 * Note: the fields named in this param should be without any options suffix (e.g. `my_field` not `my_field:name`).
81 * Any options suffixes in the $records will be resolved by the api prior to matching.
84 * @optionsCallback getMatchFields
86 protected $match = [];
89 * @throws \API_Exception
90 * @throws \Civi\API\Exception\UnauthorizedException
92 protected function validateValues() {
93 $idField = CoreUtil
::getIdFieldName($this->getEntityName());
94 // FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
96 foreach ($this->records
as $record) {
97 if (empty($record[$idField])) {
98 $unmatched = array_unique(array_merge($unmatched, $this->checkRequiredFields($record)));
102 throw new \
API_Exception("Mandatory values missing from Api4 {$this->getEntityName()}::{$this->getActionName()}: " . implode(", ", $unmatched), "mandatory_missing", ["fields" => $unmatched]);
105 if ($this->checkPermissions
) {
106 foreach ($this->records
as $record) {
107 $action = empty($record[$idField]) ?
'create' : 'update';
108 if (!CoreUtil
::checkAccessDelegated($this->getEntityName(), $action, $record, \CRM_Core_Session
::getLoggedInContactID() ?
: 0)) {
109 throw new UnauthorizedException("ACL check failed");
114 $e = new ValidateValuesEvent($this, $this->records
, new \
CRM_Utils_LazyArray(function() use ($idField) {
115 $existingIds = array_column($this->records
, $idField);
116 $existing = civicrm_api4($this->getEntityName(), 'get', [
117 'checkPermissions' => $this->checkPermissions
,
118 'where' => [[$idField, 'IN', $existingIds]],
122 foreach ($this->records
as $k => $new) {
123 $old = isset($new[$idField]) ?
$existing[$new[$idField]] : NULL;
124 $result[$k] = ['old' => $old, 'new' => $new];
128 \Civi
::dispatcher()->dispatch('civi.api4.validate', $e);
129 if (!empty($e->errors
)) {
130 throw $e->toException();
135 * Find existing record based on $this->match param
139 protected function matchExisting(&$record) {
140 $primaryKey = CoreUtil
::getIdFieldName($this->getEntityName());
141 if (empty($record[$primaryKey]) && !empty($this->match
)) {
143 foreach ($record as $key => $val) {
144 if (isset($val) && in_array($key, $this->match
, TRUE)) {
145 if ($val === '' ||
is_null($val)) {
146 // If we want to match empty string we have to match on NULL/''
147 $where[] = [$key, 'IS EMPTY'];
150 $where[] = [$key, '=', $val];
154 if (count($where) === count($this->match
)) {
155 $existing = civicrm_api4($this->getEntityName(), 'get', [
156 'select' => [$primaryKey],
158 'checkPermissions' => $this->checkPermissions
,
161 if ($existing->count() === 1) {
162 $record[$primaryKey] = $existing->first()[$primaryKey];
172 protected function getIdField() {
173 return CoreUtil
::getInfoItem($this->getEntityName(), 'primary_key')[0];
177 * Add one or more records to be saved.
178 * @param array ...$records
181 public function addRecord(array ...$records) {
182 $this->records
= array_merge($this->records
, $records);
187 * Set default value for a field.
188 * @param string $fieldName
189 * @param mixed $defaultValue
192 public function addDefault(string $fieldName, $defaultValue) {
193 $this->defaults
[$fieldName] = $defaultValue;
198 * Options callback for $this->match
201 protected function getMatchFields() {
202 return (array) civicrm_api4($this->getEntityName(), 'getFields', [
203 'checkPermissions' => FALSE,
206 ['type', 'IN', ['Field', 'Custom']],
207 ['name', 'NOT IN', CoreUtil
::getInfoItem($this->getEntityName(), 'primary_key')],