User:Daniel Kinzler (WMDE)/MCR-SlotRoleHandler

Sketch of the interface for SlotRoleHandler and SlotRoleRegistry, as well as SlotParserOutputProvider, RenderedRevision, and RevisionRenderer:

SlotRoleHandler
Considerations:

SlotRoleHandler is a base class. It can be extended to override some behaviors. Making this a concrete class allows us to add more methods later, without making a breaking change. Since most slots do not need custom behavior, this seems a good approach.

The below design assumes that a given SlotRoleHandler instance may support multiple content models. The default SlotRoleHandler implementation should indeed work for any model.

Interface:


 * getRole returns the role name this SlotRoleHandler is associated with.
 * getRawHtml( SlotRecord $record, SlotParserOutputProvider $provider, ParserOutput $target = null ).
 * Returns HTML for the content of record, wrapped in a div, perhaps together with some kind of heading. Calling code is expected to combine this HTML with the HTML from other slots and then pass it to target->setText.
 * $provider is a SlotParserOutputProvider used to acquire a ParserOutput object for the slot's content, if this is needed to generate the HTML. This is typically but not necessarily the case. Rationale: if a ParserOutput object was already previously created by the caller (e.g. for use by hook handlers or to determine DataUpdates), it should be re-used. Similarly, if a ParserOutput object is later needed by the caller, the one used for getRawHtml should be re-used. This follows the in-place caching approach of the current architecture. The alternative would be to introduce an in-process parser service that returns a ParserOutput for a ParserOutput and a RevisionRecord, and use that.
 * If $target is passed to getRawHtml, all the necessary head-items and directives will be registered in it, but no link tracking data is copied. Calling code is expected to do that. This enforces a separation of meta-data needed for display from derived data to be stored in the database.
 * getSlotHeadingText: Message. Returns a message to be used as the text of the section header for the slot, e.g. when showing diffs for multiple slots.
 * getOutputPlacementHints returns an associative array of hints that tell calling code where on the page the HTML returned by getRawHtml should be placed. Calling code is free to ignore these hints.
 * getDefaultContentModel( LinkTarget $title ) returns the default model to use for this slot on the given page.
 * MAYBE isSupportedContentModel( $model ). Returns whether the given content model is supported by this handler.
 * __construct( $role, MessageLocalizer $localizer )
 * $role the name of the role handled by this handler.
 * $localizer used to determine the heading text. They message key is constructed using the role name as a suffix.

We will probably add more methods later, e.g. for determining whether content "counts" as an article, whether it constitutes a redirect, etc.

MainSlotRoleHandler
The main slot is special in some ways, so we'll want a dedicated subclass.

Implementation:


 * getRole returns "main"
 * getRawHtml returns wrapped HTML, without an added header.
 * getSlotHeadingText: Returns a message saying "main content" or "main slot", for use e.g. on diff pages showing diffs of multiple slots, etc.
 * getOutputPlacementHints returns the equivalent of "front and center".
 * getDefaultContentModel( LinkTarget $title ) returns ContentHandler::getDefaultModelFor( $title ). This is the main reason for this class to exist.
 * MAYBE isSupportedContentModel( $model ) returns true

SlotRoleRegistry
Note: this differs from the original design in that we assume that the a SlotRoleHandler is determined based on the role and the title. Typically, only the role is relevant, but in some cases, especially for the main slot, the handler may be overwritten, perhaps based on the namespace (or page type). The original idea was to assume that a SlotRoleHandler is determined based on the role and content model, which meant that for an existing page, we needed getRoleHandler( SlotRecord ), while for creating a new page, we needed getDefaultRoleHandler( $role, LinkTarget $title ). That however binds a given SlotRoleHandler to a single content model, which seems unnecessary.

Interface:


 * defineRole( $role, $defaultModel, callable $instantiator ):
 * $role the name of the role to define.
 * $defaultModel the default content model to use for this role. May be overwritten, especially for the main slot.
 * $instantiator a callback returning a SlotRoleHandler
 * we may want to split this into defineCustomRole( $role, $defaultModel, callable $instantiator ) and defineRole( $role, $defaultModel, $outputPlacementHints ). This makes it easier for extensions that want to define a new slot but do not need to override any behavior, which would be the usual case.
 * getRoleHandler( $role, $title ) Provides a handler for the given role. Note that the same role may have different handlers depending on the page title - particularly for the main slot (maybe only for the main slot).
 * returns a SlotRoleHandler.
 * $role the role to provide a SlotHandler for.
 * $title could be a PageIdentity (to be introduced) instead of a LinkTarget. Rationale: allows the SlotRoleHandler for the main slot to be overwritten for some pages (perhaps based on namespace).
 * getAllowedRoles( $title ): returns a list of roles supported for the given title. This informs the UI layer which roles should be offered when creating or editing the page. This does not guarantee that no other slots may exist on the page. If additional slots exist, they must at least be "looped through" all edits, and must be accessible when addressed explicitly.
 * getRequiredRoles( $title ): returns a list of roles required for the given title. This informs the UI which roles to require during user interaction. This does not guarantee that no revisions exist on the page that do not have all of these slots. All operations must still be possible if any of the slots missing (except the main slot).
 * getDefinedRoles returns the list of roles defined by calling defineRole.
 * getKnownRoles returns the list of known roles, including the ones returned by getDefinedRoles, and roles that exist in the database. This means the SlotRoleRegistery needs access to the appropriate NameTableStore instance.

SlotParserOutputProvider
The SlotParserOutputProvider provides access to rendered content of a slot. Implementations of SlotParserOutputProvider typically provide caching and lazy initialization.

The main purpose for this interface to exist is to prevent infinite regress caused by SlotRoleHandler::getRawHtml causing RenderedRevision::getRevisionParserOutput to be called, which would call RevisionRenderer::makeRevisionOutput, which calls SlotRoleHandler::getRawHtml, and so on.

Interface:


 * getSlotParserOutput( $role, $generateHtml = true ) returns a ParserOutput for the given slot.

We may want to introduce an interface to use instead of ParserOutput, to avoid binding to all the thins ParserOutput depends on.

RenderedRevision
A RenderedRevision provides (cached, lazy) access to a ParserOutput object that represents the revision's content rendered for some audience. Implements SlotParserOutputProvider.

Interface:


 * getRevisionParserOutput returns a ParserOutput that combines the output of the slots of the revision, as generated by RevisionRenderer. The ParserOutput is generated when this emthod is first called, and then cached internally.
 * getSlotParserOutput( $role, $generateHtml = true ) returns a ParserOutput for the given slot. This ParserOutput is constructed by calling Revision::getRevisionParserOutput, and then cached for later use.
 * getParserOptions: returns the ParserOptions supplied to the constructor.
 * getRevision: returns the RevisionRecord supplied to the constructor.
 * POSSIBLY: setRevision( RevisionRecord $rec ): updates the RevisionRecord associated with this RenderedRevision; only allowed if the new RevisionRecord has the same content (resp hash) as the old one. May keep or purge ParserOutput cached inside the RenderedRevision, depending on whether the revision ID changed (that is, become known after saving) and whether the ParserOutput depended on the revision ID or other properties of the revision.
 * __construct( RevisionRecord $revision, ParserOptions $options, callable $outputCombiner, User $forUser = null )
 * $revision is the revision this RenderedRevision represents the rendering of.
 * $options the parser options to use when constructing ParserOutput.
 * $outputCombiner the callback for constructing the combined ParserOutput.
 * $forUser the target audience, for access restrictions. Rationale: Needed because this calls RevisionRecord::getContent internally, which performs an audience check. Supplying a user causes the FOR_USER mode to be used instead of FOR_PUBLIC, FOR_USER is needed e.g. when a sysop looks at a deleted page. RAW mode is not supported, since a a rendering always has an audience, and RAW is intended for situations where there is none (e.g. internal copying of data) [VERIFY].

RevisionRenderer
Stateless service that acts as a factory for RenderedRevision instances.

Interface:


 * getRenderedRevision( RevisionRecord $rev, ParserOptions $options = null, User $forUser = null ): RenderedRevision. See RenderedRevision::__construct.
 * $rev is the revision to render
 * $options the options to use. If not given and $forUser is not given, canonical options will be used. If $forUser is given, user-specific options will be used.
 * $forUser a user, for access permissions and custom parser options. Optional.

We may want to introduce an additional method, getRevisionOutput or renderRevison, to use instead of getRenderedRevision->getRevisionParserOutput.