Merge pull request #22928 from artfulrobot/artfulrobot-title-double-html-encoding
[civicrm-core.git] / Civi / Api4 / Event / ValidateValuesEvent.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 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
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
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).
99 * @param array|\CRM_Utils_LazyArray $diffs
100 * List of differences (comparing old values and new values).
101 */
102 public function __construct($apiRequest, $records, $diffs) {
103 $this->setApiRequest($apiRequest);
104 $this->records = $records;
105 $this->diffs = $diffs;
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 }