Merge pull request #20587 from eileenmcnaughton/rr
[civicrm-core.git] / Civi / Api4 / Generic / AbstractUpdateAction.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
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 +--------------------------------------------------------------------+
11 */
12
13 namespace Civi\Api4\Generic;
14
15 use Civi\API\Exception\UnauthorizedException;
16 use Civi\Api4\Event\ValidateValuesEvent;
17 use Civi\Api4\Utils\CoreUtil;
18
19 /**
20 * Base class for all `Update` api actions
21 *
22 * @method $this setValues(array $values) Set all field values from an array of key => value pairs.
23 * @method array getValues() Get field values.
24 * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving.
25 * @method bool getReload()
26 *
27 * @package Civi\Api4\Generic
28 */
29 abstract class AbstractUpdateAction extends AbstractBatchAction {
30
31 /**
32 * Field values to update.
33 *
34 * @var array
35 * @required
36 */
37 protected $values = [];
38
39 /**
40 * Reload $ENTITIES after saving.
41 *
42 * Setting to `true` will load complete records and return them as the api result.
43 * If `false` the api usually returns only the fields specified to be updated.
44 *
45 * @var bool
46 */
47 protected $reload = FALSE;
48
49 /**
50 * Criteria for selecting items to update.
51 *
52 * Required if no id is supplied in values.
53 *
54 * @var array
55 */
56 protected $where = [];
57
58 abstract protected function updateRecords(array $items): array;
59
60 /**
61 * @inheritDoc
62 */
63 public function _run(Result $result) {
64 $primaryKeys = CoreUtil::getInfoItem($this->getEntityName(), 'primary_key');
65 $this->formatWriteValues($this->values);
66
67 // Add primary keys from values to WHERE clause and check for mismatch
68 foreach ($primaryKeys as $id) {
69 if (!empty($this->values[$id])) {
70 $wheres = array_column($this->where, NULL, 0);
71 if (!isset($wheres[$id])) {
72 $this->addWhere($id, '=', $this->values[$id]);
73 }
74 elseif (!($wheres[$id][1] === '=' && $wheres[$id][2] == $this->values[$id])) {
75 throw new \Exception("Cannot update the $id of an existing " . $this->getEntityName() . '.');
76 }
77 }
78 }
79
80 // Require WHERE if we didn't get primary keys from values
81 if (!$this->where) {
82 throw new \API_Exception('Parameter "where" is required unless primary keys are supplied in values.');
83 }
84
85 // Update a single record by primary key (if this entity has a single primary key)
86 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])) {
87 $this->values[$id] = $this->where[0][2];
88 if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $this->values, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
89 throw new UnauthorizedException("ACL check failed");
90 }
91 $items = [$this->values];
92 $this->validateValues();
93 $result->exchangeArray($this->updateRecords($items));
94 return;
95 }
96
97 // Batch update 1 or more records based on WHERE clause
98 $items = $this->getBatchRecords();
99 foreach ($items as &$item) {
100 $item = $this->values + $item;
101 if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
102 throw new UnauthorizedException("ACL check failed");
103 }
104 }
105
106 $this->validateValues();
107 $result->exchangeArray($this->updateRecords($items));
108 }
109
110 /**
111 * @param string $fieldName
112 *
113 * @return mixed|null
114 */
115 public function getValue(string $fieldName) {
116 return $this->values[$fieldName] ?? NULL;
117 }
118
119 /**
120 * Add an item to the values array.
121 *
122 * @param string $fieldName
123 * @param mixed $value
124 * @return $this
125 */
126 public function addValue(string $fieldName, $value) {
127 $this->values[$fieldName] = $value;
128 return $this;
129 }
130
131 /**
132 * @throws \API_Exception
133 */
134 protected function validateValues() {
135 // FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
136 $e = new ValidateValuesEvent($this, [$this->values], new \CRM_Utils_LazyArray(function () {
137 $existing = $this->getBatchAction()->setSelect(['*'])->execute();
138 $result = [];
139 foreach ($existing as $record) {
140 $result[] = ['old' => $record, 'new' => $this->values];
141 }
142 return $result;
143 }));
144 \Civi::dispatcher()->dispatch('civi.api4.validate', $e);
145 if (!empty($e->errors)) {
146 throw $e->toException();
147 }
148 }
149
150 }