1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 00:42:41 +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', 'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php', 'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php', 'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php', 'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php', 'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php', 'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
@ -1607,6 +1609,7 @@ phutil_register_library_map(array(
'PhabricatorSortTableExample' => 'applications/uiexample/examples/PhabricatorSortTableExample.php', 'PhabricatorSortTableExample' => 'applications/uiexample/examples/PhabricatorSortTableExample.php',
'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/field/PhabricatorStandardCustomField.php', 'PhabricatorStandardCustomField' => 'infrastructure/customfield/field/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStatusController' => 'applications/system/PhabricatorStatusController.php', 'PhabricatorStatusController' => 'applications/system/PhabricatorStatusController.php',
'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php', 'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
@ -1677,8 +1680,9 @@ phutil_register_library_map(array(
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.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', 'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
'PhabricatorUserCustomFieldInterface' => 'applications/people/customfield/PhabricatorUserCustomFieldInterface.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', 'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
@ -3092,7 +3096,9 @@ phutil_register_library_map(array(
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception', 'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldList' => 'Phobject',
'PhabricatorCustomFieldNotAttachedException' => 'Exception', 'PhabricatorCustomFieldNotAttachedException' => 'Exception',
'PhabricatorCustomFieldNotProxyException' => 'Exception',
'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
@ -3778,11 +3784,13 @@ phutil_register_library_map(array(
), ),
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUserCustomField' => 'PhabricatorUserConfiguredCustomField' =>
array( array(
0 => 'PhabricatorCustomField', 0 => 'PhabricatorUserCustomField',
1 => 'PhabricatorUserCustomFieldInterface', 1 => 'PhabricatorStandardCustomFieldInterface',
), ),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmail' => 'PhabricatorUserDAO',

View file

@ -34,6 +34,8 @@ final class PhabricatorUserConfigOptions
$this->newOption('user.fields', $custom_field_type, $default) $this->newOption('user.fields', $custom_field_type, $default)
->setCustomData(id(new PhabricatorUser())->getCustomFieldBaseClass()) ->setCustomData(id(new PhabricatorUser())->getCustomFieldBaseClass())
->setDescription(pht("Select and reorder user profile fields.")), ->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( $fields = PhabricatorCustomField::getObjectFields(
$user, $user,
PhabricatorCustomField::ROLE_VIEW); PhabricatorCustomField::ROLE_VIEW);
$field_list = new PhabricatorCustomFieldList($fields);
foreach ($fields as $field) { $field_list->appendFieldsToPropertyList($user, $viewer, $view);
$field->setViewer($viewer);
}
$view->applyCustomFields($fields);
return $view; return $view;
} }

View file

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

View file

@ -46,7 +46,7 @@ final class PhabricatorUserBlurbField
$this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue()); $this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue());
} }
public function setValueFromRequest(AphrontRequest $request) { public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey()); $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 <?php
abstract class PhabricatorUserCustomField abstract class PhabricatorUserCustomField
extends PhabricatorCustomField extends PhabricatorCustomField {
implements PhabricatorUserCustomFieldInterface {
} }

View file

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

View file

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

View file

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

View file

@ -840,7 +840,7 @@ EOBODY;
} }
public function getCustomFieldBaseClass() { public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomFieldInterface'; return 'PhabricatorUserCustomField';
} }
public function getCustomFields($role) { 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 apps Building Applications with Custom Fields
* @task core Core Properties and Field Identity * @task core Core Properties and Field Identity
* @task proxy Field Proxies
* @task context Contextual Data * @task context Contextual Data
* @task storage Field Storage * @task storage Field Storage
* @task appsearch Integration with ApplicationSearch * @task appsearch Integration with ApplicationSearch
@ -15,6 +16,7 @@ abstract class PhabricatorCustomField {
private $viewer; private $viewer;
private $object; private $object;
private $proxy;
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions'; const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch'; const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
@ -148,7 +150,12 @@ abstract class PhabricatorCustomField {
* @return string String which uniquely identifies this field. * @return string String which uniquely identifies this field.
* @task core * @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 * @task core
*/ */
public function getFieldName() { public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getFieldKey(); return $this->getFieldKey();
} }
@ -170,6 +180,9 @@ abstract class PhabricatorCustomField {
* @task core * @task core
*/ */
public function getFieldDescription() { public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null; return null;
} }
@ -200,6 +213,9 @@ abstract class PhabricatorCustomField {
* @task core * @task core
*/ */
public function isFieldEnabled() { public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true; return true;
} }
@ -212,19 +228,23 @@ abstract class PhabricatorCustomField {
* *
* Normally, you do not need to override this method. Instead, override the * Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement * 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. * @return bool True to enable the field for the given role.
* @task core * @task core
*/ */
public function shouldEnableForRole($role) { public function shouldEnableForRole($role) {
if ($this->proxy) {
return $this->proxy->shouldEnableForRole($role);
}
switch ($role) { switch ($role) {
case self::ROLE_APPLICATIONTRANSACTIONS: case self::ROLE_APPLICATIONTRANSACTIONS:
return $this->shouldAppearInApplicationTransactions(); return $this->shouldAppearInApplicationTransactions();
case self::ROLE_APPLICATIONSEARCH: case self::ROLE_APPLICATIONSEARCH:
return $this->shouldAppearInApplicationSearch(); return $this->shouldAppearInApplicationSearch();
case self::ROLE_STORAGE: case self::ROLE_STORAGE:
return ($this->getStorageKey() !== null); return $this->shouldUseStorage();
case self::ROLE_EDIT: case self::ROLE_EDIT:
return $this->shouldAppearInEditView(); return $this->shouldAppearInEditView();
case self::ROLE_VIEW: 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 )---------------------------------------------------- */ /* -( Contextual Data )---------------------------------------------------- */
@ -274,6 +348,11 @@ abstract class PhabricatorCustomField {
* @task context * @task context
*/ */
final public function setObject(PhabricatorCustomFieldInterface $object) { final public function setObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->setObject($object);
return $this;
}
$this->object = $object; $this->object = $object;
$this->didSetObject($object); $this->didSetObject($object);
return $this; return $this;
@ -287,6 +366,10 @@ abstract class PhabricatorCustomField {
* @task context * @task context
*/ */
final public function getObject() { final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object; return $this->object;
} }
@ -306,6 +389,11 @@ abstract class PhabricatorCustomField {
* @task context * @task context
*/ */
final public function setViewer(PhabricatorUser $viewer) { final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer; $this->viewer = $viewer;
return $this; return $this;
} }
@ -315,6 +403,10 @@ abstract class PhabricatorCustomField {
* @task context * @task context
*/ */
final public function getViewer() { final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer; return $this->viewer;
} }
@ -323,6 +415,10 @@ abstract class PhabricatorCustomField {
* @task context * @task context
*/ */
final protected function requireViewer() { final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) { if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this); 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 * Return true to use field storage.
* "mycompany.fieldname" or similar. You can return null (the default) to
* indicate that this field does not use any storage.
* *
* Fields which can be edited by the user will most commonly use 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 * 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} * If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}. * and @{method:setValueFromStorage}.
* *
* In most cases, a reasonable implementation is to simply reuse the field * @return bool True to use storage.
* 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.
* @task storage * @task storage
*/ */
public function getStorageKey() { public function shouldUseStorage() {
return null; if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
} }
@ -369,7 +459,10 @@ abstract class PhabricatorCustomField {
* @return PhabricatorCustomFieldStorage New empty storage object. * @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage * @task storage
*/ */
public function getStorageObject() { public function newStorageObject() {
if ($this->proxy) {
return $this->proxy->newStorageObject();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -377,7 +470,7 @@ abstract class PhabricatorCustomField {
/** /**
* Return a serialized representation of the field value, appropriate for * Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if * 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, * If the field value is a scalar, it can be returned unmodiifed. If not,
* it should be serialized (for example, using JSON). * it should be serialized (for example, using JSON).
@ -386,6 +479,9 @@ abstract class PhabricatorCustomField {
* @task storage * @task storage
*/ */
public function getValueForStorage() { public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -394,7 +490,7 @@ abstract class PhabricatorCustomField {
* Set the field's value given a serialized storage value. This is called * 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 * when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement * 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 * Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in * need to undo whatever serialization you applied in
@ -407,6 +503,9 @@ abstract class PhabricatorCustomField {
* @task storage * @task storage
*/ */
public function setValueFromStorage($value) { public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -422,6 +521,9 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
public function shouldAppearInApplicationSearch() { public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false; return false;
} }
@ -449,6 +551,9 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
public function buildFieldIndexes() { public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array(); return array();
} }
@ -462,6 +567,9 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newStringIndexStorage() { protected function newStringIndexStorage() {
if ($this->proxy) {
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -475,6 +583,9 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newNumericIndexStorage() { protected function newNumericIndexStorage() {
if ($this->proxy) {
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -487,6 +598,10 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newStringIndex($value) { protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndexKey(); $key = $this->getFieldIndexKey();
return $this->newStringIndexStorage() return $this->newStringIndexStorage()
->setIndexKey($key) ->setIndexKey($key)
@ -502,6 +617,9 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newNumericIndex($value) { protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndexKey(); $key = $this->getFieldIndexKey();
return $this->newNumericIndexStorage() return $this->newNumericIndexStorage()
->setIndexKey($key) ->setIndexKey($key)
@ -520,6 +638,9 @@ abstract class PhabricatorCustomField {
* @task appxaction * @task appxaction
*/ */
public function shouldAppearInApplicationTransactions() { public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false; return false;
} }
@ -528,6 +649,9 @@ abstract class PhabricatorCustomField {
* @task appxaction * @task appxaction
*/ */
public function getOldValueForApplicationTransactions() { public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage(); return $this->getValueForStorage();
} }
@ -536,6 +660,9 @@ abstract class PhabricatorCustomField {
* @task appxaction * @task appxaction
*/ */
public function getNewValueForApplicationTransactions() { public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage(); return $this->getValueForStorage();
} }
@ -544,6 +671,9 @@ abstract class PhabricatorCustomField {
* @task appxaction * @task appxaction
*/ */
public function setValueFromApplicationTransactions($value) { public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value); return $this->setValueFromStorage($value);
} }
@ -553,6 +683,9 @@ abstract class PhabricatorCustomField {
*/ */
public function getNewValueFromApplicationTransactions( public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue(); return $xaction->getNewValue();
} }
@ -562,6 +695,9 @@ abstract class PhabricatorCustomField {
*/ */
public function getApplicationTransactionHasEffect( public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue()); return ($xaction->getOldValue() !== $xaction->getNewValue());
} }
@ -571,6 +707,9 @@ abstract class PhabricatorCustomField {
*/ */
public function applyApplicationTransactionInternalEffects( public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return; return;
} }
@ -580,11 +719,15 @@ abstract class PhabricatorCustomField {
*/ */
public function applyApplicationTransactionExternalEffects( public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) { if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return; return;
} }
$this->setValueFromApplicationTransaction($xaction->getNewValue()); $this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage(); $value = $this->getValueForStorage();
$table = $this->newStorageObject(); $table = $this->newStorageObject();
@ -594,6 +737,7 @@ abstract class PhabricatorCustomField {
queryfx( queryfx(
$conn_w, $conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s', 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(), $this->getObject()->getPHID(),
$this->getFieldIndex()); $this->getFieldIndex());
} else { } else {
@ -602,6 +746,7 @@ abstract class PhabricatorCustomField {
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s) VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
$table->getTableName(),
$this->getObject()->getPHID(), $this->getObject()->getPHID(),
$this->getFieldIndex(), $this->getFieldIndex(),
$value); $value);
@ -618,6 +763,9 @@ abstract class PhabricatorCustomField {
* @task edit * @task edit
*/ */
public function shouldAppearInEditView() { public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false; return false;
} }
@ -626,6 +774,9 @@ abstract class PhabricatorCustomField {
* @task edit * @task edit
*/ */
public function readValueFromRequest(AphrontRequest $request) { public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -634,6 +785,9 @@ abstract class PhabricatorCustomField {
* @task edit * @task edit
*/ */
public function renderEditControl() { public function renderEditControl() {
if ($this->proxy) {
return $this->proxy->renderEditControl();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -645,6 +799,9 @@ abstract class PhabricatorCustomField {
* @task view * @task view
*/ */
public function shouldAppearInPropertyView() { public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false; return false;
} }
@ -653,6 +810,9 @@ abstract class PhabricatorCustomField {
* @task view * @task view
*/ */
public function renderPropertyViewLabel() { public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName(); return $this->getFieldName();
} }
@ -661,6 +821,9 @@ abstract class PhabricatorCustomField {
* @task view * @task view
*/ */
public function renderPropertyViewValue() { public function renderPropertyViewValue() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -669,6 +832,9 @@ abstract class PhabricatorCustomField {
* @task view * @task view
*/ */
public function getStyleForPropertyView() { public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property'; return 'property';
} }
@ -680,6 +846,9 @@ abstract class PhabricatorCustomField {
* @task list * @task list
*/ */
public function shouldAppearInListView() { public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false; return false;
} }
@ -688,9 +857,11 @@ abstract class PhabricatorCustomField {
* @task list * @task list
*/ */
public function renderOnListItem(PhabricatorObjectItemView $view) { public function renderOnListItem(PhabricatorObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); 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 <?php
abstract class PhabricatorStandardCustomField final class PhabricatorStandardCustomField
extends PhabricatorCustomField { extends PhabricatorCustomField {
private $fieldKey; private $fieldKey;
@ -8,11 +8,44 @@ abstract class PhabricatorStandardCustomField
private $fieldType; private $fieldType;
private $fieldValue; private $fieldValue;
private $fieldDescription; 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) { public function __construct($key) {
$this->fieldKey = $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) { public function setFieldName($name) {
$this->fieldName = $name; $this->fieldName = $name;
return $this; return $this;
@ -37,6 +70,25 @@ abstract class PhabricatorStandardCustomField
return $this; 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 )--------------------------------------------- */ /* -( PhabricatorCustomField )--------------------------------------------- */
@ -53,8 +105,8 @@ abstract class PhabricatorStandardCustomField
return coalesce($this->fieldDescription, parent::getFieldDescription()); return coalesce($this->fieldDescription, parent::getFieldDescription());
} }
public function getStorageKey() { public function shouldUseStorage() {
return $this->getFieldKey(); return true;
} }
public function getValueForStorage() { public function getValueForStorage() {
@ -69,4 +121,37 @@ abstract class PhabricatorStandardCustomField
return true; 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', 'type' => 'sql',
'name' => $this->getPatchPath('20130731.releephcutpointidentifier.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; $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() { public function render() {
$this->invokeWillRenderEvent(); $this->invokeWillRenderEvent();