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