diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 35ef89d..c486aa8 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -368,7 +368,7 @@ public function __isset($name) { return isset($this->values[$name]); } elseif ($this->getPropertyDefinition($name)) { - return (bool) count($this->get($name)); + return $this->get($name)->valueIsSet(); } } @@ -380,7 +380,7 @@ public function __unset($name) { unset($this->values[$name]); } elseif ($this->getPropertyDefinition($name)) { - $this->get($name)->setValue(array()); + $this->get($name)->unsetValue(); } } diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php index cc5951c..0e7a982 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -51,6 +51,13 @@ class Field extends TypedData implements IteratorAggregate, FieldInterface { protected $list = array(); /** + * Flag to indicate if this field has been set. + * + * @var bool + */ + protected $isset = FALSE; + + /** * Implements TypedDataInterface::getValue(). */ public function getValue() { @@ -68,6 +75,7 @@ public function getValue() { * An array of values of the field items. */ public function setValue($values) { + $this->isset = TRUE; if (isset($values) && $values !== array()) { // Support passing in only the value of the first item. if (!is_array($values) || !is_numeric(current(array_keys($values)))) { @@ -101,6 +109,14 @@ public function setValue($values) { } /** + * Mark this field as not set. + */ + public function unsetValue() { + $this->list = array(); + $this->isset = FALSE; + } + + /** * Returns a string representation of the field. * * @return string @@ -256,13 +272,14 @@ public function get($property_name) { */ public function __set($property_name, $value) { $this->offsetGet(0)->__set($property_name, $value); + $this->isset = TRUE; } /** * Delegate. */ public function __isset($property_name) { - return $this->offsetGet(0)->__isset($property_name); + return $this->isset && $this->offsetGet(0)->__isset($property_name); } /** @@ -285,6 +302,15 @@ public function isEmpty() { } /** + * Determines if this field has been set. + * + * @return bool + */ + public function valueIsSet() { + return $this->isset; + } + + /** * Implements a deep clone. */ public function __clone() { diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php index 2ab909e..d2fee3a 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php @@ -79,11 +79,17 @@ public function denormalize($data, $class, $format = null) { if ($fieldName[0] === '@') { continue; } + // If the incoming value is an empty array we set the property to mark it + // for deletion. + if (empty($incomingFieldValues) && is_array($incomingFieldValues)) { + $entity->{$fieldName} = array(); + } // Figure out the designated class for this field type, which is used by // the Serializer to determine which Denormalizer to use. // @todo Is there a better way to get the field type's associated class? $fieldItemClass = get_class($entity->get($fieldName)->offsetGet(0)); + // Iterate through the language keyed values and add them to the entity. // The vnd.drupal.ld+json mime type will always use language keys, per // http://drupal.org/node/1838700. diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php index 2b246b7..1b6f111 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php @@ -86,6 +86,40 @@ public function post($id, EntityInterface $entity) { } /** + * Responds to entity PATCH requests. + * + * @param mixed $id + * The entity ID. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\rest\ResourceResponse + * The HTTP response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function patch($id, EntityInterface $entity) { + if (empty($id)) { + throw new NotFoundHttpException(); + } + // Load the existing entity and overwrite the received properties. + $original_entity = entity_load($entity->entityType(), $id); + foreach ($entity->getProperties() as $name => $property) { + if (isset($entity->{$name})) { + $original_entity->{$name} = $property; + } + } + try { + $original_entity->save(); + // Update responses have an empty body. + return new ResourceResponse(NULL, 200); + } + catch (EntityStorageException $e) { + throw new HttpException(500, 'Internal Server Error', $e); + } + } + + /** * Responds to entity DELETE requests. * * @param mixed $id diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php index f5ef159..b47f296 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php @@ -48,7 +48,7 @@ public function testCreate() { $entity = entity_create($entity_type, $entity_values); $serialized = $serializer->serialize($entity, 'drupal_jsonld'); // Create the entity over the web API. - $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); $this->assertResponse('201', 'HTTP response code is correct.'); // Get the new entity ID from the location header and try to read it from @@ -63,13 +63,14 @@ public function testCreate() { // entity references is implemented. unset($entity_values['user_id']); foreach ($entity_values as $property => $value) { - $actual_value = $loaded_entity->get($property); - $this->assertEqual($value, $actual_value->value, 'Created property ' . $property . ' expected: ' . $value . ', actual: ' . $actual_value->value); + $actual_value = $loaded_entity->get($property)->value; + $send_value = $entity->get($property)->value; + $this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value); } // Try to create an entity without proper permissions. $this->drupalLogout(); - $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); $this->assertResponse(403); // Try to create a resource which is not web API enabled. diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index 0f8e7c6..fc59130 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -68,6 +68,17 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati ); break; + case 'PATCH': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_POSTFIELDS => $body, + CURLOPT_URL => url($url, array('absolute' => TRUE)), + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => array('Content-Type: ' . $format), + ); + break; + case 'DELETE': $curl_options = array( CURLOPT_HTTPGET => FALSE, @@ -127,7 +138,11 @@ protected function entityCreate($entity_type) { protected function entityValues($entity_type) { switch ($entity_type) { case 'entity_test': - return array('name' => $this->randomName(), 'user_id' => 1); + return array( + 'name' => $this->randomName(), + 'user_id' => 1, + 'field_test_text' => array(0 => array('value' => 'text')) + ); case 'node': return array('title' => $this->randomString()); case 'user': diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php new file mode 100644 index 0000000..2d60c33 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php @@ -0,0 +1,92 @@ + 'Update resource', + 'description' => 'Tests the update of resources.', + 'group' => 'REST', + ); + } + + /** + * Tests several valid and invalid partial update requests on test entities. + */ + public function testPatchUpdate() { + $serializer = drupal_container()->get('serializer'); + // @todo once EntityNG is implemented for other entity types test all other + // entity types here as well. + $entity_type = 'entity_test'; + $entity_info = entity_get_info($entity_type); + + $this->enableService('entity:' . $entity_type); + // Create a user account that has the required permissions to create + // resources via the web API. + $account = $this->drupalCreateUser(array('restful patch entity:' . $entity_type)); + $this->drupalLogin($account); + + // Create an entity and save it to the database. + $entity_values = $this->entityValues($entity_type); + $entity = entity_create($entity_type, $entity_values); + $entity->save(); + + // Create a second stub entity where the UUID is different. + $patch_entity = entity_create($entity_type, array()); + $serialized = $serializer->serialize($patch_entity, 'drupal_jsonld'); + + // Update the entity over the web API. + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(200); + + // Re-load updated entity from the database. + $entity = entity_load($entity_type, $entity->id(), TRUE); + $this->assertEqual($entity->uuid(), $patch_entity->uuid(), 'UUID was successfully updated.'); + + // Try to empty a field. + $normalized = $serializer->normalize($patch_entity, 'drupal_jsonld'); + $normalized['field_test_text'] = array(); + $serialized = $serializer->encode($normalized, 'jsonld'); + + // Update the entity over the web API. + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(200); + + // Re-load updated entity from the database. + $entity = entity_load($entity_type, $entity->id(), TRUE); + $this->assertNull($entity->field_test_text->value, 'Test field has been cleared.'); + + // Try to update an entity without proper permissions. + $this->drupalLogout(); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + + // Try to update a resource which is not web API enabled. + $this->enableService(FALSE); + // Reset cURL here because it is confused from our previously used cURL + // options. + unset($this->curlHandle); + $this->drupalLogin($account); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(404); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php index a152bfa..3dd810c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -128,6 +128,12 @@ public function testReadWrite() { $this->assertFalse(isset($entity->name[0]->value), 'Name is not set.'); $this->assertFalse(isset($entity->name->value), 'Name is not set.'); + $entity->name = array(); + $this->assertTrue(isset($entity->name), 'Name field is set.'); + $this->assertFalse(isset($entity->name[0]), 'Name field item is not set.'); + $this->assertFalse(isset($entity->name[0]->value), 'First name item value is not set.'); + $this->assertFalse(isset($entity->name->value), 'Name value is not set.'); + $entity->name->value = 'a value'; $this->assertTrue(isset($entity->name->value), 'Name is set.'); unset($entity->name);