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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 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 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 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 Transactions being applied, * which may be empty if this field is not being edited. * @return list 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 @@ 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; + } + + }