@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support configuration-driven custom fields

Summary:
Ref T1702. Ref T3718. There are a couple of things going on here:

**PhabricatorCustomFieldList**: I added `PhabricatorCustomFieldList`, which is just a convenience class for dealing with lists of fields. Often, current field code does something like this inline in a Controller:

foreach ($fields as $field) {
// do some junk
}

Often, that junk has some slightly subtle implications. Move all of it to `$list->doSomeJunk()` methods (like `appendFieldsToForm()`, `loadFieldsFromStorage()`) to reduce code duplication and prevent errors. This additionally moves an existing list-convenience method there, out of `PhabricatorPropertyListView`.

**PhabricatorUserConfiguredCustomFieldStorage**: Adds `PhabricatorUserConfiguredCustomFieldStorage` for storing custom field data (like "ICQ Handle", "Phone Number", "Desk", "Favorite Flower", etc).

**Configuration-Driven Custom Fields**: Previously, I was thinking about doing these with interfaces, but as I thought about it more I started to dislike that approach. Instead, I built proxies into `PhabricatorCustomField`. Basically, this means that fields (like a custom, configuration-driven "Favorite Flower" field) can just use some other Field to actually provide their implementation (like a "standard" field which knows how to render text areas). The previous approach would have involed subclasssing the "standard" field and implementing an interface, but that would mean that every application would have at least two "base" fields and generally just seemed bleh as I worked through it.

The cost of this approach is that we need a bunch of `proxy` junk in the base class, but that's a one-time cost and I think it simplifies all the implementations and makes them a lot less magical (e.g., all of the custom fields now extend the right base field classes).

**Fixed Some Bugs**: Some of this code hadn't really been run yet and had minor bugs.

Test Plan:
{F54240}
{F54241}
{F54242}

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1702, T1703, T3718

Differential Revision: https://secure.phabricator.com/D6749

+496 -95
+7
resources/sql/patches/20130814.usercustom.sql
··· 1 + CREATE TABLE {$NAMESPACE}_user.user_configuredcustomfieldstorage ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + fieldIndex CHAR(12) NOT NULL COLLATE utf8_bin, 5 + fieldValue LONGTEXT NOT NULL, 6 + UNIQUE KEY (objectPHID, fieldIndex) 7 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+12 -4
src/__phutil_library_map__.php
··· 1030 1030 'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php', 1031 1031 'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php', 1032 1032 'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php', 1033 + 'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php', 1033 1034 'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php', 1035 + 'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php', 1034 1036 'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php', 1035 1037 'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php', 1036 1038 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', ··· 1607 1609 'PhabricatorSortTableExample' => 'applications/uiexample/examples/PhabricatorSortTableExample.php', 1608 1610 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', 1609 1611 'PhabricatorStandardCustomField' => 'infrastructure/customfield/field/PhabricatorStandardCustomField.php', 1612 + 'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php', 1610 1613 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 1611 1614 'PhabricatorStatusController' => 'applications/system/PhabricatorStatusController.php', 1612 1615 'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php', ··· 1677 1680 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 1678 1681 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', 1679 1682 'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php', 1683 + 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 1684 + 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', 1680 1685 'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php', 1681 - 'PhabricatorUserCustomFieldInterface' => 'applications/people/customfield/PhabricatorUserCustomFieldInterface.php', 1682 1686 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', 1683 1687 'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', 1684 1688 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', ··· 3092 3096 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', 3093 3097 'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception', 3094 3098 'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO', 3099 + 'PhabricatorCustomFieldList' => 'Phobject', 3095 3100 'PhabricatorCustomFieldNotAttachedException' => 'Exception', 3101 + 'PhabricatorCustomFieldNotProxyException' => 'Exception', 3096 3102 'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 3097 3103 'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO', 3098 3104 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', ··· 3778 3784 ), 3779 3785 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 3780 3786 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 3781 - 'PhabricatorUserCustomField' => 3787 + 'PhabricatorUserConfiguredCustomField' => 3782 3788 array( 3783 - 0 => 'PhabricatorCustomField', 3784 - 1 => 'PhabricatorUserCustomFieldInterface', 3789 + 0 => 'PhabricatorUserCustomField', 3790 + 1 => 'PhabricatorStandardCustomFieldInterface', 3785 3791 ), 3792 + 'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 3793 + 'PhabricatorUserCustomField' => 'PhabricatorCustomField', 3786 3794 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 3787 3795 'PhabricatorUserEditor' => 'PhabricatorEditor', 3788 3796 'PhabricatorUserEmail' => 'PhabricatorUserDAO',
+2
src/applications/people/config/PhabricatorUserConfigOptions.php
··· 34 34 $this->newOption('user.fields', $custom_field_type, $default) 35 35 ->setCustomData(id(new PhabricatorUser())->getCustomFieldBaseClass()) 36 36 ->setDescription(pht("Select and reorder user profile fields.")), 37 + $this->newOption('user.custom-field-definitions', 'map', array()) 38 + ->setDescription(pht("Add new simple fields to user profiles.")), 37 39 ); 38 40 } 39 41
+2 -6
src/applications/people/controller/PhabricatorPeopleProfileController.php
··· 100 100 $fields = PhabricatorCustomField::getObjectFields( 101 101 $user, 102 102 PhabricatorCustomField::ROLE_VIEW); 103 - 104 - foreach ($fields as $field) { 105 - $field->setViewer($viewer); 106 - } 107 - 108 - $view->applyCustomFields($fields); 103 + $field_list = new PhabricatorCustomFieldList($fields); 104 + $field_list->appendFieldsToPropertyList($user, $viewer, $view); 109 105 110 106 return $view; 111 107 }
+5 -4
src/applications/people/controller/PhabricatorPeopleProfileEditController.php
··· 35 35 $fields = PhabricatorCustomField::getObjectFields( 36 36 $user, 37 37 PhabricatorCustomField::ROLE_EDIT); 38 + $field_list = new PhabricatorCustomFieldList($fields); 38 39 39 40 if ($request->isFormPost()) { 40 41 $xactions = array(); 41 42 foreach ($fields as $field) { 42 - $field->setValueFromRequest($request); 43 + $field->readValueFromRequest($request); 43 44 $xactions[] = id(new PhabricatorUserTransaction()) 44 45 ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) 45 46 ->setMetadataValue('customfield:key', $field->getFieldKey()) ··· 55 56 $editor->applyTransactions($user, $xactions); 56 57 57 58 return id(new AphrontRedirectResponse())->setURI($profile_uri); 59 + } else { 60 + $field_list->readFieldsFromStorage($user); 58 61 } 59 62 60 63 $title = pht('Edit Profile'); ··· 70 73 $form = id(new AphrontFormView()) 71 74 ->setUser($viewer); 72 75 73 - foreach ($fields as $field) { 74 - $form->appendChild($field->renderEditControl()); 75 - } 76 + $field_list->appendFieldsToForm($form); 76 77 77 78 $form 78 79 ->appendChild(
+1 -1
src/applications/people/customfield/PhabricatorUserBlurbField.php
··· 46 46 $this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue()); 47 47 } 48 48 49 - public function setValueFromRequest(AphrontRequest $request) { 49 + public function readValueFromRequest(AphrontRequest $request) { 50 50 $this->value = $request->getStr($this->getFieldKey()); 51 51 } 52 52
+21
src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserConfiguredCustomField 4 + extends PhabricatorUserCustomField 5 + implements PhabricatorStandardCustomFieldInterface { 6 + 7 + public function getStandardCustomFieldNamespace() { 8 + return 'user'; 9 + } 10 + 11 + public function createFields() { 12 + return PhabricatorStandardCustomField::buildStandardFields( 13 + $this, 14 + PhabricatorEnv::getEnvConfig('user.custom-field-definitions', array())); 15 + } 16 + 17 + public function newStorageObject() { 18 + return new PhabricatorUserConfiguredCustomFieldStorage(); 19 + } 20 + 21 + }
+1 -2
src/applications/people/customfield/PhabricatorUserCustomField.php
··· 1 1 <?php 2 2 3 3 abstract class PhabricatorUserCustomField 4 - extends PhabricatorCustomField 5 - implements PhabricatorUserCustomFieldInterface { 4 + extends PhabricatorCustomField { 6 5 7 6 8 7 }
-6
src/applications/people/customfield/PhabricatorUserCustomFieldInterface.php
··· 1 - <?php 2 - 3 - interface PhabricatorUserCustomFieldInterface { 4 - 5 - 6 - }
+1 -1
src/applications/people/customfield/PhabricatorUserRealNameField.php
··· 49 49 $this->getObject()->setRealName($xaction->getNewValue()); 50 50 } 51 51 52 - public function setValueFromRequest(AphrontRequest $request) { 52 + public function readValueFromRequest(AphrontRequest $request) { 53 53 $this->value = $request->getStr($this->getFieldKey()); 54 54 } 55 55
+1 -1
src/applications/people/customfield/PhabricatorUserTitleField.php
··· 46 46 $this->getObject()->loadUserProfile()->setTitle($xaction->getNewValue()); 47 47 } 48 48 49 - public function setValueFromRequest(AphrontRequest $request) { 49 + public function readValueFromRequest(AphrontRequest $request) { 50 50 $this->value = $request->getStr($this->getFieldKey()); 51 51 } 52 52
+1 -1
src/applications/people/storage/PhabricatorUser.php
··· 840 840 } 841 841 842 842 public function getCustomFieldBaseClass() { 843 - return 'PhabricatorUserCustomFieldInterface'; 843 + return 'PhabricatorUserCustomField'; 844 844 } 845 845 846 846 public function getCustomFields($role) {
+11
src/applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserConfiguredCustomFieldStorage 4 + extends PhabricatorCustomFieldStorage { 5 + 6 + public function getApplicationName() { 7 + return 'user'; 8 + } 9 + 10 + } 11 +
+17
src/infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php
··· 1 + <?php 2 + 3 + final class PhabricatorCustomFieldNotProxyException 4 + extends Exception { 5 + 6 + public function __construct(PhabricatorCustomField $field) { 7 + $key = $field->getFieldKey(); 8 + $name = $field->getFieldName(); 9 + $class = get_class($field); 10 + 11 + parent::__construct( 12 + "Custom field '{$name}' (with key '{$key}', of class '{$class}') can ". 13 + "not have a proxy set with setProxy(), because it returned false from ". 14 + "canSetProxy()."); 15 + } 16 + 17 + }
+192 -21
src/infrastructure/customfield/field/PhabricatorCustomField.php
··· 3 3 /** 4 4 * @task apps Building Applications with Custom Fields 5 5 * @task core Core Properties and Field Identity 6 + * @task proxy Field Proxies 6 7 * @task context Contextual Data 7 8 * @task storage Field Storage 8 9 * @task appsearch Integration with ApplicationSearch ··· 15 16 16 17 private $viewer; 17 18 private $object; 19 + private $proxy; 18 20 19 21 const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions'; 20 22 const ROLE_APPLICATIONSEARCH = 'ApplicationSearch'; ··· 148 150 * @return string String which uniquely identifies this field. 149 151 * @task core 150 152 */ 151 - abstract public function getFieldKey(); 153 + public function getFieldKey() { 154 + if ($this->proxy) { 155 + return $this->proxy->getFieldKey(); 156 + } 157 + throw new PhabricatorCustomFieldImplementationIncompleteException($this); 158 + } 152 159 153 160 154 161 /** ··· 158 165 * @task core 159 166 */ 160 167 public function getFieldName() { 168 + if ($this->proxy) { 169 + return $this->proxy->getFieldName(); 170 + } 161 171 return $this->getFieldKey(); 162 172 } 163 173 ··· 170 180 * @task core 171 181 */ 172 182 public function getFieldDescription() { 183 + if ($this->proxy) { 184 + return $this->proxy->getFieldDescription(); 185 + } 173 186 return null; 174 187 } 175 188 ··· 200 213 * @task core 201 214 */ 202 215 public function isFieldEnabled() { 216 + if ($this->proxy) { 217 + return $this->proxy->isFieldEnabled(); 218 + } 203 219 return true; 204 220 } 205 221 ··· 212 228 * 213 229 * Normally, you do not need to override this method. Instead, override the 214 230 * methods specific to roles you want to enable. For example, implement 215 - * @{method:getStorageKey()} to activate the `'storage'` role. 231 + * @{method:shouldUseStorage()} to activate the `'storage'` role. 216 232 * 217 233 * @return bool True to enable the field for the given role. 218 234 * @task core 219 235 */ 220 236 public function shouldEnableForRole($role) { 237 + if ($this->proxy) { 238 + return $this->proxy->shouldEnableForRole($role); 239 + } 240 + 221 241 switch ($role) { 222 242 case self::ROLE_APPLICATIONTRANSACTIONS: 223 243 return $this->shouldAppearInApplicationTransactions(); 224 244 case self::ROLE_APPLICATIONSEARCH: 225 245 return $this->shouldAppearInApplicationSearch(); 226 246 case self::ROLE_STORAGE: 227 - return ($this->getStorageKey() !== null); 247 + return $this->shouldUseStorage(); 228 248 case self::ROLE_EDIT: 229 249 return $this->shouldAppearInEditView(); 230 250 case self::ROLE_VIEW: ··· 264 284 } 265 285 266 286 287 + /* -( Field Proxies )------------------------------------------------------ */ 288 + 289 + 290 + /** 291 + * Proxies allow a field to use some other field's implementation for most 292 + * of their behavior while still subclassing an application field. When a 293 + * proxy is set for a field with @{method:setProxy}, all of its methods will 294 + * call through to the proxy by default. 295 + * 296 + * This is most commonly used to implement configuration-driven custom fields 297 + * using @{class:PhabricatorStandardCustomField}. 298 + * 299 + * This method must be overridden to return `true` before a field can accept 300 + * proxies. 301 + * 302 + * @return bool True if you can @{method:setProxy} this field. 303 + * @task proxy 304 + */ 305 + public function canSetProxy() { 306 + if ($this instanceof PhabricatorStandardCustomFieldInterface) { 307 + return true; 308 + } 309 + return false; 310 + } 311 + 312 + 313 + /** 314 + * Set the proxy implementation for this field. See @{method:canSetProxy} for 315 + * discussion of field proxies. 316 + * 317 + * @param PhabricatorCustomField Field implementation. 318 + * @return this 319 + */ 320 + final public function setProxy(PhabricatorCustomField $proxy) { 321 + if (!$this->canSetProxy()) { 322 + throw new PhabricatorCustomFieldNotProxyException($this); 323 + } 324 + 325 + $this->proxy = $proxy; 326 + return $this; 327 + } 328 + 329 + 330 + /** 331 + * Get the field's proxy implementation, if any. For discussion, see 332 + * @{method:canSetProxy}. 333 + * 334 + * @return PhabricatorCustomField|null Proxy field, if one is set. 335 + */ 336 + final public function getProxy() { 337 + return $this->proxy; 338 + } 339 + 340 + 267 341 /* -( Contextual Data )---------------------------------------------------- */ 268 342 269 343 ··· 274 348 * @task context 275 349 */ 276 350 final public function setObject(PhabricatorCustomFieldInterface $object) { 351 + if ($this->proxy) { 352 + $this->proxy->setObject($object); 353 + return $this; 354 + } 355 + 277 356 $this->object = $object; 278 357 $this->didSetObject($object); 279 358 return $this; ··· 287 366 * @task context 288 367 */ 289 368 final public function getObject() { 369 + if ($this->proxy) { 370 + return $this->proxy->getObject(); 371 + } 372 + 290 373 return $this->object; 291 374 } 292 375 ··· 306 389 * @task context 307 390 */ 308 391 final public function setViewer(PhabricatorUser $viewer) { 392 + if ($this->proxy) { 393 + $this->proxy->setViewer($viewer); 394 + return $this; 395 + } 396 + 309 397 $this->viewer = $viewer; 310 398 return $this; 311 399 } ··· 315 403 * @task context 316 404 */ 317 405 final public function getViewer() { 406 + if ($this->proxy) { 407 + return $this->proxy->getViewer(); 408 + } 409 + 318 410 return $this->viewer; 319 411 } 320 412 ··· 323 415 * @task context 324 416 */ 325 417 final protected function requireViewer() { 418 + if ($this->proxy) { 419 + return $this->proxy->requireViewer(); 420 + } 421 + 326 422 if (!$this->viewer) { 327 423 throw new PhabricatorCustomFieldDataNotAvailableException($this); 328 424 } ··· 334 430 335 431 336 432 /** 337 - * Return a unique string used to key storage of this field's value, like 338 - * "mycompany.fieldname" or similar. You can return null (the default) to 339 - * indicate that this field does not use any storage. 433 + * Return true to use field storage. 340 434 * 341 435 * Fields which can be edited by the user will most commonly use storage, 342 436 * while some other types of fields (for instance, those which just display ··· 346 440 * If you implement this, you must also implement @{method:getValueForStorage} 347 441 * and @{method:setValueFromStorage}. 348 442 * 349 - * In most cases, a reasonable implementation is to simply reuse the field 350 - * key: 351 - * 352 - * return $this->getFieldKey(); 353 - * 354 - * @return string|null Unique key which identifies this field in auxiliary 355 - * field storage. Alternatively, return null (default) 356 - * to indicate that this field does not use storage. 443 + * @return bool True to use storage. 357 444 * @task storage 358 445 */ 359 - public function getStorageKey() { 360 - return null; 446 + public function shouldUseStorage() { 447 + if ($this->proxy) { 448 + return $this->proxy->shouldUseStorage(); 449 + } 450 + return false; 361 451 } 362 452 363 453 ··· 369 459 * @return PhabricatorCustomFieldStorage New empty storage object. 370 460 * @task storage 371 461 */ 372 - public function getStorageObject() { 462 + public function newStorageObject() { 463 + if ($this->proxy) { 464 + return $this->proxy->newStorageObject(); 465 + } 373 466 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 374 467 } 375 468 ··· 377 470 /** 378 471 * Return a serialized representation of the field value, appropriate for 379 472 * storing in auxiliary field storage. You must implement this method if 380 - * you implement @{method:getStorageKey}. 473 + * you implement @{method:shouldUseStorage}. 381 474 * 382 475 * If the field value is a scalar, it can be returned unmodiifed. If not, 383 476 * it should be serialized (for example, using JSON). ··· 386 479 * @task storage 387 480 */ 388 481 public function getValueForStorage() { 482 + if ($this->proxy) { 483 + return $this->proxy->getValueForStorage(); 484 + } 389 485 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 390 486 } 391 487 ··· 394 490 * Set the field's value given a serialized storage value. This is called 395 491 * when the field is loaded; if no data is available, the value will be 396 492 * null. You must implement this method if you implement 397 - * @{method:getStorageKey}. 493 + * @{method:shouldUseStorage}. 398 494 * 399 495 * Usually, the value can be loaded directly. If it isn't a scalar, you'll 400 496 * need to undo whatever serialization you applied in ··· 407 503 * @task storage 408 504 */ 409 505 public function setValueFromStorage($value) { 506 + if ($this->proxy) { 507 + return $this->proxy->setValueFromStorage($value); 508 + } 410 509 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 411 510 } 412 511 ··· 422 521 * @task appsearch 423 522 */ 424 523 public function shouldAppearInApplicationSearch() { 524 + if ($this->proxy) { 525 + return $this->proxy->shouldAppearInApplicationSearch(); 526 + } 425 527 return false; 426 528 } 427 529 ··· 449 551 * @task appsearch 450 552 */ 451 553 public function buildFieldIndexes() { 554 + if ($this->proxy) { 555 + return $this->proxy->buildFieldIndexes(); 556 + } 452 557 return array(); 453 558 } 454 559 ··· 462 567 * @task appsearch 463 568 */ 464 569 protected function newStringIndexStorage() { 570 + if ($this->proxy) { 571 + return $this->proxy->newStringIndexStorage(); 572 + } 465 573 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 466 574 } 467 575 ··· 475 583 * @task appsearch 476 584 */ 477 585 protected function newNumericIndexStorage() { 586 + if ($this->proxy) { 587 + return $this->proxy->newStringIndexStorage(); 588 + } 478 589 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 479 590 } 480 591 ··· 487 598 * @task appsearch 488 599 */ 489 600 protected function newStringIndex($value) { 601 + if ($this->proxy) { 602 + return $this->proxy->newStringIndex(); 603 + } 604 + 490 605 $key = $this->getFieldIndexKey(); 491 606 return $this->newStringIndexStorage() 492 607 ->setIndexKey($key) ··· 502 617 * @task appsearch 503 618 */ 504 619 protected function newNumericIndex($value) { 620 + if ($this->proxy) { 621 + return $this->proxy->newNumericIndex(); 622 + } 505 623 $key = $this->getFieldIndexKey(); 506 624 return $this->newNumericIndexStorage() 507 625 ->setIndexKey($key) ··· 520 638 * @task appxaction 521 639 */ 522 640 public function shouldAppearInApplicationTransactions() { 641 + if ($this->proxy) { 642 + return $this->proxy->shouldAppearInApplicationTransactions(); 643 + } 523 644 return false; 524 645 } 525 646 ··· 528 649 * @task appxaction 529 650 */ 530 651 public function getOldValueForApplicationTransactions() { 652 + if ($this->proxy) { 653 + return $this->proxy->getOldValueForApplicationTransactions(); 654 + } 531 655 return $this->getValueForStorage(); 532 656 } 533 657 ··· 536 660 * @task appxaction 537 661 */ 538 662 public function getNewValueForApplicationTransactions() { 663 + if ($this->proxy) { 664 + return $this->proxy->getNewValueForApplicationTransactions(); 665 + } 539 666 return $this->getValueForStorage(); 540 667 } 541 668 ··· 544 671 * @task appxaction 545 672 */ 546 673 public function setValueFromApplicationTransactions($value) { 674 + if ($this->proxy) { 675 + return $this->proxy->setValueFromApplicationTransactions($value); 676 + } 547 677 return $this->setValueFromStorage($value); 548 678 } 549 679 ··· 553 683 */ 554 684 public function getNewValueFromApplicationTransactions( 555 685 PhabricatorApplicationTransaction $xaction) { 686 + if ($this->proxy) { 687 + return $this->proxy->getNewValueFromApplicationTransactions($xaction); 688 + } 556 689 return $xaction->getNewValue(); 557 690 } 558 691 ··· 562 695 */ 563 696 public function getApplicationTransactionHasEffect( 564 697 PhabricatorApplicationTransaction $xaction) { 698 + if ($this->proxy) { 699 + return $this->proxy->getApplicationTransactionHasEffect($xaction); 700 + } 565 701 return ($xaction->getOldValue() !== $xaction->getNewValue()); 566 702 } 567 703 ··· 571 707 */ 572 708 public function applyApplicationTransactionInternalEffects( 573 709 PhabricatorApplicationTransaction $xaction) { 710 + if ($this->proxy) { 711 + return $this->proxy->applyApplicationTransactionInternalEffects($xaction); 712 + } 574 713 return; 575 714 } 576 715 ··· 580 719 */ 581 720 public function applyApplicationTransactionExternalEffects( 582 721 PhabricatorApplicationTransaction $xaction) { 722 + if ($this->proxy) { 723 + return $this->proxy->applyApplicationTransactionExternalEffects($xaction); 724 + } 725 + 583 726 if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) { 584 727 return; 585 728 } 586 729 587 - $this->setValueFromApplicationTransaction($xaction->getNewValue()); 730 + $this->setValueFromApplicationTransactions($xaction->getNewValue()); 588 731 $value = $this->getValueForStorage(); 589 732 590 733 $table = $this->newStorageObject(); ··· 594 737 queryfx( 595 738 $conn_w, 596 739 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s', 740 + $table->getTableName(), 597 741 $this->getObject()->getPHID(), 598 742 $this->getFieldIndex()); 599 743 } else { ··· 602 746 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) 603 747 VALUES (%s, %s, %s) 604 748 ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', 749 + $table->getTableName(), 605 750 $this->getObject()->getPHID(), 606 751 $this->getFieldIndex(), 607 752 $value); ··· 618 763 * @task edit 619 764 */ 620 765 public function shouldAppearInEditView() { 766 + if ($this->proxy) { 767 + return $this->proxy->shouldAppearInEditView(); 768 + } 621 769 return false; 622 770 } 623 771 ··· 626 774 * @task edit 627 775 */ 628 776 public function readValueFromRequest(AphrontRequest $request) { 777 + if ($this->proxy) { 778 + return $this->proxy->readValueFromRequest($request); 779 + } 629 780 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 630 781 } 631 782 ··· 634 785 * @task edit 635 786 */ 636 787 public function renderEditControl() { 788 + if ($this->proxy) { 789 + return $this->proxy->renderEditControl(); 790 + } 637 791 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 638 792 } 639 793 ··· 645 799 * @task view 646 800 */ 647 801 public function shouldAppearInPropertyView() { 802 + if ($this->proxy) { 803 + return $this->proxy->shouldAppearInPropertyView(); 804 + } 648 805 return false; 649 806 } 650 807 ··· 653 810 * @task view 654 811 */ 655 812 public function renderPropertyViewLabel() { 813 + if ($this->proxy) { 814 + return $this->proxy->renderPropertyViewLabel(); 815 + } 656 816 return $this->getFieldName(); 657 817 } 658 818 ··· 661 821 * @task view 662 822 */ 663 823 public function renderPropertyViewValue() { 824 + if ($this->proxy) { 825 + return $this->proxy->renderPropertyViewValue(); 826 + } 664 827 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 665 828 } 666 829 ··· 669 832 * @task view 670 833 */ 671 834 public function getStyleForPropertyView() { 835 + if ($this->proxy) { 836 + return $this->proxy->getStyleForPropertyView(); 837 + } 672 838 return 'property'; 673 839 } 674 840 ··· 680 846 * @task list 681 847 */ 682 848 public function shouldAppearInListView() { 849 + if ($this->proxy) { 850 + return $this->proxy->shouldAppearInListView(); 851 + } 683 852 return false; 684 853 } 685 854 ··· 688 857 * @task list 689 858 */ 690 859 public function renderOnListItem(PhabricatorObjectItemView $view) { 860 + if ($this->proxy) { 861 + return $this->proxy->renderOnListItem($view); 862 + } 691 863 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 692 864 } 693 - 694 865 695 866 696 867 }
+123
src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
··· 1 + <?php 2 + 3 + /** 4 + * Convenience class to perform operations on an entire field list, like reading 5 + * all values from storage. 6 + * 7 + * $field_list = new PhabricatorCustomFieldList($fields); 8 + * 9 + */ 10 + final class PhabricatorCustomFieldList extends Phobject { 11 + 12 + private $fields; 13 + 14 + public function __construct(array $fields) { 15 + assert_instances_of($fields, 'PhabricatorCustomField'); 16 + $this->fields = $fields; 17 + } 18 + 19 + 20 + /** 21 + * Read stored values for all fields which support storage. 22 + * 23 + * @param PhabricatorCustomFieldInterface Object to read field values for. 24 + * @return void 25 + */ 26 + public function readFieldsFromStorage( 27 + PhabricatorCustomFieldInterface $object) { 28 + 29 + $keys = array(); 30 + foreach ($this->fields as $field) { 31 + if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_STORAGE)) { 32 + $keys[$field->getFieldIndex()] = $field; 33 + } 34 + } 35 + 36 + if (!$keys) { 37 + return; 38 + } 39 + 40 + // NOTE: We assume all fields share the same storage. This isn't guaranteed 41 + // to be true, but always is for now. 42 + 43 + $table = head($keys)->newStorageObject(); 44 + 45 + $objects = $table->loadAllWhere( 46 + 'objectPHID = %s AND fieldIndex IN (%Ls)', 47 + $object->getPHID(), 48 + array_keys($keys)); 49 + $objects = mpull($objects, null, 'getFieldIndex'); 50 + 51 + foreach ($keys as $key => $field) { 52 + $storage = idx($objects, $key); 53 + if ($storage) { 54 + $field->setValueFromStorage($storage->getFieldValue()); 55 + } else { 56 + $field->setValueFromStorage(null); 57 + } 58 + } 59 + } 60 + 61 + public function appendFieldsToForm(AphrontFormView $form) { 62 + foreach ($this->fields as $field) { 63 + if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) { 64 + $form->appendChild($field->renderEditControl()); 65 + } 66 + } 67 + } 68 + 69 + public function appendFieldsToPropertyList( 70 + PhabricatorCustomFieldInterface $object, 71 + PhabricatorUser $viewer, 72 + PhabricatorPropertyListView $view) { 73 + 74 + $this->readFieldsFromStorage($object); 75 + $fields = $this->fields; 76 + 77 + foreach ($fields as $field) { 78 + $field->setViewer($viewer); 79 + } 80 + 81 + // Move all the blocks to the end, regardless of their configuration order, 82 + // because it always looks silly to render a block in the middle of a list 83 + // of properties. 84 + $head = array(); 85 + $tail = array(); 86 + foreach ($fields as $key => $field) { 87 + $style = $field->getStyleForPropertyView(); 88 + switch ($style) { 89 + case 'property': 90 + $head[$key] = $field; 91 + break; 92 + case 'block': 93 + $tail[$key] = $field; 94 + break; 95 + default: 96 + throw new Exception( 97 + "Unknown field property view style '{$style}'; valid styles are ". 98 + "'block' and 'property'."); 99 + } 100 + } 101 + $fields = $head + $tail; 102 + 103 + foreach ($fields as $field) { 104 + $label = $field->renderPropertyViewLabel(); 105 + $value = $field->renderPropertyViewValue(); 106 + if ($value !== null) { 107 + switch ($field->getStyleForPropertyView()) { 108 + case 'property': 109 + $view->addProperty($label, $value); 110 + break; 111 + case 'block': 112 + $view->invokeWillRenderEvent(); 113 + if ($label !== null) { 114 + $view->addSectionHeader($label); 115 + } 116 + $view->addTextContent($value); 117 + break; 118 + } 119 + } 120 + } 121 + } 122 + 123 + }
+88 -3
src/infrastructure/customfield/field/PhabricatorStandardCustomField.php
··· 1 1 <?php 2 2 3 - abstract class PhabricatorStandardCustomField 3 + final class PhabricatorStandardCustomField 4 4 extends PhabricatorCustomField { 5 5 6 6 private $fieldKey; ··· 8 8 private $fieldType; 9 9 private $fieldValue; 10 10 private $fieldDescription; 11 + private $fieldConfig; 12 + private $applicationField; 13 + 14 + public static function buildStandardFields( 15 + PhabricatorCustomField $template, 16 + array $config) { 17 + 18 + $fields = array(); 19 + foreach ($config as $key => $value) { 20 + $namespace = $template->getStandardCustomFieldNamespace(); 21 + $full_key = "std:{$namespace}:{$key}"; 22 + 23 + $template = clone $template; 24 + $standard = id(new PhabricatorStandardCustomField($full_key)) 25 + ->setFieldConfig($value) 26 + ->setApplicationField($template); 27 + 28 + $field = $template->setProxy($standard); 29 + $fields[] = $field; 30 + } 31 + 32 + return $fields; 33 + } 11 34 12 35 public function __construct($key) { 13 36 $this->fieldKey = $key; 37 + } 38 + 39 + public function setApplicationField( 40 + PhabricatorStandardCustomFieldInterface $application_field) { 41 + $this->applicationField = $application_field; 42 + return $this; 43 + } 44 + 45 + public function getApplicationField() { 46 + return $this->applicationField; 14 47 } 15 48 16 49 public function setFieldName($name) { ··· 37 70 return $this; 38 71 } 39 72 73 + public function setFieldConfig(array $config) { 74 + foreach ($config as $key => $value) { 75 + switch ($key) { 76 + case 'name': 77 + $this->setFieldName($value); 78 + break; 79 + case 'type': 80 + $this->setFieldType($value); 81 + break; 82 + } 83 + } 84 + $this->fieldConfig = $config; 85 + return $this; 86 + } 87 + 88 + public function getFieldConfigValue($key, $default = null) { 89 + return idx($this->fieldConfig, $key, $default); 90 + } 91 + 40 92 41 93 /* -( PhabricatorCustomField )--------------------------------------------- */ 42 94 ··· 53 105 return coalesce($this->fieldDescription, parent::getFieldDescription()); 54 106 } 55 107 56 - public function getStorageKey() { 57 - return $this->getFieldKey(); 108 + public function shouldUseStorage() { 109 + return true; 58 110 } 59 111 60 112 public function getValueForStorage() { ··· 68 120 public function shouldAppearInApplicationTransactions() { 69 121 return true; 70 122 } 123 + 124 + public function shouldAppearInEditView() { 125 + return $this->getFieldConfigValue('edit', true); 126 + } 127 + 128 + public function readValueFromRequest(AphrontRequest $request) { 129 + $this->setFieldValue($request->getStr($this->getFieldKey())); 130 + } 131 + 132 + public function renderEditControl() { 133 + $type = $this->getFieldConfigValue('type', 'text'); 134 + switch ($type) { 135 + case 'text': 136 + default: 137 + return id(new AphrontFormTextControl()) 138 + ->setName($this->getFieldKey()) 139 + ->setValue($this->getFieldValue()) 140 + ->setLabel($this->getFieldName()); 141 + } 142 + } 143 + 144 + public function newStorageObject() { 145 + return $this->getApplicationField()->newStorageObject(); 146 + } 147 + 148 + public function shouldAppearInPropertyView() { 149 + return $this->getFieldConfigValue('view', true); 150 + } 151 + 152 + public function renderPropertyViewValue() { 153 + return $this->getFieldValue(); 154 + } 155 + 71 156 72 157 }
+7
src/infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php
··· 1 + <?php 2 + 3 + interface PhabricatorStandardCustomFieldInterface { 4 + 5 + public function getStandardCustomFieldNamespace(); 6 + 7 + }
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1547 1547 'type' => 'sql', 1548 1548 'name' => $this->getPatchPath('20130731.releephcutpointidentifier.sql'), 1549 1549 ), 1550 + '20130814.usercustom.sql' => array( 1551 + 'type' => 'sql', 1552 + 'name' => $this->getPatchPath('20130814.usercustom.sql'), 1553 + ), 1550 1554 ); 1551 1555 } 1552 1556 }
-45
src/view/layout/PhabricatorPropertyListView.php
··· 81 81 $this->invokedWillRenderEvent = true; 82 82 } 83 83 84 - public function applyCustomFields(array $fields) { 85 - assert_instances_of($fields, 'PhabricatorCustomField'); 86 - 87 - // Move all the blocks to the end, regardless of their configuration order, 88 - // because it always looks silly to render a block in the middle of a list 89 - // of properties. 90 - $head = array(); 91 - $tail = array(); 92 - foreach ($fields as $key => $field) { 93 - $style = $field->getStyleForPropertyView(); 94 - switch ($style) { 95 - case 'property': 96 - $head[$key] = $field; 97 - break; 98 - case 'block': 99 - $tail[$key] = $field; 100 - break; 101 - default: 102 - throw new Exception( 103 - "Unknown field property view style '{$style}'; valid styles are ". 104 - "'block' and 'property'."); 105 - } 106 - } 107 - $fields = $head + $tail; 108 - 109 - foreach ($fields as $field) { 110 - $label = $field->renderPropertyViewLabel(); 111 - $value = $field->renderPropertyViewValue(); 112 - if ($value !== null) { 113 - switch ($field->getStyleForPropertyView()) { 114 - case 'property': 115 - $this->addProperty($label, $value); 116 - break; 117 - case 'block': 118 - $this->invokeWillRenderEvent(); 119 - if ($label !== null) { 120 - $this->addSectionHeader($label); 121 - } 122 - $this->addTextContent($value); 123 - break; 124 - } 125 - } 126 - } 127 - } 128 - 129 84 public function render() { 130 85 $this->invokeWillRenderEvent(); 131 86