[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Represents an abstract search engine for an application. It supports 5 * creating and storing saved queries. 6 * 7 * @task construct Constructing Engines 8 * @task app Applications 9 * @task builtin Builtin Queries 10 * @task uri Query URIs 11 * @task dates Date Filters 12 * @task read Reading Utilities 13 * @task exec Paging and Executing Queries 14 * @task render Rendering Results 15 */ 16 abstract class PhabricatorApplicationSearchEngine { 17 18 private $application; 19 private $viewer; 20 private $errors = array(); 21 private $customFields = false; 22 private $request; 23 private $context; 24 25 const CONTEXT_LIST = 'list'; 26 const CONTEXT_PANEL = 'panel'; 27 28 public function setViewer(PhabricatorUser $viewer) { 29 $this->viewer = $viewer; 30 return $this; 31 } 32 33 protected function requireViewer() { 34 if (!$this->viewer) { 35 throw new Exception('Call setViewer() before using an engine!'); 36 } 37 return $this->viewer; 38 } 39 40 public function setContext($context) { 41 $this->context = $context; 42 return $this; 43 } 44 45 public function isPanelContext() { 46 return ($this->context == self::CONTEXT_PANEL); 47 } 48 49 public function saveQuery(PhabricatorSavedQuery $query) { 50 $query->setEngineClassName(get_class($this)); 51 52 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 53 try { 54 $query->save(); 55 } catch (AphrontDuplicateKeyQueryException $ex) { 56 // Ignore, this is just a repeated search. 57 } 58 unset($unguarded); 59 } 60 61 /** 62 * Create a saved query object from the request. 63 * 64 * @param AphrontRequest The search request. 65 * @return PhabricatorSavedQuery 66 */ 67 abstract public function buildSavedQueryFromRequest( 68 AphrontRequest $request); 69 70 /** 71 * Executes the saved query. 72 * 73 * @param PhabricatorSavedQuery The saved query to operate on. 74 * @return The result of the query. 75 */ 76 abstract public function buildQueryFromSavedQuery( 77 PhabricatorSavedQuery $saved); 78 79 /** 80 * Builds the search form using the request. 81 * 82 * @param AphrontFormView Form to populate. 83 * @param PhabricatorSavedQuery The query from which to build the form. 84 * @return void 85 */ 86 abstract public function buildSearchForm( 87 AphrontFormView $form, 88 PhabricatorSavedQuery $query); 89 90 public function getErrors() { 91 return $this->errors; 92 } 93 94 public function addError($error) { 95 $this->errors[] = $error; 96 return $this; 97 } 98 99 /** 100 * Return an application URI corresponding to the results page of a query. 101 * Normally, this is something like `/application/query/QUERYKEY/`. 102 * 103 * @param string The query key to build a URI for. 104 * @return string URI where the query can be executed. 105 * @task uri 106 */ 107 public function getQueryResultsPageURI($query_key) { 108 return $this->getURI('query/'.$query_key.'/'); 109 } 110 111 112 /** 113 * Return an application URI for query management. This is used when, e.g., 114 * a query deletion operation is cancelled. 115 * 116 * @return string URI where queries can be managed. 117 * @task uri 118 */ 119 public function getQueryManagementURI() { 120 return $this->getURI('query/edit/'); 121 } 122 123 124 /** 125 * Return the URI to a path within the application. Used to construct default 126 * URIs for management and results. 127 * 128 * @return string URI to path. 129 * @task uri 130 */ 131 abstract protected function getURI($path); 132 133 134 /** 135 * Return a human readable description of the type of objects this query 136 * searches for. 137 * 138 * For example, "Tasks" or "Commits". 139 * 140 * @return string Human-readable description of what this engine is used to 141 * find. 142 */ 143 abstract public function getResultTypeDescription(); 144 145 146 public function newSavedQuery() { 147 return id(new PhabricatorSavedQuery()) 148 ->setEngineClassName(get_class($this)); 149 } 150 151 public function addNavigationItems(PHUIListView $menu) { 152 $viewer = $this->requireViewer(); 153 154 $menu->newLabel(pht('Queries')); 155 156 $named_queries = $this->loadEnabledNamedQueries(); 157 158 foreach ($named_queries as $query) { 159 $key = $query->getQueryKey(); 160 $uri = $this->getQueryResultsPageURI($key); 161 $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); 162 } 163 164 if ($viewer->isLoggedIn()) { 165 $manage_uri = $this->getQueryManagementURI(); 166 $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); 167 } 168 169 $menu->newLabel(pht('Search')); 170 $advanced_uri = $this->getQueryResultsPageURI('advanced'); 171 $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); 172 173 return $this; 174 } 175 176 public function loadAllNamedQueries() { 177 $viewer = $this->requireViewer(); 178 179 $named_queries = id(new PhabricatorNamedQueryQuery()) 180 ->setViewer($viewer) 181 ->withUserPHIDs(array($viewer->getPHID())) 182 ->withEngineClassNames(array(get_class($this))) 183 ->execute(); 184 $named_queries = mpull($named_queries, null, 'getQueryKey'); 185 186 $builtin = $this->getBuiltinQueries($viewer); 187 $builtin = mpull($builtin, null, 'getQueryKey'); 188 189 foreach ($named_queries as $key => $named_query) { 190 if ($named_query->getIsBuiltin()) { 191 if (isset($builtin[$key])) { 192 $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); 193 unset($builtin[$key]); 194 } else { 195 unset($named_queries[$key]); 196 } 197 } 198 199 unset($builtin[$key]); 200 } 201 202 $named_queries = msort($named_queries, 'getSortKey'); 203 204 return $named_queries + $builtin; 205 } 206 207 public function loadEnabledNamedQueries() { 208 $named_queries = $this->loadAllNamedQueries(); 209 foreach ($named_queries as $key => $named_query) { 210 if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { 211 unset($named_queries[$key]); 212 } 213 } 214 return $named_queries; 215 } 216 217 218 /* -( Applications )------------------------------------------------------- */ 219 220 221 protected function getApplicationURI($path = '') { 222 return $this->getApplication()->getApplicationURI($path); 223 } 224 225 protected function getApplication() { 226 if (!$this->application) { 227 $class = $this->getApplicationClassName(); 228 229 $this->application = id(new PhabricatorApplicationQuery()) 230 ->setViewer($this->requireViewer()) 231 ->withClasses(array($class)) 232 ->withInstalled(true) 233 ->executeOne(); 234 235 if (!$this->application) { 236 throw new Exception( 237 pht( 238 'Application "%s" is not installed!', 239 $class)); 240 } 241 } 242 243 return $this->application; 244 } 245 246 protected function getApplicationClassName() { 247 throw new PhutilMethodNotImplementedException(); 248 } 249 250 251 /* -( Constructing Engines )----------------------------------------------- */ 252 253 254 /** 255 * Load all available application search engines. 256 * 257 * @return list<PhabricatorApplicationSearchEngine> All available engines. 258 * @task construct 259 */ 260 public static function getAllEngines() { 261 $engines = id(new PhutilSymbolLoader()) 262 ->setAncestorClass(__CLASS__) 263 ->loadObjects(); 264 265 return $engines; 266 } 267 268 269 /** 270 * Get an engine by class name, if it exists. 271 * 272 * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does 273 * not exist. 274 * @task construct 275 */ 276 public static function getEngineByClassName($class_name) { 277 return idx(self::getAllEngines(), $class_name); 278 } 279 280 281 /* -( Builtin Queries )---------------------------------------------------- */ 282 283 284 /** 285 * @task builtin 286 */ 287 public function getBuiltinQueries() { 288 $names = $this->getBuiltinQueryNames(); 289 290 $queries = array(); 291 $sequence = 0; 292 foreach ($names as $key => $name) { 293 $queries[$key] = id(new PhabricatorNamedQuery()) 294 ->setUserPHID($this->requireViewer()->getPHID()) 295 ->setEngineClassName(get_class($this)) 296 ->setQueryName($name) 297 ->setQueryKey($key) 298 ->setSequence((1 << 24) + $sequence++) 299 ->setIsBuiltin(true); 300 } 301 302 return $queries; 303 } 304 305 306 /** 307 * @task builtin 308 */ 309 public function getBuiltinQuery($query_key) { 310 if (!$this->isBuiltinQuery($query_key)) { 311 throw new Exception("'{$query_key}' is not a builtin!"); 312 } 313 return idx($this->getBuiltinQueries(), $query_key); 314 } 315 316 317 /** 318 * @task builtin 319 */ 320 protected function getBuiltinQueryNames() { 321 return array(); 322 } 323 324 325 /** 326 * @task builtin 327 */ 328 public function isBuiltinQuery($query_key) { 329 $builtins = $this->getBuiltinQueries(); 330 return isset($builtins[$query_key]); 331 } 332 333 334 /** 335 * @task builtin 336 */ 337 public function buildSavedQueryFromBuiltin($query_key) { 338 throw new Exception("Builtin '{$query_key}' is not supported!"); 339 } 340 341 342 /* -( Reading Utilities )--------------------------------------------------- */ 343 344 345 /** 346 * Read a list of user PHIDs from a request in a flexible way. This method 347 * supports either of these forms: 348 * 349 * users[]=alincoln&users[]=htaft 350 * users=alincoln,htaft 351 * 352 * Additionally, users can be specified either by PHID or by name. 353 * 354 * The main goal of this flexibility is to allow external programs to generate 355 * links to pages (like "alincoln's open revisions") without needing to make 356 * API calls. 357 * 358 * @param AphrontRequest Request to read user PHIDs from. 359 * @param string Key to read in the request. 360 * @param list<const> Other permitted PHID types. 361 * @return list<phid> List of user PHIDs. 362 * 363 * @task read 364 */ 365 protected function readUsersFromRequest( 366 AphrontRequest $request, 367 $key, 368 array $allow_types = array()) { 369 370 $list = $this->readListFromRequest($request, $key); 371 372 $phids = array(); 373 $names = array(); 374 $allow_types = array_fuse($allow_types); 375 $user_type = PhabricatorPHIDConstants::PHID_TYPE_USER; 376 foreach ($list as $item) { 377 $type = phid_get_type($item); 378 if ($type == $user_type) { 379 $phids[] = $item; 380 } else if (isset($allow_types[$type])) { 381 $phids[] = $item; 382 } else { 383 $names[] = $item; 384 } 385 } 386 387 if ($names) { 388 $users = id(new PhabricatorPeopleQuery()) 389 ->setViewer($this->requireViewer()) 390 ->withUsernames($names) 391 ->execute(); 392 foreach ($users as $user) { 393 $phids[] = $user->getPHID(); 394 } 395 $phids = array_unique($phids); 396 } 397 398 return $phids; 399 } 400 401 402 /** 403 * Read a list of generic PHIDs from a request in a flexible way. Like 404 * @{method:readUsersFromRequest}, this method supports either array or 405 * comma-delimited forms. Objects can be specified either by PHID or by 406 * object name. 407 * 408 * @param AphrontRequest Request to read PHIDs from. 409 * @param string Key to read in the request. 410 * @param list<const> Optional, list of permitted PHID types. 411 * @return list<phid> List of object PHIDs. 412 * 413 * @task read 414 */ 415 protected function readPHIDsFromRequest( 416 AphrontRequest $request, 417 $key, 418 array $allow_types = array()) { 419 420 $list = $this->readListFromRequest($request, $key); 421 422 $objects = id(new PhabricatorObjectQuery()) 423 ->setViewer($this->requireViewer()) 424 ->withNames($list) 425 ->execute(); 426 $list = mpull($objects, 'getPHID'); 427 428 if (!$list) { 429 return array(); 430 } 431 432 // If only certain PHID types are allowed, filter out all the others. 433 if ($allow_types) { 434 $allow_types = array_fuse($allow_types); 435 foreach ($list as $key => $phid) { 436 if (empty($allow_types[phid_get_type($phid)])) { 437 unset($list[$key]); 438 } 439 } 440 } 441 442 return $list; 443 } 444 445 446 /** 447 * Read a list of items from the request, in either array format or string 448 * format: 449 * 450 * list[]=item1&list[]=item2 451 * list=item1,item2 452 * 453 * This provides flexibility when constructing URIs, especially from external 454 * sources. 455 * 456 * @param AphrontRequest Request to read strings from. 457 * @param string Key to read in the request. 458 * @return list<string> List of values. 459 */ 460 protected function readListFromRequest( 461 AphrontRequest $request, 462 $key) { 463 $list = $request->getArr($key, null); 464 if ($list === null) { 465 $list = $request->getStrList($key); 466 } 467 468 if (!$list) { 469 return array(); 470 } 471 472 return $list; 473 } 474 475 protected function readDateFromRequest( 476 AphrontRequest $request, 477 $key) { 478 479 return id(new AphrontFormDateControl()) 480 ->setUser($this->requireViewer()) 481 ->setName($key) 482 ->setAllowNull(true) 483 ->readValueFromRequest($request); 484 } 485 486 protected function readBoolFromRequest( 487 AphrontRequest $request, 488 $key) { 489 if (!strlen($request->getStr($key))) { 490 return null; 491 } 492 return $request->getBool($key); 493 } 494 495 496 protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) { 497 $value = $query->getParameter($key); 498 if ($value === null) { 499 return $value; 500 } 501 return $value ? 'true' : 'false'; 502 } 503 504 505 /* -( Dates )-------------------------------------------------------------- */ 506 507 508 /** 509 * @task dates 510 */ 511 protected function parseDateTime($date_time) { 512 if (!strlen($date_time)) { 513 return null; 514 } 515 516 return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); 517 } 518 519 520 /** 521 * @task dates 522 */ 523 protected function buildDateRange( 524 AphrontFormView $form, 525 PhabricatorSavedQuery $saved_query, 526 $start_key, 527 $start_name, 528 $end_key, 529 $end_name) { 530 531 $start_str = $saved_query->getParameter($start_key); 532 $start = null; 533 if (strlen($start_str)) { 534 $start = $this->parseDateTime($start_str); 535 if (!$start) { 536 $this->addError( 537 pht( 538 '"%s" date can not be parsed.', 539 $start_name)); 540 } 541 } 542 543 544 $end_str = $saved_query->getParameter($end_key); 545 $end = null; 546 if (strlen($end_str)) { 547 $end = $this->parseDateTime($end_str); 548 if (!$end) { 549 $this->addError( 550 pht( 551 '"%s" date can not be parsed.', 552 $end_name)); 553 } 554 } 555 556 if ($start && $end && ($start >= $end)) { 557 $this->addError( 558 pht( 559 '"%s" must be a date before "%s".', 560 $start_name, 561 $end_name)); 562 } 563 564 $form 565 ->appendChild( 566 id(new PHUIFormFreeformDateControl()) 567 ->setName($start_key) 568 ->setLabel($start_name) 569 ->setValue($start_str)) 570 ->appendChild( 571 id(new AphrontFormTextControl()) 572 ->setName($end_key) 573 ->setLabel($end_name) 574 ->setValue($end_str)); 575 } 576 577 578 /* -( Paging and Executing Queries )--------------------------------------- */ 579 580 581 public function getPageSize(PhabricatorSavedQuery $saved) { 582 return $saved->getParameter('limit', 100); 583 } 584 585 586 public function shouldUseOffsetPaging() { 587 return false; 588 } 589 590 591 public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { 592 if ($this->shouldUseOffsetPaging()) { 593 $pager = new AphrontPagerView(); 594 } else { 595 $pager = new AphrontCursorPagerView(); 596 } 597 598 $page_size = $this->getPageSize($saved); 599 if (is_finite($page_size)) { 600 $pager->setPageSize($page_size); 601 } else { 602 // Consider an INF pagesize to mean a large finite pagesize. 603 604 // TODO: It would be nice to handle this more gracefully, but math 605 // with INF seems to vary across PHP versions, systems, and runtimes. 606 $pager->setPageSize(0xFFFF); 607 } 608 609 return $pager; 610 } 611 612 613 public function executeQuery( 614 PhabricatorPolicyAwareQuery $query, 615 AphrontView $pager) { 616 617 $query->setViewer($this->requireViewer()); 618 619 if ($this->shouldUseOffsetPaging()) { 620 $objects = $query->executeWithOffsetPager($pager); 621 } else { 622 $objects = $query->executeWithCursorPager($pager); 623 } 624 625 return $objects; 626 } 627 628 629 /* -( Rendering )---------------------------------------------------------- */ 630 631 632 public function setRequest(AphrontRequest $request) { 633 $this->request = $request; 634 return $this; 635 } 636 637 public function getRequest() { 638 return $this->request; 639 } 640 641 public function renderResults( 642 array $objects, 643 PhabricatorSavedQuery $query) { 644 645 $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query); 646 647 if ($phids) { 648 $handles = id(new PhabricatorHandleQuery()) 649 ->setViewer($this->requireViewer()) 650 ->witHPHIDs($phids) 651 ->execute(); 652 } else { 653 $handles = array(); 654 } 655 656 return $this->renderResultList($objects, $query, $handles); 657 } 658 659 protected function getRequiredHandlePHIDsForResultList( 660 array $objects, 661 PhabricatorSavedQuery $query) { 662 return array(); 663 } 664 665 protected function renderResultList( 666 array $objects, 667 PhabricatorSavedQuery $query, 668 array $handles) { 669 throw new Exception(pht('Not supported here yet!')); 670 } 671 672 673 /* -( Application Search )------------------------------------------------- */ 674 675 676 /** 677 * Retrieve an object to use to define custom fields for this search. 678 * 679 * To integrate with custom fields, subclasses should override this method 680 * and return an instance of the application object which implements 681 * @{interface:PhabricatorCustomFieldInterface}. 682 * 683 * @return PhabricatorCustomFieldInterface|null Object with custom fields. 684 * @task appsearch 685 */ 686 public function getCustomFieldObject() { 687 return null; 688 } 689 690 691 /** 692 * Get the custom fields for this search. 693 * 694 * @return PhabricatorCustomFieldList|null Custom fields, if this search 695 * supports custom fields. 696 * @task appsearch 697 */ 698 public function getCustomFieldList() { 699 if ($this->customFields === false) { 700 $object = $this->getCustomFieldObject(); 701 if ($object) { 702 $fields = PhabricatorCustomField::getObjectFields( 703 $object, 704 PhabricatorCustomField::ROLE_APPLICATIONSEARCH); 705 $fields->setViewer($this->requireViewer()); 706 } else { 707 $fields = null; 708 } 709 $this->customFields = $fields; 710 } 711 return $this->customFields; 712 } 713 714 715 /** 716 * Moves data from the request into a saved query. 717 * 718 * @param AphrontRequest Request to read. 719 * @param PhabricatorSavedQuery Query to write to. 720 * @return void 721 * @task appsearch 722 */ 723 protected function readCustomFieldsFromRequest( 724 AphrontRequest $request, 725 PhabricatorSavedQuery $saved) { 726 727 $list = $this->getCustomFieldList(); 728 if (!$list) { 729 return; 730 } 731 732 foreach ($list->getFields() as $field) { 733 $key = $this->getKeyForCustomField($field); 734 $value = $field->readApplicationSearchValueFromRequest( 735 $this, 736 $request); 737 $saved->setParameter($key, $value); 738 } 739 } 740 741 742 /** 743 * Applies data from a saved query to an executable query. 744 * 745 * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. 746 * @param PhabricatorSavedQuery Saved query to read. 747 * @return void 748 */ 749 protected function applyCustomFieldsToQuery( 750 PhabricatorCursorPagedPolicyAwareQuery $query, 751 PhabricatorSavedQuery $saved) { 752 753 $list = $this->getCustomFieldList(); 754 if (!$list) { 755 return; 756 } 757 758 foreach ($list->getFields() as $field) { 759 $key = $this->getKeyForCustomField($field); 760 $value = $field->applyApplicationSearchConstraintToQuery( 761 $this, 762 $query, 763 $saved->getParameter($key)); 764 } 765 } 766 767 protected function applyOrderByToQuery( 768 PhabricatorCursorPagedPolicyAwareQuery $query, 769 array $standard_values, 770 $order) { 771 772 if (substr($order, 0, 7) === 'custom:') { 773 $list = $this->getCustomFieldList(); 774 if (!$list) { 775 $query->setOrderBy(head($standard_values)); 776 return; 777 } 778 779 foreach ($list->getFields() as $field) { 780 $key = $this->getKeyForCustomField($field); 781 782 if ($key === $order) { 783 $index = $field->buildOrderIndex(); 784 785 if ($index === null) { 786 $query->setOrderBy(head($standard_values)); 787 return; 788 } 789 790 $query->withApplicationSearchOrder( 791 $field, 792 $index, 793 false); 794 break; 795 } 796 } 797 } else { 798 $order = idx($standard_values, $order); 799 if ($order) { 800 $query->setOrderBy($order); 801 } else { 802 $query->setOrderBy(head($standard_values)); 803 } 804 } 805 } 806 807 808 protected function getCustomFieldOrderOptions() { 809 $list = $this->getCustomFieldList(); 810 if (!$list) { 811 return; 812 } 813 814 $custom_order = array(); 815 foreach ($list->getFields() as $field) { 816 if ($field->shouldAppearInApplicationSearch()) { 817 if ($field->buildOrderIndex() !== null) { 818 $key = $this->getKeyForCustomField($field); 819 $custom_order[$key] = $field->getFieldName(); 820 } 821 } 822 } 823 824 return $custom_order; 825 } 826 827 /** 828 * Get a unique key identifying a field. 829 * 830 * @param PhabricatorCustomField Field to identify. 831 * @return string Unique identifier, suitable for use as an input name. 832 */ 833 public function getKeyForCustomField(PhabricatorCustomField $field) { 834 return 'custom:'.$field->getFieldIndex(); 835 } 836 837 838 /** 839 * Add inputs to an application search form so the user can query on custom 840 * fields. 841 * 842 * @param AphrontFormView Form to update. 843 * @param PhabricatorSavedQuery Values to prefill. 844 * @return void 845 */ 846 protected function appendCustomFieldsToForm( 847 AphrontFormView $form, 848 PhabricatorSavedQuery $saved) { 849 850 $list = $this->getCustomFieldList(); 851 if (!$list) { 852 return; 853 } 854 855 $phids = array(); 856 foreach ($list->getFields() as $field) { 857 $key = $this->getKeyForCustomField($field); 858 $value = $saved->getParameter($key); 859 $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value); 860 } 861 $all_phids = array_mergev($phids); 862 863 $handles = array(); 864 if ($all_phids) { 865 $handles = id(new PhabricatorHandleQuery()) 866 ->setViewer($this->requireViewer()) 867 ->withPHIDs($all_phids) 868 ->execute(); 869 } 870 871 foreach ($list->getFields() as $field) { 872 $key = $this->getKeyForCustomField($field); 873 $value = $saved->getParameter($key); 874 $field->appendToApplicationSearchForm( 875 $this, 876 $form, 877 $value, 878 array_select_keys($handles, $phids[$key])); 879 } 880 } 881 882 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |