Commit | Line | Data |
---|---|---|
4bf92107 TO |
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 | */ | |
4bf92107 TO |
12 | namespace Civi\Api4\Event; |
13 | ||
14 | use Civi\API\Event\RequestTrait; | |
15 | use Civi\Core\Event\GenericHookEvent; | |
16 | ||
17 | /** | |
18 | * The ValidateValuesEvent ('civi.api4.validate') is emitted when creating or saving an entire record via APIv4. | |
19 | * It is emitted once for every record is updated. | |
20 | * | |
21 | * Example #1: Walk each record and validate some fields | |
22 | * | |
23 | * function(ValidateValuesEvent $e) { | |
24 | * if ($e->entity !== 'Foozball') return; | |
25 | * foreach ($e->records as $r => $record) { | |
26 | * if (strtotime($record['start_time']) < CRM_Utils_Time::time()) { | |
27 | * $e->addError($r, 'start_time', 'past', ts('Start time has already passed.')); | |
28 | * } | |
29 | * if ($record['length'] * $record['width'] * $record['height'] > VOLUME_LIMIT) { | |
30 | * $e->addError($r, ['length', 'width', 'height'], 'excessive_volume', ts('The record is too big.')); | |
31 | * } | |
32 | * } | |
33 | * } | |
34 | * | |
35 | * Example #2: Prohibit recording `Contribution` records on `Student` contacts. | |
36 | * | |
37 | * function(ValidateValuesEvent $e) { | |
38 | * if ($e->entity !== 'Contribution') return; | |
39 | * $contactSubTypes = CRM_Utils_SQL_Select::from('civicrm_contact') | |
40 | * ->where('id IN (#ids)', ['ids' => array_column($e->records, 'contact_id')]) | |
41 | * ->select('id, contact_sub_type') | |
42 | * ->execute()->fetchMap('id', 'contact_sub_type'); | |
43 | * foreach ($e->records as $r => $record) { | |
44 | * if ($contactSubTypes[$record['contact_id']] === 'Student') { | |
45 | * $e->addError($r, 'contact_id', 'student_prohibited', ts('Donations from student records are strictly prohibited.')); | |
46 | * } | |
47 | * } | |
48 | * } | |
49 | */ | |
50 | class ValidateValuesEvent extends GenericHookEvent { | |
51 | ||
52 | use RequestTrait; | |
53 | ||
54 | /** | |
55 | * List of updated records. | |
56 | * | |
57 | * The list of `$records` reflects only the list of new values assigned | |
58 | * by this action. It may or may not correspond to an existing row in the database. | |
59 | * It is similar to the `$records` list used by `save()`. | |
60 | * | |
61 | * @var array|\CRM_Utils_LazyArray | |
62 | * @see \Civi\Api4\Generic\AbstractSaveAction::$records | |
63 | */ | |
64 | public $records; | |
65 | ||
9f04ea33 TO |
66 | /** |
67 | * Detailed, side-by-side comparison of old and new values. | |
68 | * | |
69 | * This requires loading the list of old values from the database. Consequently, | |
70 | * reading `$diffs` is more expensive than reading `$records`, so you should only use it if | |
71 | * really necessary. | |
72 | * | |
73 | * The list of $diffs may be important if you are enforcing a rule that involves | |
74 | * multiple fields. (Ex: "Validate that the state_id and country_id match.") | |
75 | * | |
76 | * When possible, $records and $diffs will have the same number of items (with corresponding | |
77 | * keys). However, in the case of a batch `update()`, the list of diffs will be longer. | |
78 | * | |
79 | * @var array|\CRM_Utils_LazyArray | |
80 | * Each item is a record of the form ['old' => $fieldValues, 'new' => $fieldValues] | |
81 | */ | |
82 | public $diffs; | |
83 | ||
4bf92107 TO |
84 | /** |
85 | * List of error messages. | |
86 | * | |
87 | * @var array | |
88 | * Array(string $errorName => string $errorMessage) | |
89 | * Note: | |
90 | */ | |
91 | public $errors = []; | |
92 | ||
93 | /** | |
94 | * ValidateValuesEvent constructor. | |
95 | * | |
96 | * @param \Civi\Api4\Generic\AbstractAction $apiRequest | |
97 | * @param array|\CRM_Utils_LazyArray $records | |
98 | * List of updates (akin to SaveAction::$records). | |
9f04ea33 TO |
99 | * @param array|\CRM_Utils_LazyArray $diffs |
100 | * List of differences (comparing old values and new values). | |
4bf92107 | 101 | */ |
9f04ea33 | 102 | public function __construct($apiRequest, $records, $diffs) { |
4bf92107 TO |
103 | $this->setApiRequest($apiRequest); |
104 | $this->records = $records; | |
9f04ea33 | 105 | $this->diffs = $diffs; |
4bf92107 TO |
106 | $this->errors = []; |
107 | } | |
108 | ||
109 | /** | |
110 | * @inheritDoc | |
111 | */ | |
112 | public function getHookValues() { | |
113 | return [$this->getApiRequest(), $this->records, &$this->errors]; | |
114 | } | |
115 | ||
116 | /** | |
117 | * Add an error. | |
118 | * | |
119 | * @param string|int $recordKey | |
120 | * The validator may work with multiple records. This should identify the specific record. | |
121 | * Each record is identified by its offset (`$records[$recordKey] === [...the record...]`). | |
122 | * @param string|array $field | |
123 | * The name of the field which has an error. | |
124 | * If the error is multi-field (e.g. mismatched password-confirmation), then use an array. | |
125 | * If the error is independent of any field, then use []. | |
126 | * @param string $name | |
127 | * @param string|NULL $message | |
128 | * @return $this | |
129 | */ | |
130 | public function addError($recordKey, $field, string $name, string $message = NULL): self { | |
131 | $this->errors[] = [ | |
132 | 'record' => $recordKey, | |
133 | 'fields' => (array) $field, | |
134 | 'name' => $name, | |
135 | 'message' => $message ?: ts('Error code (%1)', [1 => $name]), | |
136 | ]; | |
137 | return $this; | |
138 | } | |
139 | ||
140 | /** | |
141 | * Convert the list of errors an exception. | |
142 | * | |
143 | * @return \API_Exception | |
144 | */ | |
145 | public function toException() { | |
146 | // We should probably have a better way to report the errors in a structured/list format. | |
147 | return new \API_Exception(ts('Found %1 error(s) in submitted %2 record(s) of type "%3": %4', [ | |
148 | 1 => count($this->errors), | |
149 | 2 => count(array_unique(array_column($this->errors, 'record'))), | |
150 | 3 => $this->getEntityName(), | |
151 | 4 => implode(', ', array_column($this->errors, 'message')), | |
152 | ])); | |
153 | } | |
154 | ||
155 | } |