diff --git a/src/Plugin/VersionNegotiation/VersionByRel.php b/src/Plugin/VersionNegotiation/VersionByRel.php index 33b31f3..b1533f0 100644 --- a/src/Plugin/VersionNegotiation/VersionByRel.php +++ b/src/Plugin/VersionNegotiation/VersionByRel.php @@ -4,11 +4,15 @@ namespace Drupal\jsonapi\Plugin\VersionNegotiation; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\RevisionableStorageInterface; +use Drupal\Core\Http\Exception\CacheableHttpException; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\jsonapi\Revisions\InvalidVersionIdentifierException; use Drupal\jsonapi\Revisions\PluginNegotiationBase; +use Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer; use Drupal\jsonapi\Revisions\VersionNegotiationInterface; use Drupal\jsonapi\Revisions\VersionNotFoundException; @@ -31,51 +35,103 @@ class VersionByRel extends PluginNegotiationBase implements ContainerFactoryPlug const NEGOTIATOR_NAME = 'rel'; /** - * Version identifier which loads the latest revision. + * Version argument which loads the latest revision. * * @var string */ const WORKING_COPY = 'working-copy'; /** - * Version identifier which loads the latest default revision. + * Version argument which loads the latest default revision. * * @var string */ const LATEST_VERSION = 'latest-version'; + /** + * Version argument which loads the default revision prior to the current one. + * + * @var string + */ + const PREDECESSOR_VERSION = 'predecessor-version'; + + /** + * Version argument which loads the first prior default revision. + * + * @var string + */ + const WORKING_COPY_OF = 'working-copy-of'; + + /** + * Version argument which loads the revision prior to the current one. + * + * Unlike the other link relation types used here, this one is not defined by + * RFC5829. + * + * @var string + */ + const PRIOR_WORKING_COPY = 'prior-working-copy'; + + /** + * Version argument which loads the revision after the current one. + * + * Unlike the other link relation types used here, this one is not defined by + * RFC5829. + * + * @var string + */ + const SUBSEQUENT_WORKING_COPY = 'subsequent-working-copy'; + /** * {@inheritdoc} */ - protected function getRevisionId(EntityInterface $entity, $input_data) { - $revision_id = NULL; - $handles_versions = $entity instanceof RevisionableInterface - && $entity->getEntityType()->isRevisionable(); - if (!$handles_versions) { - throw new \InvalidArgumentException('Revision requested on a non versionable entity ' . $entity->uuid()); - } - switch ($input_data) { + protected function getRevisionId(EntityInterface $entity, $version_argument) { + assert($entity instanceof RevisionableInterface); + $entity_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + switch ($version_argument) { case static::WORKING_COPY: - $revision_id = $entity->getLoadedRevisionId(); - break; + /* @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */ + $revision_id = $entity_storage->getLatestRevisionId($entity->id()); + if (is_null($revision_id)) { + throw new VersionNotFoundException(); + } + return $revision_id; case static::LATEST_VERSION: - try { - $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - if ($storage instanceof RevisionableStorageInterface) { - $revision_id = $storage->getLatestRevisionId($entity->id()); - } - } - catch (PluginException $e) { - // Cast the exception type to an \InvalidArgumentException. - throw new \InvalidArgumentException($e->getMessage(), $e); - } - break; + // The already loaded revision will be the latest version by default. + return $entity->getLoadedRevisionId(); + + // @todo: Implement these. We can copy the solution in https://www.drupal.org/project/drupal/issues/2986027. + case static::PREDECESSOR_VERSION: + case static::WORKING_COPY_OF: + case static::PRIOR_WORKING_COPY: + case static::SUBSEQUENT_WORKING_COPY: + $this->checkVersionHistorySupport($entity_storage); + $message = sprintf('The link relation type `%s` is not implemented yet.'); + $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:' . ResourceVersionRouteEnhancer::RESOURCE_VERSION_QUERY_PARAMETER]); + // @todo: uncomment the next line and remove the following line after https://www.drupal.org/project/drupal/issues/3002352 lands. + /* throw new CacheableHttpException($cacheability, 501, $message); */ + throw new CacheableHttpException($cacheability, 501, $message, []); + + default: + throw new InvalidVersionIdentifierException(sprintf('The version identifier argument, `%s` is unrecognized.', $version_argument)); } - if (empty($revision_id)) { - throw new \InvalidArgumentException('Invalid revision ID value for entity ' . $entity->uuid()); + } + + /** + * Checks if the storage supports listing all the revisions of an entity. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage + * The storage that may or may not support listing revision IDs. + * + * @throws \Drupal\jsonapi\Revisions\InvalidVersionIdentifierException + * If the version history is not supported for this storage. + */ + protected function checkVersionHistorySupport(EntityStorageInterface $entity_storage) { + if (!method_exists($entity_storage, 'revisionIds')) { + $message = sprintf('The storage class for the %s entity type does not support version history.', $entity_storage->getEntityTypeId()); + throw new InvalidVersionIdentifierException($message); } - return $revision_id; } }