@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.

Improve handle semantics with HandlePool / HandleList

Summary:
Ref T7689, which discusses some of the motivation here. Briefly, these methods are awkward:

- Controller->loadHandles()
- Controller->loadViewerHandles()
- Controller->renderHandlesForPHIDs()

This moves us toward better semantics, less awkwardness, and a more reasonable attack on T7688 which won't double-fetch a bunch of data.

Test Plan:
- Added unit tests.
- Converted one controller to the new stuff.
- Viewed countdown lists, saw handles render.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7689

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

+292 -5
+11
src/__phutil_library_map__.php
··· 1873 1873 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', 1874 1874 'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php', 1875 1875 'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php', 1876 + 'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php', 1876 1877 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 1878 + 'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php', 1879 + 'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php', 1877 1880 'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php', 1878 1881 'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php', 1879 1882 'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php', ··· 5195 5198 'PhabricatorGlobalLock' => 'PhutilLock', 5196 5199 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 5197 5200 'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider', 5201 + 'PhabricatorHandleList' => array( 5202 + 'Phobject', 5203 + 'Iterator', 5204 + 'ArrayAccess', 5205 + 'Countable', 5206 + ), 5207 + 'PhabricatorHandlePool' => 'Phobject', 5208 + 'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase', 5198 5209 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5199 5210 'PhabricatorHarbormasterApplication' => 'PhabricatorApplication', 5200 5211 'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
+3 -5
src/applications/countdown/controller/PhabricatorCountdownViewController.php
··· 100 100 PhabricatorCountdown $countdown, 101 101 PhabricatorActionListView $actions) { 102 102 103 - $request = $this->getRequest(); 104 - $viewer = $request->getUser(); 105 - 106 - $this->loadHandles(array($countdown->getAuthorPHID())); 103 + $viewer = $this->getViewer(); 104 + $handles = $viewer->loadHandles(array($countdown->getAuthorPHID())); 107 105 108 106 $view = id(new PHUIPropertyListView()) 109 107 ->setUser($viewer) ··· 111 109 112 110 $view->addProperty( 113 111 pht('Author'), 114 - $this->getHandle($countdown->getAuthorPHID())->renderLink()); 112 + $handles[$countdown->getAuthorPHID()]->renderLink()); 115 113 116 114 return $view; 117 115 }
+21
src/applications/people/storage/PhabricatorUser.php
··· 51 51 private $session = self::ATTACHABLE; 52 52 53 53 private $authorities = array(); 54 + private $handlePool; 54 55 55 56 protected function readField($field) { 56 57 switch ($field) { ··· 797 798 $user->makeEphemeral(); 798 799 } 799 800 return $user; 801 + } 802 + 803 + 804 + /* -( Handles )------------------------------------------------------------ */ 805 + 806 + 807 + /** 808 + * Get a @{class:PhabricatorHandleList} which benefits from this viewer's 809 + * internal handle pool. 810 + * 811 + * @param list<phid> List of PHIDs to load. 812 + * @return PhabricatorHandleList Handle list object. 813 + */ 814 + public function loadHandles(array $phids) { 815 + if ($this->handlePool === null) { 816 + $this->handlePool = id(new PhabricatorHandlePool()) 817 + ->setViewer($this); 818 + } 819 + 820 + return $this->handlePool->newHandleList($phids); 800 821 } 801 822 802 823
+124
src/applications/phid/handle/pool/PhabricatorHandleList.php
··· 1 + <?php 2 + 3 + /** 4 + * A list of object handles. 5 + * 6 + * This is a convenience class which behaves like an array but makes working 7 + * with handles more convenient, improves their caching and batching semantics, 8 + * and provides some utility behavior. 9 + * 10 + * Load a handle list by calling `loadHandles()` on a `$viewer`: 11 + * 12 + * $handles = $viewer->loadHandles($phids); 13 + * 14 + * This creates a handle list object, which behaves like an array of handles. 15 + * However, it benefits from the viewer's internal handle cache and performs 16 + * just-in-time bulk loading. 17 + */ 18 + final class PhabricatorHandleList 19 + extends Phobject 20 + implements 21 + Iterator, 22 + ArrayAccess, 23 + Countable { 24 + 25 + private $handlePool; 26 + private $phids; 27 + private $handles; 28 + private $cursor; 29 + 30 + public function setHandlePool(PhabricatorHandlePool $pool) { 31 + $this->handlePool = $pool; 32 + return $this; 33 + } 34 + 35 + public function setPHIDs(array $phids) { 36 + $this->phids = $phids; 37 + return $this; 38 + } 39 + 40 + private function loadHandles() { 41 + $this->handles = $this->handlePool->loadPHIDs($this->phids); 42 + } 43 + 44 + private function getHandle($phid) { 45 + if ($this->handles === null) { 46 + $this->loadHandles(); 47 + } 48 + 49 + if (empty($this->handles[$phid])) { 50 + throw new Exception( 51 + pht( 52 + 'Requested handle "%s" was not loaded.', 53 + $phid)); 54 + } 55 + 56 + return $this->handles[$phid]; 57 + } 58 + 59 + 60 + /* -( Iterator )----------------------------------------------------------- */ 61 + 62 + 63 + public function rewind() { 64 + $this->cursor = 0; 65 + } 66 + 67 + public function current() { 68 + return $this->getHandle($this->phids[$this->cursor]); 69 + } 70 + 71 + public function key() { 72 + return $this->phids[$this->cursor]; 73 + } 74 + 75 + public function next() { 76 + ++$this->cursor; 77 + } 78 + 79 + public function valid() { 80 + return isset($this->phids[$this->cursor]); 81 + } 82 + 83 + 84 + /* -( ArrayAccess )-------------------------------------------------------- */ 85 + 86 + 87 + public function offsetExists($offset) { 88 + if ($this->handles === null) { 89 + $this->loadHandles(); 90 + } 91 + return isset($this->handles[$offset]); 92 + } 93 + 94 + public function offsetGet($offset) { 95 + if ($this->handles === null) { 96 + $this->loadHandles(); 97 + } 98 + return $this->handles[$offset]; 99 + } 100 + 101 + public function offsetSet($offset, $value) { 102 + $this->raiseImmutableException(); 103 + } 104 + 105 + public function offsetUnset($offset) { 106 + $this->raiseImmutableException(); 107 + } 108 + 109 + private function raiseImmutableException() { 110 + throw new Exception( 111 + pht( 112 + 'Trying to mutate a PhabricatorHandleList, but this is not permitted; '. 113 + 'handle lists are immutable.')); 114 + } 115 + 116 + 117 + /* -( Countable )---------------------------------------------------------- */ 118 + 119 + 120 + public function count() { 121 + return count($this->phids); 122 + } 123 + 124 + }
+75
src/applications/phid/handle/pool/PhabricatorHandlePool.php
··· 1 + <?php 2 + 3 + /** 4 + * Coordinates loading object handles. 5 + * 6 + * This is a low-level piece of plumbing which code will not normally interact 7 + * with directly. For discussion of the handle pool mechanism, see 8 + * @{class:PhabricatorHandleList}. 9 + */ 10 + final class PhabricatorHandlePool extends Phobject { 11 + 12 + private $viewer; 13 + private $handles = array(); 14 + private $unloadedPHIDs = array(); 15 + 16 + public function setViewer(PhabricatorUser $user) { 17 + $this->viewer = $user; 18 + return $this; 19 + } 20 + 21 + public function getViewer() { 22 + return $this->viewer; 23 + } 24 + 25 + public function newHandleList(array $phids) { 26 + // Mark any PHIDs we haven't loaded yet as unloaded. This will let us bulk 27 + // load them later. 28 + foreach ($phids as $phid) { 29 + if (empty($this->handles[$phid])) { 30 + $this->unloadedPHIDs[$phid] = true; 31 + } 32 + } 33 + 34 + $unique = array(); 35 + foreach ($phids as $phid) { 36 + $unique[$phid] = $phid; 37 + } 38 + 39 + return id(new PhabricatorHandleList()) 40 + ->setHandlePool($this) 41 + ->setPHIDs(array_values($unique)); 42 + } 43 + 44 + public function loadPHIDs(array $phids) { 45 + $need = array(); 46 + foreach ($phids as $phid) { 47 + if (empty($this->handles[$phid])) { 48 + $need[$phid] = true; 49 + } 50 + } 51 + 52 + foreach ($need as $phid => $ignored) { 53 + if (empty($this->unloadedPHIDs[$phid])) { 54 + throw new Exception( 55 + pht( 56 + 'Attempting to load PHID "%s", but it was not requested by any '. 57 + 'handle list.', 58 + $phid)); 59 + } 60 + } 61 + 62 + // If we need any handles, bulk load everything in the queue. 63 + if ($need) { 64 + $handles = id(new PhabricatorHandleQuery()) 65 + ->setViewer($this->getViewer()) 66 + ->withPHIDs(array_keys($this->unloadedPHIDs)) 67 + ->execute(); 68 + $this->handles += $handles; 69 + $this->unloadedPHIDs = array(); 70 + } 71 + 72 + return array_select_keys($this->handles, $phids); 73 + } 74 + 75 + }
+58
src/applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorHandlePoolTestCase extends PhabricatorTestCase { 4 + 5 + protected function getPhabricatorTestCaseConfiguration() { 6 + return array( 7 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 8 + ); 9 + } 10 + 11 + public function testHandlePools() { 12 + // A lot of the batch/just-in-time/cache behavior of handle pools is not 13 + // observable by design, so these tests don't directly cover it. 14 + 15 + $viewer = $this->generateNewTestUser(); 16 + $viewer_phid = $viewer->getPHID(); 17 + 18 + $phids = array($viewer_phid); 19 + 20 + $handles = $viewer->loadHandles($phids); 21 + 22 + // The handle load hasn't happened yet, but we can't directly observe that. 23 + 24 + // Test Countable behaviors. 25 + $this->assertEqual(1, count($handles)); 26 + 27 + // Test ArrayAccess behaviors. 28 + $this->assertEqual( 29 + array($viewer_phid), 30 + array_keys(iterator_to_array($handles))); 31 + $this->assertEqual(true, $handles[$viewer_phid]->isComplete()); 32 + $this->assertEqual($viewer_phid, $handles[$viewer_phid]->getPHID()); 33 + $this->assertTrue(isset($handles[$viewer_phid])); 34 + $this->assertFalse(isset($handles['quack'])); 35 + 36 + // Test Iterator behaviors. 37 + foreach ($handles as $key => $handle) { 38 + $this->assertEqual($viewer_phid, $key); 39 + $this->assertEqual($viewer_phid, $handle->getPHID()); 40 + } 41 + 42 + // Do this twice to make sure the handle list is rewindable. 43 + foreach ($handles as $key => $handle) { 44 + $this->assertEqual($viewer_phid, $key); 45 + $this->assertEqual($viewer_phid, $handle->getPHID()); 46 + } 47 + 48 + $more_handles = $viewer->loadHandles($phids); 49 + 50 + // This is testing that we got back a reference to the exact same object, 51 + // which implies the caching behavior is working correctly. 52 + $this->assertEqual( 53 + $handles[$viewer_phid], 54 + $more_handles[$viewer_phid], 55 + pht('Handles should use viewer handle pool cache.')); 56 + } 57 + 58 + }