1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 16:30:59 +01:00

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
This commit is contained in:
epriestley 2013-08-14 08:10:16 -07:00
parent 9f41032693
commit ca0115b361
20 changed files with 496 additions and 95 deletions

View file

@ -0,0 +1,7 @@
CREATE TABLE {$NAMESPACE}_user.user_configuredcustomfieldstorage (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
fieldIndex CHAR(12) NOT NULL COLLATE utf8_bin,
fieldValue LONGTEXT NOT NULL,
UNIQUE KEY (objectPHID, fieldIndex)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -1030,7 +1030,9 @@ phutil_register_library_map(array(
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
@ -1607,6 +1609,7 @@ phutil_register_library_map(array(
'PhabricatorSortTableExample' => 'applications/uiexample/examples/PhabricatorSortTableExample.php',
'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/field/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStatusController' => 'applications/system/PhabricatorStatusController.php',
'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
@ -1677,8 +1680,9 @@ phutil_register_library_map(array(
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
'PhabricatorUserCustomFieldInterface' => 'applications/people/customfield/PhabricatorUserCustomFieldInterface.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
@ -3092,7 +3096,9 @@ phutil_register_library_map(array(
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldList' => 'Phobject',
'PhabricatorCustomFieldNotAttachedException' => 'Exception',
'PhabricatorCustomFieldNotProxyException' => 'Exception',
'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
@ -3778,11 +3784,13 @@ phutil_register_library_map(array(
),
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUserCustomField' =>
'PhabricatorUserConfiguredCustomField' =>
array(
0 => 'PhabricatorCustomField',
1 => 'PhabricatorUserCustomFieldInterface',
0 => 'PhabricatorUserCustomField',
1 => 'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',

View file

@ -34,6 +34,8 @@ final class PhabricatorUserConfigOptions
$this->newOption('user.fields', $custom_field_type, $default)
->setCustomData(id(new PhabricatorUser())->getCustomFieldBaseClass())
->setDescription(pht("Select and reorder user profile fields.")),
$this->newOption('user.custom-field-definitions', 'map', array())
->setDescription(pht("Add new simple fields to user profiles.")),
);
}

View file

@ -100,12 +100,8 @@ final class PhabricatorPeopleProfileController
$fields = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
foreach ($fields as $field) {
$field->setViewer($viewer);
}
$view->applyCustomFields($fields);
$field_list = new PhabricatorCustomFieldList($fields);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
return $view;
}

View file

@ -35,11 +35,12 @@ final class PhabricatorPeopleProfileEditController
$fields = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_EDIT);
$field_list = new PhabricatorCustomFieldList($fields);
if ($request->isFormPost()) {
$xactions = array();
foreach ($fields as $field) {
$field->setValueFromRequest($request);
$field->readValueFromRequest($request);
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD)
->setMetadataValue('customfield:key', $field->getFieldKey())
@ -55,6 +56,8 @@ final class PhabricatorPeopleProfileEditController
$editor->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($profile_uri);
} else {
$field_list->readFieldsFromStorage($user);
}
$title = pht('Edit Profile');
@ -70,9 +73,7 @@ final class PhabricatorPeopleProfileEditController
$form = id(new AphrontFormView())
->setUser($viewer);
foreach ($fields as $field) {
$form->appendChild($field->renderEditControl());
}
$field_list->appendFieldsToForm($form);
$form
->appendChild(

View file

@ -46,7 +46,7 @@ final class PhabricatorUserBlurbField
$this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue());
}
public function setValueFromRequest(AphrontRequest $request) {
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}

View file

@ -0,0 +1,21 @@
<?php
final class PhabricatorUserConfiguredCustomField
extends PhabricatorUserCustomField
implements PhabricatorStandardCustomFieldInterface {
public function getStandardCustomFieldNamespace() {
return 'user';
}
public function createFields() {
return PhabricatorStandardCustomField::buildStandardFields(
$this,
PhabricatorEnv::getEnvConfig('user.custom-field-definitions', array()));
}
public function newStorageObject() {
return new PhabricatorUserConfiguredCustomFieldStorage();
}
}

View file

@ -1,8 +1,7 @@
<?php
abstract class PhabricatorUserCustomField
extends PhabricatorCustomField
implements PhabricatorUserCustomFieldInterface {
extends PhabricatorCustomField {
}

View file

@ -1,6 +0,0 @@
<?php
interface PhabricatorUserCustomFieldInterface {
}

View file

@ -49,7 +49,7 @@ final class PhabricatorUserRealNameField
$this->getObject()->setRealName($xaction->getNewValue());
}
public function setValueFromRequest(AphrontRequest $request) {
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}

View file

@ -46,7 +46,7 @@ final class PhabricatorUserTitleField
$this->getObject()->loadUserProfile()->setTitle($xaction->getNewValue());
}
public function setValueFromRequest(AphrontRequest $request) {
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}

View file

@ -840,7 +840,7 @@ EOBODY;
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomFieldInterface';
return 'PhabricatorUserCustomField';
}
public function getCustomFields($role) {

View file

@ -0,0 +1,11 @@
<?php
final class PhabricatorUserConfiguredCustomFieldStorage
extends PhabricatorCustomFieldStorage {
public function getApplicationName() {
return 'user';
}
}

View file

@ -0,0 +1,17 @@
<?php
final class PhabricatorCustomFieldNotProxyException
extends Exception {
public function __construct(PhabricatorCustomField $field) {
$key = $field->getFieldKey();
$name = $field->getFieldName();
$class = get_class($field);
parent::__construct(
"Custom field '{$name}' (with key '{$key}', of class '{$class}') can ".
"not have a proxy set with setProxy(), because it returned false from ".
"canSetProxy().");
}
}

View file

@ -3,6 +3,7 @@
/**
* @task apps Building Applications with Custom Fields
* @task core Core Properties and Field Identity
* @task proxy Field Proxies
* @task context Contextual Data
* @task storage Field Storage
* @task appsearch Integration with ApplicationSearch
@ -15,6 +16,7 @@ abstract class PhabricatorCustomField {
private $viewer;
private $object;
private $proxy;
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
@ -148,7 +150,12 @@ abstract class PhabricatorCustomField {
* @return string String which uniquely identifies this field.
* @task core
*/
abstract public function getFieldKey();
public function getFieldKey() {
if ($this->proxy) {
return $this->proxy->getFieldKey();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
@ -158,6 +165,9 @@ abstract class PhabricatorCustomField {
* @task core
*/
public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getFieldKey();
}
@ -170,6 +180,9 @@ abstract class PhabricatorCustomField {
* @task core
*/
public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null;
}
@ -200,6 +213,9 @@ abstract class PhabricatorCustomField {
* @task core
*/
public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true;
}
@ -212,19 +228,23 @@ abstract class PhabricatorCustomField {
*
* Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement
* @{method:getStorageKey()} to activate the `'storage'` role.
* @{method:shouldUseStorage()} to activate the `'storage'` role.
*
* @return bool True to enable the field for the given role.
* @task core
*/
public function shouldEnableForRole($role) {
if ($this->proxy) {
return $this->proxy->shouldEnableForRole($role);
}
switch ($role) {
case self::ROLE_APPLICATIONTRANSACTIONS:
return $this->shouldAppearInApplicationTransactions();
case self::ROLE_APPLICATIONSEARCH:
return $this->shouldAppearInApplicationSearch();
case self::ROLE_STORAGE:
return ($this->getStorageKey() !== null);
return $this->shouldUseStorage();
case self::ROLE_EDIT:
return $this->shouldAppearInEditView();
case self::ROLE_VIEW:
@ -264,6 +284,60 @@ abstract class PhabricatorCustomField {
}
/* -( Field Proxies )------------------------------------------------------ */
/**
* Proxies allow a field to use some other field's implementation for most
* of their behavior while still subclassing an application field. When a
* proxy is set for a field with @{method:setProxy}, all of its methods will
* call through to the proxy by default.
*
* This is most commonly used to implement configuration-driven custom fields
* using @{class:PhabricatorStandardCustomField}.
*
* This method must be overridden to return `true` before a field can accept
* proxies.
*
* @return bool True if you can @{method:setProxy} this field.
* @task proxy
*/
public function canSetProxy() {
if ($this instanceof PhabricatorStandardCustomFieldInterface) {
return true;
}
return false;
}
/**
* Set the proxy implementation for this field. See @{method:canSetProxy} for
* discussion of field proxies.
*
* @param PhabricatorCustomField Field implementation.
* @return this
*/
final public function setProxy(PhabricatorCustomField $proxy) {
if (!$this->canSetProxy()) {
throw new PhabricatorCustomFieldNotProxyException($this);
}
$this->proxy = $proxy;
return $this;
}
/**
* Get the field's proxy implementation, if any. For discussion, see
* @{method:canSetProxy}.
*
* @return PhabricatorCustomField|null Proxy field, if one is set.
*/
final public function getProxy() {
return $this->proxy;
}
/* -( Contextual Data )---------------------------------------------------- */
@ -274,6 +348,11 @@ abstract class PhabricatorCustomField {
* @task context
*/
final public function setObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->setObject($object);
return $this;
}
$this->object = $object;
$this->didSetObject($object);
return $this;
@ -287,6 +366,10 @@ abstract class PhabricatorCustomField {
* @task context
*/
final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object;
}
@ -306,6 +389,11 @@ abstract class PhabricatorCustomField {
* @task context
*/
final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer;
return $this;
}
@ -315,6 +403,10 @@ abstract class PhabricatorCustomField {
* @task context
*/
final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer;
}
@ -323,6 +415,10 @@ abstract class PhabricatorCustomField {
* @task context
*/
final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this);
}
@ -334,9 +430,7 @@ abstract class PhabricatorCustomField {
/**
* Return a unique string used to key storage of this field's value, like
* "mycompany.fieldname" or similar. You can return null (the default) to
* indicate that this field does not use any storage.
* Return true to use field storage.
*
* Fields which can be edited by the user will most commonly use storage,
* while some other types of fields (for instance, those which just display
@ -346,18 +440,14 @@ abstract class PhabricatorCustomField {
* If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}.
*
* In most cases, a reasonable implementation is to simply reuse the field
* key:
*
* return $this->getFieldKey();
*
* @return string|null Unique key which identifies this field in auxiliary
* field storage. Alternatively, return null (default)
* to indicate that this field does not use storage.
* @return bool True to use storage.
* @task storage
*/
public function getStorageKey() {
return null;
public function shouldUseStorage() {
if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
}
@ -369,7 +459,10 @@ abstract class PhabricatorCustomField {
* @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage
*/
public function getStorageObject() {
public function newStorageObject() {
if ($this->proxy) {
return $this->proxy->newStorageObject();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -377,7 +470,7 @@ abstract class PhabricatorCustomField {
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* you implement @{method:getStorageKey}.
* you implement @{method:shouldUseStorage}.
*
* If the field value is a scalar, it can be returned unmodiifed. If not,
* it should be serialized (for example, using JSON).
@ -386,6 +479,9 @@ abstract class PhabricatorCustomField {
* @task storage
*/
public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -394,7 +490,7 @@ abstract class PhabricatorCustomField {
* Set the field's value given a serialized storage value. This is called
* when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement
* @{method:getStorageKey}.
* @{method:shouldUseStorage}.
*
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in
@ -407,6 +503,9 @@ abstract class PhabricatorCustomField {
* @task storage
*/
public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -422,6 +521,9 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false;
}
@ -449,6 +551,9 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array();
}
@ -462,6 +567,9 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
protected function newStringIndexStorage() {
if ($this->proxy) {
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -475,6 +583,9 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
protected function newNumericIndexStorage() {
if ($this->proxy) {
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -487,6 +598,10 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndexKey();
return $this->newStringIndexStorage()
->setIndexKey($key)
@ -502,6 +617,9 @@ abstract class PhabricatorCustomField {
* @task appsearch
*/
protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndexKey();
return $this->newNumericIndexStorage()
->setIndexKey($key)
@ -520,6 +638,9 @@ abstract class PhabricatorCustomField {
* @task appxaction
*/
public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false;
}
@ -528,6 +649,9 @@ abstract class PhabricatorCustomField {
* @task appxaction
*/
public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
@ -536,6 +660,9 @@ abstract class PhabricatorCustomField {
* @task appxaction
*/
public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
@ -544,6 +671,9 @@ abstract class PhabricatorCustomField {
* @task appxaction
*/
public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value);
}
@ -553,6 +683,9 @@ abstract class PhabricatorCustomField {
*/
public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue();
}
@ -562,6 +695,9 @@ abstract class PhabricatorCustomField {
*/
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
@ -571,6 +707,9 @@ abstract class PhabricatorCustomField {
*/
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return;
}
@ -580,11 +719,15 @@ abstract class PhabricatorCustomField {
*/
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return;
}
$this->setValueFromApplicationTransaction($xaction->getNewValue());
$this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage();
$table = $this->newStorageObject();
@ -594,6 +737,7 @@ abstract class PhabricatorCustomField {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex());
} else {
@ -602,6 +746,7 @@ abstract class PhabricatorCustomField {
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex(),
$value);
@ -618,6 +763,9 @@ abstract class PhabricatorCustomField {
* @task edit
*/
public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false;
}
@ -626,6 +774,9 @@ abstract class PhabricatorCustomField {
* @task edit
*/
public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -634,6 +785,9 @@ abstract class PhabricatorCustomField {
* @task edit
*/
public function renderEditControl() {
if ($this->proxy) {
return $this->proxy->renderEditControl();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -645,6 +799,9 @@ abstract class PhabricatorCustomField {
* @task view
*/
public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false;
}
@ -653,6 +810,9 @@ abstract class PhabricatorCustomField {
* @task view
*/
public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName();
}
@ -661,6 +821,9 @@ abstract class PhabricatorCustomField {
* @task view
*/
public function renderPropertyViewValue() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
@ -669,6 +832,9 @@ abstract class PhabricatorCustomField {
* @task view
*/
public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property';
}
@ -680,6 +846,9 @@ abstract class PhabricatorCustomField {
* @task list
*/
public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false;
}
@ -688,9 +857,11 @@ abstract class PhabricatorCustomField {
* @task list
*/
public function renderOnListItem(PhabricatorObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
}

View file

@ -0,0 +1,123 @@
<?php
/**
* Convenience class to perform operations on an entire field list, like reading
* all values from storage.
*
* $field_list = new PhabricatorCustomFieldList($fields);
*
*/
final class PhabricatorCustomFieldList extends Phobject {
private $fields;
public function __construct(array $fields) {
assert_instances_of($fields, 'PhabricatorCustomField');
$this->fields = $fields;
}
/**
* Read stored values for all fields which support storage.
*
* @param PhabricatorCustomFieldInterface Object to read field values for.
* @return void
*/
public function readFieldsFromStorage(
PhabricatorCustomFieldInterface $object) {
$keys = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_STORAGE)) {
$keys[$field->getFieldIndex()] = $field;
}
}
if (!$keys) {
return;
}
// NOTE: We assume all fields share the same storage. This isn't guaranteed
// to be true, but always is for now.
$table = head($keys)->newStorageObject();
$objects = $table->loadAllWhere(
'objectPHID = %s AND fieldIndex IN (%Ls)',
$object->getPHID(),
array_keys($keys));
$objects = mpull($objects, null, 'getFieldIndex');
foreach ($keys as $key => $field) {
$storage = idx($objects, $key);
if ($storage) {
$field->setValueFromStorage($storage->getFieldValue());
} else {
$field->setValueFromStorage(null);
}
}
}
public function appendFieldsToForm(AphrontFormView $form) {
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) {
$form->appendChild($field->renderEditControl());
}
}
}
public function appendFieldsToPropertyList(
PhabricatorCustomFieldInterface $object,
PhabricatorUser $viewer,
PhabricatorPropertyListView $view) {
$this->readFieldsFromStorage($object);
$fields = $this->fields;
foreach ($fields as $field) {
$field->setViewer($viewer);
}
// Move all the blocks to the end, regardless of their configuration order,
// because it always looks silly to render a block in the middle of a list
// of properties.
$head = array();
$tail = array();
foreach ($fields as $key => $field) {
$style = $field->getStyleForPropertyView();
switch ($style) {
case 'property':
$head[$key] = $field;
break;
case 'block':
$tail[$key] = $field;
break;
default:
throw new Exception(
"Unknown field property view style '{$style}'; valid styles are ".
"'block' and 'property'.");
}
}
$fields = $head + $tail;
foreach ($fields as $field) {
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue();
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'property':
$view->addProperty($label, $value);
break;
case 'block':
$view->invokeWillRenderEvent();
if ($label !== null) {
$view->addSectionHeader($label);
}
$view->addTextContent($value);
break;
}
}
}
}
}

View file

@ -1,6 +1,6 @@
<?php
abstract class PhabricatorStandardCustomField
final class PhabricatorStandardCustomField
extends PhabricatorCustomField {
private $fieldKey;
@ -8,11 +8,44 @@ abstract class PhabricatorStandardCustomField
private $fieldType;
private $fieldValue;
private $fieldDescription;
private $fieldConfig;
private $applicationField;
public static function buildStandardFields(
PhabricatorCustomField $template,
array $config) {
$fields = array();
foreach ($config as $key => $value) {
$namespace = $template->getStandardCustomFieldNamespace();
$full_key = "std:{$namespace}:{$key}";
$template = clone $template;
$standard = id(new PhabricatorStandardCustomField($full_key))
->setFieldConfig($value)
->setApplicationField($template);
$field = $template->setProxy($standard);
$fields[] = $field;
}
return $fields;
}
public function __construct($key) {
$this->fieldKey = $key;
}
public function setApplicationField(
PhabricatorStandardCustomFieldInterface $application_field) {
$this->applicationField = $application_field;
return $this;
}
public function getApplicationField() {
return $this->applicationField;
}
public function setFieldName($name) {
$this->fieldName = $name;
return $this;
@ -37,6 +70,25 @@ abstract class PhabricatorStandardCustomField
return $this;
}
public function setFieldConfig(array $config) {
foreach ($config as $key => $value) {
switch ($key) {
case 'name':
$this->setFieldName($value);
break;
case 'type':
$this->setFieldType($value);
break;
}
}
$this->fieldConfig = $config;
return $this;
}
public function getFieldConfigValue($key, $default = null) {
return idx($this->fieldConfig, $key, $default);
}
/* -( PhabricatorCustomField )--------------------------------------------- */
@ -53,8 +105,8 @@ abstract class PhabricatorStandardCustomField
return coalesce($this->fieldDescription, parent::getFieldDescription());
}
public function getStorageKey() {
return $this->getFieldKey();
public function shouldUseStorage() {
return true;
}
public function getValueForStorage() {
@ -69,4 +121,37 @@ abstract class PhabricatorStandardCustomField
return true;
}
public function shouldAppearInEditView() {
return $this->getFieldConfigValue('edit', true);
}
public function readValueFromRequest(AphrontRequest $request) {
$this->setFieldValue($request->getStr($this->getFieldKey()));
}
public function renderEditControl() {
$type = $this->getFieldConfigValue('type', 'text');
switch ($type) {
case 'text':
default:
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setValue($this->getFieldValue())
->setLabel($this->getFieldName());
}
}
public function newStorageObject() {
return $this->getApplicationField()->newStorageObject();
}
public function shouldAppearInPropertyView() {
return $this->getFieldConfigValue('view', true);
}
public function renderPropertyViewValue() {
return $this->getFieldValue();
}
}

View file

@ -0,0 +1,7 @@
<?php
interface PhabricatorStandardCustomFieldInterface {
public function getStandardCustomFieldNamespace();
}

View file

@ -1547,6 +1547,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20130731.releephcutpointidentifier.sql'),
),
'20130814.usercustom.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130814.usercustom.sql'),
),
);
}
}

View file

@ -81,51 +81,6 @@ final class PhabricatorPropertyListView extends AphrontView {
$this->invokedWillRenderEvent = true;
}
public function applyCustomFields(array $fields) {
assert_instances_of($fields, 'PhabricatorCustomField');
// Move all the blocks to the end, regardless of their configuration order,
// because it always looks silly to render a block in the middle of a list
// of properties.
$head = array();
$tail = array();
foreach ($fields as $key => $field) {
$style = $field->getStyleForPropertyView();
switch ($style) {
case 'property':
$head[$key] = $field;
break;
case 'block':
$tail[$key] = $field;
break;
default:
throw new Exception(
"Unknown field property view style '{$style}'; valid styles are ".
"'block' and 'property'.");
}
}
$fields = $head + $tail;
foreach ($fields as $field) {
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue();
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'property':
$this->addProperty($label, $value);
break;
case 'block':
$this->invokeWillRenderEvent();
if ($label !== null) {
$this->addSectionHeader($label);
}
$this->addTextContent($value);
break;
}
}
}
}
public function render() {
$this->invokeWillRenderEvent();