Commit | Line | Data |
---|---|---|
19b53e5b C |
1 | <?php |
2 | ||
380f3545 TO |
3 | /* |
4 | +--------------------------------------------------------------------+ | |
41498ac5 | 5 | | Copyright CiviCRM LLC. All rights reserved. | |
380f3545 | 6 | | | |
41498ac5 TO |
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 | | |
380f3545 TO |
10 | +--------------------------------------------------------------------+ |
11 | */ | |
12 | ||
19b53e5b C |
13 | namespace Civi\Api4\Generic; |
14 | ||
4bf92107 | 15 | use Civi\Api4\Event\ValidateValuesEvent; |
929a9585 CW |
16 | use Civi\API\Exception\UnauthorizedException; |
17 | use Civi\Api4\Utils\CoreUtil; | |
4bf92107 | 18 | |
19b53e5b | 19 | /** |
58efbb45 CW |
20 | * Create or update one or more $ENTITIES. |
21 | * | |
22 | * Pass an array of one or more $ENTITY to save in the `records` param. | |
23 | * | |
24 | * If creating more than one $ENTITY with similar values, use the `defaults` param. | |
25 | * | |
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). | |
19b53e5b | 28 | * |
121ec912 | 29 | * @method $this setRecords(array $records) Set array of records to be saved. |
19b53e5b C |
30 | * @method array getRecords() |
31 | * @method $this setDefaults(array $defaults) Array of defaults. | |
19b53e5b C |
32 | * @method array getDefaults() |
33 | * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving. | |
34 | * @method bool getReload() | |
db11224f CW |
35 | * @method $this setMatch(array $match) Specify fields to match for update. |
36 | * @method bool getMatch() | |
19b53e5b C |
37 | * |
38 | * @package Civi\Api4\Generic | |
39 | */ | |
40 | abstract class AbstractSaveAction extends AbstractAction { | |
41 | ||
42 | /** | |
e3c6d5ff | 43 | * Array of $ENTITIES to save. |
19b53e5b | 44 | * |
fc95d9a5 | 45 | * Should be in the same format as returned by `Get`. |
19b53e5b C |
46 | * |
47 | * @var array | |
48 | * @required | |
49 | */ | |
50 | protected $records = []; | |
51 | ||
52 | /** | |
53 | * Array of default values. | |
54 | * | |
9fe720f0 CW |
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, | |
e3c6d5ff | 57 | * but updating existing $ENTITIES will overwrite current values with these defaults. |
19b53e5b C |
58 | * |
59 | * @var array | |
60 | */ | |
61 | protected $defaults = []; | |
62 | ||
63 | /** | |
e3c6d5ff | 64 | * Reload $ENTITIES after saving. |
19b53e5b | 65 | * |
fc95d9a5 CW |
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. | |
19b53e5b C |
69 | * |
70 | * @var bool | |
71 | */ | |
72 | protected $reload = FALSE; | |
73 | ||
db11224f CW |
74 | /** |
75 | * Specify fields to match for update. | |
76 | * | |
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. | |
79 | * | |
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. | |
82 | * | |
83 | * @var array | |
84 | * @optionsCallback getMatchFields | |
85 | */ | |
86 | protected $match = []; | |
87 | ||
19b53e5b C |
88 | /** |
89 | * @throws \API_Exception | |
929a9585 | 90 | * @throws \Civi\API\Exception\UnauthorizedException |
19b53e5b C |
91 | */ |
92 | protected function validateValues() { | |
12e4505a | 93 | $idField = CoreUtil::getIdFieldName($this->getEntityName()); |
4bf92107 | 94 | // FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception? |
19b53e5b C |
95 | $unmatched = []; |
96 | foreach ($this->records as $record) { | |
29ab318b | 97 | if (empty($record[$idField])) { |
19b53e5b C |
98 | $unmatched = array_unique(array_merge($unmatched, $this->checkRequiredFields($record))); |
99 | } | |
100 | } | |
101 | if ($unmatched) { | |
102 | throw new \API_Exception("Mandatory values missing from Api4 {$this->getEntityName()}::{$this->getActionName()}: " . implode(", ", $unmatched), "mandatory_missing", ["fields" => $unmatched]); | |
103 | } | |
929a9585 CW |
104 | |
105 | if ($this->checkPermissions) { | |
106 | foreach ($this->records as $record) { | |
29ab318b | 107 | $action = empty($record[$idField]) ? 'create' : 'update'; |
70da3927 | 108 | if (!CoreUtil::checkAccessDelegated($this->getEntityName(), $action, $record, \CRM_Core_Session::getLoggedInContactID() ?: 0)) { |
929a9585 CW |
109 | throw new UnauthorizedException("ACL check failed"); |
110 | } | |
111 | } | |
112 | } | |
113 | ||
29ab318b CW |
114 | $e = new ValidateValuesEvent($this, $this->records, new \CRM_Utils_LazyArray(function() use ($idField) { |
115 | $existingIds = array_column($this->records, $idField); | |
9f04ea33 TO |
116 | $existing = civicrm_api4($this->getEntityName(), 'get', [ |
117 | 'checkPermissions' => $this->checkPermissions, | |
29ab318b CW |
118 | 'where' => [[$idField, 'IN', $existingIds]], |
119 | ], $idField); | |
9f04ea33 TO |
120 | |
121 | $result = []; | |
122 | foreach ($this->records as $k => $new) { | |
29ab318b | 123 | $old = isset($new[$idField]) ? $existing[$new[$idField]] : NULL; |
9f04ea33 TO |
124 | $result[$k] = ['old' => $old, 'new' => $new]; |
125 | } | |
126 | return $result; | |
127 | })); | |
4bf92107 TO |
128 | \Civi::dispatcher()->dispatch('civi.api4.validate', $e); |
129 | if (!empty($e->errors)) { | |
130 | throw $e->toException(); | |
131 | } | |
19b53e5b C |
132 | } |
133 | ||
db11224f CW |
134 | /** |
135 | * Find existing record based on $this->match param | |
136 | * | |
137 | * @param $record | |
138 | */ | |
139 | protected function matchExisting(&$record) { | |
140 | $primaryKey = CoreUtil::getIdFieldName($this->getEntityName()); | |
141 | if (empty($record[$primaryKey]) && !empty($this->match)) { | |
142 | $where = []; | |
143 | foreach ($record as $key => $val) { | |
144 | if (isset($val) && in_array($key, $this->match, TRUE)) { | |
26c8e1fe MW |
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']; | |
148 | } | |
149 | else { | |
150 | $where[] = [$key, '=', $val]; | |
151 | } | |
db11224f CW |
152 | } |
153 | } | |
154 | if (count($where) === count($this->match)) { | |
155 | $existing = civicrm_api4($this->getEntityName(), 'get', [ | |
156 | 'select' => [$primaryKey], | |
157 | 'where' => $where, | |
158 | 'checkPermissions' => $this->checkPermissions, | |
159 | 'limit' => 2, | |
160 | ]); | |
161 | if ($existing->count() === 1) { | |
162 | $record[$primaryKey] = $existing->first()[$primaryKey]; | |
163 | } | |
164 | } | |
165 | } | |
166 | } | |
167 | ||
19b53e5b C |
168 | /** |
169 | * @return string | |
db11224f | 170 | * @deprecated |
19b53e5b C |
171 | */ |
172 | protected function getIdField() { | |
29ab318b | 173 | return CoreUtil::getInfoItem($this->getEntityName(), 'primary_key')[0]; |
19b53e5b C |
174 | } |
175 | ||
121ec912 CW |
176 | /** |
177 | * Add one or more records to be saved. | |
178 | * @param array ...$records | |
179 | * @return $this | |
180 | */ | |
181 | public function addRecord(array ...$records) { | |
182 | $this->records = array_merge($this->records, $records); | |
183 | return $this; | |
184 | } | |
185 | ||
186 | /** | |
187 | * Set default value for a field. | |
188 | * @param string $fieldName | |
189 | * @param mixed $defaultValue | |
190 | * @return $this | |
191 | */ | |
192 | public function addDefault(string $fieldName, $defaultValue) { | |
193 | $this->defaults[$fieldName] = $defaultValue; | |
194 | return $this; | |
195 | } | |
196 | ||
db11224f CW |
197 | /** |
198 | * Options callback for $this->match | |
199 | * @return array | |
200 | */ | |
201 | protected function getMatchFields() { | |
202 | return (array) civicrm_api4($this->getEntityName(), 'getFields', [ | |
203 | 'checkPermissions' => FALSE, | |
204 | 'action' => 'get', | |
205 | 'where' => [ | |
206 | ['type', 'IN', ['Field', 'Custom']], | |
207 | ['name', 'NOT IN', CoreUtil::getInfoItem($this->getEntityName(), 'primary_key')], | |
208 | ], | |
209 | ], ['name']); | |
210 | } | |
211 | ||
19b53e5b | 212 | } |