Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / infrastructure / customfield / field / PhabricatorCustomField.php
blob9e2bf6895f6e201ee66d814b6491356561cd85b7
1 <?php
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
19 abstract class PhabricatorCustomField extends Phobject {
21 private $viewer;
22 private $object;
23 private $proxy;
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';
41 /* -( Building Applications with Custom Fields )--------------------------- */
44 /**
45 * @task apps
47 public static function getObjectFields(
48 PhabricatorCustomFieldInterface $object,
49 $role) {
51 try {
52 $attachment = $object->getCustomFields();
53 } catch (PhabricatorDataNotAttachedException $ex) {
54 $attachment = new PhabricatorCustomFieldAttachment();
55 $object->attachCustomFields($attachment);
58 try {
59 $field_list = $attachment->getCustomFieldList($role);
60 } catch (PhabricatorCustomFieldNotAttachedException $ex) {
61 $base_class = $object->getCustomFieldBaseClass();
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)));
72 $fields = self::buildFieldList(
73 $base_class,
74 $spec,
75 $object);
77 $fields = self::adjustCustomFieldsForObjectSubtype(
78 $object,
79 $role,
80 $fields);
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;
90 if (!$field->shouldEnableForRole($role)) {
91 unset($fields[$key]);
92 continue;
96 foreach ($fields as $field) {
97 $field->setObject($object);
100 $field_list = new PhabricatorCustomFieldList($fields);
101 $attachment->addCustomFieldList($role, $field_list);
104 return $field_list;
109 * @task apps
111 public static function getObjectField(
112 PhabricatorCustomFieldInterface $object,
113 $role,
114 $field_key) {
116 $fields = self::getObjectFields($object, $role)->getFields();
118 return idx($fields, $field_key);
123 * @task apps
125 public static function buildFieldList(
126 $base_class,
127 array $spec,
128 $object,
129 array $options = array()) {
131 $field_objects = id(new PhutilClassMapQuery())
132 ->setAncestorClass($base_class)
133 ->execute();
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));
149 $fields[$key] = $field;
153 foreach ($fields as $key => $field) {
154 if (!$field->isFieldEnabled()) {
155 unset($fields[$key]);
159 $fields = array_select_keys($fields, array_keys($spec)) + $fields;
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();
169 if ($is_disabled) {
170 if ($field->canDisableField()) {
171 unset($fields[$key]);
177 return $fields;
181 /* -( Core Properties and Field Identity )--------------------------------- */
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.
189 * @return string String which uniquely identifies this field.
190 * @task core
192 public function getFieldKey() {
193 if ($this->proxy) {
194 return $this->proxy->getFieldKey();
196 throw new PhabricatorCustomFieldImplementationIncompleteException(
197 $this,
198 $field_key_is_incomplete = true);
201 public function getModernFieldKey() {
202 if ($this->proxy) {
203 return $this->proxy->getModernFieldKey();
205 return $this->getFieldKey();
210 * Return a human-readable field name.
212 * @return string Human readable field name.
213 * @task core
215 public function getFieldName() {
216 if ($this->proxy) {
217 return $this->proxy->getFieldName();
219 return $this->getModernFieldKey();
224 * Return a short, human-readable description of the field's behavior. This
225 * provides more context to administrators when they are customizing fields.
227 * @return string|null Optional human-readable description.
228 * @task core
230 public function getFieldDescription() {
231 if ($this->proxy) {
232 return $this->proxy->getFieldDescription();
234 return null;
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.
243 * For general implementations, the general field implementation can return
244 * multiple field instances here.
246 * @param object The object to create fields for.
247 * @return list<PhabricatorCustomField> List of fields.
248 * @task core
250 public function createFields($object) {
251 return array($this);
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.
261 * @return bool False to completely disable this field for all roles.
262 * @task core
264 public function isFieldEnabled() {
265 if ($this->proxy) {
266 return $this->proxy->isFieldEnabled();
268 return true;
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.
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.
282 * @return bool True to enable the field for the given role.
283 * @task core
285 public function shouldEnableForRole($role) {
287 // NOTE: All of these calls proxy individually, so we don't need to
288 // proxy this call as a whole.
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));
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.
331 * @return bool False to prevent this field from being disabled through
332 * configuration.
333 * @task core
335 public function canDisableField() {
336 return true;
339 public function shouldDisableByDefault() {
340 return false;
345 * Return an index string which uniquely identifies this field.
347 * @return string Index string which uniquely identifies this field.
348 * @task core
350 final public function getFieldIndex() {
351 return PhabricatorHash::digestForIndex($this->getFieldKey());
355 /* -( Field Proxies )------------------------------------------------------ */
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.
364 * This is most commonly used to implement configuration-driven custom fields
365 * using @{class:PhabricatorStandardCustomField}.
367 * This method must be overridden to return `true` before a field can accept
368 * proxies.
370 * @return bool True if you can @{method:setProxy} this field.
371 * @task proxy
373 public function canSetProxy() {
374 if ($this instanceof PhabricatorStandardCustomFieldInterface) {
375 return true;
377 return false;
382 * Set the proxy implementation for this field. See @{method:canSetProxy} for
383 * discussion of field proxies.
385 * @param PhabricatorCustomField Field implementation.
386 * @return this
388 final public function setProxy(PhabricatorCustomField $proxy) {
389 if (!$this->canSetProxy()) {
390 throw new PhabricatorCustomFieldNotProxyException($this);
393 $this->proxy = $proxy;
394 return $this;
399 * Get the field's proxy implementation, if any. For discussion, see
400 * @{method:canSetProxy}.
402 * @return PhabricatorCustomField|null Proxy field, if one is set.
404 final public function getProxy() {
405 return $this->proxy;
409 /* -( Contextual Data )---------------------------------------------------- */
413 * Sets the object this field belongs to.
415 * @param PhabricatorCustomFieldInterface The object this field belongs to.
416 * @return this
417 * @task context
419 final public function setObject(PhabricatorCustomFieldInterface $object) {
420 if ($this->proxy) {
421 $this->proxy->setObject($object);
422 return $this;
425 $this->object = $object;
426 $this->didSetObject($object);
427 return $this;
432 * Read object data into local field storage, if applicable.
434 * @param PhabricatorCustomFieldInterface The object this field belongs to.
435 * @return this
436 * @task context
438 public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
439 if ($this->proxy) {
440 $this->proxy->readValueFromObject($object);
442 return $this;
447 * Get the object this field belongs to.
449 * @return PhabricatorCustomFieldInterface The object this field belongs to.
450 * @task context
452 final public function getObject() {
453 if ($this->proxy) {
454 return $this->proxy->getObject();
457 return $this->object;
462 * This is a hook, primarily for subclasses to load object data.
464 * @return PhabricatorCustomFieldInterface The object this field belongs to.
465 * @return void
467 protected function didSetObject(PhabricatorCustomFieldInterface $object) {
468 return;
473 * @task context
475 final public function setViewer(PhabricatorUser $viewer) {
476 if ($this->proxy) {
477 $this->proxy->setViewer($viewer);
478 return $this;
481 $this->viewer = $viewer;
482 return $this;
487 * @task context
489 final public function getViewer() {
490 if ($this->proxy) {
491 return $this->proxy->getViewer();
494 return $this->viewer;
499 * @task context
501 final protected function requireViewer() {
502 if ($this->proxy) {
503 return $this->proxy->requireViewer();
506 if (!$this->viewer) {
507 throw new PhabricatorCustomFieldDataNotAvailableException($this);
509 return $this->viewer;
513 /* -( Rendering Utilities )------------------------------------------------ */
517 * @task render
519 protected function renderHandleList(array $handles) {
520 if (!$handles) {
521 return null;
524 $out = array();
525 foreach ($handles as $handle) {
526 $out[] = $handle->renderHovercardLink();
529 return phutil_implode_html(phutil_tag('br'), $out);
533 /* -( Storage )------------------------------------------------------------ */
537 * Return true to use field storage.
539 * Fields which can be edited by the user will most commonly use storage,
540 * while some other types of fields (for instance, those which just display
541 * information in some stylized way) may not. Many builtin fields do not use
542 * storage because their data is available on the object itself.
544 * If you implement this, you must also implement @{method:getValueForStorage}
545 * and @{method:setValueFromStorage}.
547 * @return bool True to use storage.
548 * @task storage
550 public function shouldUseStorage() {
551 if ($this->proxy) {
552 return $this->proxy->shouldUseStorage();
554 return false;
559 * Return a new, empty storage object. This should be a subclass of
560 * @{class:PhabricatorCustomFieldStorage} which is bound to the application's
561 * database.
563 * @return PhabricatorCustomFieldStorage New empty storage object.
564 * @task storage
566 public function newStorageObject() {
567 // NOTE: This intentionally isn't proxied, to avoid call cycles.
568 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
573 * Return a serialized representation of the field value, appropriate for
574 * storing in auxiliary field storage. You must implement this method if
575 * you implement @{method:shouldUseStorage}.
577 * If the field value is a scalar, it can be returned unmodiifed. If not,
578 * it should be serialized (for example, using JSON).
580 * @return string Serialized field value.
581 * @task storage
583 public function getValueForStorage() {
584 if ($this->proxy) {
585 return $this->proxy->getValueForStorage();
587 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
592 * Set the field's value given a serialized storage value. This is called
593 * when the field is loaded; if no data is available, the value will be
594 * null. You must implement this method if you implement
595 * @{method:shouldUseStorage}.
597 * Usually, the value can be loaded directly. If it isn't a scalar, you'll
598 * need to undo whatever serialization you applied in
599 * @{method:getValueForStorage}.
601 * @param string|null Serialized field representation (from
602 * @{method:getValueForStorage}) or null if no value has
603 * ever been stored.
604 * @return this
605 * @task storage
607 public function setValueFromStorage($value) {
608 if ($this->proxy) {
609 return $this->proxy->setValueFromStorage($value);
611 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
614 public function didSetValueFromStorage() {
615 if ($this->proxy) {
616 return $this->proxy->didSetValueFromStorage();
618 return $this;
622 /* -( ApplicationSearch )-------------------------------------------------- */
626 * Appearing in ApplicationSearch allows a field to be indexed and searched
627 * for.
629 * @return bool True to appear in ApplicationSearch.
630 * @task appsearch
632 public function shouldAppearInApplicationSearch() {
633 if ($this->proxy) {
634 return $this->proxy->shouldAppearInApplicationSearch();
636 return false;
641 * Return one or more indexes which this field can meaningfully query against
642 * to implement ApplicationSearch.
644 * Normally, you should build these using @{method:newStringIndex} and
645 * @{method:newNumericIndex}. For example, if a field holds a numeric value
646 * it might return a single numeric index:
648 * return array($this->newNumericIndex($this->getValue()));
650 * If a field holds a more complex value (like a list of users), it might
651 * return several string indexes:
653 * $indexes = array();
654 * foreach ($this->getValue() as $phid) {
655 * $indexes[] = $this->newStringIndex($phid);
657 * return $indexes;
659 * @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
660 * @task appsearch
662 public function buildFieldIndexes() {
663 if ($this->proxy) {
664 return $this->proxy->buildFieldIndexes();
666 return array();
671 * Return an index against which this field can be meaningfully ordered
672 * against to implement ApplicationSearch.
674 * This should be a single index, normally built using
675 * @{method:newStringIndex} and @{method:newNumericIndex}.
677 * The value of the index is not used.
679 * Return null from this method if the field can not be ordered.
681 * @return PhabricatorCustomFieldIndexStorage A single index to order by.
682 * @task appsearch
684 public function buildOrderIndex() {
685 if ($this->proxy) {
686 return $this->proxy->buildOrderIndex();
688 return null;
693 * Build a new empty storage object for storing string indexes. Normally,
694 * this should be a concrete subclass of
695 * @{class:PhabricatorCustomFieldStringIndexStorage}.
697 * @return PhabricatorCustomFieldStringIndexStorage Storage object.
698 * @task appsearch
700 protected function newStringIndexStorage() {
701 // NOTE: This intentionally isn't proxied, to avoid call cycles.
702 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
707 * Build a new empty storage object for storing string indexes. Normally,
708 * this should be a concrete subclass of
709 * @{class:PhabricatorCustomFieldStringIndexStorage}.
711 * @return PhabricatorCustomFieldStringIndexStorage Storage object.
712 * @task appsearch
714 protected function newNumericIndexStorage() {
715 // NOTE: This intentionally isn't proxied, to avoid call cycles.
716 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
721 * Build and populate storage for a string index.
723 * @param string String to index.
724 * @return PhabricatorCustomFieldStringIndexStorage Populated storage.
725 * @task appsearch
727 protected function newStringIndex($value) {
728 if ($this->proxy) {
729 return $this->proxy->newStringIndex();
732 $key = $this->getFieldIndex();
733 return $this->newStringIndexStorage()
734 ->setIndexKey($key)
735 ->setIndexValue($value);
740 * Build and populate storage for a numeric index.
742 * @param string Numeric value to index.
743 * @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
744 * @task appsearch
746 protected function newNumericIndex($value) {
747 if ($this->proxy) {
748 return $this->proxy->newNumericIndex();
750 $key = $this->getFieldIndex();
751 return $this->newNumericIndexStorage()
752 ->setIndexKey($key)
753 ->setIndexValue($value);
758 * Read a query value from a request, for storage in a saved query. Normally,
759 * this method should, e.g., read a string out of the request.
761 * @param PhabricatorApplicationSearchEngine Engine building the query.
762 * @param AphrontRequest Request to read from.
763 * @return wild
764 * @task appsearch
766 public function readApplicationSearchValueFromRequest(
767 PhabricatorApplicationSearchEngine $engine,
768 AphrontRequest $request) {
769 if ($this->proxy) {
770 return $this->proxy->readApplicationSearchValueFromRequest(
771 $engine,
772 $request);
774 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
779 * Constrain a query, given a field value. Generally, this method should
780 * use `with...()` methods to apply filters or other constraints to the
781 * query.
783 * @param PhabricatorApplicationSearchEngine Engine executing the query.
784 * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
785 * @param wild Constraint provided by the user.
786 * @return void
787 * @task appsearch
789 public function applyApplicationSearchConstraintToQuery(
790 PhabricatorApplicationSearchEngine $engine,
791 PhabricatorCursorPagedPolicyAwareQuery $query,
792 $value) {
793 if ($this->proxy) {
794 return $this->proxy->applyApplicationSearchConstraintToQuery(
795 $engine,
796 $query,
797 $value);
799 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
804 * Append search controls to the interface.
806 * @param PhabricatorApplicationSearchEngine Engine constructing the form.
807 * @param AphrontFormView The form to update.
808 * @param wild Value from the saved query.
809 * @return void
810 * @task appsearch
812 public function appendToApplicationSearchForm(
813 PhabricatorApplicationSearchEngine $engine,
814 AphrontFormView $form,
815 $value) {
816 if ($this->proxy) {
817 return $this->proxy->appendToApplicationSearchForm(
818 $engine,
819 $form,
820 $value);
822 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
826 /* -( ApplicationTransactions )-------------------------------------------- */
830 * Appearing in ApplicationTrasactions allows a field to be edited using
831 * standard workflows.
833 * @return bool True to appear in ApplicationTransactions.
834 * @task appxaction
836 public function shouldAppearInApplicationTransactions() {
837 if ($this->proxy) {
838 return $this->proxy->shouldAppearInApplicationTransactions();
840 return false;
845 * @task appxaction
847 public function getApplicationTransactionType() {
848 if ($this->proxy) {
849 return $this->proxy->getApplicationTransactionType();
851 return PhabricatorTransactions::TYPE_CUSTOMFIELD;
856 * @task appxaction
858 public function getApplicationTransactionMetadata() {
859 if ($this->proxy) {
860 return $this->proxy->getApplicationTransactionMetadata();
862 return array();
867 * @task appxaction
869 public function getOldValueForApplicationTransactions() {
870 if ($this->proxy) {
871 return $this->proxy->getOldValueForApplicationTransactions();
873 return $this->getValueForStorage();
878 * @task appxaction
880 public function getNewValueForApplicationTransactions() {
881 if ($this->proxy) {
882 return $this->proxy->getNewValueForApplicationTransactions();
884 return $this->getValueForStorage();
889 * @task appxaction
891 public function setValueFromApplicationTransactions($value) {
892 if ($this->proxy) {
893 return $this->proxy->setValueFromApplicationTransactions($value);
895 return $this->setValueFromStorage($value);
900 * @task appxaction
902 public function getNewValueFromApplicationTransactions(
903 PhabricatorApplicationTransaction $xaction) {
904 if ($this->proxy) {
905 return $this->proxy->getNewValueFromApplicationTransactions($xaction);
907 return $xaction->getNewValue();
912 * @task appxaction
914 public function getApplicationTransactionHasEffect(
915 PhabricatorApplicationTransaction $xaction) {
916 if ($this->proxy) {
917 return $this->proxy->getApplicationTransactionHasEffect($xaction);
919 return ($xaction->getOldValue() !== $xaction->getNewValue());
924 * @task appxaction
926 public function applyApplicationTransactionInternalEffects(
927 PhabricatorApplicationTransaction $xaction) {
928 if ($this->proxy) {
929 return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
931 return;
936 * @task appxaction
938 public function getApplicationTransactionRemarkupBlocks(
939 PhabricatorApplicationTransaction $xaction) {
940 if ($this->proxy) {
941 return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
943 return array();
948 * @task appxaction
950 public function applyApplicationTransactionExternalEffects(
951 PhabricatorApplicationTransaction $xaction) {
952 if ($this->proxy) {
953 return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
956 if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
957 return;
960 $this->setValueFromApplicationTransactions($xaction->getNewValue());
961 $value = $this->getValueForStorage();
963 $table = $this->newStorageObject();
964 $conn_w = $table->establishConnection('w');
966 if ($value === null) {
967 queryfx(
968 $conn_w,
969 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
970 $table->getTableName(),
971 $this->getObject()->getPHID(),
972 $this->getFieldIndex());
973 } else {
974 queryfx(
975 $conn_w,
976 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
977 VALUES (%s, %s, %s)
978 ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
979 $table->getTableName(),
980 $this->getObject()->getPHID(),
981 $this->getFieldIndex(),
982 $value);
985 return;
990 * Validate transactions for an object. This allows you to raise an error
991 * when a transaction would set a field to an invalid value, or when a field
992 * is required but no transactions provide value.
994 * @param PhabricatorLiskDAO Editor applying the transactions.
995 * @param string Transaction type. This type is always
996 * `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
997 * convenience when constructing exceptions.
998 * @param list<PhabricatorApplicationTransaction> Transactions being applied,
999 * which may be empty if this field is not being edited.
1000 * @return list<PhabricatorApplicationTransactionValidationError> Validation
1001 * errors.
1003 * @task appxaction
1005 public function validateApplicationTransactions(
1006 PhabricatorApplicationTransactionEditor $editor,
1007 $type,
1008 array $xactions) {
1009 if ($this->proxy) {
1010 return $this->proxy->validateApplicationTransactions(
1011 $editor,
1012 $type,
1013 $xactions);
1015 return array();
1018 public function getApplicationTransactionTitle(
1019 PhabricatorApplicationTransaction $xaction) {
1020 if ($this->proxy) {
1021 return $this->proxy->getApplicationTransactionTitle(
1022 $xaction);
1025 $author_phid = $xaction->getAuthorPHID();
1026 return pht(
1027 '%s updated this object.',
1028 $xaction->renderHandleLink($author_phid));
1031 public function getApplicationTransactionTitleForFeed(
1032 PhabricatorApplicationTransaction $xaction) {
1033 if ($this->proxy) {
1034 return $this->proxy->getApplicationTransactionTitleForFeed(
1035 $xaction);
1038 $author_phid = $xaction->getAuthorPHID();
1039 $object_phid = $xaction->getObjectPHID();
1040 return pht(
1041 '%s updated %s.',
1042 $xaction->renderHandleLink($author_phid),
1043 $xaction->renderHandleLink($object_phid));
1047 public function getApplicationTransactionHasChangeDetails(
1048 PhabricatorApplicationTransaction $xaction) {
1049 if ($this->proxy) {
1050 return $this->proxy->getApplicationTransactionHasChangeDetails(
1051 $xaction);
1053 return false;
1056 public function getApplicationTransactionChangeDetails(
1057 PhabricatorApplicationTransaction $xaction,
1058 PhabricatorUser $viewer) {
1059 if ($this->proxy) {
1060 return $this->proxy->getApplicationTransactionChangeDetails(
1061 $xaction,
1062 $viewer);
1064 return null;
1067 public function getApplicationTransactionRequiredHandlePHIDs(
1068 PhabricatorApplicationTransaction $xaction) {
1069 if ($this->proxy) {
1070 return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
1071 $xaction);
1073 return array();
1076 public function shouldHideInApplicationTransactions(
1077 PhabricatorApplicationTransaction $xaction) {
1078 if ($this->proxy) {
1079 return $this->proxy->shouldHideInApplicationTransactions($xaction);
1081 return false;
1085 /* -( Transaction Mail )--------------------------------------------------- */
1089 * @task xactionmail
1091 public function shouldAppearInTransactionMail() {
1092 if ($this->proxy) {
1093 return $this->proxy->shouldAppearInTransactionMail();
1095 return false;
1100 * @task xactionmail
1102 public function updateTransactionMailBody(
1103 PhabricatorMetaMTAMailBody $body,
1104 PhabricatorApplicationTransactionEditor $editor,
1105 array $xactions) {
1106 if ($this->proxy) {
1107 return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
1109 return;
1113 /* -( Edit View )---------------------------------------------------------- */
1116 public function getEditEngineFields(PhabricatorEditEngine $engine) {
1117 $field = $this->newStandardEditField();
1119 return array(
1120 $field,
1124 protected function newEditField() {
1125 $field = id(new PhabricatorCustomFieldEditField())
1126 ->setCustomField($this);
1128 $http_type = $this->getHTTPParameterType();
1129 if ($http_type) {
1130 $field->setCustomFieldHTTPParameterType($http_type);
1133 $conduit_type = $this->getConduitEditParameterType();
1134 if ($conduit_type) {
1135 $field->setCustomFieldConduitParameterType($conduit_type);
1138 $bulk_type = $this->getBulkParameterType();
1139 if ($bulk_type) {
1140 $field->setCustomFieldBulkParameterType($bulk_type);
1143 $comment_action = $this->getCommentAction();
1144 if ($comment_action) {
1145 $field
1146 ->setCustomFieldCommentAction($comment_action)
1147 ->setCommentActionLabel(
1148 pht(
1149 'Change %s',
1150 $this->getFieldName()));
1153 return $field;
1156 protected function newStandardEditField() {
1157 if ($this->proxy) {
1158 return $this->proxy->newStandardEditField();
1161 if ($this->shouldAppearInEditView()) {
1162 $form_field = true;
1163 } else {
1164 $form_field = false;
1167 $bulk_label = $this->getBulkEditLabel();
1169 return $this->newEditField()
1170 ->setKey($this->getFieldKey())
1171 ->setEditTypeKey($this->getModernFieldKey())
1172 ->setLabel($this->getFieldName())
1173 ->setBulkEditLabel($bulk_label)
1174 ->setDescription($this->getFieldDescription())
1175 ->setTransactionType($this->getApplicationTransactionType())
1176 ->setIsFormField($form_field)
1177 ->setValue($this->getNewValueForApplicationTransactions());
1180 protected function getBulkEditLabel() {
1181 if ($this->proxy) {
1182 return $this->proxy->getBulkEditLabel();
1185 return pht('Set "%s" to', $this->getFieldName());
1188 public function getBulkParameterType() {
1189 return $this->newBulkParameterType();
1192 protected function newBulkParameterType() {
1193 if ($this->proxy) {
1194 return $this->proxy->newBulkParameterType();
1196 return null;
1199 protected function getHTTPParameterType() {
1200 if ($this->proxy) {
1201 return $this->proxy->getHTTPParameterType();
1203 return null;
1207 * @task edit
1209 public function shouldAppearInEditView() {
1210 if ($this->proxy) {
1211 return $this->proxy->shouldAppearInEditView();
1213 return false;
1217 * @task edit
1219 public function shouldAppearInEditEngine() {
1220 if ($this->proxy) {
1221 return $this->proxy->shouldAppearInEditEngine();
1223 return false;
1228 * @task edit
1230 public function readValueFromRequest(AphrontRequest $request) {
1231 if ($this->proxy) {
1232 return $this->proxy->readValueFromRequest($request);
1234 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1239 * @task edit
1241 public function getRequiredHandlePHIDsForEdit() {
1242 if ($this->proxy) {
1243 return $this->proxy->getRequiredHandlePHIDsForEdit();
1245 return array();
1250 * @task edit
1252 public function getInstructionsForEdit() {
1253 if ($this->proxy) {
1254 return $this->proxy->getInstructionsForEdit();
1256 return null;
1261 * @task edit
1263 public function renderEditControl(array $handles) {
1264 if ($this->proxy) {
1265 return $this->proxy->renderEditControl($handles);
1267 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1271 /* -( Property View )------------------------------------------------------ */
1275 * @task view
1277 public function shouldAppearInPropertyView() {
1278 if ($this->proxy) {
1279 return $this->proxy->shouldAppearInPropertyView();
1281 return false;
1286 * @task view
1288 public function renderPropertyViewLabel() {
1289 if ($this->proxy) {
1290 return $this->proxy->renderPropertyViewLabel();
1292 return $this->getFieldName();
1297 * @task view
1299 public function renderPropertyViewValue(array $handles) {
1300 if ($this->proxy) {
1301 return $this->proxy->renderPropertyViewValue($handles);
1303 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1308 * @task view
1310 public function getStyleForPropertyView() {
1311 if ($this->proxy) {
1312 return $this->proxy->getStyleForPropertyView();
1314 return 'property';
1319 * @task view
1321 public function getIconForPropertyView() {
1322 if ($this->proxy) {
1323 return $this->proxy->getIconForPropertyView();
1325 return null;
1330 * @task view
1332 public function getRequiredHandlePHIDsForPropertyView() {
1333 if ($this->proxy) {
1334 return $this->proxy->getRequiredHandlePHIDsForPropertyView();
1336 return array();
1340 /* -( List View )---------------------------------------------------------- */
1344 * @task list
1346 public function shouldAppearInListView() {
1347 if ($this->proxy) {
1348 return $this->proxy->shouldAppearInListView();
1350 return false;
1355 * @task list
1357 public function renderOnListItem(PHUIObjectItemView $view) {
1358 if ($this->proxy) {
1359 return $this->proxy->renderOnListItem($view);
1361 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1365 /* -( Global Search )------------------------------------------------------ */
1369 * @task globalsearch
1371 public function shouldAppearInGlobalSearch() {
1372 if ($this->proxy) {
1373 return $this->proxy->shouldAppearInGlobalSearch();
1375 return false;
1380 * @task globalsearch
1382 public function updateAbstractDocument(
1383 PhabricatorSearchAbstractDocument $document) {
1384 if ($this->proxy) {
1385 return $this->proxy->updateAbstractDocument($document);
1387 return $document;
1391 /* -( Data Export )-------------------------------------------------------- */
1394 public function shouldAppearInDataExport() {
1395 if ($this->proxy) {
1396 return $this->proxy->shouldAppearInDataExport();
1399 try {
1400 $this->newExportFieldType();
1401 return true;
1402 } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
1403 return false;
1407 public function newExportField() {
1408 if ($this->proxy) {
1409 return $this->proxy->newExportField();
1412 return $this->newExportFieldType()
1413 ->setLabel($this->getFieldName());
1416 public function newExportData() {
1417 if ($this->proxy) {
1418 return $this->proxy->newExportData();
1420 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1423 protected function newExportFieldType() {
1424 if ($this->proxy) {
1425 return $this->proxy->newExportFieldType();
1427 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1431 /* -( Conduit )------------------------------------------------------------ */
1435 * @task conduit
1437 public function shouldAppearInConduitDictionary() {
1438 if ($this->proxy) {
1439 return $this->proxy->shouldAppearInConduitDictionary();
1441 return false;
1446 * @task conduit
1448 public function getConduitDictionaryValue() {
1449 if ($this->proxy) {
1450 return $this->proxy->getConduitDictionaryValue();
1452 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1456 public function shouldAppearInConduitTransactions() {
1457 if ($this->proxy) {
1458 return $this->proxy->shouldAppearInConduitDictionary();
1460 return false;
1463 public function getConduitSearchParameterType() {
1464 return $this->newConduitSearchParameterType();
1467 protected function newConduitSearchParameterType() {
1468 if ($this->proxy) {
1469 return $this->proxy->newConduitSearchParameterType();
1471 return null;
1474 public function getConduitEditParameterType() {
1475 return $this->newConduitEditParameterType();
1478 protected function newConduitEditParameterType() {
1479 if ($this->proxy) {
1480 return $this->proxy->newConduitEditParameterType();
1482 return null;
1485 public function getCommentAction() {
1486 return $this->newCommentAction();
1489 protected function newCommentAction() {
1490 if ($this->proxy) {
1491 return $this->proxy->newCommentAction();
1493 return null;
1497 /* -( Herald )------------------------------------------------------------- */
1501 * Return `true` to make this field available in Herald.
1503 * @return bool True to expose the field in Herald.
1504 * @task herald
1506 public function shouldAppearInHerald() {
1507 if ($this->proxy) {
1508 return $this->proxy->shouldAppearInHerald();
1510 return false;
1515 * Get the name of the field in Herald. By default, this uses the
1516 * normal field name.
1518 * @return string Herald field name.
1519 * @task herald
1521 public function getHeraldFieldName() {
1522 if ($this->proxy) {
1523 return $this->proxy->getHeraldFieldName();
1525 return $this->getFieldName();
1530 * Get the field value for evaluation by Herald.
1532 * @return wild Field value.
1533 * @task herald
1535 public function getHeraldFieldValue() {
1536 if ($this->proxy) {
1537 return $this->proxy->getHeraldFieldValue();
1539 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1544 * Get the available conditions for this field in Herald.
1546 * @return list<const> List of Herald condition constants.
1547 * @task herald
1549 public function getHeraldFieldConditions() {
1550 if ($this->proxy) {
1551 return $this->proxy->getHeraldFieldConditions();
1553 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1558 * Get the Herald value type for the given condition.
1560 * @param const Herald condition constant.
1561 * @return const|null Herald value type, or null to use the default.
1562 * @task herald
1564 public function getHeraldFieldValueType($condition) {
1565 if ($this->proxy) {
1566 return $this->proxy->getHeraldFieldValueType($condition);
1568 return null;
1571 public function getHeraldFieldStandardType() {
1572 if ($this->proxy) {
1573 return $this->proxy->getHeraldFieldStandardType();
1575 return null;
1578 public function getHeraldDatasource() {
1579 if ($this->proxy) {
1580 return $this->proxy->getHeraldDatasource();
1582 return null;
1586 public function shouldAppearInHeraldActions() {
1587 if ($this->proxy) {
1588 return $this->proxy->shouldAppearInHeraldActions();
1590 return false;
1594 public function getHeraldActionName() {
1595 if ($this->proxy) {
1596 return $this->proxy->getHeraldActionName();
1599 return null;
1603 public function getHeraldActionStandardType() {
1604 if ($this->proxy) {
1605 return $this->proxy->getHeraldActionStandardType();
1608 return null;
1612 public function getHeraldActionDescription($value) {
1613 if ($this->proxy) {
1614 return $this->proxy->getHeraldActionDescription($value);
1617 return null;
1621 public function getHeraldActionEffectDescription($value) {
1622 if ($this->proxy) {
1623 return $this->proxy->getHeraldActionEffectDescription($value);
1626 return null;
1630 public function getHeraldActionDatasource() {
1631 if ($this->proxy) {
1632 return $this->proxy->getHeraldActionDatasource();
1635 return null;
1638 private static function adjustCustomFieldsForObjectSubtype(
1639 PhabricatorCustomFieldInterface $object,
1640 $role,
1641 array $fields) {
1642 assert_instances_of($fields, __CLASS__);
1644 // We only apply subtype adjustment for some roles. For example, when
1645 // writing Herald rules or building a Search interface, we always want to
1646 // show all the fields in their default state, so we do not apply any
1647 // adjustments.
1648 $subtype_roles = array(
1649 self::ROLE_EDITENGINE,
1650 self::ROLE_VIEW,
1651 self::ROLE_EDIT,
1654 $subtype_roles = array_fuse($subtype_roles);
1655 if (!isset($subtype_roles[$role])) {
1656 return $fields;
1659 // If the object doesn't support subtypes, we can't possibly make
1660 // any adjustments based on subtype.
1661 if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {
1662 return $fields;
1665 $subtype_map = $object->newEditEngineSubtypeMap();
1666 $subtype_key = $object->getEditEngineSubtype();
1667 $subtype_object = $subtype_map->getSubtype($subtype_key);
1669 $map = array();
1670 foreach ($fields as $field) {
1671 $modern_key = $field->getModernFieldKey();
1672 if (!strlen($modern_key)) {
1673 continue;
1676 $map[$modern_key] = $field;
1679 foreach ($map as $field_key => $field) {
1680 // For now, only support overriding standard custom fields. In the
1681 // future there's no technical or product reason we couldn't let you
1682 // override (some properites of) other fields like "Title", but they
1683 // don't usually support appropriate "setX()" methods today.
1684 if (!($field instanceof PhabricatorStandardCustomField)) {
1685 // For fields that are proxies on top of StandardCustomField, which
1686 // is how most application custom fields work today, we can reconfigure
1687 // the proxied field instead.
1688 $field = $field->getProxy();
1689 if (!$field || !($field instanceof PhabricatorStandardCustomField)) {
1690 continue;
1694 $subtype_config = $subtype_object->getSubtypeFieldConfiguration(
1695 $field_key);
1697 if (!$subtype_config) {
1698 continue;
1701 if (isset($subtype_config['disabled'])) {
1702 $field->setIsEnabled(!$subtype_config['disabled']);
1705 if (isset($subtype_config['name'])) {
1706 $field->setFieldName($subtype_config['name']);
1710 return $fields;