Daniel Kinzler has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/392310 )

Change subject: Introduce PageIdentity, and use it instead of Title.
......................................................................

Introduce PageIdentity, and use it instead of Title.

PageIdentity is similar to LinkTarget, but contains a page ID and
always refers to a page that can exist in the page table.

PageIdentity should replace Title for all use cases in which only
the page's identity (namespace, title, ID) are relevant.

Methods for converting between PageIdentity and Title have been
added to the Title class. A round-trip conversion will yield the
same Title object, to avoid memory churn.

Change-Id: Id36fb61986d5804de5cc1a509f38b2fa3daf73e3
---
M autoload.php
M includes/Revision.php
M includes/Storage/MutableRevisionRecord.php
A includes/Storage/PageIdentity.php
A includes/Storage/PageIdentityValue.php
M includes/Storage/RevisionArchiveRecord.php
M includes/Storage/RevisionFactory.php
M includes/Storage/RevisionLookup.php
M includes/Storage/RevisionRecord.php
M includes/Storage/RevisionStore.php
M includes/Storage/RevisionStoreRecord.php
M includes/Title.php
A tests/phpunit/includes/Storage/PageIdentityValueTest.php
M tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
M tests/phpunit/includes/TitleTest.php
15 files changed, 678 insertions(+), 176 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/10/392310/1

diff --git a/autoload.php b/autoload.php
index fd1f343..1f06795 100644
--- a/autoload.php
+++ b/autoload.php
@@ -944,6 +944,8 @@
        'MediaWiki\\Storage\\IncompleteRevisionException' => __DIR__ . 
'/includes/Storage/IncompleteRevisionException.php',
        'MediaWiki\\Storage\\MutableRevisionRecord' => __DIR__ . 
'/includes/Storage/MutableRevisionRecord.php',
        'MediaWiki\\Storage\\MutableRevisionSlots' => __DIR__ . 
'/includes/Storage/MutableRevisionSlots.php',
+       'MediaWiki\\Storage\\PageIdentity' => __DIR__ . 
'/includes/Storage/PageIdentity.php',
+       'MediaWiki\\Storage\\PageIdentityValue' => __DIR__ . 
'/includes/Storage/PageIdentityValue.php',
        'MediaWiki\\Storage\\RevisionAccessException' => __DIR__ . 
'/includes/Storage/RevisionAccessException.php',
        'MediaWiki\\Storage\\RevisionArchiveRecord' => __DIR__ . 
'/includes/Storage/RevisionArchiveRecord.php',
        'MediaWiki\\Storage\\RevisionFactory' => __DIR__ . 
'/includes/Storage/RevisionFactory.php',
diff --git a/includes/Revision.php b/includes/Revision.php
index 21d5789..ec1072f 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -21,6 +21,7 @@
  */
 
 use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\PageIdentity;
 use MediaWiki\Storage\RevisionAccessException;
 use MediaWiki\Storage\RevisionRecord;
 use MediaWiki\Storage\RevisionStore;
@@ -208,7 +209,11 @@
         * @return Revision|null
         */
        public static function loadFromTitle( $db, $title, $id = 0 ) {
-               $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, 
$title, $id );
+               $rec = self::getRevisionStore()->loadRevisionFromTitle(
+                       $db,
+                       $title->getPageIdentity(),
+                       $id
+               );
                return $rec === null ? null : new Revision( $rec );
        }
 
@@ -226,7 +231,11 @@
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
                // XXX: replace loadRevisionFromTimestamp by 
getRevisionByTimestamp?
-               $rec = self::getRevisionStore()->loadRevisionFromTimestamp( 
$db, $title, $timestamp );
+               $rec = self::getRevisionStore()->loadRevisionFromTimestamp(
+                       $db,
+                       $title->getPageIdentity(),
+                       $timestamp
+               );
                return $rec === null ? null : new Revision( $rec );
        }
 
@@ -436,13 +445,13 @@
                        $this->mRecord = 
self::getRevisionStore()->newMutableRevisionFromArray(
                                $row,
                                $queryFlags,
-                               $title
+                               $title ? $title->getPageIdentity() : null
                        );
                } elseif ( is_object( $row ) ) {
                        $this->mRecord = 
self::getRevisionStore()->newRevisionFromRow(
                                $row,
                                $queryFlags,
-                               $title
+                               $title ? $title->getPageIdentity() : null
                        );
                } else {
                        throw new InvalidArgumentException(
@@ -573,8 +582,11 @@
         * @return Title
         */
        public function getTitle() {
-               $linkTarget = $this->mRecord->getPageAsLinkTarget();
-               return Title::newFromLinkTarget( $linkTarget );
+               $page = $this->mRecord->getPageIdentity();
+
+               // Note that newFromPageIdentity() merely unwraps a Title 
object if the PageIdentity
+               // was originally constructed by Title::getPageIdentity().
+               return Title::newFromPageIdentity( $page );
        }
 
        /**
@@ -589,7 +601,7 @@
                        throw new InvalidArgumentException(
                                $title->getPrefixedText()
                                        . ' is not the same as '
-                                       . 
$this->mRecord->getPageAsLinkTarget()->__toString()
+                                       . 
$this->mRecord->getPageIdentity()->__toString()
                        );
                }
        }
@@ -1001,7 +1013,8 @@
                $comment = CommentStoreComment::newUnsavedComment( $summary, 
null );
 
                $title = Title::newFromID( $pageId );
-               $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, 
$comment, $minor, $user );
+               $page = $title->getPageIdentity();
+               $rec = self::getRevisionStore()->newNullRevision( $dbw, $page, 
$comment, $minor, $user );
 
                return new Revision( $rec );
        }
@@ -1043,6 +1056,8 @@
                        $user = $wgUser;
                }
 
+               // NOTE: Compatibility with this call is the only reason that
+               //       RevisionRecord::userCanBitfield() accepts a Title 
object as the last parameter.
                return RevisionRecord::userCanBitfield( $bitfield, $field, 
$user, $title );
        }
 
@@ -1055,7 +1070,7 @@
         * @return string|bool False if not found
         */
        static function getTimestampFromId( $title, $id, $flags = 0 ) {
-               return self::getRevisionStore()->getTimestampFromId( $title, 
$id, $flags );
+               return self::getRevisionStore()->getTimestampFromId( 
$title->getPageIdentity(), $id, $flags );
        }
 
        /**
@@ -1077,7 +1092,7 @@
         * @return int
         */
        static function countByTitle( $db, $title ) {
-               return self::getRevisionStore()->countRevisionsByTitle( $db, 
$title );
+               return self::getRevisionStore()->countRevisionsByTitle( $db, 
$title->getPageIdentity() );
        }
 
        /**
@@ -1112,17 +1127,28 @@
         * The title will also be loaded if $pageIdOrTitle is an integer ID.
         *
         * @param IDatabase $db
-        * @param int|Title $pageIdOrTitle Page ID or Title object
+        * @param int|Title|PageIdentity $pageOrTitle Page ID or Title or 
PageIdentity object
         * @param int $revId Known current revision of this page. Determined 
automatically if not given.
         * @return Revision|bool Returns false if missing
         * @since 1.28
         */
-       public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, 
$revId = 0 ) {
-               $title = $pageIdOrTitle instanceof Title
-                       ? $pageIdOrTitle
-                       : Title::newFromID( $pageIdOrTitle );
+       public static function newKnownCurrent( IDatabase $db, $pageOrTitle, 
$revId = 0 ) {
+               if ( $pageOrTitle instanceof PageIdentity && $revId > 0 ) {
+                       $page = $pageOrTitle;
+               } else {
+                       // We'll need a title object to determine the latest 
revision and construct a PageIdentity.
+                       $title = $pageOrTitle instanceof Title
+                               ? $pageOrTitle
+                               : Title::newFromID( $pageOrTitle );
 
-               $record = self::getRevisionStore()->getKnownCurrentRevision( 
$db, $title, $revId );
+                       if ( $revId <= 0 ) {
+                               $revId = $title->getLatestRevID();
+                       }
+
+                       $page = $title->getPageIdentity();
+               }
+
+               $record = self::getRevisionStore()->getKnownCurrentRevision( 
$db, $page, $revId );
                return $record ? new Revision( $record ) : false;
        }
 }
diff --git a/includes/Storage/MutableRevisionRecord.php 
b/includes/Storage/MutableRevisionRecord.php
index 35a93a6..b2ada2d 100644
--- a/includes/Storage/MutableRevisionRecord.php
+++ b/includes/Storage/MutableRevisionRecord.php
@@ -56,9 +56,7 @@
                UserIdentity $user,
                $timestamp
        ) {
-               // TODO: ideally, we wouldn't need a Title here
-               $title = Title::newFromLinkTarget( 
$parent->getPageAsLinkTarget() );
-               $rev = new MutableRevisionRecord( $title, $parent->getWikiId() 
);
+               $rev = new MutableRevisionRecord( $parent->getPageIdentity(), 
$parent->getWikiId() );
 
                $rev->setComment( $comment );
                $rev->setUser( $user );
@@ -79,16 +77,16 @@
         * @note Avoid calling this constructor directly. Use the appropriate 
methods
         * in RevisionStore instead.
         *
-        * @param Title $title The title of the page this Revision is 
associated with.
+        * @param PageIdentity $pageIdentity The identity of the page this 
Revision is associated with.
         * @param bool|string $wikiId the wiki ID of the site this Revision 
belongs to,
         *        or false for the local site.
         *
         * @throws MWException
         */
-       function __construct( Title $title, $wikiId = false ) {
+       function __construct( PageIdentity $pageIdentity, $wikiId = false ) {
                $slots = new MutableRevisionSlots();
 
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $pageIdentity, $slots, $wikiId );
 
                $this->mSlots = $slots; // redundant, but nice for static 
analysis
        }
@@ -264,9 +262,10 @@
        public function setPageId( $pageId ) {
                Assert::parameterType( 'integer', $pageId, '$pageId' );
 
-               if ( $this->mTitle->exists() && $pageId !== 
$this->mTitle->getArticleID() ) {
+               if ( $this->mPageIdentity->exists() && $pageId !== 
$this->mPageIdentity->getId() ) {
                        throw new InvalidArgumentException(
-                               'The given Title does not belong to page ID ' . 
$this->mPageId
+                               'The given page ID does not match the 
PageIdentity provided to the constructor: '
+                                       . $this->mPageId
                        );
                }
 
diff --git a/includes/Storage/PageIdentity.php 
b/includes/Storage/PageIdentity.php
new file mode 100644
index 0000000..d19768c
--- /dev/null
+++ b/includes/Storage/PageIdentity.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * An interface representing page identity.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * An interface representing page identity.
+ *
+ * This can be used for existing and non-existing pages. It cannot however be 
used for
+ * pages that cannot exist.
+ *
+ * @note This is intended as a drop-in replacement for uses of Title that only 
need the page ID,
+ * namespace, and title text. PageIdentity is similar to LinkTarget, but can 
only refer to local
+ * pages, and it provides the page ID (along with the information whether the 
page exists).
+ *
+ * FIXME: change RevisionRecord:::getPageAsLinkTarget to getPageIdentity!
+ *
+ * FIXME: create Title::newFromPageIdentity and Title::asPageIdentity.
+ * NOTE: Title should probably not implement PageIdentity, since PageIdentity 
guarantees
+ * that it's a page that can at least potentially exist locally as a 
non-special page,
+ * while Title can also be external, or a special page, or refer to a fragment.
+ */
+interface PageIdentity {
+
+       /**
+        * @return boolean Whether the page exists
+        */
+       public function exists();
+
+       /**
+        * @return int The page ID. May be 0 if the page does not exist.
+        */
+       public function getId();
+
+       /**
+        * @return LinkTarget A LinkTarget giving the title and namespace of 
the page.
+        */
+       public function getAsLinkTarget();
+
+       /**
+        * Get the namespace index.
+        *
+        * @see LinkTarget::getNamespace()
+        *
+        * @return int Namespace index
+        */
+       public function getNamespace();
+
+       /**
+        * Convenience function to test if it is in the namespace
+        *
+        * @see LinkTarget::inNamespace()
+        *
+        * @param int $ns
+        * @return bool
+        */
+       public function inNamespace( $ns );
+
+       /**
+        * Returns the page's title in database key form (with underscores), 
without namespace prefix.
+        *
+        * @see LinkTarget::getDBkey()
+        *
+        * @return string Main part of the link, with underscores (for use in 
href attributes)
+        */
+       public function getTitleDBkey();
+
+       /**
+        * Returns the page's title in text form (with spaces), without 
namespace prefix.
+        *
+        * @see LinkTarget::getText()
+        *
+        * @return string
+        */
+       public function getTitleText();
+
+       /**
+        * Returns an informative human readable representation of the page,
+        * for use in logging and debugging.
+        *
+        * @return string
+        */
+       public function __toString();
+
+}
diff --git a/includes/Storage/PageIdentityValue.php 
b/includes/Storage/PageIdentityValue.php
new file mode 100644
index 0000000..77a8773
--- /dev/null
+++ b/includes/Storage/PageIdentityValue.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * A value object representing page identity.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use MediaWiki\Linker\LinkTarget;
+use TitleValue;
+
+/**
+ * A value object representing page identity.
+ *
+ * This can be used for existing and non-existing pages. It cannot however be 
used for
+ * pages that cannot exist.
+ *
+ * @note This is intended as a drop-in replacement for uses of Title that only 
need the page ID,
+ * namespace, and title text. PageIdentity is similar to LinkTarget, but can 
only refer to local
+ * pages, and it provides the page ID (along with the information whether the 
page exists).
+ *
+ * NOTE: Title should not implement PageIdentity, since PageIdentity guarantees
+ * that it's a page that can at least potentially exist locally as a 
non-special page,
+ * while Title can also be external, or a special page, or refer to a fragment.
+ */
+class PageIdentityValue implements PageIdentity {
+
+       /**
+        * @var int
+        */
+       private $id;
+
+       /**
+        * @var LinkTarget
+        */
+       private $asLinkTarget;
+
+       /**
+        * @param int $id The page id, with 0 indicating that the page does not 
exist.
+        * @param int $namespace The namespace ID. This is not validated.
+        * @param string $dbkey The page title in valid DBkey form. No 
normalization is applied.
+        *
+        * @return PageIdentity
+        */
+       public static function newFromDBKey( $id, $namespace, $dbkey ) {
+               return new self( $id, new TitleValue( $namespace, $dbkey ) );
+       }
+
+       /**
+        * PageIdentity constructor.
+        *
+        * @param int $id The page id, with 0 indicating that the page does not 
exist.
+        * @param LinkTarget $asLinkTarget
+        */
+       public function __construct( $id, LinkTarget $asLinkTarget ) {
+               $this->id = $id;
+               $this->asLinkTarget = $asLinkTarget;
+       }
+
+       /**
+        * @return boolean Whether the page exists
+        */
+       public function exists() {
+               return $this->getId() > 0;
+       }
+
+       /**
+        * @return int The page ID. May be 0 if the page does not exist.
+        */
+       public function getId() {
+               return $this->id;
+       }
+
+       /**
+        * @return LinkTarget A LinkTarget giving the title and namespace of 
the page.
+        */
+       public function getAsLinkTarget() {
+               $this->asLinkTarget;
+       }
+
+       /**
+        * Get the namespace index.
+        *
+        * @see LinkTarget::getNamespace()
+        *
+        * @return int Namespace index
+        */
+       public function getNamespace() {
+               return $this->asLinkTarget->getNamespace();
+       }
+
+       /**
+        * Convenience function to test if it is in the namespace
+        *
+        * @see LinkTarget::inNamespace()
+        *
+        * @param int $ns
+        * @return bool
+        */
+       public function inNamespace( $ns ) {
+               return $this->asLinkTarget->inNamespace( $ns );
+       }
+
+       /**
+        * Returns the page's title in database key form (with underscores), 
without namespace prefix.
+        *
+        * @see LinkTarget::getDBkey()
+        *
+        * @return string Main part of the link, with underscores (for use in 
href attributes)
+        */
+       public function getTitleDBkey() {
+               return $this->asLinkTarget->getDBkey();
+       }
+
+       /**
+        * Returns the page's title in text form (with spaces), without 
namespace prefix.
+        *
+        * @see LinkTarget::getText()
+        *
+        * @return string
+        */
+       public function getTitleText() {
+               return $this->asLinkTarget->getText();
+       }
+
+       /**
+        * Returns an informative human readable representation of the page,
+        * for use in logging and debugging.
+        *
+        * @return string
+        */
+       public function __toString() {
+               return "page #{$this->id} ($this->asLinkTarget)";
+       }
+
+}
diff --git a/includes/Storage/RevisionArchiveRecord.php 
b/includes/Storage/RevisionArchiveRecord.php
index 71ecc70..a0d33890 100644
--- a/includes/Storage/RevisionArchiveRecord.php
+++ b/includes/Storage/RevisionArchiveRecord.php
@@ -47,7 +47,7 @@
         * @note Avoid calling this constructor directly. Use the appropriate 
methods
         * in RevisionStore instead.
         *
-        * @param Title $title The title of the page this Revision is 
associated with.
+        * @param PageIdentity $pageIdentity The identity of the page this 
Revision is associated with.
         * @param UserIdentity $user
         * @param CommentStoreComment $comment
         * @param object $row An archive table row
@@ -56,22 +56,22 @@
         *        or false for the local site.
         */
        function __construct(
-               Title $title,
+               PageIdentity $pageIdentity,
                UserIdentity $user,
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
                $wikiId = false
        ) {
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $pageIdentity, $slots, $wikiId );
                Assert::parameterType( 'object', $row, '$row' );
 
                $this->mArchiveId = intval( $row->ar_id );
 
-               // NOTE: ar_page_id may be different from 
$this->mTitle->getArticleID() in some cases,
+               // NOTE: ar_page_id may be different from 
$this->mPageIdentity->getId() in some cases,
                // notably when a partially restored page has been moved, and a 
new page has been created
                // with the same title. Archive rows for that title will then 
have the wrong page id.
-               $this->mPageId = isset( $row->ar_page_id ) ? intval( 
$row->ar_page_id ) : $title->getArticleID();
+               $this->mPageId = isset( $row->ar_page_id ) ? intval( 
$row->ar_page_id ) : $pageIdentity->getId();
 
                // NOTE: ar_parent_id = 0 indicates that there is no parent 
revision, while null
                // indicates that the parent revision is unknown. As per MW 
1.31, the database schema
diff --git a/includes/Storage/RevisionFactory.php 
b/includes/Storage/RevisionFactory.php
index 181c97e..af87f5f 100644
--- a/includes/Storage/RevisionFactory.php
+++ b/includes/Storage/RevisionFactory.php
@@ -45,12 +45,12 @@
         *
         * @param array $fields
         * @param int $queryFlags Flags for lazy loading behavior, see 
IDBAccessObject::READ_XXX.
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         *
         * @return MutableRevisionRecord
         * @throws MWException
         */
-       public function newMutableRevisionFromArray( array $fields, $queryFlags 
= 0, Title $title = null );
+       public function newMutableRevisionFromArray( array $fields, $queryFlags 
= 0, PageIdentity $page = null );
 
        /**
         * Constructs a RevisionRecord given a database row and content slots.
@@ -60,12 +60,12 @@
         *
         * @param object $row
         * @param int $queryFlags Flags for lazy loading behavior, see 
IDBAccessObject::READ_XXX.
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         *
         * @return RevisionRecord
         * @internal param RevisionSlots $slots
         */
-       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title 
= null );
+       public function newRevisionFromRow( $row, $queryFlags = 0, PageIdentity 
$page = null );
 
        /**
         * Make a fake revision object from an archive table row. This is 
queried
@@ -75,7 +75,7 @@
         *
         * @param object $row
         * @param int $queryFlags Flags for lazy loading behavior, see 
IDBAccessObject::READ_XXX.
-        * @param Title $title
+        * @param PageIdentity $page
         * @param array $overrides
         *
         * @return RevisionRecord
@@ -83,7 +83,7 @@
        public function newRevisionFromArchiveRow(
                $row,
                $queryFlags = 0,
-               Title $title = null,
+               PageIdentity $page = null,
                array $overrides = []
        );
 
diff --git a/includes/Storage/RevisionLookup.php 
b/includes/Storage/RevisionLookup.php
index 69e274b..4557cd2 100644
--- a/includes/Storage/RevisionLookup.php
+++ b/includes/Storage/RevisionLookup.php
@@ -119,11 +119,11 @@
         * MCR migration note: this replaces Revision::newKnownCurrent
         *
         * @param IDatabase $db
-        * @param Title $title the associated page title
+        * @param PageIdentity $page the associated page title
         * @param int $revId current revision of this page
         *
         * @return RevisionRecord|bool Returns false if missing
         */
-       public function getKnownCurrentRevision( IDatabase $db, Title $title, 
$revId );
+       public function getKnownCurrentRevision( IDatabase $db, PageIdentity 
$page, $revId );
 
 }
diff --git a/includes/Storage/RevisionRecord.php 
b/includes/Storage/RevisionRecord.php
index f858582..ccb9f6b 100644
--- a/includes/Storage/RevisionRecord.php
+++ b/includes/Storage/RevisionRecord.php
@@ -26,7 +26,6 @@
 use Content;
 use InvalidArgumentException;
 use LogicException;
-use MediaWiki\Linker\LinkTarget;
 use MediaWiki\User\UserIdentity;
 use MWException;
 use Title;
@@ -79,8 +78,8 @@
        /** @var CommentStoreComment|null */
        protected $mComment;
 
-       /**  @var Title */
-       protected $mTitle; // TODO: we only need the title for permission 
checks!
+       /**  @var PageIdentity */
+       protected $mPageIdentity;
 
        /** @var RevisionSlots */
        protected $mSlots;
@@ -89,22 +88,22 @@
         * @note Avoid calling this constructor directly. Use the appropriate 
methods
         * in RevisionStore instead.
         *
-        * @param Title $title The title of the page this Revision is 
associated with.
+        * @param PageIdentity $pageIdentity The title of the page this 
Revision is associated with.
         * @param RevisionSlots $slots The slots of this revision.
         * @param bool|string $wikiId the wiki ID of the site this Revision 
belongs to,
         *        or false for the local site.
         *
         * @throws MWException
         */
-       function __construct( Title $title, RevisionSlots $slots, $wikiId = 
false ) {
+       function __construct( PageIdentity $pageIdentity, RevisionSlots $slots, 
$wikiId = false ) {
                Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
 
-               $this->mTitle = $title;
+               $this->mPageIdentity = $pageIdentity;
                $this->mSlots = $slots;
                $this->mWiki = $wikiId;
 
                // XXX: this is a sensible default, but we may not have a Title 
object here in the future.
-               $this->mPageId = $title->getArticleID();
+               $this->mPageId = $pageIdentity->getId();
        }
 
        /**
@@ -279,10 +278,10 @@
         *
         * MCR migration note: this replaces Revision::getTitle
         *
-        * @return LinkTarget
+        * @return PageIdentity
         */
-       public function getPageAsLinkTarget() {
-               return $this->mTitle;
+       public function getPageIdentity() {
+               return $this->mPageIdentity;
        }
 
        /**
@@ -420,27 +419,28 @@
         * @return bool
         */
        protected function userCan( $field, User $user ) {
-               // TODO: use callback for permission checks, so we don't need 
to know a Title object!
-               return self::userCanBitfield( $this->getVisibility(), $field, 
$user, $this->mTitle );
+               // TODO: use callback for permission checks, so we can move 
userCanBitfield() to a service!
+               return self::userCanBitfield( $this->getVisibility(), $field, 
$user, $this->mPageIdentity );
        }
 
        /**
         * Determine if the current user is allowed to view a particular
-        * field of this revision, if it's marked as deleted. This is used
-        * by various classes to avoid duplication.
+        * field of this revision, if it's marked as deleted.
         *
         * MCR migration note: this replaces Revision::userCanBitfield
+        *
+        * @todo Move this to a dedicated PagePermission service, along with 
Title::userCan.
         *
         * @param int $bitfield Current field
         * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
         *                               self::DELETED_COMMENT = 
File::DELETED_COMMENT,
         *                               self::DELETED_USER = File::DELETED_USER
         * @param User $user User object to check
-        * @param Title|null $title A Title object to check for per-page 
restrictions on,
-        *                          instead of just plain userrights
+        * @param PageIdentity|Title|null $page Identity of the page to check. 
This accepts as Title
+        *        object for backwards compatibility. Use of Title is however 
deprecated.
         * @return bool
         */
-       public static function userCanBitfield( $bitfield, $field, User $user, 
Title $title = null ) {
+       public static function userCanBitfield( $bitfield, $field, User $user, 
$page = null ) {
                if ( $bitfield & $field ) { // aspect is deleted
                        if ( $bitfield & self::DELETED_RESTRICTED ) {
                                $permissions = [ 'suppressrevision', 
'viewsuppressed' ];
@@ -450,12 +450,17 @@
                                $permissions = [ 'deletedhistory' ];
                        }
                        $permissionlist = implode( ', ', $permissions );
-                       if ( $title === null ) {
+                       if ( $page === null ) {
                                wfDebug( "Checking for $permissionlist due to 
$field match on $bitfield\n" );
                                return call_user_func_array( [ $user, 
'isAllowedAny' ], $permissions );
                        } else {
-                               $text = $title->getPrefixedText();
-                               wfDebug( "Checking for $permissionlist on $text 
due to $field match on $bitfield\n" );
+                               wfDebug( "Checking for $permissionlist on $page 
due to $field match on $bitfield\n" );
+
+                               // NOTE: We shouldn't need a Title here.
+                               // The below is a B/C cludge to avoid 
wrapping/unwrapping Titles objects that
+                               // came in via Revision::userCanBitfield().
+                               $title = $page instanceof Title ? $page : 
Title::newFromPageIdentity( $page );
+
                                foreach ( $permissions as $perm ) {
                                        if ( $title->userCan( $perm, $user ) ) {
                                                return true;
diff --git a/includes/Storage/RevisionStore.php 
b/includes/Storage/RevisionStore.php
index dddb775..4bdd983 100644
--- a/includes/Storage/RevisionStore.php
+++ b/includes/Storage/RevisionStore.php
@@ -196,7 +196,7 @@
                                ],
                                [ 'rev_id' => $revId ],
                                __METHOD__,
-                               [],
+                               $dbOptions,
                                [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
                        );
                        if ( $row ) {
@@ -272,8 +272,7 @@
                        throw new MWException( 'Only the main slot is supported 
for now!' );
                }
 
-               // TODO: we shouldn't need an actual Title here.
-               $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() 
);
+               $page = $rev->getPageIdentity();
                $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page 
field' ); // check this early
 
                $parentId = $rev->getParentId() === null
@@ -291,7 +290,7 @@
                        $format = $content->getDefaultFormat();
                        $model = $content->getModel();
 
-                       $this->checkContentModel( $content, $title );
+                       $this->checkContentModel( $content, $page );
 
                        $data = $content->serialize( $format );
 
@@ -352,6 +351,7 @@
                if ( $this->contentHandlerUseDB ) {
                        // MCR migration note: rev_content_model and 
rev_content_format will go away
 
+                       $title = Title::newFromPageIdentity( $page ); // TODO: 
a LinkTarget should be enough here!
                        $defaultModel = ContentHandler::getDefaultModelFor( 
$title );
                        $defaultFormat = ContentHandler::getForModelID( 
$defaultModel )->getDefaultFormat();
 
@@ -383,7 +383,7 @@
                $user = new UserIdentityValue( $row['rev_user'], 
$row['rev_user_text'] );
 
                $rev = new RevisionStoreRecord(
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -411,14 +411,16 @@
         * MCR migration note: this corresponds to Revision::checkContentModel
         *
         * @param Content $content
-        * @param Title $title
+        * @param PageIdentity $page
         *
         * @throws MWException
         * @throws MWUnknownContentModelException
         */
-       private function checkContentModel( Content $content, Title $title ) {
-               // Note: may return null for revisions that have not yet been 
inserted
+       private function checkContentModel( Content $content, PageIdentity 
$page ) {
+               // Note: if the PageIdentity was constructed by 
Title::getPageIdentity(), this just unwraps.
+               $title = Title::newFromPageIdentity( $page );
 
+               // Note: may return null for revisions that have not yet been 
inserted
                $model = $content->getModel();
                $format = $content->getDefaultFormat();
                $handler = $content->getContentHandler();
@@ -470,7 +472,7 @@
         * MCR migration note: this replaces Revision::newNullRevision
         *
         * @param IDatabase $dbw
-        * @param Title $title Title of the page to read from
+        * @param PageIdentity $page Title of the page to read from
         * @param CommentStoreComment $comment RevisionRecord's summary
         * @param bool $minor Whether the revision should be considered as minor
         * @param User $user The user to attribute the revision to
@@ -478,7 +480,7 @@
         */
        public function newNullRevision(
                IDatabase $dbw,
-               Title $title,
+               PageIdentity $page,
                CommentStoreComment $comment,
                $minor,
                User $user
@@ -497,7 +499,7 @@
                        [ 'page', 'revision' ],
                        $fields,
                        [
-                               'page_id' => $title->getArticleID(),
+                               'page_id' => $page->getId(),
                                'page_latest=rev_id',
                        ],
                        __METHOD__,
@@ -506,7 +508,7 @@
 
                if ( $current ) {
                        $fields = [
-                               'page'       => $title->getArticleID(),
+                               'page'       => $page->getId(),
                                'user_text'  => $user->getName(),
                                'user'       => $user->getId(),
                                'comment'    => $comment,
@@ -524,8 +526,8 @@
 
                        $fields['title'] = Title::makeTitle( 
$current->page_namespace, $current->page_title );
 
-                       $mainSlot = $this->emulateMainSlot_1_29( $fields, 0, 
$title );
-                       $revision = new MutableRevisionRecord( $title, 
$this->wikiId );
+                       $mainSlot = $this->emulateMainSlot_1_29( $fields, 0, 
$page );
+                       $revision = new MutableRevisionRecord( $page, 
$this->wikiId );
                        $this->initializeMutableRevisionFromArray( $revision, 
$fields );
                        $revision->setSlot( $mainSlot );
                } else {
@@ -647,12 +649,12 @@
         *
         * @param object|array $row Either a database row or an array
         * @param int $queryFlags for callbacks
-        * @param Title $title
+        * @param PageIdentity $page
         *
         * @return SlotRecord The main slot, extracted from the MW 1.29 style 
row.
         * @throws MWException
         */
-       private function emulateMainSlot_1_29( $row, $queryFlags, Title $title 
) {
+       private function emulateMainSlot_1_29( $row, $queryFlags, PageIdentity 
$page ) {
                $mainSlotRow = new stdClass();
                $mainSlotRow->role_name = 'main';
 
@@ -732,9 +734,10 @@
                $mainSlotRow->slot_inherited = 0;
 
                if ( $mainSlotRow->model_name === null ) {
-                       $mainSlotRow->model_name = function ( SlotRecord $slot 
) use ( $title ) {
+                       $mainSlotRow->model_name = function ( SlotRecord $slot 
) use ( $page ) {
                                // TODO: MCR: consider slot role in 
getDefaultModelFor()! Use LinkTarget!
                                // TODO: MCR: deprecate $title->getModel().
+                               $title = Title::newFromPageIdentity( $page ); 
// unwrap
                                return ContentHandler::getDefaultModelFor( 
$title );
                        };
                }
@@ -906,19 +909,19 @@
         *
         * MCR migration note: this replaces Revision::loadFromTimestamp
         *
-        * @param Title $title
+        * @param PageIdentity $page
         * @param string $timestamp
         * @return RevisionRecord|null
         */
-       public function getRevisionFromTimestamp( $title, $timestamp ) {
+       public function getRevisionFromTimestamp( PageIdentity $page, 
$timestamp ) {
                return $this->newRevisionFromConds(
                        [
                                'rev_timestamp' => $timestamp,
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
+                               'page_namespace' => $page->getNamespace(),
+                               'page_title' => $page->getTitleDBkey()
                        ],
                        0,
-                       $title
+                       $page
                );
        }
 
@@ -930,7 +933,7 @@
         *
         * @param object $row
         * @param int $queryFlags
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         * @param array $overrides
         *
         * @return RevisionRecord
@@ -939,7 +942,7 @@
        public function newRevisionFromArchiveRow(
                $row,
                $queryFlags = 0,
-               Title $title = null,
+               PageIdentity $page = null,
                array $overrides = []
        ) {
                Assert::parameterType( 'object', $row, '$row' );
@@ -947,20 +950,27 @@
                // check second argument, since Revision::newFromArchiveRow had 
$overrides in that spot.
                Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
 
-               if ( !$title && isset( $overrides['title'] ) ) {
+               if ( !$page && isset( $overrides['title'] ) ) {
                        if ( !( $overrides['title'] instanceof Title ) ) {
                                throw new MWException( 'title field override 
must contain a Title object.' );
                        }
 
+                       /** @var Title $title */
                        $title = $overrides['title'];
+                       $page = $title->getPageIdentity();
                }
 
-               if ( !isset( $title ) ) {
+               if ( !$page ) {
                        if ( isset( $row->ar_namespace ) && isset( 
$row->ar_title ) ) {
-                               $title = Title::makeTitle( $row->ar_namespace, 
$row->ar_title );
+                               $pageId = isset( $row->ar_page_id ) ? intval( 
$row->ar_page_id ) : 0;
+                               $page = PageIdentityValue::newFromDBKey(
+                                       $pageId,
+                                       intval( $row->ar_namespace ),
+                                       $row->ar_title
+                               );
                        } else {
                                throw new InvalidArgumentException(
-                                       'A Title or ar_namespace and ar_title 
must be given'
+                                       'A PageIdentity or ar_namespace and 
ar_title must be given'
                                );
                        }
                }
@@ -976,10 +986,10 @@
                        // Legacy because $row may have come from 
self::selectFields()
                        ->getCommentLegacy( $this->getDBConnection( DB_REPLICA 
), $row, true );
 
-               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, 
$title );
+               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, 
$page );
                $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
 
-               return new RevisionArchiveRecord( $title, $user, $comment, 
$row, $slots, $this->wikiId );
+               return new RevisionArchiveRecord( $page, $user, $comment, $row, 
$slots, $this->wikiId );
        }
 
        /**
@@ -1017,23 +1027,29 @@
         *
         * @param object $row
         * @param int $queryFlags
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         *
         * @return RevisionRecord
         * @throws MWException
         * @throws RevisionAccessException
         */
-       private function newRevisionFromRow_1_29( $row, $queryFlags = 0, Title 
$title = null ) {
+       private function newRevisionFromRow_1_29( $row, $queryFlags = 0, 
PageIdentity $page = null ) {
                Assert::parameterType( 'object', $row, '$row' );
+               $title = null;
 
-               if ( !$title ) {
+               if ( !$page ) {
                        $pageId = isset( $row->rev_page ) ? $row->rev_page : 0; 
// XXX: also check page_id?
                        $revId = isset( $row->rev_id ) ? $row->rev_id : 0;
 
                        $title = $this->getTitle( $pageId, $revId );
+                       $page = $title->getPageIdentity();
                }
 
                if ( !isset( $row->page_latest ) ) {
+                       if ( !$title ) {
+                               $title = Title::newFromPageIdentity( $page );
+                       }
+
                        $row->page_latest = $title->getLatestRevID();
                        if ( $row->page_latest === 0 && $title->exists() ) {
                                wfWarn( 'Encountered title object in limbo: ID 
' . $title->getArticleID() );
@@ -1046,10 +1062,10 @@
                        // Legacy because $row may have come from 
self::selectFields()
                        ->getCommentLegacy( $this->getDBConnection( DB_REPLICA 
), $row, true );
 
-               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, 
$title );
+               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, 
$page );
                $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
 
-               return new RevisionStoreRecord( $title, $user, $comment, $row, 
$slots, $this->wikiId );
+               return new RevisionStoreRecord( $page, $user, $comment, $row, 
$slots, $this->wikiId );
        }
 
        /**
@@ -1059,12 +1075,12 @@
         *
         * @param object $row
         * @param int $queryFlags
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         *
         * @return RevisionRecord
         */
-       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title 
= null ) {
-               return $this->newRevisionFromRow_1_29( $row, $queryFlags, 
$title );
+       public function newRevisionFromRow( $row, $queryFlags = 0, PageIdentity 
$page = null ) {
+               return $this->newRevisionFromRow_1_29( $row, $queryFlags, $page 
);
        }
 
        /**
@@ -1075,7 +1091,7 @@
         *
         * @param array $fields
         * @param int $queryFlags
-        * @param Title|null $title
+        * @param PageIdentity|null $page
         *
         * @return MutableRevisionRecord
         * @throws MWException
@@ -1084,25 +1100,28 @@
        public function newMutableRevisionFromArray(
                array $fields,
                $queryFlags = 0,
-               Title $title = null
+               PageIdentity $page = null
        ) {
-               if ( !$title && isset( $fields['title'] ) ) {
+               if ( !$page && isset( $fields['title'] ) ) {
                        if ( !( $fields['title'] instanceof Title ) ) {
                                throw new MWException( 'title field must 
contain a Title object.' );
                        }
 
+                       /** @var Title $title */
                        $title = $fields['title'];
+                       $page = $title->getPageIdentity();
                }
 
-               if ( !$title ) {
+               if ( !$page ) {
                        $pageId = isset( $fields['page'] ) ? $fields['page'] : 
0;
                        $revId = isset( $fields['id'] ) ? $fields['id'] : 0;
 
                        $title = $this->getTitle( $pageId, $revId );
+                       $page = $title->getPageIdentity();
                }
 
                if ( !isset( $fields['page'] ) ) {
-                       $fields['page'] = $title->getArticleID( $queryFlags );
+                       $fields['page'] = $page->getId();
                }
 
                // if we have a content object, use it to set the model and type
@@ -1160,9 +1179,9 @@
                        }
                }
 
-               $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, 
$title );
+               $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, 
$page );
 
-               $revision = new MutableRevisionRecord( $title, $this->wikiId );
+               $revision = new MutableRevisionRecord( $page, $this->wikiId );
                $this->initializeMutableRevisionFromArray( $revision, $fields );
                $revision->setSlot( $mainSlot );
 
@@ -1286,15 +1305,15 @@
         * MCR migration note: this replaces Revision::loadFromTitle
         *
         * @note direct use is deprecated!
-        * @todo remove when unused!
+        * @todo remove when unused! If we have a Title or PageIdentity, use 
loadRevisionFromPageId()
         *
         * @param IDatabase $db
-        * @param Title $title
+        * @param PageIdentity $page
         * @param int $id
         *
         * @return RevisionRecord|null
         */
-       public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) 
{
+       public function loadRevisionFromTitle( IDatabase $db, PageIdentity 
$page, $id = 0 ) {
                if ( $id ) {
                        $matchId = intval( $id );
                } else {
@@ -1305,11 +1324,11 @@
                        $db,
                        [
                                "rev_id=$matchId",
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
+                               'page_namespace' => $page->getNamespace(),
+                               'page_title' => $page->getTitleDBkey()
                        ],
                        0,
-                       $title
+                       $page
                );
        }
 
@@ -1324,19 +1343,19 @@
         * @todo remove when unused!
         *
         * @param IDatabase $db
-        * @param Title $title
+        * @param PageIdentity $page
         * @param string $timestamp
         * @return RevisionRecord|null
         */
-       public function loadRevisionFromTimestamp( IDatabase $db, $title, 
$timestamp ) {
+       public function loadRevisionFromTimestamp( IDatabase $db, PageIdentity 
$page, $timestamp ) {
                return $this->loadRevisionFromConds( $db,
                        [
                                'rev_timestamp' => $db->timestamp( $timestamp ),
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
+                               'page_namespace' => $page->getNamespace(),
+                               'page_title' => $page->getTitleDBkey()
                        ],
                        0,
-                       $title
+                       $page
                );
        }
 
@@ -1350,11 +1369,11 @@
         *
         * @param array $conditions
         * @param int $flags (optional)
-        * @param Title $title
+        * @param PageIdentity $page
         *
         * @return RevisionRecord|null
         */
-       private function newRevisionFromConds( $conditions, $flags = 0, Title 
$title = null ) {
+       private function newRevisionFromConds( $conditions, $flags = 0, 
PageIdentity $page = null ) {
                $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? 
DB_MASTER : DB_REPLICA );
                $rev = $this->loadRevisionFromConds( $db, $conditions, $flags );
                $this->releaseDBConnection( $db );
@@ -1386,7 +1405,7 @@
         * @param IDatabase $db
         * @param array $conditions
         * @param int $flags (optional)
-        * @param Title $title
+        * @param PageIdentity $page
         *
         * @return RevisionRecord|null
         */
@@ -1394,11 +1413,11 @@
                IDatabase $db,
                $conditions,
                $flags = 0,
-               Title $title = null
+               PageIdentity $page = null
        ) {
                $row = $this->fetchRevisionRowFromConds( $db, $conditions, 
$flags );
                if ( $row ) {
-                       $rev = $this->newRevisionFromRow( $row, $flags, $title 
);
+                       $rev = $this->newRevisionFromRow( $row, $flags, $page );
 
                        return $rev;
                }
@@ -1697,12 +1716,12 @@
         *
         * MCR migration note: this replaces Revision::getTimestampFromId
         *
-        * @param Title $title
+        * @param PageIdentity $page
         * @param int $id
         * @param int $flags
         * @return string|bool False if not found
         */
-       public function getTimestampFromId( $title, $id, $flags = 0 ) {
+       public function getTimestampFromId( PageIdentity $page, $id, $flags = 0 
) {
                $db = $this->getDBConnection(
                        ( $flags & IDBAccessObject::READ_LATEST ) ? DB_MASTER : 
DB_REPLICA
                );
@@ -1712,7 +1731,7 @@
                        $id = 0;
                }
                $conds = [ 'rev_id' => $id ];
-               $conds['rev_page'] = $title->getArticleID();
+               $conds['rev_page'] = $page->getId();
                $timestamp = $db->selectField( 'revision', 'rev_timestamp', 
$conds, __METHOD__ );
 
                $this->releaseDBConnection( $db );
@@ -1748,11 +1767,11 @@
         * MCR migration note: this replaces Revision::countByTitle
         *
         * @param IDatabase $db
-        * @param Title $title
+        * @param PageIdentity $page
         * @return int
         */
-       public function countRevisionsByTitle( IDatabase $db, $title ) {
-               $id = $title->getArticleID();
+       public function countRevisionsByTitle( IDatabase $db, PageIdentity 
$page ) {
+               $id = $page->getId();
                if ( $id ) {
                        return $this->countRevisionsByPageId( $db, $id );
                }
@@ -1811,27 +1830,23 @@
         * MCR migration note: this replaces Revision::newKnownCurrent
         *
         * @param IDatabase $db
-        * @param Title $title the associated page title
-        * @param int $revId current revision of this page. Defaults to 
$title->getLatestRevID().
+        * @param PageIdentity $page the associated page title
+        * @param int $revId current revision of this page.
         *
         * @return RevisionRecord|bool Returns false if missing
         */
-       public function getKnownCurrentRevision( IDatabase $db, Title $title, 
$revId = 0 ) {
+       public function getKnownCurrentRevision( IDatabase $db, PageIdentity 
$page, $revId ) {
                $this->checkDatabaseWikiId( $db );
 
-               $pageId = $title->getArticleID();
+               $pageId = $page->getId();
 
                if ( !$pageId ) {
                        return false;
                }
 
                if ( !$revId ) {
-                       $revId = $title->getLatestRevID();
-               }
-
-               if ( !$revId ) {
                        wfWarn(
-                               'No latest revision known for page ' . 
$title->getPrefixedDBkey()
+                               'No latest revision known for page ' . $page
                                . ' even though it exists with page ID ' . 
$pageId
                        );
                        return false;
@@ -1857,7 +1872,7 @@
 
                // Reflect revision deletion and user renames
                if ( $row ) {
-                       return $this->newRevisionFromRow( $row, 0, $title );
+                       return $this->newRevisionFromRow( $row, 0, $page );
                } else {
                        return false;
                }
diff --git a/includes/Storage/RevisionStoreRecord.php 
b/includes/Storage/RevisionStoreRecord.php
index d86f370..dd04393 100644
--- a/includes/Storage/RevisionStoreRecord.php
+++ b/includes/Storage/RevisionStoreRecord.php
@@ -44,7 +44,7 @@
         * @note Avoid calling this constructor directly. Use the appropriate 
methods
         * in RevisionStore instead.
         *
-        * @param Title $title The title of the page this Revision is 
associated with.
+        * @param PageIdentity $pageIdentity The identity of the page this 
Revision is associated with.
         * @param UserIdentity $user
         * @param CommentStoreComment $comment
         * @param object $row A row from the revision table.
@@ -53,14 +53,14 @@
         *        or false for the local site.
         */
        function __construct(
-               Title $title,
+               PageIdentity $pageIdentity,
                UserIdentity $user,
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
                $wikiId = false
        ) {
-               parent::__construct( $title, $slots, $wikiId );
+               parent::__construct( $pageIdentity, $slots, $wikiId );
                Assert::parameterType( 'object', $row, '$row' );
 
                $this->mId = intval( $row->rev_id );
@@ -82,18 +82,15 @@
                $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) 
: null;
                $this->mSha1 = isset( $row->rev_sha1 ) ? $row->rev_sha1 : null;
 
-               // NOTE: we must not call $this->mTitle->getLatestRevID() here, 
since the state of
-               // page_latest may be in limbo during revision creation. In 
that case, calling
-               // $this->mTitle->getLatestRevID() would cause a bad value to 
be cached in the Title
-               // object. During page creation, that bad value would be 0.
                if ( isset( $row->page_latest ) ) {
                        $this->mCurrent = ( $row->rev_id == $row->page_latest );
                }
 
                // sanity check
-               if ( $this->mPageId && $this->mPageId !== 
$this->mTitle->getArticleID() ) {
+               if ( $this->mPageId && $this->mPageId !== 
$this->mPageIdentity->getId() ) {
                        throw new InvalidArgumentException(
-                               'The given Title does not belong to page ID ' . 
$this->mPageId
+                               'The given page ID does not match the given 
PageIdentity: '
+                               . $this->mPageId
                        );
                }
        }
diff --git a/includes/Title.php b/includes/Title.php
index 829be44..0d12afb 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -22,6 +22,8 @@
  * @file
  */
 
+use MediaWiki\Storage\PageIdentity;
+use MediaWiki\Storage\PageIdentityValue;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
@@ -247,6 +249,35 @@
                        $linkTarget->getFragment(),
                        $linkTarget->getInterwiki()
                );
+       }
+
+       /**
+        * Create a new Title from a LinkTarget
+        *
+        * @note New code should avoid using Title objects, and bind to the 
LinkTarget and PageIdentity
+        * interfaces instead. This method should be used to interface between 
old and new code.
+        *
+        * @param PageIdentity $pageIdentity Assumed to be safe.
+        *
+        * @return Title
+        */
+       public static function newFromPageIdentity( PageIdentity $page ) {
+               // NOTE: Title cannot implement PageIdentity, since PageIdentity
+               // guarantees a non-special local page.
+
+               if ( $page->getAsLinkTarget() instanceof Title ) {
+                       // See Title::getPageIdentity()
+                       return $page->getAsLinkTarget();
+               }
+
+               $title = self::makeTitle(
+                       $page->getNamespace(),
+                       $page->getTitleText()
+               );
+
+               $title->mArticleID = $page->getId();
+
+               return $title;
        }
 
        /**
@@ -3349,6 +3380,27 @@
        }
 
        /**
+        * Returns the PageIdentity corresponding to this Title.
+        * If this title is special or not local, this method returns null.
+        *
+        * @note New code should avoid using Title objects, and bind to the 
LinkTarget and PageIdentity
+        * interfaces instead. This method should be used to interface between 
old and new code.
+        *
+        * @return PageIdentityValue|null
+        */
+       public function getPageIdentity() {
+               // NOTE: This is why Title cannot implement PageIdentity!
+               if ( !$this->isLocal() || !$this->canExist() ) {
+                       return null;
+               }
+
+               return new PageIdentityValue(
+                       $this->getArticleID(),
+                       $this
+               );
+       }
+
+       /**
         * Is this an article that is a redirect page?
         * Uses link cache, adding it if necessary
         *
diff --git a/tests/phpunit/includes/Storage/PageIdentityValueTest.php 
b/tests/phpunit/includes/Storage/PageIdentityValueTest.php
new file mode 100644
index 0000000..30eed56
--- /dev/null
+++ b/tests/phpunit/includes/Storage/PageIdentityValueTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Storage\PageIdentityValue;
+use MediaWikiTestCase;
+use TitleValue;
+
+/**
+ * @covers MediaWiki\Storage\PageIdentityValue
+ */
+class PageIdentityValueTest extends MediaWikiTestCase {
+
+       public function newFromDBKey() {
+               $page = PageIdentityValue::newFromDBKey( 17, 23, 'PIV_Test' );
+
+               $this->assertSame( 17, $page->getId(), 'getId()' );
+               $this->assertSame( 23, $page->getNamespace(), 'getNamespace()' 
);
+               $this->assertSame( 'PIV_Test', $page->getTitleDBKey(), 
'getTitleDBKey()' );
+
+               $target = $page->getAsLinkTarget();
+               $this->assertInstanceOf( LinkTarget::class, 'getAsLinkTarget()' 
);
+               $this->assertSame( 23, $target->getNamespace(), 
'getAsLinkTarget()->getNamespace()' );
+               $this->assertSame( 'PIV_Test', $target->getDBKey(), 
'getAsLinkTarget()->getDBKey()' );
+       }
+
+       public function testConstructor() {
+               $page = new PageIdentityValue( 17, new TitleValue( 23, 
'PIV_Test' ) );
+
+               $this->assertSame( 17, $page->getId(), 'getId()' );
+               $this->assertSame( 23, $page->getNamespace(), 'getNamespace()' 
);
+               $this->assertSame( 'PIV_Test', $page->getTitleDBKey(), 
'getTitleDBKey()' );
+
+               $target = $page->getAsLinkTarget();
+               $this->assertInstanceOf( LinkTarget::class, 'getAsLinkTarget()' 
);
+               $this->assertSame( 23, $target->getNamespace(), 
'getAsLinkTarget()->getNamespace()' );
+               $this->assertSame( 'PIV_Test', $target->getDBKey(), 
'getAsLinkTarget()->getDBKey()' );
+       }
+
+       public function testExists() {
+               $page = new PageIdentityValue( 17, new TitleValue( 23, 
'PIV_Test' ) );
+               $this->assertTrue( $page->exists(), 'exists()' );
+
+               $page = new PageIdentityValue( 0, new TitleValue( 23, 
'PIV_Test' ) );
+               $this->assertFalse( $page->exists(), 'exists()' );
+       }
+
+       public function testGetTitleText() {
+               $page = new PageIdentityValue( 17, new TitleValue( 23, 
'PIV_Test' ) );
+
+               $this->assertSame( 'PIV Test', $page->getTitleDBKey(), 
'getTitleText()' );
+       }
+
+       public function testInNamespace() {
+               $page = new PageIdentityValue( 17, new TitleValue( 23, 
'PIV_Test' ) );
+
+               $this->assertTrue( $page->inNamespace( 23 ) );
+               $this->assertFalse( $page->inNamespace( 44 ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php 
b/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
index 7fb39cf..885c3e0 100644
--- a/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
+++ b/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
@@ -5,6 +5,8 @@
 use CommentStoreComment;
 use InvalidArgumentException;
 use LogicException;
+use MediaWiki\Storage\PageIdentity;
+use MediaWiki\Storage\PageIdentityValue;
 use MediaWiki\Storage\RevisionRecord;
 use MediaWiki\Storage\RevisionSlots;
 use MediaWiki\Storage\RevisionStoreRecord;
@@ -26,8 +28,7 @@
         * @return RevisionStoreRecord
         */
        public function newRevision( array $overrides = [] ) {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
+               $page = PageIdentityValue::newFromDBKey( 17, NS_MAIN, 'Dummy' );
 
                $user = new UserIdentityValue( 11, 'Tester' );
                $comment = CommentStoreComment::newUnsavedComment( 'Hello 
World' );
@@ -38,7 +39,7 @@
 
                $row = [
                        'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_page' => strval( $page->getId() ),
                        'rev_timestamp' => '20200101000000',
                        'rev_deleted' => 0,
                        'rev_minor_edit' => 0,
@@ -50,12 +51,11 @@
 
                $row = array_merge( $row, $overrides );
 
-               return new RevisionStoreRecord( $title, $user, $comment, 
(object)$row, $slots );
+               return new RevisionStoreRecord( $page, $user, $comment, 
(object)$row, $slots );
        }
 
        public function provideConstructor() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
+               $page = PageIdentityValue::newFromDBKey( 17, NS_MAIN, 'Dummy' );
 
                $user = new UserIdentityValue( 11, 'Tester' );
                $comment = CommentStoreComment::newUnsavedComment( 'Hello 
World' );
@@ -66,7 +66,7 @@
 
                $protoRow = [
                        'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_page' => strval( $page->getId() ),
                        'rev_timestamp' => '20200101000000',
                        'rev_deleted' => 0,
                        'rev_minor_edit' => 0,
@@ -78,7 +78,7 @@
 
                $row = $protoRow;
                yield 'all info' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -91,7 +91,7 @@
                $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
 
                yield 'minor deleted' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -102,7 +102,7 @@
                $row['page_latest'] = $row['rev_id'];
 
                yield 'latest' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -113,7 +113,7 @@
                unset( $row['rev_parent'] );
 
                yield 'no parent' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -125,7 +125,7 @@
                unset( $row['rev_sha1'] );
 
                yield 'no length, no hash' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -136,7 +136,7 @@
        /**
         * @dataProvider provideConstructor
         *
-        * @param Title $title
+        * @param PageIdentity $pageIdentity
         * @param UserIdentity $user
         * @param CommentStoreComment $comment
         * @param object $row
@@ -144,16 +144,16 @@
         * @param bool $wikiId
         */
        public function testConstructorAndGetters(
-               Title $title,
+               PageIdentity $pageIdentity,
                UserIdentity $user,
                CommentStoreComment $comment,
                $row,
                RevisionSlots $slots,
                $wikiId = false
        ) {
-               $rec = new RevisionStoreRecord( $title, $user, $comment, $row, 
$slots, $wikiId );
+               $rec = new RevisionStoreRecord( $pageIdentity, $user, $comment, 
$row, $slots, $wikiId );
 
-               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 
'getPageAsLinkTarget' );
+               $this->assertSame( $pageIdentity, $rec->getPageIdentity() );
                $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 
'getUser' );
                $this->assertSame( $comment, $rec->getComment(), 'getComment' );
 
@@ -200,8 +200,7 @@
        }
 
        public function provideConstructorFailure() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
+               $page = PageIdentityValue::newFromDBKey( 17, NS_MAIN, 
__METHOD__ );
 
                $user = new UserIdentityValue( 11, 'Tester' );
 
@@ -213,7 +212,7 @@
 
                $protoRow = [
                        'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_page' => strval( $page->getId() ),
                        'rev_timestamp' => '20200101000000',
                        'rev_deleted' => 0,
                        'rev_minor_edit' => 0,
@@ -224,7 +223,7 @@
                ];
 
                yield 'not a row' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        'not a row',
@@ -236,7 +235,7 @@
                $row['rev_timestamp'] = 'kittens';
 
                yield 'bad timestamp' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -247,7 +246,7 @@
                $row['rev_page'] = 99;
 
                yield 'page ID mismatch' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -257,7 +256,7 @@
                $row = $protoRow;
 
                yield 'bad wiki' => [
-                       $title,
+                       $page,
                        $user,
                        $comment,
                        (object)$row,
@@ -269,7 +268,7 @@
        /**
         * @dataProvider provideConstructorFailure
         *
-        * @param Title $title
+        * @param PageIdentity $page
         * @param UserIdentity $user
         * @param CommentStoreComment $comment
         * @param object $row
@@ -277,7 +276,7 @@
         * @param bool $wikiId
         */
        public function testConstructorFailure(
-               Title $title,
+               PageIdentity $page,
                UserIdentity $user,
                CommentStoreComment $comment,
                $row,
@@ -285,7 +284,7 @@
                $wikiId = false
        ) {
                $this->setExpectedException( InvalidArgumentException::class );
-               new RevisionStoreRecord( $title, $user, $comment, $row, $slots, 
$wikiId );
+               new RevisionStoreRecord( $page, $user, $comment, $row, $slots, 
$wikiId );
        }
 
        private function provideAudienceCheckData( $field ) {
@@ -629,7 +628,22 @@
                        null,
                        true,
                ];
-               // Check permissions using the title
+               // Check permissions using the PageIdentity
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [ 'sysop' ],
+                       PageIdentityValue::newFromDBKey( 17, NS_MAIN, 
__METHOD__ ),
+                       true,
+               ];
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [],
+                       PageIdentityValue::newFromDBKey( 17, NS_MAIN, 
__METHOD__ ),
+                       false,
+               ];
+               // Check permissions using the Title
                yield [
                        RevisionRecord::DELETED_TEXT,
                        RevisionRecord::DELETED_TEXT,
diff --git a/tests/phpunit/includes/TitleTest.php 
b/tests/phpunit/includes/TitleTest.php
index b0febe8..113c8b7 100644
--- a/tests/phpunit/includes/TitleTest.php
+++ b/tests/phpunit/includes/TitleTest.php
@@ -1,4 +1,7 @@
 <?php
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Storage\PageIdentity;
+use MediaWiki\Storage\PageIdentityValue;
 
 /**
  * @group Database
@@ -548,7 +551,7 @@
                return [
                        [ new TitleValue( NS_MAIN, 'Foo' ) ],
                        [ new TitleValue( NS_MAIN, 'Foo', 'bar' ) ],
-                       [ new TitleValue( NS_USER, 'Hansi_Maier' ) ],
+                       [ new TitleValue( NS_USER, 'Hansi_Maier', '', 'xyz' ) ],
                ];
        }
 
@@ -566,17 +569,17 @@
 
        public static function provideGetTitleValue() {
                return [
-                       [ 'Foo' ],
-                       [ 'Foo#bar' ],
-                       [ 'User:Hansi_Maier' ],
+                       [ Title::newFromText( 'Foo' ) ],
+                       [ Title::newFromText( 'Foo#bar' ) ],
+                       [ Title::newFromText( 'User:Hansi_Maier' ) ],
+                       [ Title::makeTitle( NS_MAIN, 'Foo', '', 'xy' ) ],
                ];
        }
 
        /**
         * @dataProvider provideGetTitleValue
         */
-       public function testGetTitleValue( $text ) {
-               $title = Title::newFromText( $text );
+       public function testGetTitleValue( Title $title ) {
                $value = $title->getTitleValue();
 
                $dbkey = str_replace( ' ', '_', $value->getText() );
@@ -585,6 +588,75 @@
                $this->assertEquals( $title->getFragment(), 
$value->getFragment() );
        }
 
+       public static function provideNewFromLinkTarget() {
+               return [
+                       [ new TitleValue( NS_MAIN, 'Foo' ) ],
+                       [ new TitleValue( NS_MAIN, 'Foo', 'bar' ) ],
+                       [ new TitleValue( NS_USER, 'Hansi_Maier', '', 'xyz' ) ],
+                       [ Title::makeTitle( NS_USER, 'Tester' ) ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromLinkTarget
+        */
+       public function testNewFromLinkTarget( LinkTarget $link ) {
+               $title = Title::newFromLinkTarget( $link );
+
+               $dbkey = str_replace( ' ', '_', $link->getText() );
+               $this->assertSame( $dbkey, $title->getDBkey() );
+               $this->assertSame( $link->getNamespace(), 
$title->getNamespace() );
+               $this->assertSame( $link->getFragment(), $title->getFragment() 
);
+               $this->assertSame( $link->getInterwiki(), 
$title->getInterwiki() );
+
+               $this->assertSame( $link instanceof Title, $link === $title, 
'Title stays Title' );
+       }
+
+       public static function provideNewFromPageIdentity() {
+               return [
+                       [ PageIdentityValue::newFromDBKey( 0, NS_MAIN, 'Foo' ) 
],
+                       [ PageIdentityValue::newFromDBKey( 8, NS_USER, 
'Hansi_Maier' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromPageIdentity
+        */
+       public function testNewFromPageIdentity( PageIdentity $page ) {
+               $title = Title::newFromPageIdentity( $page );
+
+               $this->assertSame( $page->getNamespace(), 
$title->getNamespace() );
+               $this->assertSame( $page->getTitleDBkey(), $title->getDBkey() );
+               $this->assertSame( $page->getTitleText(), $title->getText() );
+               $this->assertSame( $page->exists(), $title->exists() );
+               $this->assertSame( $page->getId(), $title->getArticleID() );
+       }
+
+       public static function provideGetPageIdentity() {
+               $foo = Title::makeTitle( NS_MAIN, 'Foo' );
+               $bar = Title::makeTitle( NS_MAIN, 'Bar' );
+               $bar->resetArticleID( 8 );
+
+               return [
+                       [ $foo ],
+                       [ $bar ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetPageIdentity
+        */
+       public function testGetPageIdentity( Title $title ) {
+               $page = $title->getPageIdentity();
+
+               $this->assertSame( $title, $page->getAsLinkTarget() );
+               $this->assertSame( $title->getNamespace(), 
$page->getNamespace() );
+               $this->assertSame( $title->getText(), $page->getTitleText() );
+               $this->assertSame( $title->getDBkey(), $page->getTitleDBkey() );
+               $this->assertSame( $title->getArticleID(), $page->getId() );
+               $this->assertSame( $title->exists(), $page->exists() );
+       }
+
        public static function provideGetFragment() {
                return [
                        [ 'Foo', '' ],

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Id36fb61986d5804de5cc1a509f38b2fa3daf73e3
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <daniel.kinz...@wikimedia.de>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to