diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 35ef89d20e1e33b05b63731bf6290eada3a24c15..c486aa8ddcf25f6f0024531f3c8e9cbd7706acc2 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 cc5951c878c77ca3bb9f9cadc07bbd79114b6c1b..0e7a982e64f4d107265f9081dbcdd009784ada61 100644
--- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php
+++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php
@@ -50,6 +50,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().
*/
@@ -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)))) {
@@ -100,6 +108,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.
*
@@ -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);
}
/**
@@ -284,6 +301,15 @@ public function isEmpty() {
return TRUE;
}
+ /**
+ * Determines if this field has been set.
+ *
+ * @return bool
+ */
+ public function valueIsSet() {
+ return $this->isset;
+ }
+
/**
* Implements a deep clone.
*/
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php
index 2ab909e7ec5c674cdd630eedcfdd1dea9d78e792..d2fee3ad6c89e9bd905dfec0ac829d6aaa3ef895 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 2b246b75d9610e9bddfe8fc97da909f40f932aec..bf8c7c1c02017181a81e8792b4bd8c6b220f2391 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
@@ -85,6 +85,49 @@ 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();
+ }
+ $definition = $this->getDefinition();
+ if ($entity->entityType() != $definition['entity_type']) {
+ throw new BadRequestHttpException('Invalid entity type');
+ }
+ $original_entity = entity_load($definition['entity_type'], $id);
+ // We don't support creating entities with PATCH, so we throw an error if
+ // there is no existing entity.
+ if ($original_entity == FALSE) {
+ throw new NotFoundHttpException();
+ }
+ // Overwrite the received properties.
+ 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, 204);
+ }
+ catch (EntityStorageException $e) {
+ throw new HttpException(500, 'Internal Server Error', $e);
+ }
+ }
+
/**
* Responds to entity DELETE requests.
*
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
index f5ef15908ce9068612c904ca7b1f2e26dcc20135..b47f296edf246baa2513e9dbb5be7eae7cd7da91 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 0f8e7c68c9360790c2773f6ac780b97fb5ae7a74..c1a4d24098654cc1b7347572450f797efbc60f14 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' => $this->randomString())),
+ );
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 0000000000000000000000000000000000000000..eed2b2ff744270dcd8f1c58160e3282595319d46
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\rest\test\UpdateTest.
+ */
+
+namespace Drupal\rest\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+
+/**
+ * Tests resource updates on test entities.
+ */
+class UpdateTest extends RESTTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('rest', 'entity_test');
+
+ public static function getInfo() {
+ return array(
+ 'name' => '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';
+
+ $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 = $this->entityCreate($entity_type);
+ $entity->save();
+
+ // Create a second stub entity for overwriting a field.
+ $patch_values['field_test_text'] = array(0 => array('value' => $this->randomString()));
+ $patch_entity = entity_create($entity_type, $patch_values);
+ // We don't want to overwrite the UUID.
+ unset($patch_entity->uuid);
+ $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(204);
+
+ // Re-load updated entity from the database.
+ $entity = entity_load($entity_type, $entity->id(), TRUE);
+ $this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field 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(204);
+
+ // 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 a non-existing entity with ID 9999.
+ $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+ $this->assertResponse(404);
+ $loaded_entity = entity_load($entity_type, 9999, TRUE);
+ $this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
+
+ // 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 a152bfa43e9cf124dc6ee23e11ea7b36792b3b6e..3dd810c7df3476f6ea2ec8731a117818106f4173 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);