A new generic revision UI system is now available for entity types to make use of. The system allows developers to make use of all, or a combination of:
- Revision history page
- Revision view page
- Revision revert form
- Revision delete form

Example of revision history page.
Add Revision UI to an entity type
Add the following to support revision UI:
- Route provider
- Link templates
- Form handlers
- Access control handler, and optionally, permissions
Route provider
Add the revision route provider to the entity annotation:
/**
* handlers = {
* ...
* route_provider = {
* ...
* "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class,
* }
* }
*/
Link templates
Add a combination of any of the following link templates to the entity annotation:
The paths can be changed to any pattern, however the entity and entity revision parameters (in {curly braces}) are specially named. Replace MYENTITYTYPE with entity type ID.
/**
* links = {
* ...
* "revision" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/view",
* "revision-delete-form" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/delete",
* "revision-revert-form" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/revert",
* "version-history" = "/myentitytype/{MYENTITYTYPE}/revisions",
* }
*/
Form handlers
Add a combination of the following form handlers to the entity annotation:
/**
* handlers = {
* ...
* "form" = {
* ...
* "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class,
* "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class,
* }
* }
*/
Access control
By default access can be granted by responding to revision operations. However access may be overridden by altering the routes supplied by RevisionHtmlRouteProvider.
Example of responding to revision operations:
Modify or create an entity access control handler, and the following to checkAccess() method:
return match ($operation) {
'view all revisions' => AccessResult::allowedIf(CONDITION),
'view revision' => AccessResult::allowedIf(CONDITION),
'revert' => AccessResult::allowedIf(CONDITION)->andIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
'delete revision' => AccessResult::allowedIf(CONDITION),
default => throw new \LogicException('Unknown operation')
};
Access to revision routes could be based on permissions supplied by a module. Implement permissions, and change above to:
return match ($operation) {
'view all revisions' => AccessResult::allowedIfHasPermission($account, 'my permission'),
'view revision' => AccessResult::allowedIfHasPermission($account, 'my permission'),
'revert' => AccessResult::allowedIfHasPermission($account, 'my permission')->andIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
'delete revision' => AccessResult::allowedIfHasPermission($account, 'my permission'),
default => throw new \LogicException('Unknown operation')
};
Local tasks
Local tasks (tabs) are automatically generated. Existing entity types which implemented their own revision UI, or for example via Entity project, will need to remove their local task implementation to avoid duplicate/redundant local tasks.
Comments
See the 3.x branch of media
See the 3.x branch of media_revisions_ui for an example of this integration.
Albert Skibinski - Homepage
You might observe that
You might observe that EntityAccessControlHandler doesn't respect the EntityInterface of the $entity parameter as below.
------ --------------------------------------------------------------------------------------
Line Access/MyCustomEntityAccessControlHandler.php
------ --------------------------------------------------------------------------------------
33 Call to an undefined method Drupal\Core\Entity\EntityInterface::isDefaultRevision().
33 Call to an undefined method Drupal\Core\Entity\EntityInterface::isLatestRevision().
------ --------------------------------------------------------------------------------------
To fix this just assert our custom entity interface by adding below code in our checkAccess method.
Related #2951487: EntityViewBuilder::getBuildDefaults doesn't respect the EntityInterface of the $entity parameter