mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-29 17:00:59 +01:00
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
This commit is contained in:
parent
3f738e1935
commit
1752be630c
6 changed files with 292 additions and 5 deletions
|
@ -1873,7 +1873,10 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
|
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
|
||||||
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
|
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
|
||||||
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
|
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
|
||||||
|
'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
|
||||||
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
|
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
|
||||||
|
'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
|
||||||
|
'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
|
||||||
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
|
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
|
||||||
'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
|
'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
|
||||||
'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php',
|
'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php',
|
||||||
|
@ -5195,6 +5198,14 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorGlobalLock' => 'PhutilLock',
|
'PhabricatorGlobalLock' => 'PhutilLock',
|
||||||
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
|
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
|
||||||
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
||||||
|
'PhabricatorHandleList' => array(
|
||||||
|
'Phobject',
|
||||||
|
'Iterator',
|
||||||
|
'ArrayAccess',
|
||||||
|
'Countable',
|
||||||
|
),
|
||||||
|
'PhabricatorHandlePool' => 'Phobject',
|
||||||
|
'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
|
||||||
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
|
'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
|
||||||
'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||||
|
|
|
@ -100,10 +100,8 @@ final class PhabricatorCountdownViewController
|
||||||
PhabricatorCountdown $countdown,
|
PhabricatorCountdown $countdown,
|
||||||
PhabricatorActionListView $actions) {
|
PhabricatorActionListView $actions) {
|
||||||
|
|
||||||
$request = $this->getRequest();
|
$viewer = $this->getViewer();
|
||||||
$viewer = $request->getUser();
|
$handles = $viewer->loadHandles(array($countdown->getAuthorPHID()));
|
||||||
|
|
||||||
$this->loadHandles(array($countdown->getAuthorPHID()));
|
|
||||||
|
|
||||||
$view = id(new PHUIPropertyListView())
|
$view = id(new PHUIPropertyListView())
|
||||||
->setUser($viewer)
|
->setUser($viewer)
|
||||||
|
@ -111,7 +109,7 @@ final class PhabricatorCountdownViewController
|
||||||
|
|
||||||
$view->addProperty(
|
$view->addProperty(
|
||||||
pht('Author'),
|
pht('Author'),
|
||||||
$this->getHandle($countdown->getAuthorPHID())->renderLink());
|
$handles[$countdown->getAuthorPHID()]->renderLink());
|
||||||
|
|
||||||
return $view;
|
return $view;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ final class PhabricatorUser
|
||||||
private $session = self::ATTACHABLE;
|
private $session = self::ATTACHABLE;
|
||||||
|
|
||||||
private $authorities = array();
|
private $authorities = array();
|
||||||
|
private $handlePool;
|
||||||
|
|
||||||
protected function readField($field) {
|
protected function readField($field) {
|
||||||
switch ($field) {
|
switch ($field) {
|
||||||
|
@ -800,6 +801,26 @@ EOBODY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Handles )------------------------------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
|
||||||
|
* internal handle pool.
|
||||||
|
*
|
||||||
|
* @param list<phid> List of PHIDs to load.
|
||||||
|
* @return PhabricatorHandleList Handle list object.
|
||||||
|
*/
|
||||||
|
public function loadHandles(array $phids) {
|
||||||
|
if ($this->handlePool === null) {
|
||||||
|
$this->handlePool = id(new PhabricatorHandlePool())
|
||||||
|
->setViewer($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->handlePool->newHandleList($phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
124
src/applications/phid/handle/pool/PhabricatorHandleList.php
Normal file
124
src/applications/phid/handle/pool/PhabricatorHandleList.php
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of object handles.
|
||||||
|
*
|
||||||
|
* This is a convenience class which behaves like an array but makes working
|
||||||
|
* with handles more convenient, improves their caching and batching semantics,
|
||||||
|
* and provides some utility behavior.
|
||||||
|
*
|
||||||
|
* Load a handle list by calling `loadHandles()` on a `$viewer`:
|
||||||
|
*
|
||||||
|
* $handles = $viewer->loadHandles($phids);
|
||||||
|
*
|
||||||
|
* This creates a handle list object, which behaves like an array of handles.
|
||||||
|
* However, it benefits from the viewer's internal handle cache and performs
|
||||||
|
* just-in-time bulk loading.
|
||||||
|
*/
|
||||||
|
final class PhabricatorHandleList
|
||||||
|
extends Phobject
|
||||||
|
implements
|
||||||
|
Iterator,
|
||||||
|
ArrayAccess,
|
||||||
|
Countable {
|
||||||
|
|
||||||
|
private $handlePool;
|
||||||
|
private $phids;
|
||||||
|
private $handles;
|
||||||
|
private $cursor;
|
||||||
|
|
||||||
|
public function setHandlePool(PhabricatorHandlePool $pool) {
|
||||||
|
$this->handlePool = $pool;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPHIDs(array $phids) {
|
||||||
|
$this->phids = $phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadHandles() {
|
||||||
|
$this->handles = $this->handlePool->loadPHIDs($this->phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHandle($phid) {
|
||||||
|
if ($this->handles === null) {
|
||||||
|
$this->loadHandles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->handles[$phid])) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Requested handle "%s" was not loaded.',
|
||||||
|
$phid));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->handles[$phid];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Iterator )----------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function rewind() {
|
||||||
|
$this->cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function current() {
|
||||||
|
return $this->getHandle($this->phids[$this->cursor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function key() {
|
||||||
|
return $this->phids[$this->cursor];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next() {
|
||||||
|
++$this->cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function valid() {
|
||||||
|
return isset($this->phids[$this->cursor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( ArrayAccess )-------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function offsetExists($offset) {
|
||||||
|
if ($this->handles === null) {
|
||||||
|
$this->loadHandles();
|
||||||
|
}
|
||||||
|
return isset($this->handles[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetGet($offset) {
|
||||||
|
if ($this->handles === null) {
|
||||||
|
$this->loadHandles();
|
||||||
|
}
|
||||||
|
return $this->handles[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetSet($offset, $value) {
|
||||||
|
$this->raiseImmutableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetUnset($offset) {
|
||||||
|
$this->raiseImmutableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function raiseImmutableException() {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Trying to mutate a PhabricatorHandleList, but this is not permitted; '.
|
||||||
|
'handle lists are immutable.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Countable )---------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function count() {
|
||||||
|
return count($this->phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
75
src/applications/phid/handle/pool/PhabricatorHandlePool.php
Normal file
75
src/applications/phid/handle/pool/PhabricatorHandlePool.php
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates loading object handles.
|
||||||
|
*
|
||||||
|
* This is a low-level piece of plumbing which code will not normally interact
|
||||||
|
* with directly. For discussion of the handle pool mechanism, see
|
||||||
|
* @{class:PhabricatorHandleList}.
|
||||||
|
*/
|
||||||
|
final class PhabricatorHandlePool extends Phobject {
|
||||||
|
|
||||||
|
private $viewer;
|
||||||
|
private $handles = array();
|
||||||
|
private $unloadedPHIDs = array();
|
||||||
|
|
||||||
|
public function setViewer(PhabricatorUser $user) {
|
||||||
|
$this->viewer = $user;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewer() {
|
||||||
|
return $this->viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newHandleList(array $phids) {
|
||||||
|
// Mark any PHIDs we haven't loaded yet as unloaded. This will let us bulk
|
||||||
|
// load them later.
|
||||||
|
foreach ($phids as $phid) {
|
||||||
|
if (empty($this->handles[$phid])) {
|
||||||
|
$this->unloadedPHIDs[$phid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = array();
|
||||||
|
foreach ($phids as $phid) {
|
||||||
|
$unique[$phid] = $phid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new PhabricatorHandleList())
|
||||||
|
->setHandlePool($this)
|
||||||
|
->setPHIDs(array_values($unique));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadPHIDs(array $phids) {
|
||||||
|
$need = array();
|
||||||
|
foreach ($phids as $phid) {
|
||||||
|
if (empty($this->handles[$phid])) {
|
||||||
|
$need[$phid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($need as $phid => $ignored) {
|
||||||
|
if (empty($this->unloadedPHIDs[$phid])) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Attempting to load PHID "%s", but it was not requested by any '.
|
||||||
|
'handle list.',
|
||||||
|
$phid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need any handles, bulk load everything in the queue.
|
||||||
|
if ($need) {
|
||||||
|
$handles = id(new PhabricatorHandleQuery())
|
||||||
|
->setViewer($this->getViewer())
|
||||||
|
->withPHIDs(array_keys($this->unloadedPHIDs))
|
||||||
|
->execute();
|
||||||
|
$this->handles += $handles;
|
||||||
|
$this->unloadedPHIDs = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_select_keys($this->handles, $phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorHandlePoolTestCase extends PhabricatorTestCase {
|
||||||
|
|
||||||
|
protected function getPhabricatorTestCaseConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlePools() {
|
||||||
|
// A lot of the batch/just-in-time/cache behavior of handle pools is not
|
||||||
|
// observable by design, so these tests don't directly cover it.
|
||||||
|
|
||||||
|
$viewer = $this->generateNewTestUser();
|
||||||
|
$viewer_phid = $viewer->getPHID();
|
||||||
|
|
||||||
|
$phids = array($viewer_phid);
|
||||||
|
|
||||||
|
$handles = $viewer->loadHandles($phids);
|
||||||
|
|
||||||
|
// The handle load hasn't happened yet, but we can't directly observe that.
|
||||||
|
|
||||||
|
// Test Countable behaviors.
|
||||||
|
$this->assertEqual(1, count($handles));
|
||||||
|
|
||||||
|
// Test ArrayAccess behaviors.
|
||||||
|
$this->assertEqual(
|
||||||
|
array($viewer_phid),
|
||||||
|
array_keys(iterator_to_array($handles)));
|
||||||
|
$this->assertEqual(true, $handles[$viewer_phid]->isComplete());
|
||||||
|
$this->assertEqual($viewer_phid, $handles[$viewer_phid]->getPHID());
|
||||||
|
$this->assertTrue(isset($handles[$viewer_phid]));
|
||||||
|
$this->assertFalse(isset($handles['quack']));
|
||||||
|
|
||||||
|
// Test Iterator behaviors.
|
||||||
|
foreach ($handles as $key => $handle) {
|
||||||
|
$this->assertEqual($viewer_phid, $key);
|
||||||
|
$this->assertEqual($viewer_phid, $handle->getPHID());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do this twice to make sure the handle list is rewindable.
|
||||||
|
foreach ($handles as $key => $handle) {
|
||||||
|
$this->assertEqual($viewer_phid, $key);
|
||||||
|
$this->assertEqual($viewer_phid, $handle->getPHID());
|
||||||
|
}
|
||||||
|
|
||||||
|
$more_handles = $viewer->loadHandles($phids);
|
||||||
|
|
||||||
|
// This is testing that we got back a reference to the exact same object,
|
||||||
|
// which implies the caching behavior is working correctly.
|
||||||
|
$this->assertEqual(
|
||||||
|
$handles[$viewer_phid],
|
||||||
|
$more_handles[$viewer_phid],
|
||||||
|
pht('Handles should use viewer handle pool cache.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue