1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 00:32:42 +01:00

Give Almanac generic, custom-field-based properties

Summary:
Ref T5833. Currently, we have an `AlmanacDeviceProperty`, but it doesn't use CustomFields and is specific to devices. Make this more generic:

  - Reuse most of the CustomField infrastructure (so we can eventually get easy support for nice editor UIs, etc).
  - Make properties more generic so Services, Bindings and Devices can all have them.

The major difference between this implementation and existing CustomField implementations is that all other implementations are application-authoritative: the application code determines what the available list of fields is.

I want Almanac to be a bit more freeform (basically: you can write whatever properties you want, and we'll put nice UIs on them if we have a nice UI available). For example, we might have some sort of "ServiceTemplate" that says "a database binding should usually have the fields 'writable', 'active', 'credential'", which would do things like offer these as options and put a nice UI on them, but you should also be able to write whatever other properties you want and add services without building a specific service template for them.

This involves a little bit of rule bending, but ends up pretty clean. We can adjust CustomField to accommodate this a bit more gracefully later on if it makes sense.

Test Plan: {F229172}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5833

Differential Revision: https://secure.phabricator.com/D10777
This commit is contained in:
epriestley 2014-11-05 15:27:16 -08:00
parent dd7d8cf910
commit 2f1b5ae010
18 changed files with 598 additions and 36 deletions

View file

@ -0,0 +1 @@
DROP TABLE {$NAMESPACE}_almanac.almanac_deviceproperty;

View file

@ -0,0 +1,8 @@
CREATE TABLE {$NAMESPACE}_almanac.almanac_property (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
objectPHID VARBINARY(64) NOT NULL,
fieldIndex BINARY(12) NOT NULL,
fieldName VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
fieldValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
UNIQUE KEY `objectPHID` (objectPHID, fieldIndex)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -22,9 +22,11 @@ phutil_register_library_map(array(
'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php',
'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php',
'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php',
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
@ -32,7 +34,6 @@ phutil_register_library_map(array(
'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php',
'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
'AlmanacDeviceProperty' => 'applications/almanac/storage/AlmanacDeviceProperty.php',
'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php',
'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
@ -59,6 +60,11 @@ phutil_register_library_map(array(
'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
'AlmanacService' => 'applications/almanac/storage/AlmanacService.php',
'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php',
'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php',
@ -2978,9 +2984,14 @@ phutil_register_library_map(array(
'AlmanacConduitUtil' => 'Phobject',
'AlmanacConsoleController' => 'AlmanacController',
'AlmanacController' => 'PhabricatorController',
'AlmanacCoreCustomField' => array(
'AlmanacCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCustomField' => 'PhabricatorCustomField',
'AlmanacDAO' => 'PhabricatorLiskDAO',
'AlmanacDevice' => array(
'AlmanacDAO',
@ -2991,7 +3002,6 @@ phutil_register_library_map(array(
'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacDeviceListController' => 'AlmanacDeviceController',
'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
'AlmanacDeviceProperty' => 'AlmanacDAO',
'AlmanacDeviceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction',
@ -3024,9 +3034,19 @@ phutil_register_library_map(array(
'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction',
'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNetworkViewController' => 'AlmanacNetworkController',
'AlmanacProperty' => array(
'PhabricatorCustomFieldStorage',
'PhabricatorPolicyInterface',
),
'AlmanacPropertyController' => 'AlmanacController',
'AlmanacPropertyEditController' => 'AlmanacDeviceController',
'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacService' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'AlmanacPropertyInterface',
),
'AlmanacServiceController' => 'AlmanacController',
'AlmanacServiceEditController' => 'AlmanacServiceController',

View file

@ -56,6 +56,9 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacNetworkEditController',
'(?P<id>\d+)/' => 'AlmanacNetworkViewController',
),
'property/' => array(
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacPropertyEditController',
),
),
);
}

View file

@ -1,4 +1,65 @@
<?php
abstract class AlmanacController
extends PhabricatorController {}
extends PhabricatorController {
protected function buildAlmanacPropertiesTable($object) {
$viewer = $this->getViewer();
$properties = id(new AlmanacPropertyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
$rows = array();
foreach ($properties as $property) {
$value = $property->getFieldValue();
$rows[] = array(
$property->getFieldName(),
PhabricatorConfigJSON::prettyPrintJSON($value),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No properties.'))
->setHeaders(
array(
pht('Name'),
pht('Value'),
))
->setColumnClasses(
array(
null,
'wide',
));
$phid = $object->getPHID();
$add_uri = $this->getApplicationURI("property/edit/?objectPHID={$phid}");
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$add_button = id(new PHUIButtonView())
->setTag('a')
->setHref($add_uri)
->setWorkflow(true)
->setDisabled(!$can_edit)
->setText(pht('Add Property'))
->setIcon(
id(new PHUIIconView())
->setIconFont('fa-plus'));
$header = id(new PHUIHeaderView())
->setHeader(pht('Properties'))
->addActionLink($add_button);
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
}
}

View file

@ -0,0 +1,3 @@
<?php
abstract class AlmanacPropertyController extends AlmanacController {}

View file

@ -0,0 +1,169 @@
<?php
final class AlmanacPropertyEditController
extends AlmanacDeviceController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if ($id) {
$property = id(new AlmanacPropertyQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$property) {
return new Aphront404Response();
}
$object = $property->getObject();
$is_new = false;
$title = pht('Edit Property');
$save_button = pht('Save Changes');
} else {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($request->getStr('objectPHID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$is_new = true;
$title = pht('Add Property');
$save_button = pht('Add Property');
}
if (!($object instanceof AlmanacPropertyInterface)) {
return new Aphront404Response();
}
$cancel_uri = $object->getURI();
if ($is_new) {
$errors = array();
$property = null;
$v_name = null;
$e_name = true;
if ($request->isFormPost()) {
$name = $request->getStr('name');
if (!strlen($name)) {
$e_name = pht('Required');
$errors[] = pht('You must provide a property name.');
} else {
$caught = null;
try {
AlmanacNames::validateServiceOrDeviceName($name);
} catch (Exception $ex) {
$caught = $ex;
}
if ($caught) {
$e_name = pht('Invalid');
$errors[] = $caught->getMessage();
}
}
if (!$errors) {
$property = id(new AlmanacPropertyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->withNames(array($name))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$property) {
$property = id(new AlmanacProperty())
->setObjectPHID($object->getPHID())
->setFieldName($name);
}
}
}
if (!$property) {
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($v_name)
->setError($e_name));
return $this->newDialog()
->setTitle($title)
->setErrors($errors)
->addHiddenInput('objectPHID', $request->getStr('objectPHID'))
->appendForm($form)
->addSubmitButton(pht('Continue'))
->addCancelButton($cancel_uri);
}
}
$v_name = $property->getFieldName();
$e_name = true;
$v_value = $property->getFieldValue();
$e_value = null;
$object->attachAlmanacProperties(array($property));
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($object);
$validation_exception = null;
if ($request->isFormPost() && $request->getStr('isValueEdit')) {
$xactions = $field_list->buildFieldTransactionsFromRequest(
$object->getApplicationTransactionTemplate(),
$request);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($object, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('objectPHID', $request->getStr('objectPHID'))
->addHiddenInput('name', $request->getStr('name'))
->addHiddenInput('isValueEdit', true);
$field_list->appendFieldsToForm($form);
return $this->newDialog()
->setTitle($title)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton($save_button)
->addCancelButton($cancel_uri);
}
}

View file

@ -15,6 +15,7 @@ final class AlmanacServiceViewController
$service = id(new AlmanacServiceQuery())
->setViewer($viewer)
->withNames(array($name))
->needProperties(true)
->executeOne();
if (!$service) {
return new Aphront404Response();
@ -56,6 +57,7 @@ final class AlmanacServiceViewController
$crumbs,
$box,
$bindings,
$this->buildAlmanacPropertiesTable($service),
$xaction_view,
),
array(

View file

@ -0,0 +1,62 @@
<?php
final class AlmanacCoreCustomField
extends AlmanacCustomField
implements PhabricatorStandardCustomFieldInterface {
public function getStandardCustomFieldNamespace() {
return 'almanac:core';
}
public function createFields($object) {
$specs = array();
foreach ($object->getAlmanacProperties() as $property) {
$specs[$property->getFieldName()] = array(
'name' => $property->getFieldName(),
'type' => 'text',
);
}
return PhabricatorStandardCustomField::buildStandardFields($this, $specs);
}
public function shouldUseStorage() {
return false;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$key = $this->getProxy()->getRawStandardFieldKey();
$this->setValueFromStorage($object->getAlmanacPropertyValue($key));
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
return;
}
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
$object = $this->getObject();
$phid = $object->getPHID();
$key = $this->getProxy()->getRawStandardFieldKey();
$property = id(new AlmanacPropertyQuery())
->setViewer($this->getViewer())
->withObjectPHIDs(array($phid))
->withNames(array($key))
->executeOne();
if (!$property) {
$property = id(new AlmanacProperty())
->setObjectPHID($phid)
->setFieldIndex(PhabricatorHash::digestForIndex($key))
->setFieldName($key);
}
$property
->setFieldValue($xaction->getNewValue())
->save();
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class AlmanacCustomField
extends PhabricatorCustomField {}

View file

@ -25,15 +25,15 @@ final class AlmanacManagementRegisterWorkflow
->setName(php_uname('n'))
->save();
id(new AlmanacDeviceProperty())
->setDevicePHID($host->getPHID())
->setKey('conduitPublicOpenSSHKey')
id(new AlmanacProperty())
->setObjectPHID($host->getPHID())
->setName('conduitPublicOpenSSHKey')
->setValue($public_key)
->save();
id(new AlmanacDeviceProperty())
->setDevicePHID($host->getPHID())
->setKey('conduitPublicOpenSSLKey')
id(new AlmanacProperty())
->setObjectPHID($host->getPHID())
->setName('conduitPublicOpenSSLKey')
->setValue($this->convertToOpenSSLPublicKey($public_key))
->save();

View file

@ -0,0 +1,11 @@
<?php
interface AlmanacPropertyInterface {
public function attachAlmanacProperties(array $properties);
public function getAlmanacProperties();
public function hasAlmanacProperty($key);
public function getAlmanacProperty($key);
public function getAlmanacPropertyValue($key, $default = null);
}

View file

@ -0,0 +1,86 @@
<?php
final class AlmanacPropertyQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $objectPHIDs;
private $names;
public function withObjectPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
protected function loadPage() {
$table = new AlmanacProperty();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $properties) {
$object_phids = mpull($properties, 'getObjectPHID');
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
foreach ($properties as $key => $property) {
$object = idx($objects, $property->getObjectPHID());
if (!$object) {
unset($properties[$key]);
continue;
}
$property->attachObject($object);
}
return $properties;
}
protected function buildWhereClause($conn_r) {
$where = array();
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn_r,
'fieldIndex IN (%Ls)',
$hashes);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}

View file

@ -6,6 +6,7 @@ final class AlmanacServiceQuery
private $ids;
private $phids;
private $names;
private $needProperties;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -22,6 +23,11 @@ final class AlmanacServiceQuery
return $this;
}
public function needProperties($need) {
$this->needProperties = $need;
return $this;
}
protected function loadPage() {
$table = new AlmanacService();
$conn_r = $table->establishConnection('r');
@ -71,6 +77,25 @@ final class AlmanacServiceQuery
return $this->formatWhereClause($where);
}
protected function didFilterPage(array $services) {
// NOTE: We load properties unconditionally because CustomField assumes
// it can always generate a list of fields on an object. It may make
// sense to re-examine that assumption eventually.
$properties = id(new AlmanacPropertyQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withObjectPHIDs(mpull($services, null, 'getPHID'))
->execute();
$properties = mgroup($properties, 'getObjectPHID');
foreach ($services as $service) {
$service_properties = idx($properties, $service->getPHID(), array());
$service->attachAlmanacProperties($service_properties);
}
return $services;
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}

View file

@ -1,25 +0,0 @@
<?php
final class AlmanacDeviceProperty extends AlmanacDAO {
protected $devicePHID;
protected $key;
protected $value;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'value' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'key' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_device' => array(
'columns' => array('devicePHID', 'key'),
),
),
) + parent::getConfiguration();
}
}

View file

@ -0,0 +1,56 @@
<?php
final class AlmanacProperty
extends PhabricatorCustomFieldStorage
implements PhabricatorPolicyInterface {
protected $fieldName;
private $object = self::ATTACHABLE;
public function getApplicationName() {
return 'almanac';
}
public function getConfiguration() {
$config = parent::getConfiguration();
$config[self::CONFIG_COLUMN_SCHEMA] += array(
'fieldName' => 'text128',
);
return $config;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject(PhabricatorLiskDAO $object) {
$this->object = $object;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getObject()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Properties inherit the policies of their object.');
}
}

View file

@ -2,7 +2,11 @@
final class AlmanacService
extends AlmanacDAO
implements PhabricatorPolicyInterface {
implements
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
AlmanacPropertyInterface {
protected $name;
protected $nameIndex;
@ -10,6 +14,9 @@ final class AlmanacService
protected $viewPolicy;
protected $editPolicy;
private $customFields = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewService() {
return id(new AlmanacService())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
@ -56,6 +63,38 @@ final class AlmanacService
return '/almanac/service/view/'.$this->getName().'/';
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
@ -83,4 +122,41 @@ final class AlmanacService
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'AlmanacCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacServiceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacServiceTransaction();
}
}

View file

@ -119,7 +119,7 @@ abstract class PhabricatorApplicationTransactionQuery
// NOTE: We have to do this after loading objects, because the objects
// may help determine which handles are required (for example, in the case
// of custom fields.
// of custom fields).
if ($this->needHandles) {
$phids = array();