Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/differential/customfield/DifferentialSummaryField.php b/src/applications/differential/customfield/DifferentialSummaryField.php
index 9db2651f6f..4d95a56eb3 100644
--- a/src/applications/differential/customfield/DifferentialSummaryField.php
+++ b/src/applications/differential/customfield/DifferentialSummaryField.php
@@ -1,81 +1,92 @@
<?php
final class DifferentialSummaryField
extends DifferentialCoreCustomField {
public function getFieldKey() {
return 'differential:summary';
}
public function getFieldName() {
return pht('Summary');
}
public function getFieldDescription() {
return pht('Stores a summary of the revision.');
}
protected function readValueFromRevision(
DifferentialRevision $revision) {
return $revision->getSummary();
}
protected function writeValueToRevision(
DifferentialRevision $revision,
$value) {
$revision->setSummary($value);
}
public function readValueFromRequest(AphrontRequest $request) {
$this->setValue($request->getStr($this->getFieldKey()));
}
public function renderEditControl(array $handles) {
return id(new PhabricatorRemarkupControl())
->setName($this->getFieldKey())
->setValue($this->getValue())
->setError($this->getFieldError())
->setLabel($this->getFieldName());
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
return pht(
'%s updated the summary for this revision.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction,
PhabricatorFeedStory $story) {
$object_phid = $xaction->getObjectPHID();
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
return pht(
'%s updated the summary for %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
return true;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
return $xaction->renderTextCorpusChangeDetails(
$viewer,
$xaction->getOldValue(),
$xaction->getNewValue());
}
+ public function shouldAppearInGlobalSearch() {
+ return true;
+ }
+
+ public function updateAbstractDocument(
+ PhabricatorSearchAbstractDocument $document) {
+ if (strlen($this->getValue())) {
+ $document->addField('body', $this->getValue());
+ }
+ }
+
}
diff --git a/src/applications/differential/customfield/DifferentialTestPlanField.php b/src/applications/differential/customfield/DifferentialTestPlanField.php
index ba22675c7c..3c4dbdadc5 100644
--- a/src/applications/differential/customfield/DifferentialTestPlanField.php
+++ b/src/applications/differential/customfield/DifferentialTestPlanField.php
@@ -1,95 +1,107 @@
<?php
final class DifferentialTestPlanField
extends DifferentialCoreCustomField {
public function getFieldKey() {
return 'differential:test-plan';
}
public function getFieldName() {
return pht('Test Plan');
}
public function getFieldDescription() {
return pht('Actions performed to verify the behavior of the change.');
}
protected function readValueFromRevision(
DifferentialRevision $revision) {
return $revision->getTestPlan();
}
protected function writeValueToRevision(
DifferentialRevision $revision,
$value) {
$revision->setTestPlan($value);
}
protected function isCoreFieldRequired() {
return PhabricatorEnv::getEnvConfig('differential.require-test-plan-field');
}
public function canDisableField() {
return true;
}
protected function getCoreFieldRequiredErrorString() {
return pht(
'You must provide a test plan: describe the actions you performed '.
'to verify the behvaior of this change.');
}
public function readValueFromRequest(AphrontRequest $request) {
$this->setValue($request->getStr($this->getFieldKey()));
}
public function renderEditControl(array $handles) {
return id(new PhabricatorRemarkupControl())
->setName($this->getFieldKey())
->setValue($this->getValue())
->setError($this->getFieldError())
->setLabel($this->getFieldName());
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
return pht(
'%s updated the test plan for this revision.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction,
PhabricatorFeedStory $story) {
$object_phid = $xaction->getObjectPHID();
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
return pht(
'%s updated the test plan for %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
return true;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
return $xaction->renderTextCorpusChangeDetails(
$viewer,
$xaction->getOldValue(),
$xaction->getNewValue());
}
+
+ public function shouldAppearInGlobalSearch() {
+ return true;
+ }
+
+ public function updateAbstractDocument(
+ PhabricatorSearchAbstractDocument $document) {
+ if (strlen($this->getValue())) {
+ $document->addField('plan', $this->getValue());
+ }
+ }
+
}
diff --git a/src/applications/differential/search/DifferentialSearchIndexer.php b/src/applications/differential/search/DifferentialSearchIndexer.php
index 2dee73de54..9bc27402fe 100644
--- a/src/applications/differential/search/DifferentialSearchIndexer.php
+++ b/src/applications/differential/search/DifferentialSearchIndexer.php
@@ -1,117 +1,81 @@
<?php
-/**
- * @group differential
- */
final class DifferentialSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new DifferentialRevision();
}
+ protected function loadDocumentByPHID($phid) {
+ $object = id(new DifferentialRevisionQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array($phid))
+ ->needReviewerStatus(true)
+ ->executeOne();
+ if (!$object) {
+ throw new Exception("Unable to load object by phid '{$phid}'!");
+ }
+ return $object;
+ }
+
protected function buildAbstractDocumentByPHID($phid) {
$rev = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($rev->getPHID());
$doc->setDocumentType(DifferentialPHIDTypeRevision::TYPECONST);
$doc->setDocumentTitle($rev->getTitle());
$doc->setDocumentCreated($rev->getDateCreated());
$doc->setDocumentModified($rev->getDateModified());
- $aux_fields = DifferentialFieldSelector::newSelector()
- ->getFieldSpecifications();
- foreach ($aux_fields as $key => $aux_field) {
- $aux_field->setUser(PhabricatorUser::getOmnipotentUser());
- if (!$aux_field->shouldAddToSearchIndex()) {
- unset($aux_fields[$key]);
- }
- }
-
- $aux_fields = DifferentialAuxiliaryField::loadFromStorage(
- $rev,
- $aux_fields);
- foreach ($aux_fields as $aux_field) {
- $doc->addField(
- $aux_field->getKeyForSearchIndex(),
- $aux_field->getValueForSearchIndex());
- }
-
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$rev->getAuthorPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$rev->getDateCreated());
$doc->addRelationship(
$rev->isClosed()
? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
: PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$rev->getPHID(),
DifferentialPHIDTypeRevision::TYPECONST,
time());
- $comments = id(new DifferentialCommentQuery())
- ->withRevisionPHIDs(array($rev->getPHID()))
- ->execute();
-
- $inlines = id(new DifferentialInlineCommentQuery())
- ->withRevisionIDs(array($rev->getID()))
- ->withNotDraft(true)
- ->execute();
-
- foreach (array_merge($comments, $inlines) as $comment) {
- if (strlen($comment->getContent())) {
- $doc->addField(
- PhabricatorSearchField::FIELD_COMMENT,
- $comment->getContent());
- }
- }
-
- $rev->loadRelationships();
+ $this->indexTransactions(
+ $doc,
+ new DifferentialTransactionQuery(),
+ array($rev->getPHID()));
// If a revision needs review, the owners are the reviewers. Otherwise, the
// owner is the author (e.g., accepted, rejected, closed).
if ($rev->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
- $reviewers = $rev->getReviewers();
+ $reviewers = $rev->getReviewerStatus();
+ $reviewers = mpull($reviewers, 'getReviewerPHID', 'getReviewerPHID');
if ($reviewers) {
foreach ($reviewers as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$phid,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$rev->getDateModified()); // Bogus timestamp.
}
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
$rev->getPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$rev->getDateModified()); // Bogus timestamp.
}
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$rev->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_VOID,
$rev->getDateCreated());
}
- $ccphids = $rev->getCCPHIDs();
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs($ccphids)
- ->execute();
-
- foreach ($handles as $phid => $handle) {
- $doc->addRelationship(
- PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
- $phid,
- $handle->getType(),
- $rev->getDateModified()); // Bogus timestamp.
- }
-
return $doc;
}
}
diff --git a/src/applications/maniphest/search/ManiphestSearchIndexer.php b/src/applications/maniphest/search/ManiphestSearchIndexer.php
index 971488f64c..41e699d762 100644
--- a/src/applications/maniphest/search/ManiphestSearchIndexer.php
+++ b/src/applications/maniphest/search/ManiphestSearchIndexer.php
@@ -1,88 +1,86 @@
<?php
/**
* @group maniphest
*/
final class ManiphestSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new ManiphestTask();
}
protected function buildAbstractDocumentByPHID($phid) {
$task = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($task->getPHID());
$doc->setDocumentType(ManiphestPHIDTypeTask::TYPECONST);
$doc->setDocumentTitle($task->getTitle());
$doc->setDocumentCreated($task->getDateCreated());
$doc->setDocumentModified($task->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$task->getDescription());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$task->getAuthorPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$task->getDateCreated());
$doc->addRelationship(
(ManiphestTaskStatus::isOpenStatus($task->getStatus()))
? PhabricatorSearchRelationship::RELATIONSHIP_OPEN
: PhabricatorSearchRelationship::RELATIONSHIP_CLOSED,
$task->getPHID(),
ManiphestPHIDTypeTask::TYPECONST,
time());
$this->indexTransactions(
$doc,
new ManiphestTransactionQuery(),
array($phid));
foreach ($task->getProjectPHIDs() as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$phid,
PhabricatorProjectPHIDTypeProject::TYPECONST,
$task->getDateModified()); // Bogus.
}
$owner = $task->getOwnerPHID();
if ($owner) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$owner,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
time());
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
$task->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_VOID,
$task->getDateCreated());
}
// We need to load handles here since non-users may subscribe (mailing
// lists, e.g.)
$ccs = $task->getCCPHIDs();
$handles = id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($ccs)
->execute();
foreach ($ccs as $cc) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$handles[$cc]->getPHID(),
$handles[$cc]->getType(),
time());
}
- $this->indexCustomFields($doc, $task);
-
return $doc;
}
}
diff --git a/src/applications/people/search/PhabricatorUserSearchIndexer.php b/src/applications/people/search/PhabricatorUserSearchIndexer.php
index 8d74334ee9..bb0a2a337c 100644
--- a/src/applications/people/search/PhabricatorUserSearchIndexer.php
+++ b/src/applications/people/search/PhabricatorUserSearchIndexer.php
@@ -1,32 +1,30 @@
<?php
final class PhabricatorUserSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new PhabricatorUser();
}
protected function buildAbstractDocumentByPHID($phid) {
$user = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($user->getPHID());
$doc->setDocumentType(PhabricatorPeoplePHIDTypeUser::TYPECONST);
$doc->setDocumentTitle($user->getUserName().' ('.$user->getRealName().')');
$doc->setDocumentCreated($user->getDateCreated());
$doc->setDocumentModified($user->getDateModified());
$doc->addRelationship(
$user->isUserActivated()
? PhabricatorSearchRelationship::RELATIONSHIP_OPEN
: PhabricatorSearchRelationship::RELATIONSHIP_CLOSED,
$user->getPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
time());
- $this->indexCustomFields($doc, $user);
-
return $doc;
}
}
diff --git a/src/applications/ponder/search/PonderSearchIndexer.php b/src/applications/ponder/search/PonderSearchIndexer.php
index 854d4fd9d7..82cc13dc9a 100644
--- a/src/applications/ponder/search/PonderSearchIndexer.php
+++ b/src/applications/ponder/search/PonderSearchIndexer.php
@@ -1,53 +1,51 @@
<?php
final class PonderSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new PonderQuestion();
}
protected function buildAbstractDocumentByPHID($phid) {
$question = $this->loadDocumentByPHID($phid);
$doc = $this->newDocument($phid)
->setDocumentTitle($question->getTitle())
->setDocumentCreated($question->getDateCreated())
->setDocumentModified($question->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$question->getContent());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$question->getAuthorPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$question->getDateCreated());
$answers = id(new PonderAnswerQuery())
->setViewer($this->getViewer())
->withQuestionIDs(array($question->getID()))
->execute();
foreach ($answers as $answer) {
if (strlen($answer->getContent())) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$answer->getContent());
}
}
$this->indexTransactions(
$doc,
new PonderQuestionTransactionQuery(),
array($phid));
$this->indexTransactions(
$doc,
new PonderAnswerTransactionQuery(),
mpull($answers, 'getPHID'));
- $this->indexSubscribers($doc);
-
return $doc;
}
}
diff --git a/src/applications/project/search/PhabricatorProjectSearchIndexer.php b/src/applications/project/search/PhabricatorProjectSearchIndexer.php
index a5c8d5dca3..17a0ac7be4 100644
--- a/src/applications/project/search/PhabricatorProjectSearchIndexer.php
+++ b/src/applications/project/search/PhabricatorProjectSearchIndexer.php
@@ -1,36 +1,33 @@
<?php
final class PhabricatorProjectSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new PhabricatorProject();
}
protected function buildAbstractDocumentByPHID($phid) {
$project = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($project->getPHID());
$doc->setDocumentType(PhabricatorProjectPHIDTypeProject::TYPECONST);
$doc->setDocumentTitle($project->getName());
$doc->setDocumentCreated($project->getDateCreated());
$doc->setDocumentModified($project->getDateModified());
- $this->indexSubscribers($doc);
- $this->indexCustomFields($doc, $project);
-
$doc->addRelationship(
$project->isArchived()
? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
: PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$project->getPHID(),
PhabricatorProjectPHIDTypeProject::TYPECONST,
time());
// NOTE: This could be more full-featured, but for now we're mostly
// interested in the side effects of indexing.
return $doc;
}
}
diff --git a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
index 1965c55809..4781a8fdd2 100644
--- a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
+++ b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
@@ -1,147 +1,159 @@
<?php
/**
* @group search
*/
abstract class PhabricatorSearchDocumentIndexer {
abstract public function getIndexableObject();
abstract protected function buildAbstractDocumentByPHID($phid);
protected function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
public function shouldIndexDocumentByPHID($phid) {
$object = $this->getIndexableObject();
return (phid_get_type($phid) == phid_get_type($object->generatePHID()));
}
public function getIndexIterator() {
$object = $this->getIndexableObject();
return new LiskMigrationIterator($object);
}
protected function loadDocumentByPHID($phid) {
$object = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
throw new Exception("Unable to load object by phid '{$phid}'!");
}
return $object;
}
public function indexDocumentByPHID($phid) {
try {
$document = $this->buildAbstractDocumentByPHID($phid);
+ $object = $this->loadDocumentByPHID($phid);
+
+ // Automatically rebuild CustomField indexes if the object uses custom
+ // fields.
+ if ($object instanceof PhabricatorCustomFieldInterface) {
+ $this->indexCustomFields($document, $object);
+ }
+
+ // Automatically rebuild subscriber indexes if the object is subscribable.
+ if ($object instanceof PhabricatorSubscribableInterface) {
+ $this->indexSubscribers($document);
+ }
+
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
try {
$engine->reindexAbstractDocument($document);
} catch (Exception $ex) {
$phid = $document->getPHID();
$class = get_class($engine);
phlog("Unable to index document {$phid} with engine {$class}.");
phlog($ex);
}
$this->dispatchDidUpdateIndexEvent($phid, $document);
} catch (Exception $ex) {
$class = get_class($this);
phlog("Unable to build document {$phid} with indexer {$class}.");
phlog($ex);
}
return $this;
}
protected function newDocument($phid) {
return id(new PhabricatorSearchAbstractDocument())
->setPHID($phid)
->setDocumentType(phid_get_type($phid));
}
protected function indexSubscribers(
PhabricatorSearchAbstractDocument $doc) {
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$doc->getPHID());
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs($subscribers)
->execute();
foreach ($handles as $phid => $handle) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$phid,
$handle->getType(),
$doc->getDocumentModified()); // Bogus timestamp.
}
}
protected function indexTransactions(
PhabricatorSearchAbstractDocument $doc,
PhabricatorApplicationTransactionQuery $query,
array $phids) {
$xactions = id(clone $query)
->setViewer($this->getViewer())
->withObjectPHIDs($phids)
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))
->execute();
foreach ($xactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$comment = $xaction->getComment();
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$comment->getContent());
}
}
protected function indexCustomFields(
- PhabricatorSearchAbstractDocument $doc,
+ PhabricatorSearchAbstractDocument $document,
PhabricatorCustomFieldInterface $object) {
// Rebuild the ApplicationSearch indexes. These are internal and not part of
// the fulltext search, but putting them in this workflow allows users to
// use the same tools to rebuild the indexes, which is easy to understand.
$field_list = PhabricatorCustomField::getObjectFields(
$object,
- PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
+ PhabricatorCustomField::ROLE_DEFAULT);
$field_list->setViewer($this->getViewer());
$field_list->readFieldsFromStorage($object);
+
+ // Rebuild ApplicationSearch indexes.
$field_list->rebuildIndexes($object);
- // We could also allow fields to provide fulltext content, and index it
- // here on the document. No one has asked for this yet, though, and the
- // existing "search" key isn't a good fit to interpret to mean we should
- // index stuff here, since it can be set on a lot of fields which don't
- // contain anything resembling fulltext.
+ // Rebuild global search indexes.
+ $field_list->updateAbstractDocument($document);
}
private function dispatchDidUpdateIndexEvent(
$phid,
PhabricatorSearchAbstractDocument $document) {
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_SEARCH_DIDUPDATEINDEX,
array(
'phid' => $phid,
'object' => $this->loadDocumentByPHID($phid),
'document' => $document,
));
$event->setUser($this->getViewer());
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index 133bf382b8..3cd46b0690 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1094 +1,1124 @@
<?php
/**
- * @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
- * @task appxaction Integration with ApplicationTransactions
- * @task edit Integration with edit views
- * @task view Integration with property views
- * @task list Integration with list views
+ * @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 edit Integration with Edit Views
+ * @task view Integration with Property Views
+ * @task list Integration with List views
+ * @task appsearch Integration with ApplicationSearch
+ * @task appxaction Integration with ApplicationTransactions
+ * @task globalsearch Integration with Global Search
*/
abstract class PhabricatorCustomField {
private $viewer;
private $object;
private $proxy;
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
const ROLE_STORAGE = 'storage';
const ROLE_DEFAULT = 'default';
const ROLE_EDIT = 'edit';
const ROLE_VIEW = 'view';
const ROLE_LIST = 'list';
+ const ROLE_GLOBALSEARCH = 'GlobalSearch';
/* -( Building Applications with Custom Fields )--------------------------- */
/**
* @task apps
*/
public static function getObjectFields(
PhabricatorCustomFieldInterface $object,
$role) {
try {
$attachment = $object->getCustomFields();
} catch (PhabricatorDataNotAttachedException $ex) {
$attachment = new PhabricatorCustomFieldAttachment();
$object->attachCustomFields($attachment);
}
try {
$field_list = $attachment->getCustomFieldList($role);
} catch (PhabricatorCustomFieldNotAttachedException $ex) {
$base_class = $object->getCustomFieldBaseClass();
$spec = $object->getCustomFieldSpecificationForRole($role);
if (!is_array($spec)) {
$obj_class = get_class($object);
throw new Exception(
"Expected an array from getCustomFieldSpecificationForRole() for ".
"object of class '{$obj_class}'.");
}
$fields = PhabricatorCustomField::buildFieldList($base_class, $spec);
foreach ($fields as $key => $field) {
if (!$field->shouldEnableForRole($role)) {
unset($fields[$key]);
}
}
foreach ($fields as $field) {
$field->setObject($object);
}
$field_list = new PhabricatorCustomFieldList($fields);
$attachment->addCustomFieldList($role, $field_list);
}
return $field_list;
}
/**
* @task apps
*/
public static function getObjectField(
PhabricatorCustomFieldInterface $object,
$role,
$field_key) {
$fields = self::getObjectFields($object, $role)->getFields();
return idx($fields, $field_key);
}
/**
* @task apps
*/
public static function buildFieldList($base_class, array $spec) {
$field_objects = id(new PhutilSymbolLoader())
->setAncestorClass($base_class)
->loadObjects();
$fields = array();
$from_map = array();
foreach ($field_objects as $field_object) {
$current_class = get_class($field_object);
foreach ($field_object->createFields() as $field) {
$key = $field->getFieldKey();
if (isset($fields[$key])) {
$original_class = $from_map[$key];
throw new Exception(
"Both '{$original_class}' and '{$current_class}' define a custom ".
"field with field key '{$key}'. Field keys must be unique.");
}
$from_map[$key] = $current_class;
$fields[$key] = $field;
}
}
foreach ($fields as $key => $field) {
if (!$field->isFieldEnabled()) {
unset($fields[$key]);
}
}
$fields = array_select_keys($fields, array_keys($spec)) + $fields;
foreach ($spec as $key => $config) {
if (empty($fields[$key])) {
continue;
}
if (!empty($config['disabled'])) {
if ($fields[$key]->canDisableField()) {
unset($fields[$key]);
}
}
}
return $fields;
}
/* -( Core Properties and Field Identity )--------------------------------- */
/**
* Return a key which uniquely identifies this field, like
* "mycompany:dinosaur:count". Normally you should provide some level of
* namespacing to prevent collisions.
*
* @return string String which uniquely identifies this field.
* @task core
*/
public function getFieldKey() {
if ($this->proxy) {
return $this->proxy->getFieldKey();
}
throw new PhabricatorCustomFieldImplementationIncompleteException(
$this,
$field_key_is_incomplete = true);
}
/**
* Return a human-readable field name.
*
* @return string Human readable field name.
* @task core
*/
public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getFieldKey();
}
/**
* Return a short, human-readable description of the field's behavior. This
* provides more context to administrators when they are customizing fields.
*
* @return string|null Optional human-readable description.
* @task core
*/
public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null;
}
/**
* Most field implementations are unique, in that one class corresponds to
* one field. However, some field implementations are general and a single
* implementation may drive several fields.
*
* For general implementations, the general field implementation can return
* multiple field instances here.
*
* @return list<PhabricatorCustomField> List of fields.
* @task core
*/
public function createFields() {
return array($this);
}
/**
* You can return `false` here if the field should not be enabled for any
* role. For example, it might depend on something (like an application or
* library) which isn't installed, or might have some global configuration
* which allows it to be disabled.
*
* @return bool False to completely disable this field for all roles.
* @task core
*/
public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true;
}
/**
* Low level selector for field availability. Fields can appear in different
* roles (like an edit view, a list view, etc.), but not every field needs
* to appear everywhere. Fields that are disabled in a role won't appear in
* that context within applications.
*
* Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement
* @{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->shouldUseStorage();
case self::ROLE_EDIT:
return $this->shouldAppearInEditView();
case self::ROLE_VIEW:
return $this->shouldAppearInPropertyView();
case self::ROLE_LIST:
return $this->shouldAppearInListView();
+ case self::ROLE_GLOBALSEARCH:
+ return $this->shouldAppearInGlobalSearch();
case self::ROLE_DEFAULT:
return true;
default:
throw new Exception("Unknown field role '{$role}'!");
}
}
/**
* Allow administrators to disable this field. Most fields should allow this,
* but some are fundamental to the behavior of the application and can be
* locked down to avoid chaos, disorder, and the decline of civilization.
*
* @return bool False to prevent this field from being disabled through
* configuration.
* @task core
*/
public function canDisableField() {
return true;
}
/**
* Return an index string which uniquely identifies this field.
*
* @return string Index string which uniquely identifies this field.
* @task core
*/
final public function getFieldIndex() {
return PhabricatorHash::digestForIndex($this->getFieldKey());
}
/* -( 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 )---------------------------------------------------- */
/**
* Sets the object this field belongs to.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @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;
}
/**
* Read object data into local field storage, if applicable.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @task context
*/
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->readValueFromObject($object);
}
return $this;
}
/**
* Get the object this field belongs to.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @task context
*/
final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object;
}
/**
* This is a hook, primarily for subclasses to load object data.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @return void
*/
protected function didSetObject(PhabricatorCustomFieldInterface $object) {
return;
}
/**
* @task context
*/
final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer;
return $this;
}
/**
* @task context
*/
final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer;
}
/**
* @task context
*/
final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this);
}
return $this->viewer;
}
/* -( 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
* information in some stylized way) may not. Many builtin fields do not use
* storage because their data is available on the object itself.
*
* If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}.
*
* @return bool True to use storage.
* @task storage
*/
public function shouldUseStorage() {
if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
}
/**
* Return a new, empty storage object. This should be a subclass of
* @{class:PhabricatorCustomFieldStorage} which is bound to the application's
* database.
*
* @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage
*/
public function newStorageObject() {
if ($this->proxy) {
return $this->proxy->newStorageObject();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* 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).
*
* @return string Serialized field value.
* @task storage
*/
public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* 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:shouldUseStorage}.
*
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in
* @{method:getValueForStorage}.
*
* @param string|null Serialized field representation (from
* @{method:getValueForStorage}) or null if no value has
* ever been stored.
* @return this
* @task storage
*/
public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( ApplicationSearch )-------------------------------------------------- */
/**
* Appearing in ApplicationSearch allows a field to be indexed and searched
* for.
*
* @return bool True to appear in ApplicationSearch.
* @task appsearch
*/
public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false;
}
/**
* Return one or more indexes which this field can meaningfully query against
* to implement ApplicationSearch.
*
* Normally, you should build these using @{method:newStringIndex} and
* @{method:newNumericIndex}. For example, if a field holds a numeric value
* it might return a single numeric index:
*
* return array($this->newNumericIndex($this->getValue()));
*
* If a field holds a more complex value (like a list of users), it might
* return several string indexes:
*
* $indexes = array();
* foreach ($this->getValue() as $phid) {
* $indexes[] = $this->newStringIndex($phid);
* }
* return $indexes;
*
* @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
* @task appsearch
*/
public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array();
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newStringIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newNumericIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build and populate storage for a string index.
*
* @param string String to index.
* @return PhabricatorCustomFieldStringIndexStorage Populated storage.
* @task appsearch
*/
protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndex();
return $this->newStringIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Build and populate storage for a numeric index.
*
* @param string Numeric value to index.
* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
* @task appsearch
*/
protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndex();
return $this->newNumericIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Read a query value from a request, for storage in a saved query. Normally,
* this method should, e.g., read a string out of the request.
*
* @param PhabricatorApplicationSearchEngine Engine building the query.
* @param AphrontRequest Request to read from.
* @return wild
* @task appsearch
*/
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readApplicationSearchValueFromRequest(
$engine,
$request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Constrain a query, given a field value. Generally, this method should
* use `with...()` methods to apply filters or other constraints to the
* query.
*
* @param PhabricatorApplicationSearchEngine Engine executing the query.
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param wild Constraint provided by the user.
* @return void
* @task appsearch
*/
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if ($this->proxy) {
return $this->proxy->applyApplicationSearchConstraintToQuery(
$engine,
$query,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Append search controls to the interface. If you need handles, use
* @{method:getRequiredHandlePHIDsForApplicationSearch} to get them.
*
* @param PhabricatorApplicationSearchEngine Engine constructing the form.
* @param AphrontFormView The form to update.
* @param wild Value from the saved query.
* @param list<PhabricatorObjectHandle> List of handles.
* @return void
* @task appsearch
*/
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value,
array $handles) {
if ($this->proxy) {
return $this->proxy->appendToApplicationSearchForm(
$engine,
$form,
$value,
$handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs
* handles for. This is primarily useful if the field stores PHIDs and you
* need to (for example) render a tokenizer control.
*
* @param wild Value from the saved query.
* @return list<phid> List of PHIDs.
* @task appsearch
*/
public function getRequiredHandlePHIDsForApplicationSearch($value) {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value);
}
return array();
}
/* -( ApplicationTransactions )-------------------------------------------- */
/**
* Appearing in ApplicationTrasactions allows a field to be edited using
* standard workflows.
*
* @return bool True to appear in ApplicationTransactions.
* @task appxaction
*/
public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false;
}
/**
* @task appxaction
*/
public function getApplicationTransactionType() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionType();
}
return PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
/**
* @task appxaction
*/
public function getApplicationTransactionMetadata() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionMetadata();
}
return array();
}
/**
* @task appxaction
*/
public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value);
}
/**
* @task appxaction
*/
public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue();
}
/**
* @task appxaction
*/
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
/**
* @task appxaction
*/
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return;
}
/**
* @task appxaction
*/
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return;
}
$this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage();
$table = $this->newStorageObject();
$conn_w = $table->establishConnection('w');
if ($value === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex());
} else {
queryfx(
$conn_w,
'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);
}
return;
}
/**
* Validate transactions for an object. This allows you to raise an error
* when a transaction would set a field to an invalid value, or when a field
* is required but no transactions provide value.
*
* @param PhabricatorLiskDAO Editor applying the transactions.
* @param string Transaction type. This type is always
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
* convenience when constructing exceptions.
* @param list<PhabricatorApplicationTransaction> Transactions being applied,
* which may be empty if this field is not being edited.
* @return list<PhabricatorApplicationTransactionValidationError> Validation
* errors.
*
* @task appxaction
*/
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
if ($this->proxy) {
return $this->proxy->validateApplicationTransactions(
$editor,
$type,
$xactions);
}
return array();
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitle(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
return pht(
'%s updated this object.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction,
PhabricatorFeedStory $story) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitleForFeed(
$xaction,
$story);
}
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
return pht(
'%s updated %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasChangeDetails(
$xaction);
}
return false;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionChangeDetails(
$xaction,
$viewer);
}
return null;
}
public function getApplicationTransactionRequiredHandlePHIDs(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
$xaction);
}
return array();
}
/* -( Edit View )---------------------------------------------------------- */
/**
* @task edit
*/
public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false;
}
/**
* @task edit
*/
public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task edit
*/
public function getRequiredHandlePHIDsForEdit() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForEdit();
}
return array();
}
/**
* @task edit
*/
public function renderEditControl(array $handles) {
if ($this->proxy) {
return $this->proxy->renderEditControl($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Property View )------------------------------------------------------ */
/**
* @task view
*/
public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false;
}
/**
* @task view
*/
public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName();
}
/**
* @task view
*/
public function renderPropertyViewValue() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task view
*/
public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property';
}
/* -( List View )---------------------------------------------------------- */
/**
* @task list
*/
public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false;
}
/**
* @task list
*/
public function renderOnListItem(PHUIObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
+/* -( Global Search )------------------------------------------------------ */
+
+
+ /**
+ * @task globalsearch
+ */
+ public function shouldAppearInGlobalSearch() {
+ if ($this->proxy) {
+ return $this->proxy->shouldAppearInGlobalSearch();
+ }
+ return false;
+ }
+
+
+ /**
+ * @task globalsearch
+ */
+ public function updateAbstractDocument(
+ PhabricatorSearchAbstractDocument $document) {
+ if ($this->proxy) {
+ return $this->proxy->updateAbstractDocument($document);
+ }
+ return $document;
+ }
+
+
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
index ffe96bc285..dae4bd507a 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
@@ -1,307 +1,324 @@
<?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;
private $viewer;
public function __construct(array $fields) {
assert_instances_of($fields, 'PhabricatorCustomField');
$this->fields = $fields;
}
public function getFields() {
return $this->fields;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
foreach ($this->getFields() as $field) {
$field->setViewer($viewer);
}
return $this;
}
/**
* Read stored values for all fields which support storage.
*
* @param PhabricatorCustomFieldInterface Object to read field values for.
* @return void
*/
public function readFieldsFromStorage(
PhabricatorCustomFieldInterface $object) {
foreach ($this->fields as $field) {
$field->setObject($object);
$field->readValueFromObject($object);
}
$keys = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_STORAGE)) {
$keys[$field->getFieldIndex()] = $field;
}
}
if (!$keys) {
- return;
+ return $this;
}
// 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 = array();
if ($object->getPHID()) {
$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 if ($object->getPHID()) {
// NOTE: We set this only if the object exists. Otherwise, we allow the
// field to retain any default value it may have.
$field->setValueFromStorage(null);
}
}
+
+ return $this;
}
public function appendFieldsToForm(AphrontFormView $form) {
$enabled = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) {
$enabled[] = $field;
}
}
$phids = array();
foreach ($enabled as $field_key => $field) {
$phids[$field_key] = $field->getRequiredHandlePHIDsForEdit();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($enabled as $field_key => $field) {
$field_handles = array_select_keys($handles, $phids[$field_key]);
$form->appendChild($field->renderEditControl($field_handles));
}
}
public function appendFieldsToPropertyList(
PhabricatorCustomFieldInterface $object,
PhabricatorUser $viewer,
PHUIPropertyListView $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':
case 'header':
$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;
$add_header = null;
foreach ($fields as $field) {
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue();
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'header':
// We want to hide headers if the fields the're assciated with
// don't actually produce any visible properties. For example, in a
// list like this:
//
// Header A
// Prop A: Value A
// Header B
// Prop B: Value B
//
// ...if the "Prop A" field returns `null` when rendering its
// property value and we rendered naively, we'd get this:
//
// Header A
// Header B
// Prop B: Value B
//
// This is silly. Instead, we hide "Header A".
$add_header = $value;
break;
case 'property':
if ($add_header !== null) {
// Add the most recently seen header.
$view->addSectionHeader($add_header);
$add_header = null;
}
$view->addProperty($label, $value);
break;
case 'block':
$view->invokeWillRenderEvent();
if ($label !== null) {
$view->addSectionHeader($label);
}
$view->addTextContent($value);
break;
}
}
}
}
public function buildFieldTransactionsFromRequest(
PhabricatorApplicationTransaction $template,
AphrontRequest $request) {
$xactions = array();
$role = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$transaction_type = $field->getApplicationTransactionType();
$xaction = id(clone $template)
->setTransactionType($transaction_type);
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions only, we provide the old value
// as an input.
$old_value = $field->getOldValueForApplicationTransactions();
$xaction->setOldValue($old_value);
}
$field->readValueFromRequest($request);
$xaction
->setNewValue($field->getNewValueForApplicationTransactions());
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions, add the field key in metadata.
$xaction->setMetadataValue('customfield:key', $field->getFieldKey());
}
$metadata = $field->getApplicationTransactionMetadata();
foreach ($metadata as $key => $value) {
$xaction->setMetadataValue($key, $value);
}
$xactions[] = $xaction;
}
return $xactions;
}
/**
* Publish field indexes into index tables, so ApplicationSearch can search
* them.
*
* @return void
*/
public function rebuildIndexes(PhabricatorCustomFieldInterface $object) {
$indexes = array();
$index_keys = array();
$phid = $object->getPHID();
$role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$index_keys[$field->getFieldIndex()] = true;
foreach ($field->buildFieldIndexes() as $index) {
$index->setObjectPHID($phid);
$indexes[$index->getTableName()][] = $index;
}
}
if (!$indexes) {
return;
}
$any_index = head(head($indexes));
$conn_w = $any_index->establishConnection('w');
foreach ($indexes as $table => $index_list) {
$sql = array();
foreach ($index_list as $index) {
$sql[] = $index->formatForInsert($conn_w);
}
$indexes[$table] = $sql;
}
$any_index->openTransaction();
foreach ($indexes as $table => $sql_list) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)',
$table,
$phid,
array_keys($index_keys));
if (!$sql_list) {
continue;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q',
$table,
$chunk);
}
}
$any_index->saveTransaction();
}
+ public function updateAbstractDocument(
+ PhabricatorSearchAbstractDocument $document) {
+
+ $role = PhabricatorCustomField::ROLE_GLOBALSEARCH;
+ foreach ($this->getFields() as $field) {
+ if (!$field->shouldEnableForRole($role)) {
+ continue;
+ }
+ $field->updateAbstractDocument($document);
+ }
+
+ return $document;
+ }
+
+
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 14:49 (3 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1125664
Default Alt Text
(64 KB)

Event Timeline