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
{
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 )--------------------------- */
47 public static function getObjectFields(
48 PhabricatorCustomFieldInterface
$object,
52 $attachment = $object->getCustomFields();
53 } catch (PhabricatorDataNotAttachedException
$ex) {
54 $attachment = new PhabricatorCustomFieldAttachment();
55 $object->attachCustomFields($attachment);
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)) {
67 "Expected an array from %s for object of class '%s'.",
68 'getCustomFieldSpecificationForRole()',
72 $fields = self
::buildFieldList(
77 $fields = self
::adjustCustomFieldsForObjectSubtype(
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()) {
90 if (!$field->shouldEnableForRole($role)) {
96 foreach ($fields as $field) {
97 $field->setObject($object);
100 $field_list = new PhabricatorCustomFieldList($fields);
101 $attachment->addCustomFieldList($role, $field_list);
111 public static function getObjectField(
112 PhabricatorCustomFieldInterface
$object,
116 $fields = self
::getObjectFields($object, $role)->getFields();
118 return idx($fields, $field_key);
125 public static function buildFieldList(
129 array $options = array()) {
131 $field_objects = id(new PhutilClassMapQuery())
132 ->setAncestorClass($base_class)
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])) {
143 "Both '%s' and '%s' define a custom field with ".
144 "field key '%s'. Field keys must be unique.",
145 get_class($fields[$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'];
166 $is_disabled = $field->shouldDisableByDefault();
170 if ($field->canDisableField()) {
171 unset($fields[$key]);
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.
192 public function getFieldKey() {
194 return $this->proxy
->getFieldKey();
196 throw new PhabricatorCustomFieldImplementationIncompleteException(
198 $field_key_is_incomplete = true);
201 public function getModernFieldKey() {
203 return $this->proxy
->getModernFieldKey();
205 return $this->getFieldKey();
210 * Return a human-readable field name.
212 * @return string Human readable field name.
215 public function getFieldName() {
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.
230 public function getFieldDescription() {
232 return $this->proxy
->getFieldDescription();
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.
250 public function createFields($object) {
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.
264 public function isFieldEnabled() {
266 return $this->proxy
->isFieldEnabled();
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.
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.
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
:
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
335 public function canDisableField() {
339 public function shouldDisableByDefault() {
345 * Return an index string which uniquely identifies this field.
347 * @return string Index string which uniquely identifies this field.
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
370 * @return bool True if you can @{method:setProxy} this field.
373 public function canSetProxy() {
374 if ($this instanceof PhabricatorStandardCustomFieldInterface
) {
382 * Set the proxy implementation for this field. See @{method:canSetProxy} for
383 * discussion of field proxies.
385 * @param PhabricatorCustomField Field implementation.
388 final public function setProxy(PhabricatorCustomField
$proxy) {
389 if (!$this->canSetProxy()) {
390 throw new PhabricatorCustomFieldNotProxyException($this);
393 $this->proxy
= $proxy;
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() {
409 /* -( Contextual Data )---------------------------------------------------- */
413 * Sets the object this field belongs to.
415 * @param PhabricatorCustomFieldInterface The object this field belongs to.
419 final public function setObject(PhabricatorCustomFieldInterface
$object) {
421 $this->proxy
->setObject($object);
425 $this->object = $object;
426 $this->didSetObject($object);
432 * Read object data into local field storage, if applicable.
434 * @param PhabricatorCustomFieldInterface The object this field belongs to.
438 public function readValueFromObject(PhabricatorCustomFieldInterface
$object) {
440 $this->proxy
->readValueFromObject($object);
447 * Get the object this field belongs to.
449 * @return PhabricatorCustomFieldInterface The object this field belongs to.
452 final public function getObject() {
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.
467 protected function didSetObject(PhabricatorCustomFieldInterface
$object) {
475 final public function setViewer(PhabricatorUser
$viewer) {
477 $this->proxy
->setViewer($viewer);
481 $this->viewer
= $viewer;
489 final public function getViewer() {
491 return $this->proxy
->getViewer();
494 return $this->viewer
;
501 final protected function requireViewer() {
503 return $this->proxy
->requireViewer();
506 if (!$this->viewer
) {
507 throw new PhabricatorCustomFieldDataNotAvailableException($this);
509 return $this->viewer
;
513 /* -( Rendering Utilities )------------------------------------------------ */
519 protected function renderHandleList(array $handles) {
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.
550 public function shouldUseStorage() {
552 return $this->proxy
->shouldUseStorage();
559 * Return a new, empty storage object. This should be a subclass of
560 * @{class:PhabricatorCustomFieldStorage} which is bound to the application's
563 * @return PhabricatorCustomFieldStorage New empty storage object.
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.
583 public function getValueForStorage() {
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
607 public function setValueFromStorage($value) {
609 return $this->proxy
->setValueFromStorage($value);
611 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
614 public function didSetValueFromStorage() {
616 return $this->proxy
->didSetValueFromStorage();
622 /* -( ApplicationSearch )-------------------------------------------------- */
626 * Appearing in ApplicationSearch allows a field to be indexed and searched
629 * @return bool True to appear in ApplicationSearch.
632 public function shouldAppearInApplicationSearch() {
634 return $this->proxy
->shouldAppearInApplicationSearch();
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);
659 * @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
662 public function buildFieldIndexes() {
664 return $this->proxy
->buildFieldIndexes();
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.
684 public function buildOrderIndex() {
686 return $this->proxy
->buildOrderIndex();
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.
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.
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.
727 protected function newStringIndex($value) {
729 return $this->proxy
->newStringIndex();
732 $key = $this->getFieldIndex();
733 return $this->newStringIndexStorage()
735 ->setIndexValue($value);
740 * Build and populate storage for a numeric index.
742 * @param string Numeric value to index.
743 * @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
746 protected function newNumericIndex($value) {
748 return $this->proxy
->newNumericIndex();
750 $key = $this->getFieldIndex();
751 return $this->newNumericIndexStorage()
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.
766 public function readApplicationSearchValueFromRequest(
767 PhabricatorApplicationSearchEngine
$engine,
768 AphrontRequest
$request) {
770 return $this->proxy
->readApplicationSearchValueFromRequest(
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
783 * @param PhabricatorApplicationSearchEngine Engine executing the query.
784 * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
785 * @param wild Constraint provided by the user.
789 public function applyApplicationSearchConstraintToQuery(
790 PhabricatorApplicationSearchEngine
$engine,
791 PhabricatorCursorPagedPolicyAwareQuery
$query,
794 return $this->proxy
->applyApplicationSearchConstraintToQuery(
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.
812 public function appendToApplicationSearchForm(
813 PhabricatorApplicationSearchEngine
$engine,
814 AphrontFormView
$form,
817 return $this->proxy
->appendToApplicationSearchForm(
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.
836 public function shouldAppearInApplicationTransactions() {
838 return $this->proxy
->shouldAppearInApplicationTransactions();
847 public function getApplicationTransactionType() {
849 return $this->proxy
->getApplicationTransactionType();
851 return PhabricatorTransactions
::TYPE_CUSTOMFIELD
;
858 public function getApplicationTransactionMetadata() {
860 return $this->proxy
->getApplicationTransactionMetadata();
869 public function getOldValueForApplicationTransactions() {
871 return $this->proxy
->getOldValueForApplicationTransactions();
873 return $this->getValueForStorage();
880 public function getNewValueForApplicationTransactions() {
882 return $this->proxy
->getNewValueForApplicationTransactions();
884 return $this->getValueForStorage();
891 public function setValueFromApplicationTransactions($value) {
893 return $this->proxy
->setValueFromApplicationTransactions($value);
895 return $this->setValueFromStorage($value);
902 public function getNewValueFromApplicationTransactions(
903 PhabricatorApplicationTransaction
$xaction) {
905 return $this->proxy
->getNewValueFromApplicationTransactions($xaction);
907 return $xaction->getNewValue();
914 public function getApplicationTransactionHasEffect(
915 PhabricatorApplicationTransaction
$xaction) {
917 return $this->proxy
->getApplicationTransactionHasEffect($xaction);
919 return ($xaction->getOldValue() !== $xaction->getNewValue());
926 public function applyApplicationTransactionInternalEffects(
927 PhabricatorApplicationTransaction
$xaction) {
929 return $this->proxy
->applyApplicationTransactionInternalEffects($xaction);
938 public function getApplicationTransactionRemarkupBlocks(
939 PhabricatorApplicationTransaction
$xaction) {
941 return $this->proxy
->getApplicationTransactionRemarkupBlocks($xaction);
950 public function applyApplicationTransactionExternalEffects(
951 PhabricatorApplicationTransaction
$xaction) {
953 return $this->proxy
->applyApplicationTransactionExternalEffects($xaction);
956 if (!$this->shouldEnableForRole(self
::ROLE_STORAGE
)) {
960 $this->setValueFromApplicationTransactions($xaction->getNewValue());
961 $value = $this->getValueForStorage();
963 $table = $this->newStorageObject();
964 $conn_w = $table->establishConnection('w');
966 if ($value === null) {
969 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
970 $table->getTableName(),
971 $this->getObject()->getPHID(),
972 $this->getFieldIndex());
976 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
978 ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
979 $table->getTableName(),
980 $this->getObject()->getPHID(),
981 $this->getFieldIndex(),
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
1005 public function validateApplicationTransactions(
1006 PhabricatorApplicationTransactionEditor
$editor,
1010 return $this->proxy
->validateApplicationTransactions(
1018 public function getApplicationTransactionTitle(
1019 PhabricatorApplicationTransaction
$xaction) {
1021 return $this->proxy
->getApplicationTransactionTitle(
1025 $author_phid = $xaction->getAuthorPHID();
1027 '%s updated this object.',
1028 $xaction->renderHandleLink($author_phid));
1031 public function getApplicationTransactionTitleForFeed(
1032 PhabricatorApplicationTransaction
$xaction) {
1034 return $this->proxy
->getApplicationTransactionTitleForFeed(
1038 $author_phid = $xaction->getAuthorPHID();
1039 $object_phid = $xaction->getObjectPHID();
1042 $xaction->renderHandleLink($author_phid),
1043 $xaction->renderHandleLink($object_phid));
1047 public function getApplicationTransactionHasChangeDetails(
1048 PhabricatorApplicationTransaction
$xaction) {
1050 return $this->proxy
->getApplicationTransactionHasChangeDetails(
1056 public function getApplicationTransactionChangeDetails(
1057 PhabricatorApplicationTransaction
$xaction,
1058 PhabricatorUser
$viewer) {
1060 return $this->proxy
->getApplicationTransactionChangeDetails(
1067 public function getApplicationTransactionRequiredHandlePHIDs(
1068 PhabricatorApplicationTransaction
$xaction) {
1070 return $this->proxy
->getApplicationTransactionRequiredHandlePHIDs(
1076 public function shouldHideInApplicationTransactions(
1077 PhabricatorApplicationTransaction
$xaction) {
1079 return $this->proxy
->shouldHideInApplicationTransactions($xaction);
1085 /* -( Transaction Mail )--------------------------------------------------- */
1091 public function shouldAppearInTransactionMail() {
1093 return $this->proxy
->shouldAppearInTransactionMail();
1102 public function updateTransactionMailBody(
1103 PhabricatorMetaMTAMailBody
$body,
1104 PhabricatorApplicationTransactionEditor
$editor,
1107 return $this->proxy
->updateTransactionMailBody($body, $editor, $xactions);
1113 /* -( Edit View )---------------------------------------------------------- */
1116 public function getEditEngineFields(PhabricatorEditEngine
$engine) {
1117 $field = $this->newStandardEditField();
1124 protected function newEditField() {
1125 $field = id(new PhabricatorCustomFieldEditField())
1126 ->setCustomField($this);
1128 $http_type = $this->getHTTPParameterType();
1130 $field->setCustomFieldHTTPParameterType($http_type);
1133 $conduit_type = $this->getConduitEditParameterType();
1134 if ($conduit_type) {
1135 $field->setCustomFieldConduitParameterType($conduit_type);
1138 $bulk_type = $this->getBulkParameterType();
1140 $field->setCustomFieldBulkParameterType($bulk_type);
1143 $comment_action = $this->getCommentAction();
1144 if ($comment_action) {
1146 ->setCustomFieldCommentAction($comment_action)
1147 ->setCommentActionLabel(
1150 $this->getFieldName()));
1156 protected function newStandardEditField() {
1158 return $this->proxy
->newStandardEditField();
1161 if ($this->shouldAppearInEditView()) {
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() {
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() {
1194 return $this->proxy
->newBulkParameterType();
1199 protected function getHTTPParameterType() {
1201 return $this->proxy
->getHTTPParameterType();
1209 public function shouldAppearInEditView() {
1211 return $this->proxy
->shouldAppearInEditView();
1219 public function shouldAppearInEditEngine() {
1221 return $this->proxy
->shouldAppearInEditEngine();
1230 public function readValueFromRequest(AphrontRequest
$request) {
1232 return $this->proxy
->readValueFromRequest($request);
1234 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1241 public function getRequiredHandlePHIDsForEdit() {
1243 return $this->proxy
->getRequiredHandlePHIDsForEdit();
1252 public function getInstructionsForEdit() {
1254 return $this->proxy
->getInstructionsForEdit();
1263 public function renderEditControl(array $handles) {
1265 return $this->proxy
->renderEditControl($handles);
1267 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1271 /* -( Property View )------------------------------------------------------ */
1277 public function shouldAppearInPropertyView() {
1279 return $this->proxy
->shouldAppearInPropertyView();
1288 public function renderPropertyViewLabel() {
1290 return $this->proxy
->renderPropertyViewLabel();
1292 return $this->getFieldName();
1299 public function renderPropertyViewValue(array $handles) {
1301 return $this->proxy
->renderPropertyViewValue($handles);
1303 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1310 public function getStyleForPropertyView() {
1312 return $this->proxy
->getStyleForPropertyView();
1321 public function getIconForPropertyView() {
1323 return $this->proxy
->getIconForPropertyView();
1332 public function getRequiredHandlePHIDsForPropertyView() {
1334 return $this->proxy
->getRequiredHandlePHIDsForPropertyView();
1340 /* -( List View )---------------------------------------------------------- */
1346 public function shouldAppearInListView() {
1348 return $this->proxy
->shouldAppearInListView();
1357 public function renderOnListItem(PHUIObjectItemView
$view) {
1359 return $this->proxy
->renderOnListItem($view);
1361 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1365 /* -( Global Search )------------------------------------------------------ */
1369 * @task globalsearch
1371 public function shouldAppearInGlobalSearch() {
1373 return $this->proxy
->shouldAppearInGlobalSearch();
1380 * @task globalsearch
1382 public function updateAbstractDocument(
1383 PhabricatorSearchAbstractDocument
$document) {
1385 return $this->proxy
->updateAbstractDocument($document);
1391 /* -( Data Export )-------------------------------------------------------- */
1394 public function shouldAppearInDataExport() {
1396 return $this->proxy
->shouldAppearInDataExport();
1400 $this->newExportFieldType();
1402 } catch (PhabricatorCustomFieldImplementationIncompleteException
$ex) {
1407 public function newExportField() {
1409 return $this->proxy
->newExportField();
1412 return $this->newExportFieldType()
1413 ->setLabel($this->getFieldName());
1416 public function newExportData() {
1418 return $this->proxy
->newExportData();
1420 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1423 protected function newExportFieldType() {
1425 return $this->proxy
->newExportFieldType();
1427 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1431 /* -( Conduit )------------------------------------------------------------ */
1437 public function shouldAppearInConduitDictionary() {
1439 return $this->proxy
->shouldAppearInConduitDictionary();
1448 public function getConduitDictionaryValue() {
1450 return $this->proxy
->getConduitDictionaryValue();
1452 throw new PhabricatorCustomFieldImplementationIncompleteException($this);
1456 public function shouldAppearInConduitTransactions() {
1458 return $this->proxy
->shouldAppearInConduitDictionary();
1463 public function getConduitSearchParameterType() {
1464 return $this->newConduitSearchParameterType();
1467 protected function newConduitSearchParameterType() {
1469 return $this->proxy
->newConduitSearchParameterType();
1474 public function getConduitEditParameterType() {
1475 return $this->newConduitEditParameterType();
1478 protected function newConduitEditParameterType() {
1480 return $this->proxy
->newConduitEditParameterType();
1485 public function getCommentAction() {
1486 return $this->newCommentAction();
1489 protected function newCommentAction() {
1491 return $this->proxy
->newCommentAction();
1497 /* -( Herald )------------------------------------------------------------- */
1501 * Return `true` to make this field available in Herald.
1503 * @return bool True to expose the field in Herald.
1506 public function shouldAppearInHerald() {
1508 return $this->proxy
->shouldAppearInHerald();
1515 * Get the name of the field in Herald. By default, this uses the
1516 * normal field name.
1518 * @return string Herald field name.
1521 public function getHeraldFieldName() {
1523 return $this->proxy
->getHeraldFieldName();
1525 return $this->getFieldName();
1530 * Get the field value for evaluation by Herald.
1532 * @return wild Field value.
1535 public function getHeraldFieldValue() {
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.
1549 public function getHeraldFieldConditions() {
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.
1564 public function getHeraldFieldValueType($condition) {
1566 return $this->proxy
->getHeraldFieldValueType($condition);
1571 public function getHeraldFieldStandardType() {
1573 return $this->proxy
->getHeraldFieldStandardType();
1578 public function getHeraldDatasource() {
1580 return $this->proxy
->getHeraldDatasource();
1586 public function shouldAppearInHeraldActions() {
1588 return $this->proxy
->shouldAppearInHeraldActions();
1594 public function getHeraldActionName() {
1596 return $this->proxy
->getHeraldActionName();
1603 public function getHeraldActionStandardType() {
1605 return $this->proxy
->getHeraldActionStandardType();
1612 public function getHeraldActionDescription($value) {
1614 return $this->proxy
->getHeraldActionDescription($value);
1621 public function getHeraldActionEffectDescription($value) {
1623 return $this->proxy
->getHeraldActionEffectDescription($value);
1630 public function getHeraldActionDatasource() {
1632 return $this->proxy
->getHeraldActionDatasource();
1638 private static function adjustCustomFieldsForObjectSubtype(
1639 PhabricatorCustomFieldInterface
$object,
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
1648 $subtype_roles = array(
1649 self
::ROLE_EDITENGINE
,
1654 $subtype_roles = array_fuse($subtype_roles);
1655 if (!isset($subtype_roles[$role])) {
1659 // If the object doesn't support subtypes, we can't possibly make
1660 // any adjustments based on subtype.
1661 if (!($object instanceof PhabricatorEditEngineSubtypeInterface
)) {
1665 $subtype_map = $object->newEditEngineSubtypeMap();
1666 $subtype_key = $object->getEditEngineSubtype();
1667 $subtype_object = $subtype_map->getSubtype($subtype_key);
1670 foreach ($fields as $field) {
1671 $modern_key = $field->getModernFieldKey();
1672 if (!strlen($modern_key)) {
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
)) {
1694 $subtype_config = $subtype_object->getSubtypeFieldConfiguration(
1697 if (!$subtype_config) {
1701 if (isset($subtype_config['disabled'])) {
1702 $field->setIsEnabled(!$subtype_config['disabled']);
1705 if (isset($subtype_config['name'])) {
1706 $field->setFieldName($subtype_config['name']);