@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Begin modularizing fields in SearchEngines

Summary:
Ref T8441. Ref T7715. Add a layer of indirection to fields in search engines. This will allow us to:

- Simplify SearchEngine code, which has collected a lot of duplication around expressing what is effectively field types.
- Automatically add fields like "Spaces" and "Projects" (primary driver for T8441).
- Reorder or hide fields (not sure if we really want to do this, but it seems plausible, and this will let us play around with it, at least).
- Drive Conduit Query methods via SearchEngines, so the same code specifies both the search UI and the `application.query` endpoint (primary driver in T7715).

Test Plan:
- Searched for stuff in Paste, everything behaved exaclty like it used to (except that I removed the "no language" checkbox, which seemed like fluff from a bygone era).
- Searched for stuff in other applications, saw no changes.
- Hit date field errors.
- Used query strings to specify values.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7715, T8441

Differential Revision: https://secure.phabricator.com/D13169

+472 -66
+11
src/__phutil_library_map__.php
··· 2503 2503 'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php', 2504 2504 'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php', 2505 2505 'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php', 2506 + 'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php', 2506 2507 'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php', 2507 2508 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', 2508 2509 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php', ··· 2513 2514 'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php', 2514 2515 'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php', 2515 2516 'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php', 2517 + 'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php', 2516 2518 'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php', 2517 2519 'PhabricatorSearchIndexer' => 'applications/search/index/PhabricatorSearchIndexer.php', 2518 2520 'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php', ··· 2523 2525 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 2524 2526 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 2525 2527 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', 2528 + 'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', 2529 + 'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php', 2530 + 'PhabricatorSearchUsersField' => 'applications/search/field/PhabricatorSearchUsersField.php', 2526 2531 'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php', 2527 2532 'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php', 2528 2533 'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php', ··· 4679 4684 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 4680 4685 'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4681 4686 'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController', 4687 + 'PhabricatorApplicationSearchEngine' => 'Phobject', 4682 4688 'PhabricatorApplicationStatusView' => 'AphrontView', 4683 4689 'PhabricatorApplicationTransaction' => array( 4684 4690 'PhabricatorLiskDAO', ··· 5995 6001 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 5996 6002 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 5997 6003 'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6004 + 'PhabricatorSearchDateField' => 'PhabricatorSearchField', 5998 6005 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 5999 6006 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 6000 6007 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', ··· 6004 6011 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', 6005 6012 'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource', 6006 6013 'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController', 6014 + 'PhabricatorSearchField' => 'Phobject', 6007 6015 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 6008 6016 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', 6009 6017 'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow', ··· 6012 6020 'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 6013 6021 'PhabricatorSearchResultView' => 'AphrontView', 6014 6022 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', 6023 + 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 6024 + 'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField', 6025 + 'PhabricatorSearchUsersField' => 'PhabricatorSearchTokenizerField', 6015 6026 'PhabricatorSearchWorker' => 'PhabricatorWorker', 6016 6027 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 6017 6028 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
+16 -61
src/applications/paste/query/PhabricatorPasteSearchEngine.php
··· 11 11 return 'PhabricatorPasteApplication'; 12 12 } 13 13 14 - public function buildSavedQueryFromRequest(AphrontRequest $request) { 15 - $saved = new PhabricatorSavedQuery(); 16 - $saved->setParameter( 17 - 'authorPHIDs', 18 - $this->readUsersFromRequest($request, 'authors')); 19 - 20 - $languages = $request->getStrList('languages'); 21 - if ($request->getBool('noLanguage')) { 22 - $languages[] = null; 23 - } 24 - $saved->setParameter('languages', $languages); 25 - 26 - $saved->setParameter('createdStart', $request->getStr('createdStart')); 27 - $saved->setParameter('createdEnd', $request->getStr('createdEnd')); 28 - 29 - return $saved; 30 - } 31 - 32 14 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 33 15 $query = id(new PhabricatorPasteQuery()) 34 16 ->needContent(true) ··· 49 31 return $query; 50 32 } 51 33 52 - public function buildSearchForm( 53 - AphrontFormView $form, 54 - PhabricatorSavedQuery $saved_query) { 55 - $author_phids = $saved_query->getParameter('authorPHIDs', array()); 56 - 57 - $languages = $saved_query->getParameter('languages', array()); 58 - $no_language = false; 59 - foreach ($languages as $key => $language) { 60 - if ($language === null) { 61 - $no_language = true; 62 - unset($languages[$key]); 63 - continue; 64 - } 65 - } 66 - 67 - $form 68 - ->appendControl( 69 - id(new AphrontFormTokenizerControl()) 70 - ->setDatasource(new PhabricatorPeopleDatasource()) 71 - ->setName('authors') 72 - ->setLabel(pht('Authors')) 73 - ->setValue($author_phids)) 74 - ->appendChild( 75 - id(new AphrontFormTextControl()) 76 - ->setName('languages') 77 - ->setLabel(pht('Languages')) 78 - ->setValue(implode(', ', $languages))) 79 - ->appendChild( 80 - id(new AphrontFormCheckboxControl()) 81 - ->addCheckbox( 82 - 'noLanguage', 83 - 1, 84 - pht('Find Pastes with no specified language.'), 85 - $no_language)); 86 - 87 - $this->buildDateRange( 88 - $form, 89 - $saved_query, 90 - 'createdStart', 91 - pht('Created After'), 92 - 'createdEnd', 93 - pht('Created Before')); 94 - 34 + protected function buildCustomSearchFields() { 35 + return array( 36 + id(new PhabricatorSearchUsersField()) 37 + ->setAliases(array('authors')) 38 + ->setKey('authorPHIDs') 39 + ->setLabel(pht('Authors')), 40 + id(new PhabricatorSearchStringListField()) 41 + ->setKey('languages') 42 + ->setLabel(pht('Languages')), 43 + id(new PhabricatorSearchDateField()) 44 + ->setKey('createdStart') 45 + ->setLabel(pht('Created After')), 46 + id(new PhabricatorSearchDateField()) 47 + ->setKey('createdEnd') 48 + ->setLabel(pht('Created Before')), 49 + ); 95 50 } 96 51 97 52 protected function getURI($path) {
+50 -5
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 14 14 * @task exec Paging and Executing Queries 15 15 * @task render Rendering Results 16 16 */ 17 - abstract class PhabricatorApplicationSearchEngine { 17 + abstract class PhabricatorApplicationSearchEngine extends Phobject { 18 18 19 19 private $application; 20 20 private $viewer; ··· 69 69 * @param AphrontRequest The search request. 70 70 * @return PhabricatorSavedQuery 71 71 */ 72 - abstract public function buildSavedQueryFromRequest( 73 - AphrontRequest $request); 72 + public function buildSavedQueryFromRequest(AphrontRequest $request) { 73 + $fields = $this->buildSearchFields(); 74 + $viewer = $this->requireViewer(); 75 + 76 + $saved = new PhabricatorSavedQuery(); 77 + foreach ($fields as $field) { 78 + $field->setViewer($viewer); 79 + 80 + $value = $field->readValueFromRequest($request); 81 + $saved->setParameter($field->getKey(), $value); 82 + } 83 + 84 + return $saved; 85 + } 74 86 75 87 /** 76 88 * Executes the saved query. ··· 88 100 * @param PhabricatorSavedQuery The query from which to build the form. 89 101 * @return void 90 102 */ 91 - abstract public function buildSearchForm( 103 + public function buildSearchForm( 92 104 AphrontFormView $form, 93 - PhabricatorSavedQuery $query); 105 + PhabricatorSavedQuery $saved) { 106 + 107 + $fields = $this->buildSearchFields(); 108 + $viewer = $this->requireViewer(); 109 + 110 + foreach ($fields as $field) { 111 + $field->setViewer($viewer); 112 + $field->readValueFromSavedQuery($saved); 113 + } 114 + 115 + foreach ($fields as $field) { 116 + foreach ($field->getErrors() as $error) { 117 + $this->addError(last($error)); 118 + } 119 + } 120 + 121 + foreach ($fields as $field) { 122 + $field->appendToForm($form); 123 + } 124 + } 125 + 126 + protected function buildSearchFields() { 127 + $fields = array(); 128 + 129 + foreach ($this->buildCustomSearchFields() as $field) { 130 + $fields[] = $field; 131 + } 132 + 133 + return $fields; 134 + } 135 + 136 + protected function buildCustomSearchFields() { 137 + throw new PhutilMethodNotImplementedException(); 138 + } 94 139 95 140 public function getErrors() { 96 141 return $this->errors;
+37
src/applications/search/field/PhabricatorSearchDateField.php
··· 1 + <?php 2 + 3 + final class PhabricatorSearchDateField 4 + extends PhabricatorSearchField { 5 + 6 + protected function newControl() { 7 + return new AphrontFormTextControl(); 8 + } 9 + 10 + protected function getValueFromRequest(AphrontRequest $request, $key) { 11 + return $request->getStr($key); 12 + } 13 + 14 + protected function validateControlValue($value) { 15 + if (!strlen($value)) { 16 + return; 17 + } 18 + 19 + $epoch = $this->parseDateTime($value); 20 + if ($epoch) { 21 + return; 22 + } 23 + 24 + $this->addError( 25 + pht('Invalid'), 26 + pht('Date value for "%s" can not be parsed.', $this->getLabel())); 27 + } 28 + 29 + protected function parseDateTime($value) { 30 + if (!strlen($value)) { 31 + return null; 32 + } 33 + 34 + return PhabricatorTime::parseLocalTime($value, $this->getViewer()); 35 + } 36 + 37 + }
+260
src/applications/search/field/PhabricatorSearchField.php
··· 1 + <?php 2 + 3 + /** 4 + * @task config Configuring Fields 5 + * @task error Handling Errors 6 + * @task io Reading and Writing Field Values 7 + * @task util Utility Methods 8 + */ 9 + abstract class PhabricatorSearchField extends Phobject { 10 + 11 + private $key; 12 + private $viewer; 13 + private $value; 14 + private $label; 15 + private $aliases = array(); 16 + private $errors = array(); 17 + 18 + 19 + /* -( Configuring Fields )------------------------------------------------- */ 20 + 21 + 22 + /** 23 + * Set the primary key for the field, like `projectPHIDs`. 24 + * 25 + * You can set human-readable aliases with @{method:setAliases}. 26 + * 27 + * The key should be a short, unique (within a search engine) string which 28 + * does not contain any special characters. 29 + * 30 + * @param string Unique key which identifies the field. 31 + * @return this 32 + * @task config 33 + */ 34 + public function setKey($key) { 35 + $this->key = $key; 36 + return $this; 37 + } 38 + 39 + 40 + /** 41 + * Get the field's key. 42 + * 43 + * @return string Unique key for this field. 44 + * @task config 45 + */ 46 + public function getKey() { 47 + return $this->key; 48 + } 49 + 50 + 51 + /** 52 + * Set a human-readable label for the field. 53 + * 54 + * This should be a short text string, like "Reviewers" or "Colors". 55 + * 56 + * @param string Short, human-readable field label. 57 + * @return this 58 + * task config 59 + */ 60 + public function setLabel($label) { 61 + $this->label = $label; 62 + return $this; 63 + } 64 + 65 + 66 + /** 67 + * Get the field's human-readable label. 68 + * 69 + * @return string Short, human-readable field label. 70 + * @task config 71 + */ 72 + public function getLabel() { 73 + return $this->label; 74 + } 75 + 76 + 77 + /** 78 + * Set the acting viewer. 79 + * 80 + * Engines do not need to do this explicitly; it will be done on their 81 + * behalf by the caller. 82 + * 83 + * @param PhabricatorUser Viewer. 84 + * @return this 85 + * @task config 86 + */ 87 + public function setViewer(PhabricatorUser $viewer) { 88 + $this->viewer = $viewer; 89 + return $this; 90 + } 91 + 92 + 93 + /** 94 + * Get the acting viewer. 95 + * 96 + * @return PhabricatorUser Viewer. 97 + * @task config 98 + */ 99 + public function getViewer() { 100 + return $this->viewer; 101 + } 102 + 103 + 104 + /** 105 + * Provide alternate field aliases, usually more human-readable versions 106 + * of the key. 107 + * 108 + * These aliases can be used when building GET requests, so you can provide 109 + * an alias like `authors` to let users write `&authors=alincoln` instead of 110 + * `&authorPHIDs=alincoln`. This is a little easier to use. 111 + * 112 + * @param list<string> List of aliases for this field. 113 + * @return this 114 + * @task config 115 + */ 116 + public function setAliases(array $aliases) { 117 + $this->aliases = $aliases; 118 + return $this; 119 + } 120 + 121 + 122 + /** 123 + * Get aliases for this field. 124 + * 125 + * @return list<string> List of aliases for this field. 126 + * @task config 127 + */ 128 + public function getAliases() { 129 + return $this->aliases; 130 + } 131 + 132 + 133 + /* -( Handling Errors )---------------------------------------------------- */ 134 + 135 + 136 + protected function addError($short, $long) { 137 + $this->errors[] = array($short, $long); 138 + return $this; 139 + } 140 + 141 + public function getErrors() { 142 + return $this->errors; 143 + } 144 + 145 + protected function validateControlValue($value) { 146 + return; 147 + } 148 + 149 + protected function getShortError() { 150 + $error = head($this->getErrors()); 151 + if ($error) { 152 + return head($error); 153 + } 154 + return null; 155 + } 156 + 157 + 158 + /* -( Reading and Writing Field Values )----------------------------------- */ 159 + 160 + 161 + public function readValueFromRequest(AphrontRequest $request) { 162 + $check = array_merge(array($this->getKey()), $this->getAliases()); 163 + foreach ($check as $key) { 164 + if ($this->getValueExistsInRequest($request, $key)) { 165 + return $this->getValueFromRequest($request, $key); 166 + } 167 + } 168 + return $this->getDefaultValue(); 169 + } 170 + 171 + protected function getValueExistsInRequest(AphrontRequest $request, $key) { 172 + return $request->getExists($key); 173 + } 174 + 175 + abstract protected function getValueFromRequest( 176 + AphrontRequest $request, 177 + $key); 178 + 179 + public function readValueFromSavedQuery(PhabricatorSavedQuery $saved) { 180 + $value = $saved->getParameter( 181 + $this->getKey(), 182 + $this->getDefaultValue()); 183 + $this->value = $this->didReadValueFromSavedQuery($value); 184 + $this->validateControlValue($value); 185 + return $this; 186 + } 187 + 188 + protected function didReadValueFromSavedQuery($value) { 189 + return $value; 190 + } 191 + 192 + public function getValue() { 193 + return $this->value; 194 + } 195 + 196 + protected function getValueForControl() { 197 + return $this->value; 198 + } 199 + 200 + protected function getDefaultValue() { 201 + return null; 202 + } 203 + 204 + 205 + /* -( Rendering Controls )------------------------------------------------- */ 206 + 207 + 208 + abstract protected function newControl(); 209 + 210 + 211 + protected function renderControl() { 212 + // TODO: We should `setError($this->getShortError())` here, but it looks 213 + // terrible in the form layout. 214 + 215 + return $this->newControl() 216 + ->setValue($this->getValueForControl()) 217 + ->setName($this->getKey()) 218 + ->setLabel($this->getLabel()); 219 + } 220 + 221 + public function appendToForm(AphrontFormView $form) { 222 + $form->appendControl($this->renderControl()); 223 + return $this; 224 + } 225 + 226 + 227 + /* -( Utility Methods )----------------------------------------------------- */ 228 + 229 + 230 + /** 231 + * Read a list of items from the request, in either array format or string 232 + * format: 233 + * 234 + * list[]=item1&list[]=item2 235 + * list=item1,item2 236 + * 237 + * This provides flexibility when constructing URIs, especially from external 238 + * sources. 239 + * 240 + * @param AphrontRequest Request to read strings from. 241 + * @param string Key to read in the request. 242 + * @return list<string> List of values. 243 + * @task utility 244 + */ 245 + protected function getListFromRequest( 246 + AphrontRequest $request, 247 + $key) { 248 + 249 + $list = $request->getArr($key, null); 250 + if ($list === null) { 251 + $list = $request->getStrList($key); 252 + } 253 + 254 + if (!$list) { 255 + return array(); 256 + } 257 + 258 + return $list; 259 + } 260 + }
+22
src/applications/search/field/PhabricatorSearchStringListField.php
··· 1 + <?php 2 + 3 + final class PhabricatorSearchStringListField 4 + extends PhabricatorSearchField { 5 + 6 + protected function getDefaultValue() { 7 + return array(); 8 + } 9 + 10 + protected function getValueFromRequest(AphrontRequest $request, $key) { 11 + return $request->getStrList($key); 12 + } 13 + 14 + protected function newControl() { 15 + return new AphrontFormTextControl(); 16 + } 17 + 18 + protected function getValueForControl() { 19 + return implode(', ', parent::getValueForControl()); 20 + } 21 + 22 + }
+21
src/applications/search/field/PhabricatorSearchTokenizerField.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorSearchTokenizerField 4 + extends PhabricatorSearchField { 5 + 6 + protected function getDefaultValue() { 7 + return array(); 8 + } 9 + 10 + protected function getValueFromRequest(AphrontRequest $request, $key) { 11 + return $this->getListFromRequest($request, $key); 12 + } 13 + 14 + protected function newControl() { 15 + return id(new AphrontFormTokenizerControl()) 16 + ->setDatasource($this->newDatasource()); 17 + } 18 + 19 + abstract protected function newDatasource(); 20 + 21 + }
+55
src/applications/search/field/PhabricatorSearchUsersField.php
··· 1 + <?php 2 + 3 + final class PhabricatorSearchUsersField 4 + extends PhabricatorSearchTokenizerField { 5 + 6 + protected function getDefaultValue() { 7 + return array(); 8 + } 9 + 10 + protected function newDatasource() { 11 + // TODO: Make this use PhabricatorPeopleUserFunctionDatasource once field 12 + // support is a little more powerful. 13 + return new PhabricatorPeopleDatasource(); 14 + } 15 + 16 + protected function getValueFromRequest(AphrontRequest $request, $key) { 17 + $list = $this->getListFromRequest($request, $key); 18 + $allow_types = array(); 19 + 20 + $phids = array(); 21 + $names = array(); 22 + $allow_types = array_fuse($allow_types); 23 + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; 24 + foreach ($list as $item) { 25 + $type = phid_get_type($item); 26 + if ($type == $user_type) { 27 + $phids[] = $item; 28 + } else if (isset($allow_types[$type])) { 29 + $phids[] = $item; 30 + } else { 31 + if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { 32 + // If this is a function, pass it through unchanged; we'll evaluate 33 + // it later. 34 + $phids[] = $item; 35 + } else { 36 + $names[] = $item; 37 + } 38 + } 39 + } 40 + 41 + if ($names) { 42 + $users = id(new PhabricatorPeopleQuery()) 43 + ->setViewer($this->getViewer()) 44 + ->withUsernames($names) 45 + ->execute(); 46 + foreach ($users as $user) { 47 + $phids[] = $user->getPHID(); 48 + } 49 + $phids = array_unique($phids); 50 + } 51 + 52 + return $phids; 53 + } 54 + 55 + }