Matthias Mullie has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/111366

Change subject: Add *Revisionable classes
......................................................................

Add *Revisionable classes

In their current state, they should help in efficiently fetching certain
revisions of a certain post/header.

I would like to see them be "expanded" to hold stuff that we currently do in
revisions, but is not really revision-specific (e.g. getChildren, getCreator...)

I'll use this to change/fix some permissions in a follow-up patch, which should
make it easier to load previous & most recent revisions.

Change-Id: If53f0b4a1367f4cd7087dc0fad3326436f3a6d9f
---
M Flow.php
A includes/Model/AbstractRevisionable.php
A includes/Model/HeaderRevisionable.php
A includes/Model/LocalCacheAbstractRevisionable.php
A includes/Model/PostRevisionable.php
5 files changed, 432 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow 
refs/changes/66/111366/1

diff --git a/Flow.php b/Flow.php
index 44fb6e8..13ada32 100755
--- a/Flow.php
+++ b/Flow.php
@@ -87,6 +87,10 @@
 $wgAutoloadClasses['Flow\Model\TopicListEntry'] = $dir . 
'includes/Model/TopicListEntry.php';
 $wgAutoloadClasses['Flow\Model\Workflow'] = $dir . 
'includes/Model/Workflow.php';
 $wgAutoloadClasses['Flow\Model\UUID'] = "$dir/includes/Model/UUID.php";
+$wgAutoloadClasses['Flow\Model\AbstractRevisionable'] = $dir . 
'includes/Model/AbstractRevisionable.php';
+$wgAutoloadClasses['Flow\Model\LocalCacheAbstractRevisionable'] = $dir . 
'includes/Model/LocalCacheAbstractRevisionable.php';
+$wgAutoloadClasses['Flow\Model\PostRevisionable'] = $dir . 
'includes/Model/PostRevisionable.php';
+$wgAutoloadClasses['Flow\Model\HeaderRevisionable'] = $dir . 
'includes/Model/HeaderRevisionable.php';
 
 // Helpers for templating
 $wgAutoloadClasses['Flow\View\PostActionMenu'] = 
"$dir/includes/View/PostActionMenu.php";
diff --git a/includes/Model/AbstractRevisionable.php 
b/includes/Model/AbstractRevisionable.php
new file mode 100644
index 0000000..8ca6e33
--- /dev/null
+++ b/includes/Model/AbstractRevisionable.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace Flow\Model;
+
+use Flow\Container;
+use Flow\Exception\InvalidDataException;
+
+abstract class AbstractRevisionable {
+       /**
+        * Id of the revisionable object.
+        *
+        * @var UUID
+        */
+       protected $uuid;
+
+       /**
+        * @var \Flow\Data\ObjectManager
+        */
+       protected $storage;
+
+       /**
+        * Array of revisions for this object.
+        *
+        * @var array
+        */
+       protected $revisions = array();
+
+       /**
+        * Returns the revision class name for this specific object (e.g. 
Header,
+        * PostRevision)
+        *
+        * @return string
+        */
+       abstract public function getRevisionClass();
+
+       /**
+        * Returns the DB column that holds the revision hierarchy, where all
+        * revisions are mapped to a shared object id.
+        * E.g. a post can have multiple revisions, all of which have their own 
id;
+        * but they're identifiable as revisions of the same post because they 
share
+        * a common postId (in tree_rev_descendant_id)
+        *
+        * @return string
+        */
+       abstract public function getIdColumn();
+
+       /**
+        * Returns the object's UUID, given an AbstractRevision (not to be 
confused
+        * with that single revision's UUID!)
+        *
+        * @param AbstractRevision $revision
+        * @return UUID
+        */
+       abstract public function getId( AbstractRevision $revision );
+
+       /**
+        * Use the static methods to load an object from a given revision.
+        *
+        * @see AbstractRevisionable::newFromId
+        * @see AbstractRevisionable::newFromRevision
+        * @see AbstractRevisionable::newFromRevisionId
+        *
+        * @param AbstractRevision[optional] $revision
+        */
+       protected function __construct( UUID $uuid ) {
+               $this->uuid = $uuid ;
+               $this->storage = Container::get( 'storage' )->getStorage( 
$this->getRevisionClass() );
+       }
+
+       /**
+        * Instantiate a new object based on its id.
+        *
+        * @param UUID $uuid
+        * @return AbstractRevisionable
+        */
+       public static function newFromId( UUID $uuid ) {
+               return new static( $uuid );
+       }
+
+       /**
+        * Instantiate a new object based off of an AbstractRevision object.
+        *
+        * @param AbstractRevision $revision
+        * @return AbstractRevisionable
+        */
+       public static function newFromRevision( AbstractRevision $revision ) {
+               // create bogus object to access getId()
+               $object = new static( UUID::create() );
+               $uuid = $object->getId( $revision );
+               return static::newFromId( $uuid );
+       }
+
+       /**
+        * Instantiate a new object based off of a revision's UUID.
+        *
+        * @param UUID $uuid
+        * @return AbstractRevisionable
+        */
+       public static function newFromRevisionId( UUID $uuid ) {
+               // create bogus object to access getStorage()
+               $object = new static( UUID::create() );
+               $revision = $object->getStorage()->get( $uuid );
+               return static::newFromRevision( $revision );
+       }
+
+       /**
+        * @return \Flow\Data\ObjectManager
+        */
+       public function getStorage() {
+               return $this->storage;
+       }
+
+       /**
+        * Returns all revisions.
+        *
+        * @return AbstractRevision
+        */
+       public function getAllRevisions() {
+               if ( !$this->revisions ) {
+                       $this->revisions = $this->storage->find(
+                               array( $this->getIdColumn() => $this->uuid ),
+                               array( 'sort' => 'rev_id', 'order' => 'DESC' )
+                       );
+
+                       if ( !$this->revisions ) {
+                               throw new InvalidDataException( 'Revisions for 
' . $this->uuid . ' could not be found', 'invalid-revision-id' );
+                       }
+               }
+
+               return $this->revisions;
+       }
+
+       /**
+        * Returns the revision with the given id.
+        *
+        * @param UUID $uuid
+        * @return AbstractRevision|null null if there is no such revision
+        */
+       public function getRevision( UUID $uuid ) {
+               // make sure all revisions have been loaded
+               $this->getAllRevisions();
+
+               if ( !isset( $this->revisions[$uuid->toHex()] ) ) {
+                       return null;
+               }
+
+               // find requested id, based on given revision
+               return $this->revisions[$uuid->toHex()];
+       }
+
+       /**
+        * Returns the oldest revision.
+        *
+        * @return AbstractRevision
+        */
+       public function getFirstRevision() {
+               $revisions = (array) $this->getAllRevisions();
+               return array_shift( $revisions );
+       }
+
+       /**
+        * Returns the most recent revision.
+        *
+        * @return AbstractRevision
+        */
+       public function getLastRevision() {
+               $revisions = (array) $this->getAllRevisions();
+               return array_pop( $revisions );
+       }
+
+       /**
+        * Given a certain revision, returns the previous revision.
+        *
+        * @param AbstractRevision $revision
+        * @return AbstractRevision|null null if there is no previous revision
+        */
+       public function getPreviousRevision( AbstractRevision $revision ) {
+               return $this->getRevision( $revision->getPrevRevisionId() );
+       }
+
+       /**
+        * Given a certain revision, returns the next revision.
+        *
+        * @param AbstractRevision $revision
+        * @return AbstractRevision|null null if there is no next revision
+        */
+       public function getNextRevision( AbstractRevision $revision ) {
+               // make sure all revisions have been loaded
+               $this->getAllRevisions();
+
+               // find requested id, based on given revision
+               $ids = array_keys( $this->revisions );
+               $current = array_search( $revision->getRevisionId()->getHex(), 
$ids );
+               $next = $current + 1;
+
+               if ( $next >= count( $ids ) ) {
+                       return null;
+               }
+
+               return $this->getRevision( UUID::create( $ids[$next] ) );
+       }
+
+       /**
+        * Pass-through to call latest revision's methods.
+        *
+        * @param string $name
+        * @param array $arguments
+        * @return mixed
+        */
+       public function __call( $name, $arguments ) {
+               return $this->getLastRevision()->{$name}( $arguments );
+       }
+
+       /**
+        * Pass-through to get latest revision's properties.
+        *
+        * @param string $name
+        * @return mixed
+        */
+       public function __get( $name ) {
+               return $this->getLastRevision()->{$name};
+       }
+
+       /**
+        * Pass-through to set latest revision's properties.
+        *
+        * @param string $name
+        * @param mixed $value
+        * @return mixed
+        */
+       public function __set( $name, $value ) {
+               return $this->getLastRevision()->{$name} = $value;
+       }
+}
diff --git a/includes/Model/HeaderRevisionable.php 
b/includes/Model/HeaderRevisionable.php
new file mode 100644
index 0000000..74db8c3
--- /dev/null
+++ b/includes/Model/HeaderRevisionable.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Flow\Model;
+
+class HeaderRevisionable extends LocalCacheAbstractRevisionable {
+       public function getRevisionClass() {
+               return 'Flow\\Model\\Header';
+       }
+
+       public function getIdColumn() {
+               return 'header_workflow_id';
+       }
+
+       public function getId( AbstractRevision $revision ) {
+               return $revision->getWorkflowId();
+       }
+}
diff --git a/includes/Model/LocalCacheAbstractRevisionable.php 
b/includes/Model/LocalCacheAbstractRevisionable.php
new file mode 100644
index 0000000..a7896f0
--- /dev/null
+++ b/includes/Model/LocalCacheAbstractRevisionable.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Flow\Model;
+
+/**
+ * LocalBufferedCache saves all data that has been requested in an internal
+ * cache (in memory, per request). This provides the opportunity of (trying to)
+ * be smart about what results we fetch.
+ * The class extends the default AbstractRevisionable to make sure not all
+ * revisions are loaded unless we really need them. It could very well be that
+ * perhaps 5 recent revisions have already been loaded in other parts of the
+ * code, and we only need the 3rd most recent, in which case we shouldn't
+ * try to fetch all of them.
+ */
+abstract class LocalCacheAbstractRevisionable extends AbstractRevisionable {
+       /**
+        * Returns all revisions.
+        *
+        * @return AbstractRevision
+        */
+       public function getAllRevisions() {
+               // if we have not yet loaded everything, just clear what we 
have and
+               // fetch from cache
+               if ( $this->loaded() ) {
+                       $this->revisions = array();
+               }
+
+               return parent::getAllRevisions();
+       }
+
+       /**
+        * Returns the revision with the given id.
+        *
+        * @param UUID $uuid
+        * @return AbstractRevision|null null if there is no such revision
+        */
+       public function getRevision( UUID $uuid ) {
+               // check if fetching last already res
+               if ( isset( $this->revisions[$uuid->getHex() ] ) ) {
+                       return $this->revisions[$uuid->getHex() ];
+               }
+
+               /*
+                * The strategy here is to avoid having to call 
getAllRevisions(), which
+                * is most likely to have to load (fresh) data that is not yet 
in
+                * LocalBufferedCache's internal cache.
+                * To do so, we'll build the $this->revisions array by hand. 
Starting at
+                * the most recent revision and going up 1 revision at a time, 
checking
+                * if it is already in LocalBufferedCache's cache.
+                * If, however, we can't find the requested revisions (or one 
of the
+                * revisions on our way to the requested revision) in the 
internal cache
+                * of LocalBufferedCache, we'll just bail and load all 
revisions after
+                * all: if we do have to fetch data, might as well do it all in 
1 go!
+                */
+               while ( !$this->loaded() ) {
+                       // fetch current oldest revision
+                       $oldest = $this->getOldestLoaded();
+
+                       // fetch that one's preceeding revision id
+                       $previousId = $oldest->getPrevRevisionId();
+
+                       // check if it's in local storage already
+                       if ( $this->getStorage()->got( $previousId ) ) {
+                               $revision = $this->getStorage()->get( 
$previousId );
+
+                               // add this revision to revisions array
+                               array_unshift( $this->revisions, $revision );
+
+                               // stop iterating if we've found the one we 
wanted
+                               if ( $previousId->getHex() === $uuid->getHex() 
) {
+                                       break;
+                               }
+                       } else {
+                               // revision not found in local storage: load 
all revisions
+                               $this->getAllRevisions();
+                               break;
+                       }
+               }
+
+               if ( !isset( $this->revisions[$uuid->toHex()] ) ) {
+                       return null;
+               }
+
+               return $this->revisions[$uuid->toHex()];
+       }
+
+       /**
+        * Returns the most recent revision.
+        *
+        * @return AbstractRevision
+        */
+       public function getLastRevision() {
+               // if $revisions is not empty, it will always have the last 
revision,
+               // at the end of the array
+               if ( $this->revisions ) {
+                       return end( $this->revisions );
+               }
+
+               $attributes = array( $this->getIdColumn() => $this->uuid );
+               $options = array( 'sort' => 'rev_id', 'limit' => 1, 'order' => 
'DESC' );
+
+               if ( $this->storage->found( $attributes, $options ) ) {
+                       // if last revision is already known in local cache, 
fetch it
+                       return $this->find( $attributes, $options );
+
+               } else {
+                       // otherwise, might as well fetch al previous revisions 
while we're at
+                       // it - saved roundtrips to cache/db
+                       unset( $options['limit'] );
+                       $this->revisions = $this->storage->find( $attributes, 
$options );
+                       return end( $this->revisions );
+               }
+       }
+
+       /**
+        * Given a certain revision, returns the next revision.
+        *
+        * @param AbstractRevision $revision
+        * @return AbstractRevision|null null if there is no next revision
+        */
+       public function getNextRevision( AbstractRevision $revision ) {
+               // make sure the given revision is loaded
+               $this->getRevision( $revision->getRevisionId() );
+
+               // find requested id, based on given revision
+               $ids = array_keys( $this->revisions );
+               $current = array_search( $revision->getRevisionId()->getHex(), 
$ids );
+               $next = $current + 1;
+
+               if ( $next >= count( $ids ) ) {
+                       return null;
+               }
+
+               return $this->getRevision( UUID::create( $ids[$next] ) );
+       }
+
+
+       /**
+        * Returns true if all revisions have been loaded into $this->revisions.
+        *
+        * @return bool
+        */
+       public function loaded() {
+               $first = reset( $this->revisions );
+               return $first && $first->getPrevRevisionId() === null;
+       }
+
+       /**
+        * Returns the oldest revision that has already been fetched via this 
class.
+        *
+        * @return AbstractRevision
+        */
+       public function getOldestLoaded() {
+               if ( !$this->revisions ) {
+                       return $this->getLastRevision();
+               }
+
+               return reset( $this->revisions );
+       }
+}
diff --git a/includes/Model/PostRevisionable.php 
b/includes/Model/PostRevisionable.php
new file mode 100644
index 0000000..228fda1
--- /dev/null
+++ b/includes/Model/PostRevisionable.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Flow\Model;
+
+class PostRevisionable extends LocalCacheAbstractRevisionable {
+       public function getRevisionClass() {
+               return 'Flow\\Model\\PostRevision';
+       }
+
+       public function getIdColumn() {
+               return 'tree_rev_descendant_id';
+       }
+
+       public function getId( AbstractRevision $revision ) {
+               return $revision->getPostId();
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/111366
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: If53f0b4a1367f4cd7087dc0fad3326436f3a6d9f
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to