diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml index 4e89841..6cf1736 100644 --- a/core/modules/hal/hal.services.yml +++ b/core/modules/hal/hal.services.yml @@ -4,6 +4,11 @@ services: arguments: ['@rest.link_manager', '@serializer.entity_resolver'] tags: - { name: normalizer, priority: 10 } + serializer.normalizer.file_item.hal: + class: Drupal\hal\Normalizer\FileItemNormalizer + arguments: ['@rest.link_manager', '@serializer.entity_resolver'] + tags: + - { name: normalizer, priority: 20 } serializer.normalizer.field_item.hal: class: Drupal\hal\Normalizer\FieldItemNormalizer tags: @@ -16,7 +21,7 @@ services: class: Drupal\hal\Normalizer\FileEntityNormalizer tags: - { name: normalizer, priority: 20 } - arguments: ['@entity.manager', '@http_client', '@rest.link_manager', '@module_handler'] + arguments: ['@rest.link_manager', '@entity.manager', '@module_handler'] serializer.normalizer.entity.hal: class: Drupal\hal\Normalizer\ContentEntityNormalizer arguments: ['@rest.link_manager', '@entity.manager', '@module_handler'] diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php index d433656..313d930 100644 --- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php @@ -153,10 +153,14 @@ public function denormalize($data, $class, $format = NULL, array $context = arra // Special handling for PATCH: destroy all possible default values that // might have been set on entity creation. We want an "empty" entity that - // will only get filled with fields from the data array. + // will only get filled with fields from the data array. But keep langcode + // and the bundle key, because they are already passed in and removed from + // data. if (isset($context['request_method']) && $context['request_method'] == 'patch') { foreach ($entity as $field_name => $field) { - $entity->set($field_name, NULL); + if (!in_array($field_name, array('langcode', $entity_type->getKey('bundle')))) { + $entity->set($field_name, NULL); + } } } diff --git a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php index d5ba85a..818679b 100644 --- a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php @@ -8,6 +8,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Field\FieldItemInterface; use Drupal\rest\LinkManager\LinkManagerInterface; use Drupal\serialization\EntityResolver\EntityResolverInterface; use Drupal\serialization\EntityResolver\UuidReferenceInterface; @@ -83,9 +84,7 @@ public function normalize($field_item, $format = NULL, array $context = array()) // The returned structure will be recursively merged into the normalized // entity so that the items are properly added to the _links and _embedded // objects. - $field_name = $field_item->getParent()->getName(); - $entity = $field_item->getEntity(); - $field_uri = $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name); + $field_uri = $this->getFieldRelationUri($field_item); return array( '_links' => array( $field_uri => array($link), @@ -122,4 +121,21 @@ public function getUuid($data) { } } + /** + * Gets the relation URI of the field containing an item. + * + * The relation URI is used as a property key when building the HAL structure. + * + * @param FieldItemInterface $field_item + * The field item that is being normalized. + * + * @return string + * The relation URI of the field. + */ + protected function getFieldRelationUri(FieldItemInterface $field_item) { + $field_name = $field_item->getParent()->getName(); + $entity = $field_item->getEntity(); + return $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name); + } + } diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php index f993024..07a8b80 100644 --- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php @@ -7,10 +7,8 @@ namespace Drupal\hal\Normalizer; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\rest\LinkManager\LinkManagerInterface; -use GuzzleHttp\ClientInterface; +use Drupal\Component\Utility\String; +use Symfony\Component\Serializer\Exception\RuntimeException; /** * Converts the Drupal entity object structure to a HAL array structure. @@ -25,38 +23,14 @@ class FileEntityNormalizer extends ContentEntityNormalizer { protected $supportedInterfaceOrClass = 'Drupal\file\FileInterface'; /** - * The HTTP client. - * - * @var \GuzzleHttp\ClientInterface - */ - protected $httpClient; - - /** - * Constructs a FileEntityNormalizer object. - * - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager. - * @param \GuzzleHttp\ClientInterface $http_client - * The HTTP Client. - * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager - * The hypermedia link manager. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler) { - parent::__construct($link_manager, $entity_manager, $module_handler); - - $this->httpClient = $http_client; - } - - /** * {@inheritdoc} */ public function normalize($entity, $format = NULL, array $context = array()) { $data = parent::normalize($entity, $format, $context); - // Replace the file url with a full url for the file. - $data['uri'][0]['value'] = $this->getEntityUri($entity); - + if (!isset($context['included_fields']) || in_array('data', $context['included_fields'])) { + // Save base64-encoded file contents to the "data" property. + $data['data'][]['value'] = base64_encode(file_get_contents($entity->getFileUri())); + } return $data; } @@ -64,12 +38,25 @@ public function normalize($entity, $format = NULL, array $context = array()) { * {@inheritdoc} */ public function denormalize($data, $class, $format = NULL, array $context = array()) { - $file_data = $this->httpClient->get($data['uri'][0]['value'])->getBody(TRUE); - - $path = 'temporary://' . drupal_basename($data['uri'][0]['value']); - $data['uri'] = file_unmanaged_save_data($file_data, $path); - - return $this->entityManager->getStorage('file')->create($data); + // Avoid 'data' being treated as a field. + $file_data = $data['data'][0]['value']; + unset($data['data']); + + $entity = parent::denormalize($data, $class, $format, $context); + + // Decode and save to file if it's a new file. + if (!isset($context['request_method']) || $context['request_method'] != 'patch') { + $file_contents = base64_decode($file_data); + $dirname = drupal_dirname($entity->getFileUri()); + file_prepare_directory($dirname, FILE_CREATE_DIRECTORY); + if ($uri = file_unmanaged_save_data($file_contents, $entity->getFileUri())) { + $entity->setFileUri($uri); + } + else { + throw new RuntimeException(String::format('Failed to write @filename.', array('@filename' => $entity->getFilename()))); + } + } + return $entity; } } diff --git a/core/modules/hal/src/Normalizer/FileItemNormalizer.php b/core/modules/hal/src/Normalizer/FileItemNormalizer.php new file mode 100644 index 0000000..1043457 --- /dev/null +++ b/core/modules/hal/src/Normalizer/FileItemNormalizer.php @@ -0,0 +1,40 @@ +getFieldRelationUri($field_item); + $data['_embedded'][$field_uri][0]['display'] = $field_item->get('display')->getValue(); + $data['_embedded'][$field_uri][0]['description'] = $field_item->get('description')->getValue(); + return $data; + } +} diff --git a/core/modules/hal/src/Tests/EntityTest.php b/core/modules/hal/src/Tests/EntityTest.php index dfea6be..b3421cf 100644 --- a/core/modules/hal/src/Tests/EntityTest.php +++ b/core/modules/hal/src/Tests/EntityTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\hal\Tests\NormalizeTest. + * Contains \Drupal\hal\Tests\EntityTest. */ namespace Drupal\hal\Tests; @@ -19,7 +19,7 @@ class EntityTest extends NormalizerTestBase { * * @var array */ - public static $modules = array('node', 'taxonomy', 'comment'); + public static $modules = array('node', 'taxonomy', 'comment', 'file'); /** * {@inheritdoc} @@ -33,6 +33,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('comment'); $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('file'); } /** @@ -181,4 +182,36 @@ public function testComment() { } } + /** + * Tests the normalization of files. + */ + public function testFile() { + $user = entity_create('user', array('name' => $this->randomMachineName())); + $user->save(); + + $file_uri = 'public://' . $this->randomMachineName(); + file_put_contents($file_uri, 'hello world'); + $file = entity_create('file', array( + 'uid' => $user->id(), + 'uri' => $file_uri, + 'status' => FILE_STATUS_PERMANENT, + )); + $file->save(); + + $original_values = $file->toArray(); + unset($original_values['fid']); + + $normalized = $this->serializer->normalize($file, $this->format); + // Use PATCH to avoid trying to create new file on denormalize. + $denormalized_file = $this->serializer->denormalize($normalized, 'Drupal\file\Entity\File', $this->format, array('request_method' => 'patch')); + + // Verify that the ID was skipped by the normalizer. + $this->assertEqual(NULL, $denormalized_file->id()); + + // Loop over the remaining fields and verify that they are identical. + foreach ($original_values as $field_name => $field_values) { + $this->assertEqual($field_values, $denormalized_file->get($field_name)->getValue()); + } + } + } diff --git a/core/modules/hal/src/Tests/FileFieldNormalizeTest.php b/core/modules/hal/src/Tests/FileFieldNormalizeTest.php new file mode 100644 index 0000000..37ae5f4 --- /dev/null +++ b/core/modules/hal/src/Tests/FileFieldNormalizeTest.php @@ -0,0 +1,76 @@ +installEntitySchema('file'); + $this->installSchema('file', array('file_usage')); + } + + /** + * Tests that file field is identical before and after de/serialization. + */ + public function testFileFieldNormalize() { + // Create a file. + $file_name = $this->randomMachineName() . '.txt'; + file_put_contents("public://$file_name", $this->randomString()); + $file = entity_create('file', array( + 'uri' => "public://$file_name", + )); + $file->save(); + + // Attach a file field to the bundle. + FieldStorageConfig::create(array( + 'type' => 'file', + 'entity_type' => 'entity_test', + 'name' => 'field_file', + ))->save(); + FieldInstanceConfig::create(array( + 'field_name' => 'field_file', + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ))->save(); + + // Create an entity referencing the file. + $entity = entity_create('entity_test', array( + 'field_file' => array( + 'target_id' => $file->id(), + 'display' => 0, + 'description' => 'An attached file', + ), + )); + + $serialized = $this->container->get('serializer')->serialize($entity, $this->format); + $deserialized = $this->container->get('serializer')->deserialize($serialized, 'Drupal\entity_test\Entity\EntityTest', $this->format); + $this->assertEqual($entity->toArray()['field_file'], $deserialized->toArray()['field_file'], "File field is preserved."); + } +} diff --git a/core/modules/hal/src/Tests/NormalizerTestBase.php b/core/modules/hal/src/Tests/NormalizerTestBase.php index 4ef6037..e3a84b6 100644 --- a/core/modules/hal/src/Tests/NormalizerTestBase.php +++ b/core/modules/hal/src/Tests/NormalizerTestBase.php @@ -14,6 +14,7 @@ use Drupal\hal\Normalizer\EntityReferenceItemNormalizer; use Drupal\hal\Normalizer\FieldItemNormalizer; use Drupal\hal\Normalizer\FieldNormalizer; +use Drupal\hal\Normalizer\FileEntityNormalizer; use Drupal\rest\LinkManager\LinkManager; use Drupal\rest\LinkManager\RelationLinkManager; use Drupal\rest\LinkManager\TypeLinkManager; @@ -123,13 +124,15 @@ protected function setUp() { ))->save(); $entity_manager = \Drupal::entityManager(); + $module_handler = \Drupal::moduleHandler(); $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default')), new RelationLinkManager(new MemoryBackend('default'), $entity_manager)); $chain_resolver = new ChainEntityResolver(array(new UuidResolver($entity_manager), new TargetIdResolver())); // Set up the mock serializer. $normalizers = array( - new ContentEntityNormalizer($link_manager, $entity_manager, \Drupal::moduleHandler()), + new FileEntityNormalizer($link_manager, $entity_manager, $module_handler), + new ContentEntityNormalizer($link_manager, $entity_manager, $module_handler), new EntityReferenceItemNormalizer($link_manager, $chain_resolver), new FieldItemNormalizer(), new FieldNormalizer(), diff --git a/core/modules/rest/src/LinkManager/RelationLinkManager.php b/core/modules/rest/src/LinkManager/RelationLinkManager.php index 8cf4afa..e4a54a6 100644 --- a/core/modules/rest/src/LinkManager/RelationLinkManager.php +++ b/core/modules/rest/src/LinkManager/RelationLinkManager.php @@ -87,7 +87,7 @@ protected function writeCache() { $data = array(); foreach ($this->entityManager->getDefinitions() as $entity_type) { - if ($entity_type->isFieldable()) { + if ($entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) { foreach ($this->entityManager->getBundleInfo($entity_type->id()) as $bundle => $bundle_info) { foreach ($this->entityManager->getFieldDefinitions($entity_type->id(), $bundle) as $field_definition) { $relation_uri = $this->getRelationUri($entity_type->id(), $bundle, $field_definition->getName());