Add 'readonly' attribute to schema fields
[civicrm-core.git] / Civi / Api4 / Service / Spec / FieldSpec.php
CommitLineData
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
13/**
14 *
15 * @package CRM
ca5cec67 16 * @copyright CiviCRM LLC https://civicrm.org/licensing
380f3545
TO
17 */
18
19
19b53e5b
C
20namespace Civi\Api4\Service\Spec;
21
22use Civi\Api4\Utils\CoreUtil;
23
24class FieldSpec {
25 /**
26 * @var mixed
27 */
28 protected $defaultValue;
29
30 /**
31 * @var string
32 */
33 protected $name;
34
b6b6cb2d
CW
35 /**
36 * @var string
37 */
38 protected $label;
39
19b53e5b
C
40 /**
41 * @var string
42 */
43 protected $title;
44
45 /**
46 * @var string
47 */
48 protected $entity;
49
50 /**
51 * @var string
52 */
53 protected $description;
54
55 /**
56 * @var bool
57 */
58 protected $required = FALSE;
59
60 /**
61 * @var bool
62 */
63 protected $requiredIf;
64
65 /**
7b51867f 66 * @var array|bool
19b53e5b
C
67 */
68 protected $options;
69
70 /**
71 * @var string
72 */
73 protected $dataType;
74
75 /**
76 * @var string
77 */
78 protected $inputType;
79
80 /**
81 * @var array
82 */
83 protected $inputAttrs = [];
84
85 /**
86 * @var string
87 */
88 protected $fkEntity;
89
90 /**
91 * @var int
92 */
93 protected $serialize;
94
f274627b
CW
95 /**
96 * @var string
97 */
98 protected $helpPre;
99
100 /**
101 * @var string
102 */
103 protected $helpPost;
104
7b51867f
SL
105 /**
106 * @var array
107 */
108 protected $permission;
109
a689294c
CW
110 /**
111 * @var string
112 */
113 protected $columnName;
114
34745448
CW
115 /**
116 * @var bool
117 */
118 protected $readonly = FALSE;
119
19b53e5b
C
120 /**
121 * Aliases for the valid data types
122 *
123 * @var array
124 */
125 public static $typeAliases = [
126 'Int' => 'Integer',
127 'Link' => 'Url',
128 'Memo' => 'Text',
129 ];
130
131 /**
132 * @param string $name
133 * @param string $entity
134 * @param string $dataType
135 */
136 public function __construct($name, $entity, $dataType = 'String') {
137 $this->entity = $entity;
a689294c 138 $this->name = $this->columnName = $name;
19b53e5b
C
139 $this->setDataType($dataType);
140 }
141
142 /**
143 * @return mixed
144 */
145 public function getDefaultValue() {
146 return $this->defaultValue;
147 }
148
149 /**
150 * @param mixed $defaultValue
151 *
152 * @return $this
153 */
154 public function setDefaultValue($defaultValue) {
155 $this->defaultValue = $defaultValue;
156
157 return $this;
158 }
159
160 /**
161 * @return string
162 */
163 public function getName() {
164 return $this->name;
165 }
166
167 /**
168 * @param string $name
169 *
170 * @return $this
171 */
172 public function setName($name) {
173 $this->name = $name;
174
175 return $this;
176 }
177
b6b6cb2d
CW
178 /**
179 * @return string
180 */
181 public function getLabel() {
182 return $this->label;
183 }
184
185 /**
186 * @param string $label
187 *
188 * @return $this
189 */
190 public function setLabel($label) {
191 $this->label = $label;
192
193 return $this;
194 }
195
19b53e5b
C
196 /**
197 * @return string
198 */
199 public function getTitle() {
200 return $this->title;
201 }
202
203 /**
204 * @param string $title
205 *
206 * @return $this
207 */
208 public function setTitle($title) {
209 $this->title = $title;
210
211 return $this;
212 }
213
214 /**
215 * @return string
216 */
217 public function getEntity() {
218 return $this->entity;
219 }
220
221 /**
222 * @return string
223 */
224 public function getDescription() {
225 return $this->description;
226 }
227
228 /**
229 * @param string $description
230 *
231 * @return $this
232 */
233 public function setDescription($description) {
234 $this->description = $description;
235
236 return $this;
237 }
238
239 /**
240 * @return bool
241 */
242 public function isRequired() {
243 return $this->required;
244 }
245
246 /**
247 * @param bool $required
248 *
249 * @return $this
250 */
251 public function setRequired($required) {
252 $this->required = $required;
253
254 return $this;
255 }
256
257 /**
258 * @return bool
259 */
260 public function getRequiredIf() {
261 return $this->requiredIf;
262 }
263
264 /**
265 * @param bool $requiredIf
266 *
267 * @return $this
268 */
269 public function setRequiredIf($requiredIf) {
270 $this->requiredIf = $requiredIf;
271
272 return $this;
273 }
274
275 /**
276 * @return string
277 */
278 public function getDataType() {
279 return $this->dataType;
280 }
281
282 /**
283 * @param $dataType
284 *
285 * @return $this
286 * @throws \Exception
287 */
288 public function setDataType($dataType) {
289 if (array_key_exists($dataType, self::$typeAliases)) {
290 $dataType = self::$typeAliases[$dataType];
291 }
292
293 if (!in_array($dataType, $this->getValidDataTypes())) {
294 throw new \Exception(sprintf('Invalid data type "%s', $dataType));
295 }
296
297 $this->dataType = $dataType;
298
299 return $this;
300 }
301
302 /**
303 * @return int
304 */
305 public function getSerialize() {
306 return $this->serialize;
307 }
308
309 /**
310 * @param int|null $serialize
311 * @return $this
312 */
313 public function setSerialize($serialize) {
314 $this->serialize = $serialize;
315
316 return $this;
317 }
318
7b51867f
SL
319 /**
320 * @param array $permission
321 * @return $this
322 */
323 public function setPermission($permission) {
324 $this->permission = $permission;
325 return $this;
326 }
327
328 /**
329 * @return array
330 */
331 public function getPermission() {
332 return $this->permission;
333 }
334
19b53e5b
C
335 /**
336 * @return string
337 */
338 public function getInputType() {
339 return $this->inputType;
340 }
341
342 /**
343 * @param string $inputType
344 * @return $this
345 */
346 public function setInputType($inputType) {
347 $this->inputType = $inputType;
348
349 return $this;
350 }
351
352 /**
353 * @return array
354 */
355 public function getInputAttrs() {
356 return $this->inputAttrs;
357 }
358
359 /**
360 * @param array $inputAttrs
361 * @return $this
362 */
363 public function setInputAttrs($inputAttrs) {
364 $this->inputAttrs = $inputAttrs;
365
366 return $this;
367 }
368
34745448
CW
369 /**
370 * @return bool
371 */
372 public function getreadonly() {
373 return $this->readonly;
374 }
375
376 /**
377 * @param bool $readonly
378 * @return $this
379 */
380 public function setreadonly($readonly) {
381 $this->readonly = (bool) $readonly;
382
383 return $this;
384 }
385
f274627b
CW
386 /**
387 * @return string|NULL
388 */
389 public function getHelpPre() {
390 return $this->helpPre;
391 }
392
393 /**
394 * @param string|NULL $helpPre
395 */
396 public function setHelpPre($helpPre) {
397 $this->helpPre = is_string($helpPre) && strlen($helpPre) ? $helpPre : NULL;
398 }
399
400 /**
401 * @return string|NULL
402 */
403 public function getHelpPost() {
404 return $this->helpPost;
405 }
406
407 /**
408 * @param string|NULL $helpPost
409 */
410 public function setHelpPost($helpPost) {
411 $this->helpPost = is_string($helpPost) && strlen($helpPost) ? $helpPost : NULL;
412 }
413
19b53e5b
C
414 /**
415 * Add valid types that are not not part of \CRM_Utils_Type::dataTypes
416 *
417 * @return array
418 */
419 private function getValidDataTypes() {
64af6b6c 420 $extraTypes = ['Boolean', 'Text', 'Float', 'Url', 'Array', 'Blob', 'Mediumblob'];
19b53e5b
C
421 $extraTypes = array_combine($extraTypes, $extraTypes);
422
423 return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes);
424 }
425
426 /**
91edcf66 427 * @param array $values
bb6bfd68 428 * @param array|bool $return
19b53e5b
C
429 * @return array
430 */
bb6bfd68 431 public function getOptions($values = [], $return = TRUE) {
19b53e5b
C
432 if (!isset($this->options) || $this->options === TRUE) {
433 $fieldName = $this->getName();
434
435 if ($this instanceof CustomFieldSpec) {
436 // buildOptions relies on the custom_* type of field names
437 $fieldName = sprintf('custom_%d', $this->getCustomFieldId());
438 }
439
bb6bfd68
CW
440 // BAO::buildOptions returns a single-dimensional list, we call that first because of the hook contract,
441 // @see CRM_Utils_Hook::fieldOptions
442 // We then supplement the data with additional properties if requested.
19b53e5b 443 $bao = CoreUtil::getBAOFromApiName($this->getEntity());
bb6bfd68 444 $optionLabels = $bao::buildOptions($fieldName, NULL, $values);
19b53e5b 445
bb6bfd68
CW
446 if (!is_array($optionLabels) || !$optionLabels) {
447 $this->options = FALSE;
448 }
449 else {
450 $this->options = \CRM_Utils_Array::makeNonAssociative($optionLabels, 'id', 'label');
451 if (is_array($return)) {
452 self::addOptionProps($bao, $fieldName, $values, $return);
453 }
19b53e5b 454 }
19b53e5b
C
455 }
456 return $this->options;
457 }
458
bb6bfd68 459 /**
3068cf0f
CW
460 * Augment the 2 values returned by BAO::buildOptions (id, label) with extra properties (name, description, color, icon, etc).
461 *
462 * We start with BAO::buildOptions in order to respect hooks which may be adding/removing items, then we add the extra data.
bb6bfd68
CW
463 *
464 * @param \CRM_Core_DAO $baoName
465 * @param string $fieldName
466 * @param array $values
467 * @param array $return
468 */
469 private function addOptionProps($baoName, $fieldName, $values, $return) {
470 // FIXME: For now, call the buildOptions function again and then combine the arrays. Not an ideal approach.
471 // TODO: Teach CRM_Core_Pseudoconstant to always load multidimensional option lists so we can get more properties like 'color' and 'icon',
472 // however that might require a change to the hook_civicrm_fieldOptions signature so that's a bit tricky.
473 if (in_array('name', $return)) {
474 $props['name'] = $baoName::buildOptions($fieldName, 'validate', $values);
475 }
476 $return = array_diff($return, ['id', 'name', 'label']);
477 // CRM_Core_Pseudoconstant doesn't know how to fetch extra stuff like icon, description, color, etc., so we have to invent that wheel here...
478 if ($return) {
479 $optionIds = implode(',', array_column($this->options, 'id'));
480 $optionIndex = array_flip(array_column($this->options, 'id'));
481 if ($this instanceof CustomFieldSpec) {
482 $optionGroupId = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $this->getCustomFieldId(), 'option_group_id');
483 }
484 else {
485 $dao = new $baoName();
486 $fieldSpec = $dao->getFieldSpec($fieldName);
487 $pseudoconstant = $fieldSpec['pseudoconstant'] ?? NULL;
488 $optionGroupName = $pseudoconstant['optionGroupName'] ?? NULL;
489 $optionGroupId = $optionGroupName ? \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroupName, 'id', 'name') : NULL;
490 }
491 if (!empty($optionGroupId)) {
492 $extraStuff = \CRM_Core_BAO_OptionValue::getOptionValuesArray($optionGroupId);
493 $keyColumn = $pseudoconstant['keyColumn'] ?? 'value';
494 foreach ($extraStuff as $item) {
495 if (isset($optionIndex[$item[$keyColumn]])) {
496 foreach ($return as $ret) {
3068cf0f
CW
497 // Note: our schema is inconsistent about whether `description` fields allow html,
498 // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
499 $this->options[$optionIndex[$item[$keyColumn]]][$ret] = ($ret === 'description' && isset($item[$ret])) ? strip_tags($item[$ret]) : $item[$ret] ?? NULL;
bb6bfd68
CW
500 }
501 }
502 }
503 }
504 else {
505 // Fetch the abbr if requested using context: abbreviate
506 if (in_array('abbr', $return)) {
507 $props['abbr'] = $baoName::buildOptions($fieldName, 'abbreviate', $values);
508 $return = array_diff($return, ['abbr']);
509 }
510 // Fetch anything else (color, icon, description)
511 if ($return && !empty($pseudoconstant['table']) && \CRM_Utils_Rule::commaSeparatedIntegers($optionIds)) {
512 $sql = "SELECT * FROM {$pseudoconstant['table']} WHERE id IN (%1)";
513 $query = \CRM_Core_DAO::executeQuery($sql, [1 => [$optionIds, 'CommaSeparatedIntegers']]);
514 while ($query->fetch()) {
515 foreach ($return as $ret) {
516 if (property_exists($query, $ret)) {
3068cf0f
CW
517 // Note: our schema is inconsistent about whether `description` fields allow html,
518 // but it's usually assumed to be plain text, so we strip_tags() to standardize it.
519 $this->options[$optionIndex[$query->id]][$ret] = $ret === 'description' ? strip_tags($query->$ret) : $query->$ret;
bb6bfd68
CW
520 }
521 }
522 }
523 }
524 }
525 }
526 if (isset($props)) {
527 foreach ($this->options as &$option) {
528 foreach ($props as $name => $prop) {
529 $option[$name] = $prop[$option['id']] ?? NULL;
530 }
531 }
532 }
533 }
534
19b53e5b
C
535 /**
536 * @param array|bool $options
537 *
538 * @return $this
539 */
540 public function setOptions($options) {
541 $this->options = $options;
542 return $this;
543 }
544
545 /**
546 * @return string
547 */
548 public function getFkEntity() {
549 return $this->fkEntity;
550 }
551
552 /**
553 * @param string $fkEntity
554 *
555 * @return $this
556 */
557 public function setFkEntity($fkEntity) {
558 $this->fkEntity = $fkEntity;
559
560 return $this;
561 }
562
a689294c
CW
563 /**
564 * @return string
565 */
566 public function getColumnName() {
567 return $this->columnName;
568 }
569
570 /**
571 * @param string $columnName
572 *
573 * @return $this
574 */
575 public function setColumnName($columnName) {
576 $this->columnName = $columnName;
577 return $this;
578 }
579
19b53e5b
C
580 /**
581 * @param array $values
582 * @return array
583 */
584 public function toArray($values = []) {
585 $ret = [];
586 foreach (get_object_vars($this) as $key => $val) {
587 $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key));
588 if (!$values || in_array($key, $values)) {
589 $ret[$key] = $val;
590 }
591 }
592 return $ret;
593 }
594
595}