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 +--------------------------------------------------------------------+
12 namespace Civi\Api4\Event
;
14 use Civi\API\Event\RequestTrait
;
15 use Civi\Core\Event\GenericHookEvent
;
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.
21 * Example #1: Walk each record and validate some fields
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.'));
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.'));
35 * Example #2: Prohibit recording `Contribution` records on `Student` contacts.
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.'));
50 class ValidateValuesEvent
extends GenericHookEvent
{
55 * List of updated records.
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()`.
61 * @var array|\CRM_Utils_LazyArray
62 * @see \Civi\Api4\Generic\AbstractSaveAction::$records
67 * Detailed, side-by-side comparison of old and new values.
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
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.")
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.
79 * @var array|\CRM_Utils_LazyArray
80 * Each item is a record of the form ['old' => $fieldValues, 'new' => $fieldValues]
85 * List of error messages.
88 * Array(string $errorName => string $errorMessage)
94 * ValidateValuesEvent constructor.
96 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
97 * @param array|\CRM_Utils_LazyArray $records
98 * List of updates (akin to SaveAction::$records).
99 * @param array|\CRM_Utils_LazyArray $diffs
100 * List of differences (comparing old values and new values).
102 public function __construct($apiRequest, $records, $diffs) {
103 $this->setApiRequest($apiRequest);
104 $this->records
= $records;
105 $this->diffs
= $diffs;
112 public function getHookValues() {
113 return [$this->getApiRequest(), $this->records
, &$this->errors
];
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
130 public function addError($recordKey, $field, string $name, string $message = NULL): self
{
132 'record' => $recordKey,
133 'fields' => (array) $field,
135 'message' => $message ?
: ts('Error code (%1)', [1 => $name]),
141 * Convert the list of errors an exception.
143 * @return \API_Exception
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')),