@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @task apps Building Applications with Custom Fields
5 * @task core Core Properties and Field Identity
6 * @task proxy Field Proxies
7 * @task context Contextual Data
8 * @task render Rendering Utilities
9 * @task storage Field Storage
10 * @task edit Integration with Edit Views
11 * @task view Integration with Property Views
12 * @task list Integration with List views
13 * @task appsearch Integration with ApplicationSearch
14 * @task appxaction Integration with ApplicationTransactions
15 * @task xactionmail Integration with Transaction Mail
16 * @task globalsearch Integration with Global Search
17 * @task herald Integration with Herald
18 */
19abstract class PhabricatorCustomField extends Phobject {
20
21 private $viewer;
22 private $object;
23 private $proxy;
24
25 const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
26 const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail';
27 const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
28 const ROLE_STORAGE = 'storage';
29 const ROLE_DEFAULT = 'default';
30 const ROLE_EDIT = 'edit';
31 const ROLE_VIEW = 'view';
32 const ROLE_LIST = 'list';
33 const ROLE_GLOBALSEARCH = 'GlobalSearch';
34 const ROLE_CONDUIT = 'conduit';
35 const ROLE_HERALD = 'herald';
36 const ROLE_EDITENGINE = 'EditEngine';
37 const ROLE_HERALDACTION = 'herald.action';
38 const ROLE_EXPORT = 'export';
39
40
41/* -( Building Applications with Custom Fields )--------------------------- */
42
43
44 /**
45 * @task apps
46 */
47 public static function getObjectFields(
48 PhabricatorCustomFieldInterface $object,
49 $role) {
50
51 try {
52 $attachment = $object->getCustomFields();
53 } catch (PhabricatorDataNotAttachedException $ex) {
54 $attachment = new PhabricatorCustomFieldAttachment();
55 $object->attachCustomFields($attachment);
56 }
57
58 try {
59 $field_list = $attachment->getCustomFieldList($role);
60 } catch (PhabricatorCustomFieldNotAttachedException $ex) {
61 $base_class = $object->getCustomFieldBaseClass();
62
63 $spec = $object->getCustomFieldSpecificationForRole($role);
64 if (!is_array($spec)) {
65 throw new Exception(
66 pht(
67 "Expected an array from %s for object of class '%s'.",
68 'getCustomFieldSpecificationForRole()',
69 get_class($object)));
70 }
71
72 $fields = self::buildFieldList(
73 $base_class,
74 $spec,
75 $object);
76
77 $fields = self::adjustCustomFieldsForObjectSubtype(
78 $object,
79 $role,
80 $fields);
81
82 foreach ($fields as $key => $field) {
83 // NOTE: We perform this filtering in "buildFieldList()", but may need
84 // to filter again after subtype adjustment.
85 if (!$field->isFieldEnabled()) {
86 unset($fields[$key]);
87 continue;
88 }
89
90 if (!$field->shouldEnableForRole($role)) {
91 unset($fields[$key]);
92 continue;
93 }
94 }
95
96 foreach ($fields as $field) {
97 $field->setObject($object);
98 }
99
100 $field_list = new PhabricatorCustomFieldList($fields);
101 $attachment->addCustomFieldList($role, $field_list);
102 }
103
104 return $field_list;
105 }
106
107
108 /**
109 * @task apps
110 */
111 public static function getObjectField(
112 PhabricatorCustomFieldInterface $object,
113 $role,
114 $field_key) {
115
116 $fields = self::getObjectFields($object, $role)->getFields();
117
118 return idx($fields, $field_key);
119 }
120
121
122 /**
123 * @task apps
124 */
125 public static function buildFieldList(
126 $base_class,
127 array $spec,
128 $object,
129 array $options = array()) {
130
131 $field_objects = id(new PhutilClassMapQuery())
132 ->setAncestorClass($base_class)
133 ->execute();
134
135 $fields = array();
136 foreach ($field_objects as $field_object) {
137 $field_object = clone $field_object;
138 foreach ($field_object->createFields($object) as $field) {
139 $key = $field->getFieldKey();
140 if (isset($fields[$key])) {
141 throw new Exception(
142 pht(
143 "Both '%s' and '%s' define a custom field with ".
144 "field key '%s'. Field keys must be unique.",
145 get_class($fields[$key]),
146 get_class($field),
147 $key));
148 }
149 $fields[$key] = $field;
150 }
151 }
152
153 foreach ($fields as $key => $field) {
154 if (!$field->isFieldEnabled()) {
155 unset($fields[$key]);
156 }
157 }
158
159 $fields = array_select_keys($fields, array_keys($spec)) + $fields;
160
161 if (empty($options['withDisabled'])) {
162 foreach ($fields as $key => $field) {
163 if (isset($spec[$key]['disabled'])) {
164 $is_disabled = $spec[$key]['disabled'];
165 } else {
166 $is_disabled = $field->shouldDisableByDefault();
167 }
168
169 if ($is_disabled) {
170 if ($field->canDisableField()) {
171 unset($fields[$key]);
172 }
173 }
174 }
175 }
176
177 return $fields;
178 }
179
180
181/* -( Core Properties and Field Identity )--------------------------------- */
182
183
184 /**
185 * Return a key which uniquely identifies this field, like
186 * "mycompany:dinosaur:count". Normally you should provide some level of
187 * namespacing to prevent collisions.
188 *
189 * @return string String which uniquely identifies this field.
190 * @task core
191 */
192 public function getFieldKey() {
193 if ($this->proxy) {
194 return $this->proxy->getFieldKey();
195 }
196 throw new PhabricatorCustomFieldImplementationIncompleteException(
197 $this,
198 $field_key_is_incomplete = true);
199 }
200
201 public function getModernFieldKey() {
202 if ($this->proxy) {
203 return $this->proxy->getModernFieldKey();
204 }
205 return $this->getFieldKey();
206 }
207
208
209 /**
210 * Return a human-readable field name.
211 *
212 * @return string Human readable field name.
213 * @task core
214 */
215 public function getFieldName() {
216 if ($this->proxy) {
217 return $this->proxy->getFieldName();
218 }
219 return $this->getModernFieldKey();
220 }
221
222
223 /**
224 * Return a short, human-readable description of the field's behavior. This
225 * provides more context to administrators when they are customizing fields.
226 *
227 * @return string|null Optional human-readable description.
228 * @task core
229 */
230 public function getFieldDescription() {
231 if ($this->proxy) {
232 return $this->proxy->getFieldDescription();
233 }
234 return null;
235 }
236
237
238 /**
239 * Most field implementations are unique, in that one class corresponds to
240 * one field. However, some field implementations are general and a single
241 * implementation may drive several fields.
242 *
243 * For general implementations, the general field implementation can return
244 * multiple field instances here.
245 *
246 * @param object $object The object to create fields for.
247 * @return list<PhabricatorCustomField> List of fields.
248 * @task core
249 */
250 public function createFields($object) {
251 return array($this);
252 }
253
254
255 /**
256 * You can return `false` here if the field should not be enabled for any
257 * role. For example, it might depend on something (like an application or
258 * library) which isn't installed, or might have some global configuration
259 * which allows it to be disabled.
260 *
261 * @return bool False to completely disable this field for all roles.
262 * @task core
263 */
264 public function isFieldEnabled() {
265 if ($this->proxy) {
266 return $this->proxy->isFieldEnabled();
267 }
268 return true;
269 }
270
271
272 /**
273 * Low level selector for field availability. Fields can appear in different
274 * roles (like an edit view, a list view, etc.), but not every field needs
275 * to appear everywhere. Fields that are disabled in a role won't appear in
276 * that context within applications.
277 *
278 * Normally, you do not need to override this method. Instead, override the
279 * methods specific to roles you want to enable. For example, implement
280 * @{method:shouldUseStorage()} to activate the `'storage'` role.
281 *
282 * @return bool True to enable the field for the given role.
283 * @task core
284 */
285 public function shouldEnableForRole($role) {
286
287 // NOTE: All of these calls proxy individually, so we don't need to
288 // proxy this call as a whole.
289
290 switch ($role) {
291 case self::ROLE_APPLICATIONTRANSACTIONS:
292 return $this->shouldAppearInApplicationTransactions();
293 case self::ROLE_APPLICATIONSEARCH:
294 return $this->shouldAppearInApplicationSearch();
295 case self::ROLE_STORAGE:
296 return $this->shouldUseStorage();
297 case self::ROLE_EDIT:
298 return $this->shouldAppearInEditView();
299 case self::ROLE_VIEW:
300 return $this->shouldAppearInPropertyView();
301 case self::ROLE_LIST:
302 return $this->shouldAppearInListView();
303 case self::ROLE_GLOBALSEARCH:
304 return $this->shouldAppearInGlobalSearch();
305 case self::ROLE_CONDUIT:
306 return $this->shouldAppearInConduitDictionary();
307 case self::ROLE_TRANSACTIONMAIL:
308 return $this->shouldAppearInTransactionMail();
309 case self::ROLE_HERALD:
310 return $this->shouldAppearInHerald();
311 case self::ROLE_HERALDACTION:
312 return $this->shouldAppearInHeraldActions();
313 case self::ROLE_EDITENGINE:
314 return $this->shouldAppearInEditView() ||
315 $this->shouldAppearInEditEngine();
316 case self::ROLE_EXPORT:
317 return $this->shouldAppearInDataExport();
318 case self::ROLE_DEFAULT:
319 return true;
320 default:
321 throw new Exception(pht("Unknown field role '%s'!", $role));
322 }
323 }
324
325
326 /**
327 * Allow administrators to disable this field. Most fields should allow this,
328 * but some are fundamental to the behavior of the application and can be
329 * locked down to avoid chaos, disorder, and the decline of civilization.
330 *
331 * @return bool False to prevent this field from being disabled through
332 * configuration.
333 * @task core
334 */
335 public function canDisableField() {
336 return true;
337 }
338
339 public function shouldDisableByDefault() {
340 return false;
341 }
342
343
344 /**
345 * Return an index string which uniquely identifies this field.
346 *
347 * @return string Index string which uniquely identifies this field.
348 * @task core
349 */
350 final public function getFieldIndex() {
351 return PhabricatorHash::digestForIndex($this->getFieldKey());
352 }
353
354
355/* -( Field Proxies )------------------------------------------------------ */
356
357
358 /**
359 * Proxies allow a field to use some other field's implementation for most
360 * of their behavior while still subclassing an application field. When a
361 * proxy is set for a field with @{method:setProxy}, all of its methods will
362 * call through to the proxy by default.
363 *
364 * This is most commonly used to implement configuration-driven custom fields
365 * using @{class:PhabricatorStandardCustomField}.
366 *
367 * This method must be overridden to return `true` before a field can accept
368 * proxies.
369 *
370 * @return bool True if you can @{method:setProxy} this field.
371 * @task proxy
372 */
373 public function canSetProxy() {
374 if ($this instanceof PhabricatorStandardCustomFieldInterface) {
375 return true;
376 }
377 return false;
378 }
379
380
381 /**
382 * Set the proxy implementation for this field. See @{method:canSetProxy} for
383 * discussion of field proxies.
384 *
385 * @param PhabricatorCustomField $proxy Field implementation.
386 * @return $this
387 * @task proxy
388 */
389 final public function setProxy(PhabricatorCustomField $proxy) {
390 if (!$this->canSetProxy()) {
391 throw new PhabricatorCustomFieldNotProxyException($this);
392 }
393
394 $this->proxy = $proxy;
395 return $this;
396 }
397
398
399 /**
400 * Get the field's proxy implementation, if any. For discussion, see
401 * @{method:canSetProxy}.
402 *
403 * @return PhabricatorCustomField|null Proxy field, if one is set.
404 * @task proxy
405 */
406 final public function getProxy() {
407 return $this->proxy;
408 }
409
410 /**
411 * @task proxy
412 */
413 public function __clone() {
414 if ($this->proxy) {
415 $this->proxy = clone $this->proxy;
416 }
417 }
418/* -( Contextual Data )---------------------------------------------------- */
419
420
421 /**
422 * Sets the object this field belongs to.
423 *
424 * @param PhabricatorCustomFieldInterface $object The object this field
425 * belongs to.
426 * @return $this
427 * @task context
428 */
429 final public function setObject(PhabricatorCustomFieldInterface $object) {
430 if ($this->proxy) {
431 $this->proxy->setObject($object);
432 return $this;
433 }
434
435 $this->object = $object;
436 $this->didSetObject($object);
437 return $this;
438 }
439
440
441 /**
442 * Read object data into local field storage, if applicable.
443 *
444 * @param PhabricatorCustomFieldInterface $object The object this field
445 * belongs to.
446 * @task context
447 */
448 public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
449 if ($this->proxy) {
450 $this->proxy->readValueFromObject($object);
451 }
452 return;
453 }
454
455
456 /**
457 * Get the object this field belongs to.
458 *
459 * @return PhabricatorCustomFieldInterface The object this field belongs to.
460 * @task context
461 */
462 final public function getObject() {
463 if ($this->proxy) {
464 return $this->proxy->getObject();
465 }
466
467 return $this->object;
468 }
469
470
471 /**
472 * This is a hook, primarily for subclasses to load object data.
473 *
474 * @return PhabricatorCustomFieldInterface The object this field belongs to.
475 * @return void
476 */
477 protected function didSetObject(PhabricatorCustomFieldInterface $object) {
478 return;
479 }
480
481
482 /**
483 * @task context
484 */
485 final public function setViewer(PhabricatorUser $viewer) {
486 if ($this->proxy) {
487 $this->proxy->setViewer($viewer);
488 return $this;
489 }
490
491 $this->viewer = $viewer;
492 return $this;
493 }
494
495
496 /**
497 * @task context
498 */
499 final public function getViewer() {
500 if ($this->proxy) {
501 return $this->proxy->getViewer();
502 }
503
504 return $this->viewer;
505 }
506
507
508 /**
509 * @task context
510 */
511 final protected function requireViewer() {
512 if ($this->proxy) {
513 return $this->proxy->requireViewer();
514 }
515
516 if (!$this->viewer) {
517 throw new PhabricatorCustomFieldDataNotAvailableException($this);
518 }
519 return $this->viewer;
520 }
521
522
523/* -( Rendering Utilities )------------------------------------------------ */
524
525
526 /**
527 * @task render
528 */
529 protected function renderHandleList(array $handles) {
530 if (!$handles) {
531 return null;
532 }
533
534 $out = array();
535 foreach ($handles as $handle) {
536 $out[] = $handle->renderHovercardLink();
537 }
538
539 return phutil_implode_html(phutil_tag('br'), $out);
540 }
541
542
543/* -( Storage )------------------------------------------------------------ */
544
545
546 /**
547 * Return true to use field storage.
548 *
549 * Fields which can be edited by the user will most commonly use storage,
550 * while some other types of fields (for instance, those which just display
551 * information in some stylized way) may not. Many builtin fields do not use
552 * storage because their data is available on the object itself.
553 *
554 * If you implement this, you must also implement @{method:getValueForStorage}
555 * and @{method:setValueFromStorage}.
556 *
557 * @return bool True to use storage.
558 * @task storage
559 */
560 public function shouldUseStorage() {
561 if ($this->proxy) {
562 return $this->proxy->shouldUseStorage();
563 }
564 return false;
565 }
566
567
568 /**
569 * Return a new, empty storage object. This should be a subclass of
570 * @{class:PhabricatorCustomFieldStorage} which is bound to the application's
571 * database.
572 *
573 * @return PhabricatorCustomFieldStorage New empty storage object.
574 * @task storage
575 */
576 public function newStorageObject() {
577 // NOTE: This intentionally isn't proxied, to avoid call cycles.
578 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
579 }
580
581
582 /**
583 * Return a serialized representation of the field value, appropriate for
584 * storing in auxiliary field storage. You must implement this method if
585 * you implement @{method:shouldUseStorage}.
586 *
587 * If the field value is a scalar, it can be returned unmodified. If not,
588 * it should be serialized (for example, using JSON).
589 *
590 * @return string|null Serialized field value.
591 * @task storage
592 */
593 public function getValueForStorage() {
594 if ($this->proxy) {
595 return $this->proxy->getValueForStorage();
596 }
597 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
598 }
599
600
601 /**
602 * Set the field's value given a serialized storage value. This is called
603 * when the field is loaded; if no data is available, the value will be
604 * null. You must implement this method if you implement
605 * @{method:shouldUseStorage}.
606 *
607 * Usually, the value can be loaded directly. If it isn't a scalar, you'll
608 * need to undo whatever serialization you applied in
609 * @{method:getValueForStorage}.
610 *
611 * @param string|null $value Serialized field representation (from
612 * @{method:getValueForStorage}) or null if no value has
613 * ever been stored.
614 * @return $this
615 * @task storage
616 */
617 public function setValueFromStorage($value) {
618 if ($this->proxy) {
619 return $this->proxy->setValueFromStorage($value);
620 }
621 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
622 }
623
624 public function didSetValueFromStorage() {
625 if ($this->proxy) {
626 return $this->proxy->didSetValueFromStorage();
627 }
628 return $this;
629 }
630
631
632/* -( ApplicationSearch )-------------------------------------------------- */
633
634
635 /**
636 * Appearing in ApplicationSearch allows a field to be indexed and searched
637 * for.
638 *
639 * @return bool True to appear in ApplicationSearch.
640 * @task appsearch
641 */
642 public function shouldAppearInApplicationSearch() {
643 if ($this->proxy) {
644 return $this->proxy->shouldAppearInApplicationSearch();
645 }
646 return false;
647 }
648
649
650 /**
651 * Return one or more indexes which this field can meaningfully query against
652 * to implement ApplicationSearch.
653 *
654 * Normally, you should build these using @{method:newStringIndex} and
655 * @{method:newNumericIndex}. For example, if a field holds a numeric value
656 * it might return a single numeric index:
657 *
658 * return array($this->newNumericIndex($this->getValue()));
659 *
660 * If a field holds a more complex value (like a list of users), it might
661 * return several string indexes:
662 *
663 * $indexes = array();
664 * foreach ($this->getValue() as $phid) {
665 * $indexes[] = $this->newStringIndex($phid);
666 * }
667 * return $indexes;
668 *
669 * @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
670 * @task appsearch
671 */
672 public function buildFieldIndexes() {
673 if ($this->proxy) {
674 return $this->proxy->buildFieldIndexes();
675 }
676 return array();
677 }
678
679
680 /**
681 * Return an index against which this field can be meaningfully ordered
682 * against to implement ApplicationSearch.
683 *
684 * This should be a single index, normally built using
685 * @{method:newStringIndex} and @{method:newNumericIndex}.
686 *
687 * The value of the index is not used.
688 *
689 * @return PhabricatorCustomFieldIndexStorage|null A single index to order
690 * by, or null if this field cannot be ordered.
691 * @task appsearch
692 */
693 public function buildOrderIndex() {
694 if ($this->proxy) {
695 return $this->proxy->buildOrderIndex();
696 }
697 return null;
698 }
699
700
701 /**
702 * Build a new empty storage object for storing string indexes. Normally,
703 * this should be a concrete subclass of
704 * @{class:PhabricatorCustomFieldStringIndexStorage}.
705 *
706 * @return PhabricatorCustomFieldStringIndexStorage Storage object.
707 * @task appsearch
708 */
709 protected function newStringIndexStorage() {
710 // NOTE: This intentionally isn't proxied, to avoid call cycles.
711 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
712 }
713
714
715 /**
716 * Build a new empty storage object for storing string indexes. Normally,
717 * this should be a concrete subclass of
718 * @{class:PhabricatorCustomFieldStringIndexStorage}.
719 *
720 * @return PhabricatorCustomFieldStringIndexStorage Storage object.
721 * @task appsearch
722 */
723 protected function newNumericIndexStorage() {
724 // NOTE: This intentionally isn't proxied, to avoid call cycles.
725 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
726 }
727
728
729 /**
730 * Build and populate storage for a string index.
731 *
732 * @param string $value String to index.
733 * @return PhabricatorCustomFieldStringIndexStorage Populated storage.
734 * @task appsearch
735 */
736 protected function newStringIndex($value) {
737 if ($this->proxy) {
738 return $this->proxy->newStringIndex();
739 }
740
741 $key = $this->getFieldIndex();
742 return $this->newStringIndexStorage()
743 ->setIndexKey($key)
744 ->setIndexValue($value);
745 }
746
747
748 /**
749 * Build and populate storage for a numeric index.
750 *
751 * @param int $value Numeric value to index.
752 * @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
753 * @task appsearch
754 */
755 protected function newNumericIndex($value) {
756 if ($this->proxy) {
757 return $this->proxy->newNumericIndex();
758 }
759 $key = $this->getFieldIndex();
760 return $this->newNumericIndexStorage()
761 ->setIndexKey($key)
762 ->setIndexValue($value);
763 }
764
765
766 /**
767 * Read a query value from a request, for storage in a saved query. Normally,
768 * this method should, e.g., read a string out of the request.
769 *
770 * @param PhabricatorApplicationSearchEngine $engine Engine building the
771 * query.
772 * @param AphrontRequest $request Request to read from.
773 * @return mixed
774 * @task appsearch
775 */
776 public function readApplicationSearchValueFromRequest(
777 PhabricatorApplicationSearchEngine $engine,
778 AphrontRequest $request) {
779 if ($this->proxy) {
780 return $this->proxy->readApplicationSearchValueFromRequest(
781 $engine,
782 $request);
783 }
784 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
785 }
786
787
788 /**
789 * Constrain a query, given a field value. Generally, this method should
790 * use `with...()` methods to apply filters or other constraints to the
791 * query.
792 *
793 * @param PhabricatorApplicationSearchEngine $engine Engine executing the
794 * query.
795 * @param PhabricatorCursorPagedPolicyAwareQuery $query Query to constrain.
796 * @param mixed $value Constraint provided by the user.
797 * @task appsearch
798 */
799 public function applyApplicationSearchConstraintToQuery(
800 PhabricatorApplicationSearchEngine $engine,
801 PhabricatorCursorPagedPolicyAwareQuery $query,
802 $value) {
803 if ($this->proxy) {
804 return $this->proxy->applyApplicationSearchConstraintToQuery(
805 $engine,
806 $query,
807 $value);
808 }
809 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
810 }
811
812
813 /**
814 * Append search controls to the interface.
815 *
816 * @param PhabricatorApplicationSearchEngine $engine Engine constructing the
817 * form.
818 * @param AphrontFormView $form The form to update.
819 * @param mixed $value Value from the saved query.
820 * @task appsearch
821 */
822 public function appendToApplicationSearchForm(
823 PhabricatorApplicationSearchEngine $engine,
824 AphrontFormView $form,
825 $value) {
826 if ($this->proxy) {
827 return $this->proxy->appendToApplicationSearchForm(
828 $engine,
829 $form,
830 $value);
831 }
832 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
833 }
834
835
836/* -( ApplicationTransactions )-------------------------------------------- */
837
838
839 /**
840 * Appearing in ApplicationTransactions allows a field to be edited using
841 * standard workflows.
842 *
843 * @return bool True to appear in ApplicationTransactions.
844 * @task appxaction
845 */
846 public function shouldAppearInApplicationTransactions() {
847 if ($this->proxy) {
848 return $this->proxy->shouldAppearInApplicationTransactions();
849 }
850 return false;
851 }
852
853
854 /**
855 * @task appxaction
856 */
857 public function getApplicationTransactionType() {
858 if ($this->proxy) {
859 return $this->proxy->getApplicationTransactionType();
860 }
861 return PhabricatorTransactions::TYPE_CUSTOMFIELD;
862 }
863
864
865 /**
866 * @task appxaction
867 */
868 public function getApplicationTransactionMetadata() {
869 if ($this->proxy) {
870 return $this->proxy->getApplicationTransactionMetadata();
871 }
872 return array();
873 }
874
875
876 /**
877 * @task appxaction
878 */
879 public function getOldValueForApplicationTransactions() {
880 if ($this->proxy) {
881 return $this->proxy->getOldValueForApplicationTransactions();
882 }
883 return $this->getValueForStorage();
884 }
885
886
887 /**
888 * @task appxaction
889 */
890 public function getNewValueForApplicationTransactions() {
891 if ($this->proxy) {
892 return $this->proxy->getNewValueForApplicationTransactions();
893 }
894 return $this->getValueForStorage();
895 }
896
897
898 /**
899 * @task appxaction
900 */
901 public function setValueFromApplicationTransactions($value) {
902 if ($this->proxy) {
903 return $this->proxy->setValueFromApplicationTransactions($value);
904 }
905 return $this->setValueFromStorage($value);
906 }
907
908
909 /**
910 * @task appxaction
911 */
912 public function getNewValueFromApplicationTransactions(
913 PhabricatorApplicationTransaction $xaction) {
914 if ($this->proxy) {
915 return $this->proxy->getNewValueFromApplicationTransactions($xaction);
916 }
917 return $xaction->getNewValue();
918 }
919
920
921 /**
922 * @task appxaction
923 */
924 public function getApplicationTransactionHasEffect(
925 PhabricatorApplicationTransaction $xaction) {
926 if ($this->proxy) {
927 return $this->proxy->getApplicationTransactionHasEffect($xaction);
928 }
929 return ($xaction->getOldValue() !== $xaction->getNewValue());
930 }
931
932
933 /**
934 * @task appxaction
935 */
936 public function applyApplicationTransactionInternalEffects(
937 PhabricatorApplicationTransaction $xaction) {
938 if ($this->proxy) {
939 return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
940 }
941 return;
942 }
943
944
945 /**
946 * @task appxaction
947 */
948 public function getApplicationTransactionRemarkupBlocks(
949 PhabricatorApplicationTransaction $xaction) {
950 if ($this->proxy) {
951 return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
952 }
953 return array();
954 }
955
956
957 /**
958 * @task appxaction
959 */
960 public function applyApplicationTransactionExternalEffects(
961 PhabricatorApplicationTransaction $xaction) {
962 if ($this->proxy) {
963 return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
964 }
965
966 if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
967 return;
968 }
969
970 $this->setValueFromApplicationTransactions($xaction->getNewValue());
971 $value = $this->getValueForStorage();
972
973 $table = $this->newStorageObject();
974 $conn_w = $table->establishConnection('w');
975
976 if ($value === null) {
977 queryfx(
978 $conn_w,
979 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
980 $table->getTableName(),
981 $this->getObject()->getPHID(),
982 $this->getFieldIndex());
983 } else {
984 queryfx(
985 $conn_w,
986 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
987 VALUES (%s, %s, %s)
988 ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
989 $table->getTableName(),
990 $this->getObject()->getPHID(),
991 $this->getFieldIndex(),
992 $value);
993 }
994
995 return;
996 }
997
998
999 /**
1000 * Validate transactions for an object. This allows you to raise an error
1001 * when a transaction would set a field to an invalid value, or when a field
1002 * is required but no transactions provide value.
1003 *
1004 * @param PhabricatorApplicationTransactionEditor $editor Editor applying the
1005 * transactions.
1006 * @param string $type Transaction type. This type is always
1007 * `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
1008 * convenience when constructing exceptions.
1009 * @param list<PhabricatorApplicationTransaction> $xactions Transactions
1010 * being applied, which may be empty if this field is not being edited.
1011 * @return list<PhabricatorApplicationTransactionValidationError> Validation
1012 * errors.
1013 *
1014 * @task appxaction
1015 */
1016 public function validateApplicationTransactions(
1017 PhabricatorApplicationTransactionEditor $editor,
1018 $type,
1019 array $xactions) {
1020 if ($this->proxy) {
1021 return $this->proxy->validateApplicationTransactions(
1022 $editor,
1023 $type,
1024 $xactions);
1025 }
1026 return array();
1027 }
1028
1029 public function getApplicationTransactionTitle(
1030 PhabricatorApplicationTransaction $xaction) {
1031 if ($this->proxy) {
1032 return $this->proxy->getApplicationTransactionTitle(
1033 $xaction);
1034 }
1035
1036 $author_phid = $xaction->getAuthorPHID();
1037 return pht(
1038 '%s updated this object.',
1039 $xaction->renderHandleLink($author_phid));
1040 }
1041
1042 public function getApplicationTransactionTitleForFeed(
1043 PhabricatorApplicationTransaction $xaction) {
1044 if ($this->proxy) {
1045 return $this->proxy->getApplicationTransactionTitleForFeed(
1046 $xaction);
1047 }
1048
1049 $author_phid = $xaction->getAuthorPHID();
1050 $object_phid = $xaction->getObjectPHID();
1051 return pht(
1052 '%s updated %s.',
1053 $xaction->renderHandleLink($author_phid),
1054 $xaction->renderHandleLink($object_phid));
1055 }
1056
1057
1058 public function getApplicationTransactionHasChangeDetails(
1059 PhabricatorApplicationTransaction $xaction) {
1060 if ($this->proxy) {
1061 return $this->proxy->getApplicationTransactionHasChangeDetails(
1062 $xaction);
1063 }
1064 return false;
1065 }
1066
1067 public function getApplicationTransactionChangeDetails(
1068 PhabricatorApplicationTransaction $xaction,
1069 PhabricatorUser $viewer) {
1070 if ($this->proxy) {
1071 return $this->proxy->getApplicationTransactionChangeDetails(
1072 $xaction,
1073 $viewer);
1074 }
1075 return null;
1076 }
1077
1078 public function getApplicationTransactionRequiredHandlePHIDs(
1079 PhabricatorApplicationTransaction $xaction) {
1080 if ($this->proxy) {
1081 return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
1082 $xaction);
1083 }
1084 return array();
1085 }
1086
1087 public function shouldHideInApplicationTransactions(
1088 PhabricatorApplicationTransaction $xaction) {
1089 if ($this->proxy) {
1090 return $this->proxy->shouldHideInApplicationTransactions($xaction);
1091 }
1092 return false;
1093 }
1094
1095
1096/* -( Transaction Mail )--------------------------------------------------- */
1097
1098
1099 /**
1100 * @task xactionmail
1101 */
1102 public function shouldAppearInTransactionMail() {
1103 if ($this->proxy) {
1104 return $this->proxy->shouldAppearInTransactionMail();
1105 }
1106 return false;
1107 }
1108
1109
1110 /**
1111 * @task xactionmail
1112 */
1113 public function updateTransactionMailBody(
1114 PhabricatorMetaMTAMailBody $body,
1115 PhabricatorApplicationTransactionEditor $editor,
1116 array $xactions) {
1117 if ($this->proxy) {
1118 return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
1119 }
1120 return;
1121 }
1122
1123
1124/* -( Edit View )---------------------------------------------------------- */
1125
1126
1127 public function getEditEngineFields(PhabricatorEditEngine $engine) {
1128 $field = $this->newStandardEditField();
1129
1130 return array(
1131 $field,
1132 );
1133 }
1134
1135 protected function newEditField() {
1136 $field = id(new PhabricatorCustomFieldEditField())
1137 ->setCustomField($this);
1138
1139 $http_type = $this->getHTTPParameterType();
1140 if ($http_type) {
1141 $field->setCustomFieldHTTPParameterType($http_type);
1142 }
1143
1144 $conduit_type = $this->getConduitEditParameterType();
1145 if ($conduit_type) {
1146 $field->setCustomFieldConduitParameterType($conduit_type);
1147 }
1148
1149 $bulk_type = $this->getBulkParameterType();
1150 if ($bulk_type) {
1151 $field->setCustomFieldBulkParameterType($bulk_type);
1152 }
1153
1154 $comment_action = $this->getCommentAction();
1155 if ($comment_action) {
1156 $field
1157 ->setCustomFieldCommentAction($comment_action)
1158 ->setCommentActionLabel(
1159 pht(
1160 'Change %s',
1161 $this->getFieldName()));
1162 }
1163
1164 return $field;
1165 }
1166
1167 protected function newStandardEditField() {
1168 if ($this->proxy) {
1169 return $this->proxy->newStandardEditField();
1170 }
1171
1172 if ($this->shouldAppearInEditView()) {
1173 $form_field = true;
1174 } else {
1175 $form_field = false;
1176 }
1177
1178 $bulk_label = $this->getBulkEditLabel();
1179
1180 return $this->newEditField()
1181 ->setKey($this->getFieldKey())
1182 ->setEditTypeKey($this->getModernFieldKey())
1183 ->setLabel($this->getFieldName())
1184 ->setBulkEditLabel($bulk_label)
1185 ->setDescription($this->getFieldDescription())
1186 ->setTransactionType($this->getApplicationTransactionType())
1187 ->setIsFormField($form_field)
1188 ->setValue($this->getNewValueForApplicationTransactions());
1189 }
1190
1191 protected function getBulkEditLabel() {
1192 if ($this->proxy) {
1193 return $this->proxy->getBulkEditLabel();
1194 }
1195
1196 return pht('Set "%s" to', $this->getFieldName());
1197 }
1198
1199 public function getBulkParameterType() {
1200 return $this->newBulkParameterType();
1201 }
1202
1203 protected function newBulkParameterType() {
1204 if ($this->proxy) {
1205 return $this->proxy->newBulkParameterType();
1206 }
1207 return null;
1208 }
1209
1210 protected function getHTTPParameterType() {
1211 if ($this->proxy) {
1212 return $this->proxy->getHTTPParameterType();
1213 }
1214 return null;
1215 }
1216
1217 /**
1218 * @task edit
1219 */
1220 public function shouldAppearInEditView() {
1221 if ($this->proxy) {
1222 return $this->proxy->shouldAppearInEditView();
1223 }
1224 return false;
1225 }
1226
1227 /**
1228 * @task edit
1229 */
1230 public function shouldAppearInEditEngine() {
1231 if ($this->proxy) {
1232 return $this->proxy->shouldAppearInEditEngine();
1233 }
1234 return false;
1235 }
1236
1237
1238 /**
1239 * @task edit
1240 */
1241 public function readValueFromRequest(AphrontRequest $request) {
1242 if ($this->proxy) {
1243 return $this->proxy->readValueFromRequest($request);
1244 }
1245 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1246 }
1247
1248
1249 /**
1250 * @task edit
1251 */
1252 public function getRequiredHandlePHIDsForEdit() {
1253 if ($this->proxy) {
1254 return $this->proxy->getRequiredHandlePHIDsForEdit();
1255 }
1256 return array();
1257 }
1258
1259
1260 /**
1261 * @task edit
1262 */
1263 public function getInstructionsForEdit() {
1264 if ($this->proxy) {
1265 return $this->proxy->getInstructionsForEdit();
1266 }
1267 return null;
1268 }
1269
1270
1271 /**
1272 * @task edit
1273 */
1274 public function renderEditControl(array $handles) {
1275 if ($this->proxy) {
1276 return $this->proxy->renderEditControl($handles);
1277 }
1278 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1279 }
1280
1281
1282/* -( Property View )------------------------------------------------------ */
1283
1284
1285 /**
1286 * @task view
1287 */
1288 public function shouldAppearInPropertyView() {
1289 if ($this->proxy) {
1290 return $this->proxy->shouldAppearInPropertyView();
1291 }
1292 return false;
1293 }
1294
1295
1296 /**
1297 * @task view
1298 */
1299 public function renderPropertyViewLabel() {
1300 if ($this->proxy) {
1301 return $this->proxy->renderPropertyViewLabel();
1302 }
1303 return $this->getFieldName();
1304 }
1305
1306
1307 /**
1308 * @task view
1309 */
1310 public function renderPropertyViewValue(array $handles) {
1311 if ($this->proxy) {
1312 return $this->proxy->renderPropertyViewValue($handles);
1313 }
1314 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1315 }
1316
1317
1318 /**
1319 * @task view
1320 */
1321 public function getStyleForPropertyView() {
1322 if ($this->proxy) {
1323 return $this->proxy->getStyleForPropertyView();
1324 }
1325 return 'property';
1326 }
1327
1328
1329 /**
1330 * @task view
1331 */
1332 public function getIconForPropertyView() {
1333 if ($this->proxy) {
1334 return $this->proxy->getIconForPropertyView();
1335 }
1336 return null;
1337 }
1338
1339
1340 /**
1341 * @task view
1342 */
1343 public function getRequiredHandlePHIDsForPropertyView() {
1344 if ($this->proxy) {
1345 return $this->proxy->getRequiredHandlePHIDsForPropertyView();
1346 }
1347 return array();
1348 }
1349
1350
1351/* -( List View )---------------------------------------------------------- */
1352
1353
1354 /**
1355 * @task list
1356 */
1357 public function shouldAppearInListView() {
1358 if ($this->proxy) {
1359 return $this->proxy->shouldAppearInListView();
1360 }
1361 return false;
1362 }
1363
1364
1365 /**
1366 * @task list
1367 */
1368 public function renderOnListItem(PHUIObjectItemView $view) {
1369 if ($this->proxy) {
1370 return $this->proxy->renderOnListItem($view);
1371 }
1372 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1373 }
1374
1375
1376/* -( Global Search )------------------------------------------------------ */
1377
1378
1379 /**
1380 * @task globalsearch
1381 */
1382 public function shouldAppearInGlobalSearch() {
1383 if ($this->proxy) {
1384 return $this->proxy->shouldAppearInGlobalSearch();
1385 }
1386 return false;
1387 }
1388
1389
1390 /**
1391 * @task globalsearch
1392 */
1393 public function updateAbstractDocument(
1394 PhabricatorSearchAbstractDocument $document) {
1395 if ($this->proxy) {
1396 return $this->proxy->updateAbstractDocument($document);
1397 }
1398 return $document;
1399 }
1400
1401
1402/* -( Data Export )-------------------------------------------------------- */
1403
1404
1405 public function shouldAppearInDataExport() {
1406 if ($this->proxy) {
1407 return $this->proxy->shouldAppearInDataExport();
1408 }
1409
1410 try {
1411 $this->newExportFieldType();
1412 return true;
1413 } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
1414 return false;
1415 }
1416 }
1417
1418 public function newExportField() {
1419 if ($this->proxy) {
1420 return $this->proxy->newExportField();
1421 }
1422
1423 return $this->newExportFieldType()
1424 ->setLabel($this->getFieldName());
1425 }
1426
1427 public function newExportData() {
1428 if ($this->proxy) {
1429 return $this->proxy->newExportData();
1430 }
1431 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1432 }
1433
1434 protected function newExportFieldType() {
1435 if ($this->proxy) {
1436 return $this->proxy->newExportFieldType();
1437 }
1438 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1439 }
1440
1441
1442/* -( Conduit )------------------------------------------------------------ */
1443
1444
1445 /**
1446 * @task conduit
1447 */
1448 public function shouldAppearInConduitDictionary() {
1449 if ($this->proxy) {
1450 return $this->proxy->shouldAppearInConduitDictionary();
1451 }
1452 return false;
1453 }
1454
1455
1456 /**
1457 * @task conduit
1458 */
1459 public function getConduitDictionaryValue() {
1460 if ($this->proxy) {
1461 return $this->proxy->getConduitDictionaryValue();
1462 }
1463 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1464 }
1465
1466
1467 public function shouldAppearInConduitTransactions() {
1468 if ($this->proxy) {
1469 return $this->proxy->shouldAppearInConduitDictionary();
1470 }
1471 return false;
1472 }
1473
1474 public function getConduitSearchParameterType() {
1475 return $this->newConduitSearchParameterType();
1476 }
1477
1478 protected function newConduitSearchParameterType() {
1479 if ($this->proxy) {
1480 return $this->proxy->newConduitSearchParameterType();
1481 }
1482 return null;
1483 }
1484
1485 public function getConduitEditParameterType() {
1486 return $this->newConduitEditParameterType();
1487 }
1488
1489 protected function newConduitEditParameterType() {
1490 if ($this->proxy) {
1491 return $this->proxy->newConduitEditParameterType();
1492 }
1493 return null;
1494 }
1495
1496 public function getCommentAction() {
1497 return $this->newCommentAction();
1498 }
1499
1500 protected function newCommentAction() {
1501 if ($this->proxy) {
1502 return $this->proxy->newCommentAction();
1503 }
1504 return null;
1505 }
1506
1507
1508/* -( Herald )------------------------------------------------------------- */
1509
1510
1511 /**
1512 * Return `true` to make this field available in Herald.
1513 *
1514 * @return bool True to expose the field in Herald.
1515 * @task herald
1516 */
1517 public function shouldAppearInHerald() {
1518 if ($this->proxy) {
1519 return $this->proxy->shouldAppearInHerald();
1520 }
1521 return false;
1522 }
1523
1524
1525 /**
1526 * Get the name of the field in Herald. By default, this uses the
1527 * normal field name.
1528 *
1529 * @return string Herald field name.
1530 * @task herald
1531 */
1532 public function getHeraldFieldName() {
1533 if ($this->proxy) {
1534 return $this->proxy->getHeraldFieldName();
1535 }
1536 return $this->getFieldName();
1537 }
1538
1539
1540 /**
1541 * Get the field value for evaluation by Herald.
1542 *
1543 * @return mixed Field value.
1544 * @task herald
1545 */
1546 public function getHeraldFieldValue() {
1547 if ($this->proxy) {
1548 return $this->proxy->getHeraldFieldValue();
1549 }
1550 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1551 }
1552
1553
1554 /**
1555 * Get the available conditions for this field in Herald.
1556 *
1557 * @return list<string> List of Herald condition constants.
1558 * @task herald
1559 */
1560 public function getHeraldFieldConditions() {
1561 if ($this->proxy) {
1562 return $this->proxy->getHeraldFieldConditions();
1563 }
1564 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1565 }
1566
1567
1568 /**
1569 * Get the Herald value type for the given condition.
1570 *
1571 * @param string $condition Herald condition constant.
1572 * @return string|null Herald value type constant, or null to use the
1573 * default.
1574 * @task herald
1575 */
1576 public function getHeraldFieldValueType($condition) {
1577 if ($this->proxy) {
1578 return $this->proxy->getHeraldFieldValueType($condition);
1579 }
1580 return null;
1581 }
1582
1583 public function getHeraldFieldStandardType() {
1584 if ($this->proxy) {
1585 return $this->proxy->getHeraldFieldStandardType();
1586 }
1587 return null;
1588 }
1589
1590 public function getHeraldDatasource() {
1591 if ($this->proxy) {
1592 return $this->proxy->getHeraldDatasource();
1593 }
1594 return null;
1595 }
1596
1597
1598 public function shouldAppearInHeraldActions() {
1599 if ($this->proxy) {
1600 return $this->proxy->shouldAppearInHeraldActions();
1601 }
1602 return false;
1603 }
1604
1605
1606 public function getHeraldActionName() {
1607 if ($this->proxy) {
1608 return $this->proxy->getHeraldActionName();
1609 }
1610
1611 return null;
1612 }
1613
1614
1615 public function getHeraldActionStandardType() {
1616 if ($this->proxy) {
1617 return $this->proxy->getHeraldActionStandardType();
1618 }
1619
1620 return null;
1621 }
1622
1623
1624 public function getHeraldActionDescription($value) {
1625 if ($this->proxy) {
1626 return $this->proxy->getHeraldActionDescription($value);
1627 }
1628
1629 return null;
1630 }
1631
1632
1633 public function getHeraldActionEffectDescription($value) {
1634 if ($this->proxy) {
1635 return $this->proxy->getHeraldActionEffectDescription($value);
1636 }
1637
1638 return null;
1639 }
1640
1641
1642 public function getHeraldActionDatasource() {
1643 if ($this->proxy) {
1644 return $this->proxy->getHeraldActionDatasource();
1645 }
1646
1647 return null;
1648 }
1649
1650 private static function adjustCustomFieldsForObjectSubtype(
1651 PhabricatorCustomFieldInterface $object,
1652 $role,
1653 array $fields) {
1654 assert_instances_of($fields, self::class);
1655
1656 // We only apply subtype adjustment for some roles. For example, when
1657 // writing Herald rules or building a Search interface, we always want to
1658 // show all the fields in their default state, so we do not apply any
1659 // adjustments.
1660 $subtype_roles = array(
1661 self::ROLE_EDITENGINE,
1662 self::ROLE_VIEW,
1663 self::ROLE_EDIT,
1664 );
1665
1666 $subtype_roles = array_fuse($subtype_roles);
1667 if (!isset($subtype_roles[$role])) {
1668 return $fields;
1669 }
1670
1671 // If the object doesn't support subtypes, we can't possibly make
1672 // any adjustments based on subtype.
1673 if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {
1674 return $fields;
1675 }
1676
1677 $subtype_map = $object->newEditEngineSubtypeMap();
1678 $subtype_key = $object->getEditEngineSubtype();
1679 $subtype_object = $subtype_map->getSubtype($subtype_key);
1680
1681 $map = array();
1682 foreach ($fields as $field) {
1683 $modern_key = $field->getModernFieldKey();
1684 if (!strlen($modern_key)) {
1685 continue;
1686 }
1687
1688 $map[$modern_key] = $field;
1689 }
1690
1691 foreach ($map as $field_key => $field) {
1692 // For now, only support overriding standard custom fields. In the
1693 // future there's no technical or product reason we couldn't let you
1694 // override (some properties of) other fields like "Title", but they
1695 // don't usually support appropriate "setX()" methods today.
1696 if (!($field instanceof PhabricatorStandardCustomField)) {
1697 // For fields that are proxies on top of StandardCustomField, which
1698 // is how most application custom fields work today, we can reconfigure
1699 // the proxied field instead.
1700 $field = $field->getProxy();
1701 if (!$field || !($field instanceof PhabricatorStandardCustomField)) {
1702 continue;
1703 }
1704 }
1705
1706 $subtype_config = $subtype_object->getSubtypeFieldConfiguration(
1707 $field_key);
1708
1709 if (!$subtype_config) {
1710 continue;
1711 }
1712
1713 if (isset($subtype_config['disabled'])) {
1714 $field->setIsEnabled(!$subtype_config['disabled']);
1715 }
1716
1717 if (isset($subtype_config['name'])) {
1718 $field->setFieldName($subtype_config['name']);
1719 }
1720 }
1721
1722 return $fields;
1723 }
1724
1725}