Manual:Revision.php/Migration

For extensions that make widespread use of Revision objects, switching to RevisionRecord may be challenging. Here are some suggestions:

Constructors edit

To replace uses of the static constructors (Revision::newFrom*) while still having Revision objects, as a stop-gap measure use "new Revision( $revisionRecord )" with the RevisionRecord constructed using the RevisionLookup service. The RevisionLookup methods either return a RevisionRecord or null, and the corresponding Revision methods returned either a Revision or null:

Instead of

 $revision = Revision::newFromId( $id, $flags );

use

 $revisionRecord = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionById( $ids, $flags );
 $revision = $revisionRecord ? new Revision( $revisionRecord ) : null;

For the other static constructors, simply replace `getRevisionById` with the corresponding RevisionLookup method (note that the loadFrom* methods accept different parameters than the newFrom*):

Revision::newFromTitle -> RevisionLookup::getRevisionByTitle
Revision::newFromPageId -> RevisionLookup::getRevisionByPageId
Revision::loadFromPageId -> RevisionLookup::loadRevisionFromPageId
Revision::loadFromTitle -> RevisionLookup::loadRevisionFromTitle
Revision::loadFromTimestamp -> RevisionLookup::loadRevisionFromTimestamp

The RevisionFactory service is used to replace Revision::newFromArchiveRow and Revision::newFromRow; use RevisionFactory::newRevisionFromArchiveRow, and ::newRevisionFromRow (if you were calling ::newFromRow with an array, the replacement would be RevisionFactory::newMutableRevisionFromArray, but that method is also hard deprecated - you should construct a MutableRevisionRecord instead).

For direct uses of `new Revision`, either existing ones or those added now, see the notes below regarding the final removal of constructing Revision objects.

Fetching relative revisions edit

Revision::getNext was replaced by RevisionLookup::getNextRevision, which returns a RevisionRecord object. Likewise, Revision::getPrevious was replaced by RevisionLookup::getPreviousRevision.

Revision text edit

Revision::getRevisionText returns a string of the text content for a specific revision (based on a row from the database). To replace it, get the RevisionRecord object corresponding to the row, and then use RevisionRecord::getContent( SlotRecord::MAIN ) to get the relevant content object, and if there is such an object, use Content::serialize to get the text.

Revision::compressRevisionText can be replaced with SqlBlobStore::compressData. Revision::decompressRevisionText can be replaced with SqlBlobStore::decompressData.

Other static methods edit

The following static methods are replaced by non-static methods:

Revision::getQueryInfo -> RevisionStore::getQueryInfo
Revision::getArchiveQueryInfo -> RevisionStore::getArchiveQueryInfo
Revision::getParentLengths -> RevisionStore::getRevisionSizes
Revision::getTimestampFromId -> RevisionStore::getTimestampFromId 
The Revision method's first parameter was a Title object that was ignored, the RevisionStore method does not accept a Title, and only needs the relevant id and any query flags.
Revision::countByPageId -> RevisionStore::countRevisionsByPageId
Revision::countByTitle -> RevisionStore::countRevisionsByTitle
Revision::userWasLastToEdit -> RevisionStore::userWasLastToEdit
The Revision method's first parameter was an int or an IDatabase object, the RevisionStore method requires an IDatabase object. ADDITIONALLY, the RevisionStore method is itself soft deprecated.

userCan edit

Revision::userCanBitfield can be replaced with RevisionRecord::userCanBitfield, and Revision::userCan can be replaced with RevisionRecord::userCanBitfield with the bitfield being the int returned from RevisionRecord::getVisibility.

Both of the Revision methods fell back to $wgUser if no User object was passed; RevisionRecord::userCanBitfield requires that a User be provided.

Corresponding Revision and RevisionRecord methods edit

The following Revision methods can be replaced with identical RevisionRecord methods (though some have different names). Once a Revision object is available in a relevant class or function, I suggest immediately retrieving the corresponding RevisionRecord via Revision::getRevisionRecord and slowly making use of the RevisionRecord instead of the Revision:

Revision::getId -> RevisionRecord::getId
Revision::getParentId -> RevisionRecord::getParentId
Revision::getPage -> RevisionRecord::getPageId
Revision::isMinor -> RevisionRecord::isMinor
Revision::isDeleted -> RevisionRecord::isDeleted
Revision::getVisibility -> RevisionRecord::getVisibility
Revision::getTimestamp -> RevisionRecord::getTimestamp
Revision::isCurrent -> RevisionRecord::isCurrent

The following Revision methods can be replaced with identical RevisionRecord methods, BUT, while the Revision methods returned null if the value was unknown, the RevisionRecord methods throw RevisionAccessException exceptions:

Revision::getSize -> RevisionRecord::getSize
Revision::getSha1 -> RevisionRecord::getSha1

Revision::getTitle returned the relevant Title object for the revision. Its replacement, RevisionRecord::getPageAsLinkTarget, returns a LinkTarget instead of a Title. If a full Title object is needed, use Title::newFromLinkTarget.

Audience-based information edit

The Revision class had multiple methods that accepted a specific audience for which a value (the editing user's id and username, the edit summary used, or the revision content) was based on whether the audience could view the information (since it may have been revision deleted).

The constants used for specifying an audience are identical in the Revision and RevisionRecord classes:

Revision::FOR_PUBLIC -> RevisionRecord::FOR_PUBLIC
Revision::FOR_THIS_USER -> RevisionRecord::FOR_THIS_USER
Revision::RAW -> RevisionRecord::RAW

Replacements:

  • Revision::getUser returned the editing user's id, or 0, and Revision::getUserText returned the editing user's username, or an empty string. These were replaced with RevisionRecord::getUser, which returns a UserIdentity object if the audience specified can view the information, or null otherwise. To get the user's id, use UserIdentity::getId, and for the username, use UserIdentity::getName.
  • Revision::getComment returned the revision's edit summary (as a string), or null. It was replaced with RevisionRecord::getComment, HOWEVER instead of a string, RevisionRecord::getComment returns a CommentStoreComment.
  • Revision::getContent returned the content of the revision's main slot, or null. It was replaced with RevisionRecord::getContent, HOWEVER the RevisionRecord method requires that the slot for which the content should be retrieved be specified. Use SlotRecord::MAIN to match the behaviour of the Revision method. FURTHERMORE, the RevisionRecord method can throw a RevisionAccessException, which the Revision method silently converted to null.
When the audience specified for ::getUser, ::getUserText, ::getComment, or ::getContent was FOR_THIS_USER, and no second parameter was passed with the user, the Revision methods fell back to using $wgUser. The RevisionRecord methods have no such fallback, and will throw an InvalidArgumentException if attempting to use FOR_THIS_USER without passing a User.

Content handling edit

As part of the migration to multi-content revisions, there is no longer a single content model or format for a revision, but rather there are content models and formats for each slot.

  • Revision::getContentModel returned a string for the content model of the revision's main slot (SlotRecord::MAIN), either the model set or the default model. To replace it, get the RevisionRecord's main slot, and use SlotRecord::getModel. If the revision has no main slot, fall back to the default model for the title. Use the SlotRoleRegistry service to get the SlotRoleHandler for the relevant role (in this case SlotRecord::MAIN), and then use SlotRoleHandler::getDefaultModel.
  • Revision::getContentFormat returned a string for the content format of the revision's main slot, or falls back to the default format for the content model. To replace it, get the RevisionRecord's main slot, and use SlotRecord::getFormat. If the revision has no main slot, or if SlotRecord::getFormat returns null, fall back to the default format for the content model using ContentHandler::getDefaultFormat with the relevant ContentHandler.
  • Revision::getContentHandler returned the relevant ContentHandler object. Use ContentHandlerFactory::getContentHandler with the relevant content model as a replacement.

Setting revision information edit

  • Revision::setId was only supported if the Revision object corresponded to a MutableRevisionRecord. It can be replaced with MutableRevisionRecord::setId.
  • Revision::setUserIdAndName was only supported if the Revision object corresponded to a MutableRevisionRecord. It can be replaced with MutableRevisionRecord::setUser.
The MutableRevisionRecord method requires a UserIdentity, rather than a user id and name.
  • Revision::setTitle was not supported, and threw an exception if it was called with a different title than the one already set for the revision.

Misc edit

Revision::getTextId -> use SlotRecord::getContentAddress for retrieving an actual content address, or RevisionRecord::hasSameContent to compare content
Revision::isUnpatrolled -> RevisionStore::getRcIdIfUnpatrolled
Revision::getRecentChange -> RevisionStore::getRecentChange
Revision::getSerializedData -> use SlotRecord::getContent for retrieving a content object, and Content::serialize for the serialized form
Revision::insertOn -> RevisionStore::insertRevisionOn
Revision::base36Sha1 -> SlotRecord::base36Sha1
Revision::newNullRevision -> RevisionStore::newNullRevision
Revision::newKnownCurrent -> RevisionLookup::getKnownCurrentRevision

Constructing edit

For uses of `new Revision`, there are multiple relevant replacements:

  • If the Revision was constructed with a RevisionRecord, just use that RevisionRecord directly
  • If the Revision was constructed with an array, use a MutableRevisionRecord - construct it manually and set the various fields.
If neither the `user` nor `user_text` fields were set, the Revision class fell back to using $wgUser; MutableRevisionRecord includes no such fallback, and if no user is provided the object simply won't have a user set.
  • If the Revision was constructed with an object, use RevisionFactory::newRevisionFromRow

Hooks edit

A number of hooks that included Revision objects as parameters were hard deprecated in 1.35 and then removed in 1.37. The following is a basic guide to replacing the use of such hooks, with a focus on handling RevisionRecord objects instead of Revision objects. Note that the replacement hooks can include other changes as well as the conversion from Revision to RevisionRecord, and the code examples below simply convert back everything else where possible (eg casting a UserIdentity back to a User object) - this will add technical debt and should be properly addressed by actually updating the code in the hook handler to use the new objects. For the sake of simplicity, the code below assumes that all relevant use statements are already included. The hook replacement snippets do not include actually updating using the Revision objects to use RevisionRecord, see the notes above for how to do that.

The hooks UndeleteShowRevision and UserRetrieveNewTalks were removed without replacement.

Hook replacements
Old code New code
/**
 * @param Title $title
 * @param Revision $revision
 * @param int|null $oldPageID
 * @return bool|void
 */
public function onArticleRevisionUndeleted(
	$title,
	$revision,
	$oldPageID
) { ... }
/**
 * @param RevisionRecord $revisionRecord
 * @param ?int $oldPageID
 * @return bool|void
 */
public function onRevisionUndeleted(
	$revisionRecord,
	$oldPageID
) {
	$title = Title::newFromLinkTarget( $revisionRecord->getPageAsLinkTarget() );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param WikiPage $wikiPage
 * @param User $user
 * @param Revision $revision Revision the page was reverted back to
 * @param Revision $current Reverted revision
 * @return bool|void
 */
public function onArticleRollbackComplete(
	$wikiPage,
	$user,
	$revision,
	$current
) { ... }
/**
 * @param WikiPage $wikiPage
 * @param UserIdentity $userIdentity
 * @param RevisionRecord $revisionRecord RevisionRecord for the revision 
 *   the page was reverted back to
 * @param RevisionRecord $currentRevRecord RevisionRecord for the reverted revision
 * @return bool|void
 */
public function onRollbackComplete(
	$wikiPage,
	$userIdentity,
	$revisionRecord,
	$currentRevRecord
) {
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param Revision $newRev
 * @param string[] &$links
 * @param Revision|null $oldRev
 * @param User $user
 * @return bool|void
 */
public function onDiffRevisionTools(
	$newRev,
	&$links,
	$oldRev,
	$user
) { ... }
/**
 * @param RevisionRecord $newRevRecord
 * @param string[] &$links
 * @param RevisionRecord|null $oldRevRecord
 * @param UserIdentity $userIdentity
 * @return bool|void
 */
public function onDiffTools(
	$newRevRecord,
	&$links,
	$oldRevRecord,
	$userIdentity
) {
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param DifferenceEngine $diff
 * @param Revision|null $oldRev Old revision (may be null/invalid)
 * @param Revision $newRev New revision
 * @return bool|void
 */
public function onDiffViewHeader(
	$diff,
	$oldRev,
	$newRev
) { ... }
/**
 * @param DifferenceEngine $differenceEngine
 * @return bool|void
 */
public function onDifferenceEngineViewHeader( $differenceEngine ) {
	$oldRevRecord = $differenceEngine->getOldRevision();
	$newRevRecord = $differenceEngine->getNewRevision();
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param Revision $rev
 * @param string[] &$links
 * @param Revision|null $prevRev
 * @param User $user
 * @return bool|void
 */
public function onHistoryRevisionTools(
	$rev,
	&$links,
	$prevRev,
	$user
) { ... }
/**
 * @param RevisionRecord $revRecord
 * @param string[] &$links
 * @param RevisionRecord|null $prevRevRecord
 * @param UserIdentity $userIdentity
 * @return bool|void
 */
public function onHistoryTools(
	$revRecord,
	&$links,
	$prevRevRecord,
	$userIdentity
) {
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param WikiPage $wikiPage
 * @param Revision $revision
 * @param int|bool $originalRevId
 * @param User $user
 * @param string[] &$tags
 * @return bool|void
 */
public function onNewRevisionFromEditComplete(
	$wikiPage,
	$revision,
	$originalRevId,
	$user,
	&$tags
) { ... }
/**
 * @param WikiPage $wikiPage
 * @param RevisionRecord $revisionRecord
 * @param int|bool $originalRevId
 * @param UserIdentity $userIdentity
 * @param string[] &$tags
 * @return bool|void
 */
public function onRevisionFromEditComplete(
	$wikiPage,
	$revisionRecord,
	$originalRevId,
	$userIdentity,
	&$tags
) {
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * Hook called for new pages created
 * @param WikiPage $wikiPage
 * @param User $user
 * @param Content $content
 * @param string $summary
 * @param bool $isMinor
 * @param null $isWatch (No longer used)
 * @param null $section (No longer used)
 * @param int $flags Flags passed to WikiPage::doEditContent()
 * @param Revision $revision New Revision of the article
 * @return bool|void
 */
public function onPageContentInsertComplete(
	$wikiPage,
	$user,
	$content,
	$summary,
	$isMinor,
	$isWatch,
	$section,
	$flags,
	$revision
) { ... }
/**
 * @param WikiPage $wikiPage
 * @param UserIdentity $userIdentity
 * @param string $summary
 * @param int $flags Flags passed to WikiPage::doEditContent()
 * @param RevisionRecord $revisionRecord New RevisionRecord of the article
 * @param EditResult $editResult
 * @return bool|void
 */
public function onPageSaveComplete(
	$wikiPage,
	$userIdentity,
	$summary,
	$flags,
	$revisionRecord,
	$editResult
) {
	// The old hook was only called when new pages were created,
	// but the replacement hook is also called for edits to existing pages
	if ( !( $flags & EDIT_NEW ) ) {
		// Not a new page
		return;
	}
	$user = User::newFromIdentity( $userIdentity );
	$content = $revisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
	$isMinor = $flags & EDIT_MINOR;
	// No support for recreating $isWatch and $section, those were unused
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * Hook called for new pages created and existing pages updated
 * @param WikiPage $wikiPage
 * @param User $user
 * @param Content $content
 * @param string $summary
 * @param bool $isMinor
 * @param null $isWatch (No longer used)
 * @param null $section (No longer used)
 * @param int $flags Flags passed to WikiPage::doEditContent()
 * @param Revision $revision New Revision of the article
 * @param Status $status Status object about to be returned by doEditContent().
 * @param int|bool $originalRevId
 * @param int $undidRevId Rev ID (or 0) this edit undid
 * @return bool|void
 */
public function onPageContentSaveComplete(
	$wikiPage,
	$user,
	$content,
	$summary,
	$isMinor,
	$isWatch,
	$section,
	$flags,
	$revision,
	$status,
	$originalRevId,
	$undidRevId
) { ... }
/**
 * @param WikiPage $wikiPage
 * @param UserIdentity $userIdentity
 * @param string $summary
 * @param int $flags Flags passed to WikiPage::doEditContent()
 * @param RevisionRecord $revisionRecord New RevisionRecord of the article
 * @param EditResult $editResult
 * @return bool|void
 */
public function onPageSaveComplete(
	$wikiPage,
	$userIdentity,
	$summary,
	$flags,
	$revisionRecord,
	$editResult
) {
	$user = User::newFromIdentity( $userIdentity );
	$content = $revisionRecord->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
	$isMinor = $flags & EDIT_MINOR;
	// No support for recreating $isWatch and $section, those were unused
	// FIXME No idea how to recreate $status or if its needed
	$originalRevId = $editResult->getOriginalRevisionId();
	$undidRevId = $editResult->getUndidRevId();
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param Parser|bool $parser Parser object or false
 * @param Title $title Title object of the template to be fetched
 * @param Revision $rev Revision object of the template
 * @param string|bool|null &$text Transclusion text of the template or false or null
 * @param array &$deps Array of template dependencies with 'title', 
 *   'page_id', and 'rev_id' keys
 * @return bool|void
 */
public function onParserFetchTemplate(
	$parser,
	$title,
	$rev,
	&$text,
	&$deps
) { ... }
/**
 * @param ?LinkTarget $contextTitle The top-level page title, if any
 * @param LinkTarget $title The template link (from the literal wikitext)
 * @param bool &$skip Skip this template and link it?
 * @param ?RevisionRecord &$revRecord The desired revision record
 * @return bool|void True or no return value to continue or false to abort
 */
public function onBeforeParserFetchTemplateRevisionRecord(
	?LinkTarget $contextTitle,
	LinkTarget $title,
	bool &$skip,
	?RevisionRecord &$revRecord
) {
	// FIXME I have no idea how to handle the differences between these hooks :(
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * @param Revision $revision
 * @param null $data DEPRECATED! Always null!
 * @param null $flags DEPRECATED! Always null!
 * @return bool|void
 */
public function onRevisionInsertComplete(
	$revision,
	$data,
	$flags
) { ... }
/**
 * @param RevisionRecord $revisionRecord
 * @return bool|void
 */
public function onRevisionRecordInserted( $revisionRecord ) {
	// No support for recreating $data and $flags, those were unused
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * Page move post-commit
 * @param Title $oldTitle
 * @param Title $newTitle
 * @param User $user
 * @param int $pageid
 * @param int $redirid
 * @param string $reason
 * @param Revision $revision
 * @return bool|void
*/
public function onTitleMoveComplete(
	$oldTitle,
	$newTitle,
	$user,
	$pageid,
	$redirid,
	$reason,
	$revision
) { ... }
/**
 * Page move post-commit
 * @param LinkTarget $oldLinkTarget
 * @param LinkTarget $newLlinkTarget
 * @param UserIdentity $userIdentity
 * @param int $pageid
 * @param int $redirid
 * @param string $reason
 * @param RevisionRecord $revisionRecord
 * @return bool|void
 */
public function onPageMoveComplete(
	$oldLinkTarget,
	$newLinkTarget,
	$userIdentity,
	$pageid,
	$redirid,
	$reason,
	$revisionRecord
) {
	$oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
	$newTitle = Title::newFromLinkTarget( $newLinkTarget );
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}
/**
 * Page move pre-commit
 * @param Title $oldTitle
 * @param Title $newTitle
 * @param User $user
 * @param int $pageid
 * @param int $redirid
 * @param string $reason
 * @param Revision $revision
 * @return bool|void
 */
public function onTitleMoveCompleting(
	$oldTitle,
	$newTitle,
	$user,
	$pageid,
	$redirid,
	$reason,
	$revision
) { ... }
/**
 * Page move pre-commit
 * @param LinkTarget $oldLinkTarget
 * @param LinkTarget $newLinkTarget
 * @param UserIdentity $userIdentiy
 * @param int $pageid
 * @param int $redirid
 * @param string $reason
 * @param RevisionRecord $revisionRecord
 * @return bool|void
 */
public function onPageMoveCompleting(
	$oldLinkTarget,
	$newLinkTarget,
	$userIdentity,
	$pageid,
	$redirid,
	$reason,
	$revisionRecord
) {
	$oldTitle = Title::newFromLinkTarget( $oldLinkTarget );
	$newTitle = Title::newFromLinkTarget( $newLinkTarget );
	$user = User::newFromIdentity( $userIdentity );
	// And now update to handling RevisionRecord instead of Revision
	...
}