value pairs. * @method array getValues() Get field values. * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving. * @method bool getReload() * * @package Civi\Api4\Generic */ abstract class AbstractUpdateAction extends AbstractBatchAction { /** * Field values to update. * * @var array * @required */ protected $values = []; /** * Reload $ENTITIES after saving. * * Setting to `true` will load complete records and return them as the api result. * If `false` the api usually returns only the fields specified to be updated. * * @var bool */ protected $reload = FALSE; /** * Criteria for selecting items to update. * * Required if no id is supplied in values. * * @var array */ protected $where = []; abstract protected function updateRecords(array $items): array; /** * @inheritDoc */ public function _run(Result $result) { $primaryKeys = CoreUtil::getInfoItem($this->getEntityName(), 'primary_key'); $this->formatWriteValues($this->values); // Add primary keys from values to WHERE clause and check for mismatch foreach ($primaryKeys as $id) { if (!empty($this->values[$id])) { $wheres = array_column($this->where, NULL, 0); if (!isset($wheres[$id])) { $this->addWhere($id, '=', $this->values[$id]); } elseif (!($wheres[$id][1] === '=' && $wheres[$id][2] == $this->values[$id])) { throw new \Exception("Cannot update the $id of an existing " . $this->getEntityName() . '.'); } } } // Require WHERE if we didn't get primary keys from values if (!$this->where) { throw new \API_Exception('Parameter "where" is required unless primary keys are supplied in values.'); } // Update a single record by primary key (if this entity has a single primary key) if (count($this->where) === 1 && count($primaryKeys) === 1 && $primaryKeys === $this->getSelect() && $this->where[0][0] === $id && $this->where[0][1] === '=' && !empty($this->where[0][2])) { $this->values[$id] = $this->where[0][2]; if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $this->values, \CRM_Core_Session::getLoggedInContactID() ?: 0)) { throw new UnauthorizedException("ACL check failed"); } $items = [$this->values]; $this->validateValues(); $result->exchangeArray($this->updateRecords($items)); return; } // Batch update 1 or more records based on WHERE clause $items = $this->getBatchRecords(); foreach ($items as &$item) { $item = $this->values + $item; if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) { throw new UnauthorizedException("ACL check failed"); } } $this->validateValues(); $result->exchangeArray($this->updateRecords($items)); } /** * @param string $fieldName * * @return mixed|null */ public function getValue(string $fieldName) { return $this->values[$fieldName] ?? NULL; } /** * Add an item to the values array. * * @param string $fieldName * @param mixed $value * @return $this */ public function addValue(string $fieldName, $value) { $this->values[$fieldName] = $value; return $this; } /** * @throws \API_Exception */ protected function validateValues() { // FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception? $e = new ValidateValuesEvent($this, [$this->values], new \CRM_Utils_LazyArray(function () { $existing = $this->getBatchAction()->setSelect(['*'])->execute(); $result = []; foreach ($existing as $record) { $result[] = ['old' => $record, 'new' => $this->values]; } return $result; })); \Civi::dispatcher()->dispatch('civi.api4.validate', $e); if (!empty($e->errors)) { throw $e->toException(); } } }