Path: blob/master/src/infrastructure/customfield/field/PhabricatorCustomField.php
12242 views
<?php12/**3* @task apps Building Applications with Custom Fields4* @task core Core Properties and Field Identity5* @task proxy Field Proxies6* @task context Contextual Data7* @task render Rendering Utilities8* @task storage Field Storage9* @task edit Integration with Edit Views10* @task view Integration with Property Views11* @task list Integration with List views12* @task appsearch Integration with ApplicationSearch13* @task appxaction Integration with ApplicationTransactions14* @task xactionmail Integration with Transaction Mail15* @task globalsearch Integration with Global Search16* @task herald Integration with Herald17*/18abstract class PhabricatorCustomField extends Phobject {1920private $viewer;21private $object;22private $proxy;2324const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';25const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail';26const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';27const ROLE_STORAGE = 'storage';28const ROLE_DEFAULT = 'default';29const ROLE_EDIT = 'edit';30const ROLE_VIEW = 'view';31const ROLE_LIST = 'list';32const ROLE_GLOBALSEARCH = 'GlobalSearch';33const ROLE_CONDUIT = 'conduit';34const ROLE_HERALD = 'herald';35const ROLE_EDITENGINE = 'EditEngine';36const ROLE_HERALDACTION = 'herald.action';37const ROLE_EXPORT = 'export';383940/* -( Building Applications with Custom Fields )--------------------------- */414243/**44* @task apps45*/46public static function getObjectFields(47PhabricatorCustomFieldInterface $object,48$role) {4950try {51$attachment = $object->getCustomFields();52} catch (PhabricatorDataNotAttachedException $ex) {53$attachment = new PhabricatorCustomFieldAttachment();54$object->attachCustomFields($attachment);55}5657try {58$field_list = $attachment->getCustomFieldList($role);59} catch (PhabricatorCustomFieldNotAttachedException $ex) {60$base_class = $object->getCustomFieldBaseClass();6162$spec = $object->getCustomFieldSpecificationForRole($role);63if (!is_array($spec)) {64throw new Exception(65pht(66"Expected an array from %s for object of class '%s'.",67'getCustomFieldSpecificationForRole()',68get_class($object)));69}7071$fields = self::buildFieldList(72$base_class,73$spec,74$object);7576$fields = self::adjustCustomFieldsForObjectSubtype(77$object,78$role,79$fields);8081foreach ($fields as $key => $field) {82// NOTE: We perform this filtering in "buildFieldList()", but may need83// to filter again after subtype adjustment.84if (!$field->isFieldEnabled()) {85unset($fields[$key]);86continue;87}8889if (!$field->shouldEnableForRole($role)) {90unset($fields[$key]);91continue;92}93}9495foreach ($fields as $field) {96$field->setObject($object);97}9899$field_list = new PhabricatorCustomFieldList($fields);100$attachment->addCustomFieldList($role, $field_list);101}102103return $field_list;104}105106107/**108* @task apps109*/110public static function getObjectField(111PhabricatorCustomFieldInterface $object,112$role,113$field_key) {114115$fields = self::getObjectFields($object, $role)->getFields();116117return idx($fields, $field_key);118}119120121/**122* @task apps123*/124public static function buildFieldList(125$base_class,126array $spec,127$object,128array $options = array()) {129130$field_objects = id(new PhutilClassMapQuery())131->setAncestorClass($base_class)132->execute();133134$fields = array();135foreach ($field_objects as $field_object) {136$field_object = clone $field_object;137foreach ($field_object->createFields($object) as $field) {138$key = $field->getFieldKey();139if (isset($fields[$key])) {140throw new Exception(141pht(142"Both '%s' and '%s' define a custom field with ".143"field key '%s'. Field keys must be unique.",144get_class($fields[$key]),145get_class($field),146$key));147}148$fields[$key] = $field;149}150}151152foreach ($fields as $key => $field) {153if (!$field->isFieldEnabled()) {154unset($fields[$key]);155}156}157158$fields = array_select_keys($fields, array_keys($spec)) + $fields;159160if (empty($options['withDisabled'])) {161foreach ($fields as $key => $field) {162if (isset($spec[$key]['disabled'])) {163$is_disabled = $spec[$key]['disabled'];164} else {165$is_disabled = $field->shouldDisableByDefault();166}167168if ($is_disabled) {169if ($field->canDisableField()) {170unset($fields[$key]);171}172}173}174}175176return $fields;177}178179180/* -( Core Properties and Field Identity )--------------------------------- */181182183/**184* Return a key which uniquely identifies this field, like185* "mycompany:dinosaur:count". Normally you should provide some level of186* namespacing to prevent collisions.187*188* @return string String which uniquely identifies this field.189* @task core190*/191public function getFieldKey() {192if ($this->proxy) {193return $this->proxy->getFieldKey();194}195throw new PhabricatorCustomFieldImplementationIncompleteException(196$this,197$field_key_is_incomplete = true);198}199200public function getModernFieldKey() {201if ($this->proxy) {202return $this->proxy->getModernFieldKey();203}204return $this->getFieldKey();205}206207208/**209* Return a human-readable field name.210*211* @return string Human readable field name.212* @task core213*/214public function getFieldName() {215if ($this->proxy) {216return $this->proxy->getFieldName();217}218return $this->getModernFieldKey();219}220221222/**223* Return a short, human-readable description of the field's behavior. This224* provides more context to administrators when they are customizing fields.225*226* @return string|null Optional human-readable description.227* @task core228*/229public function getFieldDescription() {230if ($this->proxy) {231return $this->proxy->getFieldDescription();232}233return null;234}235236237/**238* Most field implementations are unique, in that one class corresponds to239* one field. However, some field implementations are general and a single240* implementation may drive several fields.241*242* For general implementations, the general field implementation can return243* multiple field instances here.244*245* @param object The object to create fields for.246* @return list<PhabricatorCustomField> List of fields.247* @task core248*/249public function createFields($object) {250return array($this);251}252253254/**255* You can return `false` here if the field should not be enabled for any256* role. For example, it might depend on something (like an application or257* library) which isn't installed, or might have some global configuration258* which allows it to be disabled.259*260* @return bool False to completely disable this field for all roles.261* @task core262*/263public function isFieldEnabled() {264if ($this->proxy) {265return $this->proxy->isFieldEnabled();266}267return true;268}269270271/**272* Low level selector for field availability. Fields can appear in different273* roles (like an edit view, a list view, etc.), but not every field needs274* to appear everywhere. Fields that are disabled in a role won't appear in275* that context within applications.276*277* Normally, you do not need to override this method. Instead, override the278* methods specific to roles you want to enable. For example, implement279* @{method:shouldUseStorage()} to activate the `'storage'` role.280*281* @return bool True to enable the field for the given role.282* @task core283*/284public function shouldEnableForRole($role) {285286// NOTE: All of these calls proxy individually, so we don't need to287// proxy this call as a whole.288289switch ($role) {290case self::ROLE_APPLICATIONTRANSACTIONS:291return $this->shouldAppearInApplicationTransactions();292case self::ROLE_APPLICATIONSEARCH:293return $this->shouldAppearInApplicationSearch();294case self::ROLE_STORAGE:295return $this->shouldUseStorage();296case self::ROLE_EDIT:297return $this->shouldAppearInEditView();298case self::ROLE_VIEW:299return $this->shouldAppearInPropertyView();300case self::ROLE_LIST:301return $this->shouldAppearInListView();302case self::ROLE_GLOBALSEARCH:303return $this->shouldAppearInGlobalSearch();304case self::ROLE_CONDUIT:305return $this->shouldAppearInConduitDictionary();306case self::ROLE_TRANSACTIONMAIL:307return $this->shouldAppearInTransactionMail();308case self::ROLE_HERALD:309return $this->shouldAppearInHerald();310case self::ROLE_HERALDACTION:311return $this->shouldAppearInHeraldActions();312case self::ROLE_EDITENGINE:313return $this->shouldAppearInEditView() ||314$this->shouldAppearInEditEngine();315case self::ROLE_EXPORT:316return $this->shouldAppearInDataExport();317case self::ROLE_DEFAULT:318return true;319default:320throw new Exception(pht("Unknown field role '%s'!", $role));321}322}323324325/**326* Allow administrators to disable this field. Most fields should allow this,327* but some are fundamental to the behavior of the application and can be328* locked down to avoid chaos, disorder, and the decline of civilization.329*330* @return bool False to prevent this field from being disabled through331* configuration.332* @task core333*/334public function canDisableField() {335return true;336}337338public function shouldDisableByDefault() {339return false;340}341342343/**344* Return an index string which uniquely identifies this field.345*346* @return string Index string which uniquely identifies this field.347* @task core348*/349final public function getFieldIndex() {350return PhabricatorHash::digestForIndex($this->getFieldKey());351}352353354/* -( Field Proxies )------------------------------------------------------ */355356357/**358* Proxies allow a field to use some other field's implementation for most359* of their behavior while still subclassing an application field. When a360* proxy is set for a field with @{method:setProxy}, all of its methods will361* call through to the proxy by default.362*363* This is most commonly used to implement configuration-driven custom fields364* using @{class:PhabricatorStandardCustomField}.365*366* This method must be overridden to return `true` before a field can accept367* proxies.368*369* @return bool True if you can @{method:setProxy} this field.370* @task proxy371*/372public function canSetProxy() {373if ($this instanceof PhabricatorStandardCustomFieldInterface) {374return true;375}376return false;377}378379380/**381* Set the proxy implementation for this field. See @{method:canSetProxy} for382* discussion of field proxies.383*384* @param PhabricatorCustomField Field implementation.385* @return this386*/387final public function setProxy(PhabricatorCustomField $proxy) {388if (!$this->canSetProxy()) {389throw new PhabricatorCustomFieldNotProxyException($this);390}391392$this->proxy = $proxy;393return $this;394}395396397/**398* Get the field's proxy implementation, if any. For discussion, see399* @{method:canSetProxy}.400*401* @return PhabricatorCustomField|null Proxy field, if one is set.402*/403final public function getProxy() {404return $this->proxy;405}406407408/* -( Contextual Data )---------------------------------------------------- */409410411/**412* Sets the object this field belongs to.413*414* @param PhabricatorCustomFieldInterface The object this field belongs to.415* @return this416* @task context417*/418final public function setObject(PhabricatorCustomFieldInterface $object) {419if ($this->proxy) {420$this->proxy->setObject($object);421return $this;422}423424$this->object = $object;425$this->didSetObject($object);426return $this;427}428429430/**431* Read object data into local field storage, if applicable.432*433* @param PhabricatorCustomFieldInterface The object this field belongs to.434* @return this435* @task context436*/437public function readValueFromObject(PhabricatorCustomFieldInterface $object) {438if ($this->proxy) {439$this->proxy->readValueFromObject($object);440}441return $this;442}443444445/**446* Get the object this field belongs to.447*448* @return PhabricatorCustomFieldInterface The object this field belongs to.449* @task context450*/451final public function getObject() {452if ($this->proxy) {453return $this->proxy->getObject();454}455456return $this->object;457}458459460/**461* This is a hook, primarily for subclasses to load object data.462*463* @return PhabricatorCustomFieldInterface The object this field belongs to.464* @return void465*/466protected function didSetObject(PhabricatorCustomFieldInterface $object) {467return;468}469470471/**472* @task context473*/474final public function setViewer(PhabricatorUser $viewer) {475if ($this->proxy) {476$this->proxy->setViewer($viewer);477return $this;478}479480$this->viewer = $viewer;481return $this;482}483484485/**486* @task context487*/488final public function getViewer() {489if ($this->proxy) {490return $this->proxy->getViewer();491}492493return $this->viewer;494}495496497/**498* @task context499*/500final protected function requireViewer() {501if ($this->proxy) {502return $this->proxy->requireViewer();503}504505if (!$this->viewer) {506throw new PhabricatorCustomFieldDataNotAvailableException($this);507}508return $this->viewer;509}510511512/* -( Rendering Utilities )------------------------------------------------ */513514515/**516* @task render517*/518protected function renderHandleList(array $handles) {519if (!$handles) {520return null;521}522523$out = array();524foreach ($handles as $handle) {525$out[] = $handle->renderHovercardLink();526}527528return phutil_implode_html(phutil_tag('br'), $out);529}530531532/* -( Storage )------------------------------------------------------------ */533534535/**536* Return true to use field storage.537*538* Fields which can be edited by the user will most commonly use storage,539* while some other types of fields (for instance, those which just display540* information in some stylized way) may not. Many builtin fields do not use541* storage because their data is available on the object itself.542*543* If you implement this, you must also implement @{method:getValueForStorage}544* and @{method:setValueFromStorage}.545*546* @return bool True to use storage.547* @task storage548*/549public function shouldUseStorage() {550if ($this->proxy) {551return $this->proxy->shouldUseStorage();552}553return false;554}555556557/**558* Return a new, empty storage object. This should be a subclass of559* @{class:PhabricatorCustomFieldStorage} which is bound to the application's560* database.561*562* @return PhabricatorCustomFieldStorage New empty storage object.563* @task storage564*/565public function newStorageObject() {566// NOTE: This intentionally isn't proxied, to avoid call cycles.567throw new PhabricatorCustomFieldImplementationIncompleteException($this);568}569570571/**572* Return a serialized representation of the field value, appropriate for573* storing in auxiliary field storage. You must implement this method if574* you implement @{method:shouldUseStorage}.575*576* If the field value is a scalar, it can be returned unmodiifed. If not,577* it should be serialized (for example, using JSON).578*579* @return string Serialized field value.580* @task storage581*/582public function getValueForStorage() {583if ($this->proxy) {584return $this->proxy->getValueForStorage();585}586throw new PhabricatorCustomFieldImplementationIncompleteException($this);587}588589590/**591* Set the field's value given a serialized storage value. This is called592* when the field is loaded; if no data is available, the value will be593* null. You must implement this method if you implement594* @{method:shouldUseStorage}.595*596* Usually, the value can be loaded directly. If it isn't a scalar, you'll597* need to undo whatever serialization you applied in598* @{method:getValueForStorage}.599*600* @param string|null Serialized field representation (from601* @{method:getValueForStorage}) or null if no value has602* ever been stored.603* @return this604* @task storage605*/606public function setValueFromStorage($value) {607if ($this->proxy) {608return $this->proxy->setValueFromStorage($value);609}610throw new PhabricatorCustomFieldImplementationIncompleteException($this);611}612613public function didSetValueFromStorage() {614if ($this->proxy) {615return $this->proxy->didSetValueFromStorage();616}617return $this;618}619620621/* -( ApplicationSearch )-------------------------------------------------- */622623624/**625* Appearing in ApplicationSearch allows a field to be indexed and searched626* for.627*628* @return bool True to appear in ApplicationSearch.629* @task appsearch630*/631public function shouldAppearInApplicationSearch() {632if ($this->proxy) {633return $this->proxy->shouldAppearInApplicationSearch();634}635return false;636}637638639/**640* Return one or more indexes which this field can meaningfully query against641* to implement ApplicationSearch.642*643* Normally, you should build these using @{method:newStringIndex} and644* @{method:newNumericIndex}. For example, if a field holds a numeric value645* it might return a single numeric index:646*647* return array($this->newNumericIndex($this->getValue()));648*649* If a field holds a more complex value (like a list of users), it might650* return several string indexes:651*652* $indexes = array();653* foreach ($this->getValue() as $phid) {654* $indexes[] = $this->newStringIndex($phid);655* }656* return $indexes;657*658* @return list<PhabricatorCustomFieldIndexStorage> List of indexes.659* @task appsearch660*/661public function buildFieldIndexes() {662if ($this->proxy) {663return $this->proxy->buildFieldIndexes();664}665return array();666}667668669/**670* Return an index against which this field can be meaningfully ordered671* against to implement ApplicationSearch.672*673* This should be a single index, normally built using674* @{method:newStringIndex} and @{method:newNumericIndex}.675*676* The value of the index is not used.677*678* Return null from this method if the field can not be ordered.679*680* @return PhabricatorCustomFieldIndexStorage A single index to order by.681* @task appsearch682*/683public function buildOrderIndex() {684if ($this->proxy) {685return $this->proxy->buildOrderIndex();686}687return null;688}689690691/**692* Build a new empty storage object for storing string indexes. Normally,693* this should be a concrete subclass of694* @{class:PhabricatorCustomFieldStringIndexStorage}.695*696* @return PhabricatorCustomFieldStringIndexStorage Storage object.697* @task appsearch698*/699protected function newStringIndexStorage() {700// NOTE: This intentionally isn't proxied, to avoid call cycles.701throw new PhabricatorCustomFieldImplementationIncompleteException($this);702}703704705/**706* Build a new empty storage object for storing string indexes. Normally,707* this should be a concrete subclass of708* @{class:PhabricatorCustomFieldStringIndexStorage}.709*710* @return PhabricatorCustomFieldStringIndexStorage Storage object.711* @task appsearch712*/713protected function newNumericIndexStorage() {714// NOTE: This intentionally isn't proxied, to avoid call cycles.715throw new PhabricatorCustomFieldImplementationIncompleteException($this);716}717718719/**720* Build and populate storage for a string index.721*722* @param string String to index.723* @return PhabricatorCustomFieldStringIndexStorage Populated storage.724* @task appsearch725*/726protected function newStringIndex($value) {727if ($this->proxy) {728return $this->proxy->newStringIndex();729}730731$key = $this->getFieldIndex();732return $this->newStringIndexStorage()733->setIndexKey($key)734->setIndexValue($value);735}736737738/**739* Build and populate storage for a numeric index.740*741* @param string Numeric value to index.742* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.743* @task appsearch744*/745protected function newNumericIndex($value) {746if ($this->proxy) {747return $this->proxy->newNumericIndex();748}749$key = $this->getFieldIndex();750return $this->newNumericIndexStorage()751->setIndexKey($key)752->setIndexValue($value);753}754755756/**757* Read a query value from a request, for storage in a saved query. Normally,758* this method should, e.g., read a string out of the request.759*760* @param PhabricatorApplicationSearchEngine Engine building the query.761* @param AphrontRequest Request to read from.762* @return wild763* @task appsearch764*/765public function readApplicationSearchValueFromRequest(766PhabricatorApplicationSearchEngine $engine,767AphrontRequest $request) {768if ($this->proxy) {769return $this->proxy->readApplicationSearchValueFromRequest(770$engine,771$request);772}773throw new PhabricatorCustomFieldImplementationIncompleteException($this);774}775776777/**778* Constrain a query, given a field value. Generally, this method should779* use `with...()` methods to apply filters or other constraints to the780* query.781*782* @param PhabricatorApplicationSearchEngine Engine executing the query.783* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.784* @param wild Constraint provided by the user.785* @return void786* @task appsearch787*/788public function applyApplicationSearchConstraintToQuery(789PhabricatorApplicationSearchEngine $engine,790PhabricatorCursorPagedPolicyAwareQuery $query,791$value) {792if ($this->proxy) {793return $this->proxy->applyApplicationSearchConstraintToQuery(794$engine,795$query,796$value);797}798throw new PhabricatorCustomFieldImplementationIncompleteException($this);799}800801802/**803* Append search controls to the interface.804*805* @param PhabricatorApplicationSearchEngine Engine constructing the form.806* @param AphrontFormView The form to update.807* @param wild Value from the saved query.808* @return void809* @task appsearch810*/811public function appendToApplicationSearchForm(812PhabricatorApplicationSearchEngine $engine,813AphrontFormView $form,814$value) {815if ($this->proxy) {816return $this->proxy->appendToApplicationSearchForm(817$engine,818$form,819$value);820}821throw new PhabricatorCustomFieldImplementationIncompleteException($this);822}823824825/* -( ApplicationTransactions )-------------------------------------------- */826827828/**829* Appearing in ApplicationTrasactions allows a field to be edited using830* standard workflows.831*832* @return bool True to appear in ApplicationTransactions.833* @task appxaction834*/835public function shouldAppearInApplicationTransactions() {836if ($this->proxy) {837return $this->proxy->shouldAppearInApplicationTransactions();838}839return false;840}841842843/**844* @task appxaction845*/846public function getApplicationTransactionType() {847if ($this->proxy) {848return $this->proxy->getApplicationTransactionType();849}850return PhabricatorTransactions::TYPE_CUSTOMFIELD;851}852853854/**855* @task appxaction856*/857public function getApplicationTransactionMetadata() {858if ($this->proxy) {859return $this->proxy->getApplicationTransactionMetadata();860}861return array();862}863864865/**866* @task appxaction867*/868public function getOldValueForApplicationTransactions() {869if ($this->proxy) {870return $this->proxy->getOldValueForApplicationTransactions();871}872return $this->getValueForStorage();873}874875876/**877* @task appxaction878*/879public function getNewValueForApplicationTransactions() {880if ($this->proxy) {881return $this->proxy->getNewValueForApplicationTransactions();882}883return $this->getValueForStorage();884}885886887/**888* @task appxaction889*/890public function setValueFromApplicationTransactions($value) {891if ($this->proxy) {892return $this->proxy->setValueFromApplicationTransactions($value);893}894return $this->setValueFromStorage($value);895}896897898/**899* @task appxaction900*/901public function getNewValueFromApplicationTransactions(902PhabricatorApplicationTransaction $xaction) {903if ($this->proxy) {904return $this->proxy->getNewValueFromApplicationTransactions($xaction);905}906return $xaction->getNewValue();907}908909910/**911* @task appxaction912*/913public function getApplicationTransactionHasEffect(914PhabricatorApplicationTransaction $xaction) {915if ($this->proxy) {916return $this->proxy->getApplicationTransactionHasEffect($xaction);917}918return ($xaction->getOldValue() !== $xaction->getNewValue());919}920921922/**923* @task appxaction924*/925public function applyApplicationTransactionInternalEffects(926PhabricatorApplicationTransaction $xaction) {927if ($this->proxy) {928return $this->proxy->applyApplicationTransactionInternalEffects($xaction);929}930return;931}932933934/**935* @task appxaction936*/937public function getApplicationTransactionRemarkupBlocks(938PhabricatorApplicationTransaction $xaction) {939if ($this->proxy) {940return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);941}942return array();943}944945946/**947* @task appxaction948*/949public function applyApplicationTransactionExternalEffects(950PhabricatorApplicationTransaction $xaction) {951if ($this->proxy) {952return $this->proxy->applyApplicationTransactionExternalEffects($xaction);953}954955if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {956return;957}958959$this->setValueFromApplicationTransactions($xaction->getNewValue());960$value = $this->getValueForStorage();961962$table = $this->newStorageObject();963$conn_w = $table->establishConnection('w');964965if ($value === null) {966queryfx(967$conn_w,968'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',969$table->getTableName(),970$this->getObject()->getPHID(),971$this->getFieldIndex());972} else {973queryfx(974$conn_w,975'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)976VALUES (%s, %s, %s)977ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',978$table->getTableName(),979$this->getObject()->getPHID(),980$this->getFieldIndex(),981$value);982}983984return;985}986987988/**989* Validate transactions for an object. This allows you to raise an error990* when a transaction would set a field to an invalid value, or when a field991* is required but no transactions provide value.992*993* @param PhabricatorLiskDAO Editor applying the transactions.994* @param string Transaction type. This type is always995* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for996* convenience when constructing exceptions.997* @param list<PhabricatorApplicationTransaction> Transactions being applied,998* which may be empty if this field is not being edited.999* @return list<PhabricatorApplicationTransactionValidationError> Validation1000* errors.1001*1002* @task appxaction1003*/1004public function validateApplicationTransactions(1005PhabricatorApplicationTransactionEditor $editor,1006$type,1007array $xactions) {1008if ($this->proxy) {1009return $this->proxy->validateApplicationTransactions(1010$editor,1011$type,1012$xactions);1013}1014return array();1015}10161017public function getApplicationTransactionTitle(1018PhabricatorApplicationTransaction $xaction) {1019if ($this->proxy) {1020return $this->proxy->getApplicationTransactionTitle(1021$xaction);1022}10231024$author_phid = $xaction->getAuthorPHID();1025return pht(1026'%s updated this object.',1027$xaction->renderHandleLink($author_phid));1028}10291030public function getApplicationTransactionTitleForFeed(1031PhabricatorApplicationTransaction $xaction) {1032if ($this->proxy) {1033return $this->proxy->getApplicationTransactionTitleForFeed(1034$xaction);1035}10361037$author_phid = $xaction->getAuthorPHID();1038$object_phid = $xaction->getObjectPHID();1039return pht(1040'%s updated %s.',1041$xaction->renderHandleLink($author_phid),1042$xaction->renderHandleLink($object_phid));1043}104410451046public function getApplicationTransactionHasChangeDetails(1047PhabricatorApplicationTransaction $xaction) {1048if ($this->proxy) {1049return $this->proxy->getApplicationTransactionHasChangeDetails(1050$xaction);1051}1052return false;1053}10541055public function getApplicationTransactionChangeDetails(1056PhabricatorApplicationTransaction $xaction,1057PhabricatorUser $viewer) {1058if ($this->proxy) {1059return $this->proxy->getApplicationTransactionChangeDetails(1060$xaction,1061$viewer);1062}1063return null;1064}10651066public function getApplicationTransactionRequiredHandlePHIDs(1067PhabricatorApplicationTransaction $xaction) {1068if ($this->proxy) {1069return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(1070$xaction);1071}1072return array();1073}10741075public function shouldHideInApplicationTransactions(1076PhabricatorApplicationTransaction $xaction) {1077if ($this->proxy) {1078return $this->proxy->shouldHideInApplicationTransactions($xaction);1079}1080return false;1081}108210831084/* -( Transaction Mail )--------------------------------------------------- */108510861087/**1088* @task xactionmail1089*/1090public function shouldAppearInTransactionMail() {1091if ($this->proxy) {1092return $this->proxy->shouldAppearInTransactionMail();1093}1094return false;1095}109610971098/**1099* @task xactionmail1100*/1101public function updateTransactionMailBody(1102PhabricatorMetaMTAMailBody $body,1103PhabricatorApplicationTransactionEditor $editor,1104array $xactions) {1105if ($this->proxy) {1106return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);1107}1108return;1109}111011111112/* -( Edit View )---------------------------------------------------------- */111311141115public function getEditEngineFields(PhabricatorEditEngine $engine) {1116$field = $this->newStandardEditField();11171118return array(1119$field,1120);1121}11221123protected function newEditField() {1124$field = id(new PhabricatorCustomFieldEditField())1125->setCustomField($this);11261127$http_type = $this->getHTTPParameterType();1128if ($http_type) {1129$field->setCustomFieldHTTPParameterType($http_type);1130}11311132$conduit_type = $this->getConduitEditParameterType();1133if ($conduit_type) {1134$field->setCustomFieldConduitParameterType($conduit_type);1135}11361137$bulk_type = $this->getBulkParameterType();1138if ($bulk_type) {1139$field->setCustomFieldBulkParameterType($bulk_type);1140}11411142$comment_action = $this->getCommentAction();1143if ($comment_action) {1144$field1145->setCustomFieldCommentAction($comment_action)1146->setCommentActionLabel(1147pht(1148'Change %s',1149$this->getFieldName()));1150}11511152return $field;1153}11541155protected function newStandardEditField() {1156if ($this->proxy) {1157return $this->proxy->newStandardEditField();1158}11591160if ($this->shouldAppearInEditView()) {1161$form_field = true;1162} else {1163$form_field = false;1164}11651166$bulk_label = $this->getBulkEditLabel();11671168return $this->newEditField()1169->setKey($this->getFieldKey())1170->setEditTypeKey($this->getModernFieldKey())1171->setLabel($this->getFieldName())1172->setBulkEditLabel($bulk_label)1173->setDescription($this->getFieldDescription())1174->setTransactionType($this->getApplicationTransactionType())1175->setIsFormField($form_field)1176->setValue($this->getNewValueForApplicationTransactions());1177}11781179protected function getBulkEditLabel() {1180if ($this->proxy) {1181return $this->proxy->getBulkEditLabel();1182}11831184return pht('Set "%s" to', $this->getFieldName());1185}11861187public function getBulkParameterType() {1188return $this->newBulkParameterType();1189}11901191protected function newBulkParameterType() {1192if ($this->proxy) {1193return $this->proxy->newBulkParameterType();1194}1195return null;1196}11971198protected function getHTTPParameterType() {1199if ($this->proxy) {1200return $this->proxy->getHTTPParameterType();1201}1202return null;1203}12041205/**1206* @task edit1207*/1208public function shouldAppearInEditView() {1209if ($this->proxy) {1210return $this->proxy->shouldAppearInEditView();1211}1212return false;1213}12141215/**1216* @task edit1217*/1218public function shouldAppearInEditEngine() {1219if ($this->proxy) {1220return $this->proxy->shouldAppearInEditEngine();1221}1222return false;1223}122412251226/**1227* @task edit1228*/1229public function readValueFromRequest(AphrontRequest $request) {1230if ($this->proxy) {1231return $this->proxy->readValueFromRequest($request);1232}1233throw new PhabricatorCustomFieldImplementationIncompleteException($this);1234}123512361237/**1238* @task edit1239*/1240public function getRequiredHandlePHIDsForEdit() {1241if ($this->proxy) {1242return $this->proxy->getRequiredHandlePHIDsForEdit();1243}1244return array();1245}124612471248/**1249* @task edit1250*/1251public function getInstructionsForEdit() {1252if ($this->proxy) {1253return $this->proxy->getInstructionsForEdit();1254}1255return null;1256}125712581259/**1260* @task edit1261*/1262public function renderEditControl(array $handles) {1263if ($this->proxy) {1264return $this->proxy->renderEditControl($handles);1265}1266throw new PhabricatorCustomFieldImplementationIncompleteException($this);1267}126812691270/* -( Property View )------------------------------------------------------ */127112721273/**1274* @task view1275*/1276public function shouldAppearInPropertyView() {1277if ($this->proxy) {1278return $this->proxy->shouldAppearInPropertyView();1279}1280return false;1281}128212831284/**1285* @task view1286*/1287public function renderPropertyViewLabel() {1288if ($this->proxy) {1289return $this->proxy->renderPropertyViewLabel();1290}1291return $this->getFieldName();1292}129312941295/**1296* @task view1297*/1298public function renderPropertyViewValue(array $handles) {1299if ($this->proxy) {1300return $this->proxy->renderPropertyViewValue($handles);1301}1302throw new PhabricatorCustomFieldImplementationIncompleteException($this);1303}130413051306/**1307* @task view1308*/1309public function getStyleForPropertyView() {1310if ($this->proxy) {1311return $this->proxy->getStyleForPropertyView();1312}1313return 'property';1314}131513161317/**1318* @task view1319*/1320public function getIconForPropertyView() {1321if ($this->proxy) {1322return $this->proxy->getIconForPropertyView();1323}1324return null;1325}132613271328/**1329* @task view1330*/1331public function getRequiredHandlePHIDsForPropertyView() {1332if ($this->proxy) {1333return $this->proxy->getRequiredHandlePHIDsForPropertyView();1334}1335return array();1336}133713381339/* -( List View )---------------------------------------------------------- */134013411342/**1343* @task list1344*/1345public function shouldAppearInListView() {1346if ($this->proxy) {1347return $this->proxy->shouldAppearInListView();1348}1349return false;1350}135113521353/**1354* @task list1355*/1356public function renderOnListItem(PHUIObjectItemView $view) {1357if ($this->proxy) {1358return $this->proxy->renderOnListItem($view);1359}1360throw new PhabricatorCustomFieldImplementationIncompleteException($this);1361}136213631364/* -( Global Search )------------------------------------------------------ */136513661367/**1368* @task globalsearch1369*/1370public function shouldAppearInGlobalSearch() {1371if ($this->proxy) {1372return $this->proxy->shouldAppearInGlobalSearch();1373}1374return false;1375}137613771378/**1379* @task globalsearch1380*/1381public function updateAbstractDocument(1382PhabricatorSearchAbstractDocument $document) {1383if ($this->proxy) {1384return $this->proxy->updateAbstractDocument($document);1385}1386return $document;1387}138813891390/* -( Data Export )-------------------------------------------------------- */139113921393public function shouldAppearInDataExport() {1394if ($this->proxy) {1395return $this->proxy->shouldAppearInDataExport();1396}13971398try {1399$this->newExportFieldType();1400return true;1401} catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {1402return false;1403}1404}14051406public function newExportField() {1407if ($this->proxy) {1408return $this->proxy->newExportField();1409}14101411return $this->newExportFieldType()1412->setLabel($this->getFieldName());1413}14141415public function newExportData() {1416if ($this->proxy) {1417return $this->proxy->newExportData();1418}1419throw new PhabricatorCustomFieldImplementationIncompleteException($this);1420}14211422protected function newExportFieldType() {1423if ($this->proxy) {1424return $this->proxy->newExportFieldType();1425}1426throw new PhabricatorCustomFieldImplementationIncompleteException($this);1427}142814291430/* -( Conduit )------------------------------------------------------------ */143114321433/**1434* @task conduit1435*/1436public function shouldAppearInConduitDictionary() {1437if ($this->proxy) {1438return $this->proxy->shouldAppearInConduitDictionary();1439}1440return false;1441}144214431444/**1445* @task conduit1446*/1447public function getConduitDictionaryValue() {1448if ($this->proxy) {1449return $this->proxy->getConduitDictionaryValue();1450}1451throw new PhabricatorCustomFieldImplementationIncompleteException($this);1452}145314541455public function shouldAppearInConduitTransactions() {1456if ($this->proxy) {1457return $this->proxy->shouldAppearInConduitDictionary();1458}1459return false;1460}14611462public function getConduitSearchParameterType() {1463return $this->newConduitSearchParameterType();1464}14651466protected function newConduitSearchParameterType() {1467if ($this->proxy) {1468return $this->proxy->newConduitSearchParameterType();1469}1470return null;1471}14721473public function getConduitEditParameterType() {1474return $this->newConduitEditParameterType();1475}14761477protected function newConduitEditParameterType() {1478if ($this->proxy) {1479return $this->proxy->newConduitEditParameterType();1480}1481return null;1482}14831484public function getCommentAction() {1485return $this->newCommentAction();1486}14871488protected function newCommentAction() {1489if ($this->proxy) {1490return $this->proxy->newCommentAction();1491}1492return null;1493}149414951496/* -( Herald )------------------------------------------------------------- */149714981499/**1500* Return `true` to make this field available in Herald.1501*1502* @return bool True to expose the field in Herald.1503* @task herald1504*/1505public function shouldAppearInHerald() {1506if ($this->proxy) {1507return $this->proxy->shouldAppearInHerald();1508}1509return false;1510}151115121513/**1514* Get the name of the field in Herald. By default, this uses the1515* normal field name.1516*1517* @return string Herald field name.1518* @task herald1519*/1520public function getHeraldFieldName() {1521if ($this->proxy) {1522return $this->proxy->getHeraldFieldName();1523}1524return $this->getFieldName();1525}152615271528/**1529* Get the field value for evaluation by Herald.1530*1531* @return wild Field value.1532* @task herald1533*/1534public function getHeraldFieldValue() {1535if ($this->proxy) {1536return $this->proxy->getHeraldFieldValue();1537}1538throw new PhabricatorCustomFieldImplementationIncompleteException($this);1539}154015411542/**1543* Get the available conditions for this field in Herald.1544*1545* @return list<const> List of Herald condition constants.1546* @task herald1547*/1548public function getHeraldFieldConditions() {1549if ($this->proxy) {1550return $this->proxy->getHeraldFieldConditions();1551}1552throw new PhabricatorCustomFieldImplementationIncompleteException($this);1553}155415551556/**1557* Get the Herald value type for the given condition.1558*1559* @param const Herald condition constant.1560* @return const|null Herald value type, or null to use the default.1561* @task herald1562*/1563public function getHeraldFieldValueType($condition) {1564if ($this->proxy) {1565return $this->proxy->getHeraldFieldValueType($condition);1566}1567return null;1568}15691570public function getHeraldFieldStandardType() {1571if ($this->proxy) {1572return $this->proxy->getHeraldFieldStandardType();1573}1574return null;1575}15761577public function getHeraldDatasource() {1578if ($this->proxy) {1579return $this->proxy->getHeraldDatasource();1580}1581return null;1582}158315841585public function shouldAppearInHeraldActions() {1586if ($this->proxy) {1587return $this->proxy->shouldAppearInHeraldActions();1588}1589return false;1590}159115921593public function getHeraldActionName() {1594if ($this->proxy) {1595return $this->proxy->getHeraldActionName();1596}15971598return null;1599}160016011602public function getHeraldActionStandardType() {1603if ($this->proxy) {1604return $this->proxy->getHeraldActionStandardType();1605}16061607return null;1608}160916101611public function getHeraldActionDescription($value) {1612if ($this->proxy) {1613return $this->proxy->getHeraldActionDescription($value);1614}16151616return null;1617}161816191620public function getHeraldActionEffectDescription($value) {1621if ($this->proxy) {1622return $this->proxy->getHeraldActionEffectDescription($value);1623}16241625return null;1626}162716281629public function getHeraldActionDatasource() {1630if ($this->proxy) {1631return $this->proxy->getHeraldActionDatasource();1632}16331634return null;1635}16361637private static function adjustCustomFieldsForObjectSubtype(1638PhabricatorCustomFieldInterface $object,1639$role,1640array $fields) {1641assert_instances_of($fields, __CLASS__);16421643// We only apply subtype adjustment for some roles. For example, when1644// writing Herald rules or building a Search interface, we always want to1645// show all the fields in their default state, so we do not apply any1646// adjustments.1647$subtype_roles = array(1648self::ROLE_EDITENGINE,1649self::ROLE_VIEW,1650self::ROLE_EDIT,1651);16521653$subtype_roles = array_fuse($subtype_roles);1654if (!isset($subtype_roles[$role])) {1655return $fields;1656}16571658// If the object doesn't support subtypes, we can't possibly make1659// any adjustments based on subtype.1660if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {1661return $fields;1662}16631664$subtype_map = $object->newEditEngineSubtypeMap();1665$subtype_key = $object->getEditEngineSubtype();1666$subtype_object = $subtype_map->getSubtype($subtype_key);16671668$map = array();1669foreach ($fields as $field) {1670$modern_key = $field->getModernFieldKey();1671if (!strlen($modern_key)) {1672continue;1673}16741675$map[$modern_key] = $field;1676}16771678foreach ($map as $field_key => $field) {1679// For now, only support overriding standard custom fields. In the1680// future there's no technical or product reason we couldn't let you1681// override (some properites of) other fields like "Title", but they1682// don't usually support appropriate "setX()" methods today.1683if (!($field instanceof PhabricatorStandardCustomField)) {1684// For fields that are proxies on top of StandardCustomField, which1685// is how most application custom fields work today, we can reconfigure1686// the proxied field instead.1687$field = $field->getProxy();1688if (!$field || !($field instanceof PhabricatorStandardCustomField)) {1689continue;1690}1691}16921693$subtype_config = $subtype_object->getSubtypeFieldConfiguration(1694$field_key);16951696if (!$subtype_config) {1697continue;1698}16991700if (isset($subtype_config['disabled'])) {1701$field->setIsEnabled(!$subtype_config['disabled']);1702}17031704if (isset($subtype_config['name'])) {1705$field->setFieldName($subtype_config['name']);1706}1707}17081709return $fields;1710}17111712}171317141715