diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php index 0e26c3bb51..c4033b5dbb 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityType.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php @@ -17,6 +17,11 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface /** * {@inheritdoc} */ + protected $supports_validation = TRUE; + + /** + * {@inheritdoc} + */ public function __construct($definition) { parent::__construct($definition); $this->handlers += [ diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index b39fb3fa46..64127a8290 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -278,6 +278,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { protected $additional = []; /** + * Supports validation of the entity type, useful for REST api support. + * + * @var bool + */ + protected $supports_validation = FALSE; + + /** * Constructs a new EntityType. * * @param array $definition diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php index c432c5bf2c..4f84ecfe1f 100644 --- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php @@ -22,7 +22,7 @@ * }, * "access" = "Drupal\config_test\ConfigTestAccessControlHandler" * }, - * rest_resource = TRUE, + * supports_validation = TRUE, * config_prefix = "dynamic", * entity_keys = { * "id" = "id", diff --git a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php index 7e16e6e3ac..9ca05f01f8 100644 --- a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php @@ -15,9 +15,7 @@ class ConfigEntityResource extends EntityResource { * {@inheritdoc} */ protected function validate(EntityInterface $entity) { - // Add a custom config entity resource class in order to provide /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ - $config = \Drupal::configFactory()->getEditable($entity->getConfigDependencyName()); $config->setData($entity->toArray()); @@ -25,15 +23,7 @@ protected function validate(EntityInterface $entity) { $violations = $typed_config->validate(); - if ($violations->count() > 0) { - $message = "Unprocessable Entity: validation failed.\n"; - foreach ($violations as $violation) { - // We strip every HTML from the error message to have a nicer to read - // message on REST responses. - $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n"; - } - throw new UnprocessableEntityHttpException($message); - } + $this->processViolations($violations); } } diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index cc7fe4a1b3..43e560e76e 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -394,7 +394,7 @@ protected function getBaseRoute($canonical_path, $method) { */ public function availableMethods() { $methods = parent::availableMethods(); - if ($this->isConfigEntityResource() && empty($this->entityType->get('rest_resource'))) { + if ($this->entityType->get('supports_validation')) { // Currently only GET is supported for Config Entities. // @todo Remove when supported https://www.drupal.org/node/2300677 $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH']; diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php index 09b4b64bae..25a9281543 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php @@ -33,6 +33,18 @@ protected function validate(EntityInterface $entity) { // changes. $violations->filterByFieldAccess(); + $this->processViolations($violations); + } + + /** + * Processes violations and creates a helpful exception message. + * + * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations + * + * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException + * Throws a HTTP exception when the validation fails. + */ + protected function processViolations($violations) { if ($violations->count() > 0) { $message = "Unprocessable Entity: validation failed.\n"; foreach ($violations as $violation) { diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php index 1ec31a5ea3..60ebb53e04 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php @@ -22,12 +22,12 @@ */ protected $entity; - protected $counter = 0; - /** - * {@inheritdoc} + * Countered used internally to have deterministic ids. + * + * @var int */ - protected $configEntityWithModifyingTest = TRUE; + protected $counter = 0; /** * {@inheritdoc} @@ -38,11 +38,7 @@ protected function setUpAuthorization($method) { $this->grantPermissionsToTestedRole(['view config_test']); break; case 'POST': - $this->grantPermissionsToTestedRole(['administer config_test']); - break; case 'PATCH': - $this->grantPermissionsToTestedRole(['administer config_test']); - break; case 'DELETE': $this->grantPermissionsToTestedRole(['administer config_test']); break; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index ebad9546c1..730e6550d1 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -131,13 +131,6 @@ public static $modules = ['rest_test', 'text']; /** - * Flag to indicate a test which can modify config entities. - * - * @var bool - */ - protected $configEntityWithModifyingTest = FALSE; - - /** * Provides an entity resource. */ protected function provisionEntityResource() { @@ -592,8 +585,8 @@ protected static function castToString(array $normalization) { */ public function testPost() { // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) { - $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support posting."); return; } @@ -605,6 +598,8 @@ public function testPost() { $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format); + // The normalized structure looks different for fieldable and non fieldable + // entities. if ($this->entity instanceof FieldableEntityInterface) { $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); } @@ -696,13 +691,11 @@ public function testPost() { $this->setUpAuthorization('POST'); - if ($this->entity instanceof FieldableEntityInterface) { - // DX: 422 when invalid entity: multiple values sent for single-value field. - $response = $this->request('POST', $url, $request_options); - $label_field = $this->entity->getEntityType() ->hasKey('label') ? $this->entity->getEntityType() ->getKey('label') : static::$labelFieldName; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field) ->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); - } + // DX: 422 when invalid entity: multiple values sent for single-value field. + $response = $this->request('POST', $url, $request_options); + $label_field = $this->entity->getEntityType() ->hasKey('label') ? $this->entity->getEntityType() ->getKey('label') : static::$labelFieldName; + $label_field_capitalized = $this->entity->getFieldDefinition($label_field) ->getLabel(); + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -795,9 +788,8 @@ public function testPost() { * Tests a PATCH request for an entity, plus edge cases to ensure good DX. */ public function testPatch() { - // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) { - $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support patching."); return; } @@ -996,9 +988,8 @@ public function testPatch() { * Tests a DELETE request for an entity, plus edge cases to ensure good DX. */ public function testDelete() { - // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) { - $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support deletion."); return; } @@ -1171,7 +1162,16 @@ protected function getPostUrl() { protected function makeNormalizationInvalid(array $normalization) { // Add a second label to this entity to make it invalid. $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $normalization[$label_field][1]['value'] = 'Second Title'; + + if ($this->entity instanceof FieldableEntityInterface) { + $normalization[$label_field][1]['value'] = 'Second Title'; + } + else { + $normalization[$label_field] = [ + $normalization[$label_field], + 'Second title', + ]; + } return $normalization; }