Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,11 @@
# Set the domain for HAL type and relation links.
# If left blank, the site's domain will be used.
link_domain: ~
# Before Drupal 8.5, the File entity 'uri' field value was overridden to return
# the absolute file URL instead of the actual (stream wrapper) URI. The default
# for new sites is now to return the actual URI as well as a root-relative file
# URL. Enable this setting to use the previous behavior. For existing sites,
# the previous behavior is kept by default.
# @see hal_update_8501()
# @see https://www.drupal.org/node/2925783
bc_file_uri_as_url_normalizer: false

View file

@ -0,0 +1,11 @@
# Schema for the configuration files of the HAL module.
hal.settings:
type: config_object
label: 'HAL settings'
mapping:
link_domain:
type: string
label: 'Domain of the relation'
bc_file_uri_as_url_normalizer:
type: boolean
label: 'Whether to retain pre Drupal 8.5 behavior of normalizing the File entity "uri" field value to an absolute URL.'

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Describes hooks provided by the HAL module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the HAL type URI.
*
* Modules may wish to alter the type URI generated for a resource based on the
* context of the serializer/normalizer operation.
*
* @param string $uri
* The URI to alter.
* @param array $context
* The context from the serializer/normalizer operation.
*
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
* @see \Symfony\Component\Serializer\SerializerInterface::deserialize()
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
*/
function hook_hal_type_uri_alter(&$uri, $context = []) {
if ($context['mymodule'] == TRUE) {
$base = \Drupal::config('hal.settings')->get('link_domain');
$uri = str_replace($base, 'http://mymodule.domain', $uri);
}
}
/**
* Alter the HAL relation URI.
*
* Modules may wish to alter the relation URI generated for a resource based on
* the context of the serializer/normalizer operation.
*
* @param string $uri
* The URI to alter.
* @param array $context
* The context from the serializer/normalizer operation.
*
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
* @see \Symfony\Component\Serializer\SerializerInterface::deserialize()
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
*/
function hook_hal_relation_uri_alter(&$uri, $context = []) {
if ($context['mymodule'] == TRUE) {
$base = \Drupal::config('hal.settings')->get('link_domain');
$uri = str_replace($base, 'http://mymodule.domain', $uri);
}
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,8 @@
name: 'HAL'
type: module
description: 'Serializes entities using Hypertext Application Language.'
package: Web services
version: VERSION
core: 8.x
dependencies:
- drupal:serialization

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Update functions for the HAL module.
*/
/**
* Move 'link_domain' from 'rest.settings' to 'hal.settings'.
*/
function hal_update_8301() {
$config_factory = \Drupal::configFactory();
// The default value for the 'link_domain' key is `~`, which is the YAML
// equivalent of PHP's `NULL`. If the REST module is not installed, this is
// the value we will store in 'hal.settings'.
$link_domain = NULL;
// But if the REST module was installed, we migrate its 'link_domain' setting,
// because we are actually moving that setting from 'rest.settings' to
// 'hal.settings'.
$rest_settings = $config_factory->getEditable('rest.settings');
if ($rest_settings->getRawData() !== []) {
$link_domain = $rest_settings->get('link_domain');
// Remove the 'link_domain' setting from 'rest.settings'.
$rest_settings->clear('link_domain')
->save();
}
$hal_settings = $config_factory->getEditable('hal.settings');
$hal_settings->set('link_domain', $link_domain);
$hal_settings->save(TRUE);
}
/**
* Add hal.settings::bc_file_uri_as_url_normalizer configuration.
*/
function hal_update_8501() {
$config_factory = \Drupal::configFactory();
$config_factory->getEditable('hal.settings')
->set('bc_file_uri_as_url_normalizer', TRUE)
->save(TRUE);
return t('Backwards compatibility mode has been enabled for File entities\' HAL normalization of the "uri" field. Like before, it will continue to return only the absolute file URL. If you want the new behavior, which returns both the stored URI and a root-relative file URL, <a href="https://www.drupal.org/node/2925783">read the change record to learn how to opt in.</a>');
}

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Adds support for serializing entities to Hypertext Application Language.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function hal_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.hal':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('<a href=":hal_spec">Hypertext Application Language (HAL)</a> is a format that supports the linking required for hypermedia APIs.', [':hal_spec' => 'http://stateless.co/hal_specification.html']) . '</p>';
$output .= '<p>' . t('Hypermedia APIs are a style of Web API that uses URIs to identify resources and the <a href="http://wikipedia.org/wiki/Link_relation">link relations</a> between them, enabling API consumers to follow links to discover API functionality.') . '</p>';
$output .= '<p>' . t('This module adds support for serializing entities (such as content items, taxonomy terms, etc.) to the JSON version of HAL. For more information, see the <a href=":hal_do">online documentation for the HAL module</a>.', [':hal_do' => 'https://www.drupal.org/documentation/modules/hal']) . '</p>';
return $output;
}
}

View file

@ -0,0 +1,45 @@
services:
serializer.normalizer.entity_reference_item.hal:
class: Drupal\hal\Normalizer\EntityReferenceItemNormalizer
arguments: ['@hal.link_manager', '@serializer.entity_resolver', '@entity_type.manager']
tags:
- { name: normalizer, priority: 10 }
serializer.normalizer.field_item.hal:
class: Drupal\hal\Normalizer\FieldItemNormalizer
tags:
- { name: normalizer, priority: 10 }
serializer.normalizer.field.hal:
class: Drupal\hal\Normalizer\FieldNormalizer
tags:
- { name: normalizer, priority: 10 }
serializer.normalizer.file_entity.hal:
class: Drupal\hal\Normalizer\FileEntityNormalizer
deprecated: 'The "%service_id%" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.'
arguments: ['@entity.manager', '@hal.link_manager', '@module_handler', '@config.factory']
tags:
- { name: normalizer, priority: 20 }
serializer.normalizer.timestamp_item.hal:
class: Drupal\hal\Normalizer\TimestampItemNormalizer
tags:
# Priority must be higher than serializer.normalizer.field_item.hal.
- { name: normalizer, priority: 20, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
serializer.normalizer.entity.hal:
class: Drupal\hal\Normalizer\ContentEntityNormalizer
arguments: ['@hal.link_manager', '@entity.manager', '@module_handler']
tags:
- { name: normalizer, priority: 10 }
serializer.encoder.hal:
class: Drupal\hal\Encoder\JsonEncoder
tags:
- { name: encoder, priority: 10, format: hal_json }
# Link managers.
hal.link_manager:
class: Drupal\hal\LinkManager\LinkManager
arguments: ['@hal.link_manager.type', '@hal.link_manager.relation']
hal.link_manager.type:
class: Drupal\hal\LinkManager\TypeLinkManager
arguments: ['@cache.default', '@module_handler', '@config.factory', '@request_stack', '@entity_type.bundle.info']
hal.link_manager.relation:
class: Drupal\hal\LinkManager\RelationLinkManager
arguments: ['@cache.default', '@entity.manager', '@module_handler', '@config.factory', '@request_stack']

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\hal\Encoder;
use Drupal\serialization\Encoder\JsonEncoder as SerializationJsonEncoder;
/**
* Encodes HAL data in JSON.
*
* Simply respond to hal_json format requests using the JSON encoder.
*/
class JsonEncoder extends SerializationJsonEncoder {
/**
* The formats that this Encoder supports.
*
* @var string
*/
protected static $format = ['hal_json'];
}

View file

@ -0,0 +1,22 @@
<?php
namespace Drupal\hal;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
/**
* Adds hal+json as known format.
*/
class HalServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), '\Drupal\Core\StackMiddleware\NegotiationMiddleware', TRUE)) {
$container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['hal_json', ['application/hal+json']]);
}
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\hal\LinkManager;
/**
* Defines an interface for a link manager with a configurable domain.
*/
interface ConfigurableLinkManagerInterface {
/**
* Sets the link domain used in constructing link URIs.
*
* @param string $domain
* The link domain to use for constructing link URIs.
*
* @return $this
*/
public function setLinkDomain($domain);
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\hal\LinkManager;
class LinkManager implements LinkManagerInterface {
/**
* The type link manager.
*
* @var \Drupal\hal\LinkManager\TypeLinkManagerInterface
*/
protected $typeLinkManager;
/**
* The relation link manager.
*
* @var \Drupal\hal\LinkManager\RelationLinkManagerInterface
*/
protected $relationLinkManager;
/**
* Constructor.
*
* @param \Drupal\hal\LinkManager\TypeLinkManagerInterface $type_link_manager
* Manager for handling bundle URIs.
* @param \Drupal\hal\LinkManager\RelationLinkManagerInterface $relation_link_manager
* Manager for handling bundle URIs.
*/
public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager) {
$this->typeLinkManager = $type_link_manager;
$this->relationLinkManager = $relation_link_manager;
}
/**
* {@inheritdoc}
*/
public function getTypeUri($entity_type, $bundle, $context = []) {
return $this->typeLinkManager->getTypeUri($entity_type, $bundle, $context);
}
/**
* {@inheritdoc}
*/
public function getTypeInternalIds($type_uri, $context = []) {
return $this->typeLinkManager->getTypeInternalIds($type_uri, $context);
}
/**
* {@inheritdoc}
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = []) {
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name, $context);
}
/**
* {@inheritdoc}
*/
public function getRelationInternalIds($relation_uri) {
return $this->relationLinkManager->getRelationInternalIds($relation_uri);
}
/**
* {@inheritdoc}
*/
public function setLinkDomain($domain) {
$this->relationLinkManager->setLinkDomain($domain);
$this->typeLinkManager->setLinkDomain($domain);
return $this;
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\hal\LinkManager;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
/**
* Defines an abstract base-class for HAL link manager objects.
*/
abstract class LinkManagerBase {
/**
* Link domain used for type links URIs.
*
* @var string
*/
protected $linkDomain;
/**
* Config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
public function setLinkDomain($domain) {
$this->linkDomain = rtrim($domain, '/');
return $this;
}
/**
* Gets the link domain.
*
* @param array $context
* Normalization/serialization context.
*
* @return string
* The link domain.
*
* @see \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
* @see \Drupal\serialization\Normalizer\CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY
*/
protected function getLinkDomain(array $context = []) {
if (empty($this->linkDomain)) {
if ($domain = $this->configFactory->get('hal.settings')->get('link_domain')) {
// Bubble the appropriate cacheability metadata whenever possible.
if (isset($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY])) {
$context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($this->configFactory->get('hal.settings'));
}
return rtrim($domain, '/');
}
else {
// Bubble the relevant cacheability metadata whenever possible.
if (isset($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY])) {
$context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheContexts(['url.site']);
}
$request = $this->requestStack->getCurrentRequest();
return $request->getSchemeAndHttpHost() . $request->getBasePath();
}
}
return $this->linkDomain;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Drupal\hal\LinkManager;
/**
* Interface implemented by link managers.
*
* There are no explicit methods on the manager interface. Instead link managers
* broker the interactions of the different components, and therefore must
* implement each component interface, which is enforced by this interface
* extending all of the component ones.
*
* While a link manager may directly implement these interface methods with
* custom logic, it is expected to be more common for plugin managers to proxy
* the method invocations to the respective components.
*/
interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface {
}

View file

@ -0,0 +1,168 @@
<?php
namespace Drupal\hal\LinkManager;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class RelationLinkManager extends LinkManagerBase implements RelationLinkManagerInterface {
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructor.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache of relation URIs and their associated Typed Data IDs.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(CacheBackendInterface $cache, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
$this->cache = $cache;
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = []) {
// Per the interface documentation of this method, the returned URI may
// optionally also serve as the URL of a documentation page about this
// field. However, Drupal does not currently implement such a documentation
// page. Therefore, we return a URI assembled relative to the site's base
// URL, which is sufficient to uniquely identify the site's entity type +
// bundle + field for use in hypermedia formats, but we do not take into
// account unclean URLs, language prefixing, or anything else that would be
// required for Drupal to be able to respond with content at this URL. If a
// module is installed that adds such content, but requires this URL to be
// different (e.g., include a language prefix), then the module must also
// override the RelationLinkManager class/service to return the desired URL.
$uri = $this->getLinkDomain($context) . "/rest/relation/$entity_type/$bundle/$field_name";
$this->moduleHandler->alter('hal_relation_uri', $uri, $context);
$this->moduleHandler->alterDeprecated('This hook is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Implement hook_hal_relation_uri_alter() instead.', 'rest_relation_uri', $uri, $context);
return $uri;
}
/**
* {@inheritdoc}
*/
public function getRelationInternalIds($relation_uri, $context = []) {
$relations = $this->getRelations($context);
if (isset($relations[$relation_uri])) {
return $relations[$relation_uri];
}
return FALSE;
}
/**
* Get the array of relation links.
*
* Any field can be handled as a relation simply by changing how it is
* normalized. Therefore, there is no prior knowledge that can be used here
* to determine which fields to assign relation URIs. Instead, each field,
* even primitives, are given a relation URI. It is up to the caller to
* determine which URIs to use.
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data IDs keyed by corresponding relation URI. The keys
* are:
* - 'entity_type_id'
* - 'bundle'
* - 'field_name'
* - 'entity_type' (deprecated)
* The values for 'entity_type_id', 'bundle' and 'field_name' are strings.
* The 'entity_type' key exists for backwards compatibility and its value is
* the full entity type object. The 'entity_type' key will be removed before
* Drupal 9.0.
*
* @see https://www.drupal.org/node/2877608
*/
protected function getRelations($context = []) {
$cid = 'hal:links:relations';
$cache = $this->cache->get($cid);
if (!$cache) {
$data = $this->writeCache($context);
}
else {
$data = $cache->data;
}
// @todo https://www.drupal.org/node/2716163 Remove this in Drupal 9.0.
foreach ($data as $relation_uri => $ids) {
$data[$relation_uri]['entity_type'] = $this->entityManager->getDefinition($ids['entity_type_id']);
}
return $data;
}
/**
* Writes the cache of relation links.
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data IDs keyed by corresponding relation URI. The keys
* are:
* - 'entity_type_id'
* - 'bundle'
* - 'field_name'
* The values for 'entity_type_id', 'bundle' and 'field_name' are strings.
*/
protected function writeCache($context = []) {
$data = [];
foreach ($this->entityManager->getDefinitions() as $entity_type) {
if ($entity_type instanceof ContentEntityTypeInterface) {
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(), $context);
$data[$relation_uri] = [
'entity_type_id' => $entity_type->id(),
'bundle' => $bundle,
'field_name' => $field_definition->getName(),
];
}
}
}
}
// These URIs only change when field info changes, so cache it permanently
// and only clear it when the fields cache is cleared.
$this->cache->set('hal:links:relations', $data, Cache::PERMANENT, ['entity_field_info']);
return $data;
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\hal\LinkManager;
interface RelationLinkManagerInterface extends ConfigurableLinkManagerInterface {
/**
* Gets the URI that corresponds to a field.
*
* When using hypermedia formats, this URI can be used to indicate which
* field the data represents. Documentation about this field can also be
* provided at this URI.
*
* @param string $entity_type
* The bundle's entity type.
* @param string $bundle
* The bundle name.
* @param string $field_name
* The field name.
* @param array $context
* (optional) Optional serializer/normalizer context.
*
* @return string
* The corresponding URI (or IANA link relation type) for the field.
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = []);
/**
* Translates a REST URI into internal IDs.
*
* @param string $relation_uri
* Relation URI (or IANA link relation type) to transform into internal IDs.
*
* @return array
* Array with keys 'entity_type_id', 'bundle' and 'field_name'. For
* backwards compatibility, the entity_type key returns the full entity type
* object, this will be removed before Drupal 9.0.
*/
public function getRelationInternalIds($relation_uri);
}

View file

@ -0,0 +1,148 @@
<?php
namespace Drupal\hal\LinkManager;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterface {
/**
* Injected cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfoService;
/**
* Constructor.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The injected cache backend for caching type URIs.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info_service
* The bundle info service.
*/
public function __construct(CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack, EntityTypeBundleInfoInterface $bundle_info_service) {
$this->cache = $cache;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->bundleInfoService = $bundle_info_service;
}
/**
* {@inheritdoc}
*/
public function getTypeUri($entity_type, $bundle, $context = []) {
// Per the interface documentation of this method, the returned URI may
// optionally also serve as the URL of a documentation page about this
// bundle. However, Drupal does not currently implement such a documentation
// page. Therefore, we return a URI assembled relative to the site's base
// URL, which is sufficient to uniquely identify the site's entity type and
// bundle for use in hypermedia formats, but we do not take into account
// unclean URLs, language prefixing, or anything else that would be required
// for Drupal to be able to respond with content at this URL. If a module is
// installed that adds such content, but requires this URL to be different
// (e.g., include a language prefix), then the module must also override the
// TypeLinkManager class/service to return the desired URL.
$uri = $this->getLinkDomain($context) . "/rest/type/$entity_type/$bundle";
$this->moduleHandler->alter('hal_type_uri', $uri, $context);
$this->moduleHandler->alterDeprecated('This hook is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Implement hook_hal_type_uri_alter() instead.', 'rest_type_uri', $uri, $context);
return $uri;
}
/**
* {@inheritdoc}
*/
public function getTypeInternalIds($type_uri, $context = []) {
$types = $this->getTypes($context);
if (isset($types[$type_uri])) {
return $types[$type_uri];
}
return FALSE;
}
/**
* Get the array of type links.
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data ids (entity_type and bundle) keyed by
* corresponding type URI.
*/
protected function getTypes($context = []) {
$cid = 'hal:links:types';
$cache = $this->cache->get($cid);
if (!$cache) {
$data = $this->writeCache($context);
}
else {
$data = $cache->data;
}
return $data;
}
/**
* Writes the cache of type links.
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data ids (entity_type and bundle) keyed by
* corresponding type URI.
*/
protected function writeCache($context = []) {
$data = [];
// Type URIs correspond to bundles. Iterate through the bundles to get the
// URI and data for them.
$entity_types = \Drupal::entityManager()->getDefinitions();
foreach ($this->bundleInfoService->getAllBundleInfo() as $entity_type_id => $bundles) {
// Only content entities are supported currently.
// @todo Consider supporting config entities.
if ($entity_types[$entity_type_id]->entityClassImplements(ConfigEntityInterface::class)) {
continue;
}
foreach ($bundles as $bundle => $bundle_info) {
// Get a type URI for the bundle.
$bundle_uri = $this->getTypeUri($entity_type_id, $bundle, $context);
$data[$bundle_uri] = [
'entity_type' => $entity_type_id,
'bundle' => $bundle,
];
}
}
// These URIs only change when entity info changes, so cache it permanently
// and only clear it when entity_info is cleared.
$this->cache->set('hal:links:types', $data, Cache::PERMANENT, ['entity_types']);
return $data;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\hal\LinkManager;
interface TypeLinkManagerInterface extends ConfigurableLinkManagerInterface {
/**
* Gets the URI that corresponds to a bundle.
*
* When using hypermedia formats, this URI can be used to indicate which
* bundle the data represents. Documentation about required and optional
* fields can also be provided at this URI.
*
* @param $entity_type
* The bundle's entity type.
* @param $bundle
* The bundle name.
* @param array $context
* (optional) Optional serializer/normalizer context.
*
* @return string
* The corresponding URI for the bundle.
*/
public function getTypeUri($entity_type, $bundle, $context = []);
/**
* Get a bundle's Typed Data IDs based on a URI.
*
* @param string $type_uri
* The type URI.
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array|bool
* If the URI matches a bundle, returns an array containing entity_type and
* bundle. Otherwise, returns false.
*/
public function getTypeInternalIds($type_uri, $context = []);
}

View file

@ -0,0 +1,241 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Drupal\hal\LinkManager\LinkManagerInterface;
use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Converts the Drupal entity object structure to a HAL array structure.
*/
class ContentEntityNormalizer extends NormalizerBase {
use FieldableEntityNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\ContentEntityInterface';
/**
* The hypermedia link manager.
*
* @var \Drupal\hal\LinkManager\LinkManagerInterface
*/
protected $linkManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs an ContentEntityNormalizer object.
*
* @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
* The hypermedia link manager.
*/
public function __construct(LinkManagerInterface $link_manager, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler) {
$this->linkManager = $link_manager;
$this->entityManager = $entity_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function normalize($entity, $format = NULL, array $context = []) {
$context += [
'account' => NULL,
'included_fields' => NULL,
];
// Create the array of normalized fields, starting with the URI.
/** @var $entity \Drupal\Core\Entity\ContentEntityInterface */
$normalized = [
'_links' => [
'self' => [
'href' => $this->getEntityUri($entity),
],
'type' => [
'href' => $this->linkManager->getTypeUri($entity->getEntityTypeId(), $entity->bundle(), $context),
],
],
];
$field_items = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData());
// If the fields to use were specified, only output those field values.
if (isset($context['included_fields'])) {
$field_items = array_intersect_key($field_items, array_flip($context['included_fields']));
}
foreach ($field_items as $field) {
// Continue if the current user does not have access to view this field.
if (!$field->access('view', $context['account'])) {
continue;
}
$normalized_property = $this->serializer->normalize($field, $format, $context);
$normalized = NestedArray::mergeDeep($normalized, $normalized_property);
}
return $normalized;
}
/**
* Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize().
*
* @param array $data
* Entity data to restore.
* @param string $class
* Unused, entity_create() is used to instantiate entity objects.
* @param string $format
* Format the given data was extracted from.
* @param array $context
* Options available to the denormalizer. Keys that can be used:
* - request_method: if set to "patch" the denormalization will clear out
* all default values for entity fields before applying $data to the
* entity.
*
* @return \Drupal\Core\Entity\EntityInterface
* An unserialized entity object containing the data in $data.
*
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
// Get type, necessary for determining which bundle to create.
if (!isset($data['_links']['type'])) {
throw new UnexpectedValueException('The type link relation must be specified.');
}
// Create the entity.
$typed_data_ids = $this->getTypedDataIds($data['_links']['type'], $context);
$entity_type = $this->getEntityTypeDefinition($typed_data_ids['entity_type']);
$default_langcode_key = $entity_type->getKey('default_langcode');
$langcode_key = $entity_type->getKey('langcode');
$values = [];
// Figure out the language to use.
if (isset($data[$default_langcode_key])) {
// Find the field item for which the default_langcode value is set to 1 and
// set the langcode the right default language.
foreach ($data[$default_langcode_key] as $item) {
if (!empty($item['value']) && isset($item['lang'])) {
$values[$langcode_key] = $item['lang'];
break;
}
}
// Remove the default langcode so it does not get iterated over below.
unset($data[$default_langcode_key]);
}
if ($entity_type->hasKey('bundle')) {
$bundle_key = $entity_type->getKey('bundle');
$values[$bundle_key] = $typed_data_ids['bundle'];
// Unset the bundle key from data, if it's there.
unset($data[$bundle_key]);
}
$entity = $this->entityManager->getStorage($typed_data_ids['entity_type'])->create($values);
// Remove links from data array.
unset($data['_links']);
// Get embedded resources and remove from data array.
$embedded = [];
if (isset($data['_embedded'])) {
$embedded = $data['_embedded'];
unset($data['_embedded']);
}
// Flatten the embedded values.
foreach ($embedded as $relation => $field) {
$field_ids = $this->linkManager->getRelationInternalIds($relation);
if (!empty($field_ids)) {
$field_name = $field_ids['field_name'];
$data[$field_name] = $field;
}
}
$this->denormalizeFieldData($data, $entity, $format, $context);
// Pass the names of the fields whose values can be merged.
// @todo https://www.drupal.org/node/2456257 remove this.
$entity->_restSubmittedFields = array_keys($data);
return $entity;
}
/**
* Constructs the entity URI.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @return string
* The entity URI.
*/
protected function getEntityUri(EntityInterface $entity) {
// Some entity types don't provide a canonical link template, at least call
// out to ->url().
if ($entity->isNew() || !$entity->hasLinkTemplate('canonical')) {
return $entity->url('canonical', []);
}
$url = $entity->urlInfo('canonical', ['absolute' => TRUE]);
return $url->setRouteParameter('_format', 'hal_json')->toString();
}
/**
* Gets the typed data IDs for a type URI.
*
* @param array $types
* The type array(s) (value of the 'type' attribute of the incoming data).
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* The typed data IDs.
*/
protected function getTypedDataIds($types, $context = []) {
// The 'type' can potentially contain an array of type objects. By default,
// Drupal only uses a single type in serializing, but allows for multiple
// types when deserializing.
if (isset($types['href'])) {
$types = [$types];
}
if (empty($types)) {
throw new UnexpectedValueException('No entity type(s) specified');
}
foreach ($types as $type) {
if (!isset($type['href'])) {
throw new UnexpectedValueException('Type must contain an \'href\' attribute.');
}
$type_uri = $type['href'];
// Check whether the URI corresponds to a known type on this site. Break
// once one does.
if ($typed_data_ids = $this->linkManager->getTypeInternalIds($type['href'], $context)) {
break;
}
}
// If none of the URIs correspond to an entity type on this site, no entity
// can be created. Throw an exception.
if (empty($typed_data_ids)) {
throw new UnexpectedValueException(sprintf('Type %s does not correspond to an entity on this site.', $type_uri));
}
return $typed_data_ids;
}
}

View file

@ -0,0 +1,186 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\hal\LinkManager\LinkManagerInterface;
use Drupal\serialization\EntityResolver\EntityResolverInterface;
use Drupal\serialization\EntityResolver\UuidReferenceInterface;
use Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizerTrait;
/**
* Converts the Drupal entity reference item object to HAL array structure.
*/
class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidReferenceInterface {
use EntityReferenceFieldItemNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
/**
* The hypermedia link manager.
*
* @var \Drupal\hal\LinkManager\LinkManagerInterface
*/
protected $linkManager;
/**
* The entity resolver.
*
* @var \Drupal\serialization\EntityResolver\EntityResolverInterface
*/
protected $entityResolver;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs an EntityReferenceItemNormalizer object.
*
* @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
* The hypermedia link manager.
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface $entity_Resolver
* The entity resolver.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entity_type_manager
* The entity type manager.
*/
public function __construct(LinkManagerInterface $link_manager, EntityResolverInterface $entity_Resolver, EntityTypeManagerInterface $entity_type_manager = NULL) {
$this->linkManager = $link_manager;
$this->entityResolver = $entity_Resolver;
$this->entityTypeManager = $entity_type_manager ?: \Drupal::service('entity_type.manager');
}
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
// If this is not a fieldable entity, let the parent implementation handle
// it, only fieldable entities are supported as embedded resources.
if (!$this->targetEntityIsFieldable($field_item)) {
return parent::normalize($field_item, $format, $context);
}
/** @var $field_item \Drupal\Core\Field\FieldItemInterface */
$target_entity = $field_item->get('entity')->getValue();
// If the parent entity passed in a langcode, unset it before normalizing
// the target entity. Otherwise, untranslatable fields of the target entity
// will include the langcode.
$langcode = isset($context['langcode']) ? $context['langcode'] : NULL;
unset($context['langcode']);
$context['included_fields'] = ['uuid'];
// Normalize the target entity.
$embedded = $this->serializer->normalize($target_entity, $format, $context);
$link = $embedded['_links']['self'];
// If the field is translatable, add the langcode to the link relation
// object. This does not indicate the language of the target entity.
if ($langcode) {
$embedded['lang'] = $link['lang'] = $langcode;
}
// 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, $context);
return [
'_links' => [
$field_uri => [$link],
],
'_embedded' => [
$field_uri => [$embedded],
],
];
}
/**
* Checks whether the referenced entity is of a fieldable entity type.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
* The reference field item whose target entity needs to be checked.
*
* @return bool
* TRUE when the referenced entity is of a fieldable entity type.
*/
protected function targetEntityIsFieldable(EntityReferenceItem $item) {
$target_entity = $item->get('entity')->getValue();
if ($target_entity !== NULL) {
return $target_entity instanceof FieldableEntityInterface;
}
$referencing_entity = $item->getEntity();
$target_entity_type_id = $item->getFieldDefinition()->getSetting('target_type');
// If the entity type is the same as the parent, we can check that. This is
// just a shortcut to avoid getting the entity type definition and checking
// the class.
if ($target_entity_type_id === $referencing_entity->getEntityTypeId()) {
return $referencing_entity instanceof FieldableEntityInterface;
}
// Otherwise, we need to get the class for the type.
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
$target_entity_type_class = $target_entity_type->getClass();
return is_a($target_entity_type_class, FieldableEntityInterface::class, TRUE);
}
/**
* {@inheritdoc}
*/
protected function constructValue($data, $context) {
$field_item = $context['target_instance'];
$field_definition = $field_item->getFieldDefinition();
$target_type = $field_definition->getSetting('target_type');
$id = $this->entityResolver->resolve($this, $data, $target_type);
if (isset($id)) {
return ['target_id' => $id] + array_intersect_key($data, $field_item->getProperties());
}
return NULL;
}
/**
* {@inheritdoc}
*/
protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
// Normalize root reference values here so we don't need to deal with hal's
// nested data structure for field items. This will be called from
// \Drupal\hal\Normalizer\FieldItemNormalizer::normalize. Which will only
// be called from this class for entities that are not fieldable.
$normalized = parent::normalizedFieldValues($field_item, $format, $context);
$this->normalizeRootReferenceValue($normalized, $field_item);
return $normalized;
}
/**
* {@inheritdoc}
*/
public function getUuid($data) {
if (isset($data['uuid'])) {
$uuid = $data['uuid'];
// The value may be a nested array like $uuid[0]['value'].
if (is_array($uuid) && isset($uuid[0]['value'])) {
$uuid = $uuid[0]['value'];
}
return $uuid;
}
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Converts the Drupal field item object structure to HAL array structure.
*/
class FieldItemNormalizer extends NormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Field\FieldItemInterface';
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
// The values are wrapped in an array, and then wrapped in another array
// keyed by field name so that field items can be merged by the
// FieldNormalizer. This is necessary for the EntityReferenceItemNormalizer
// to be able to place values in the '_links' array.
$field = $field_item->getParent();
return [
$field->getName() => [$this->normalizedFieldValues($field_item, $format, $context)],
];
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!isset($context['target_instance'])) {
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
}
if ($context['target_instance']->getParent() == NULL) {
throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
}
$field_item = $context['target_instance'];
// If this field is translatable, we need to create a translated instance.
if (isset($data['lang'])) {
$langcode = $data['lang'];
unset($data['lang']);
$field_definition = $field_item->getFieldDefinition();
if ($field_definition->isTranslatable()) {
$field_item = $this->createTranslatedInstance($field_item, $langcode);
}
}
$field_item->setValue($this->constructValue($data, $context));
return $field_item;
}
/**
* Build the field item value using the incoming data.
*
* @param $data
* The incoming data for this field item.
* @param $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
return $data;
}
/**
* Normalizes field values for an item.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item instance.
* @param string|null $format
* The normalization format.
* @param array $context
* The context passed into the normalizer.
*
* @return array
* An array of field item values, keyed by property name.
*/
protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
$normalized = [];
// We normalize each individual property, so each can do their own casting,
// if needed.
/** @var \Drupal\Core\TypedData\TypedDataInterface $property */
$field_properties = !empty($field_item->getProperties(TRUE))
? TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item)
: $field_item->getValue();
foreach ($field_properties as $property_name => $property) {
$normalized[$property_name] = $this->serializer->normalize($property, $format, $context);
}
if (isset($context['langcode'])) {
$normalized['lang'] = $context['langcode'];
}
return $normalized;
}
/**
* Get a translated version of the field item instance.
*
* To indicate that a field item applies to one translation of an entity and
* not another, the property path must originate with a translation of the
* entity. This is the reason for using target_instances, from which the
* property path can be traversed up to the root.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The untranslated field item instance.
* @param $langcode
* The langcode.
*
* @return \Drupal\Core\Field\FieldItemInterface
* The translated field item instance.
*/
protected function createTranslatedInstance(FieldItemInterface $item, $langcode) {
// Remove the untranslated item that was created for the default language
// by FieldNormalizer::denormalize().
$items = $item->getParent();
$delta = $item->getName();
unset($items[$delta]);
// Instead, create a new item for the entity in the requested language.
$entity = $item->getEntity();
$entity_translation = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : $entity->addTranslation($langcode);
$field_name = $item->getFieldDefinition()->getName();
return $entity_translation->get($field_name)->appendItem();
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\serialization\Normalizer\FieldNormalizer as SerializationFieldNormalizer;
/**
* Converts the Drupal field structure to HAL array structure.
*/
class FieldNormalizer extends SerializationFieldNormalizer {
/**
* {@inheritdoc}
*/
protected $format = ['hal_json'];
/**
* {@inheritdoc}
*/
public function normalize($field_items, $format = NULL, array $context = []) {
$normalized_field_items = [];
// Get the field definition.
$entity = $field_items->getEntity();
$field_name = $field_items->getName();
$field_definition = $field_items->getFieldDefinition();
// If this field is not translatable, it can simply be normalized without
// separating it into different translations.
if (!$field_definition->isTranslatable()) {
$normalized_field_items = $this->normalizeFieldItems($field_items, $format, $context);
}
// Otherwise, the languages have to be extracted from the entity and passed
// in to the field item normalizer in the context. The langcode is appended
// to the field item values.
else {
foreach ($entity->getTranslationLanguages() as $language) {
$context['langcode'] = $language->getId();
$translation = $entity->getTranslation($language->getId());
$translated_field_items = $translation->get($field_name);
$normalized_field_items = array_merge($normalized_field_items, $this->normalizeFieldItems($translated_field_items, $format, $context));
}
}
// Merge deep so that links set in entity reference normalizers are merged
// into the links property.
return NestedArray::mergeDeepArray($normalized_field_items);
}
/**
* Helper function to normalize field items.
*
* @param \Drupal\Core\Field\FieldItemListInterface $field_items
* The field item list object.
* @param string $format
* The format.
* @param array $context
* The context array.
*
* @return array
* The array of normalized field items.
*/
protected function normalizeFieldItems($field_items, $format, $context) {
$normalized_field_items = [];
if (!$field_items->isEmpty()) {
foreach ($field_items as $field_item) {
$normalized_field_items[] = $this->serializer->normalize($field_item, $format, $context);
}
}
return $normalized_field_items;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\hal\LinkManager\LinkManagerInterface;
/**
* Converts the Drupal entity object structure to a HAL array structure.
*
* @deprecated in Drupal 8.5.0, to be removed before Drupal 9.0.0.
*/
class FileEntityNormalizer extends ContentEntityNormalizer {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\file\FileInterface';
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The HAL settings config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $halSettings;
/**
* Constructs a FileEntityNormalizer object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
* The hypermedia link manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(EntityManagerInterface $entity_manager, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) {
parent::__construct($link_manager, $entity_manager, $module_handler);
$this->halSettings = $config_factory->get('hal.settings');
}
/**
* {@inheritdoc}
*/
public function normalize($entity, $format = NULL, array $context = []) {
$data = parent::normalize($entity, $format, $context);
$this->addCacheableDependency($context, $this->halSettings);
if ($this->halSettings->get('bc_file_uri_as_url_normalizer')) {
// Replace the file url with a full url for the file.
$data['uri'][0]['value'] = $this->getEntityUri($entity);
}
return $data;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Base class for Normalizers.
*/
abstract class NormalizerBase extends SerializationNormalizerBase implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $format = ['hal_json'];
/**
* {@inheritdoc}
*/
protected function checkFormat($format = NULL) {
if (isset($this->formats)) {
@trigger_error('::formats is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Use ::$format instead. See https://www.drupal.org/node/2868275', E_USER_DEPRECATED);
$this->format = $this->formats;
}
return parent::checkFormat($format);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\hal\Normalizer;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
/**
* Converts values for TimestampItem to and from common formats for hal.
*/
class TimestampItemNormalizer extends FieldItemNormalizer {
use TimeStampItemNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = TimestampItem::class;
/**
* {@inheritdoc}
*/
protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
$normalized = parent::normalizedFieldValues($field_item, $format, $context);
return $this->processNormalizedValues($normalized);
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of hal_update_8301().
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->insert('key_value')
->fields([
'collection' => 'system.schema',
'name' => 'hal',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['hal'] = 0;
$extensions['module']['serialization'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();

View file

@ -0,0 +1,6 @@
name: HAL test module
type: module
description: "Support module for HAL tests."
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,50 @@
<?php
/**
* @file
* Contains hook implementations for testing HAL module.
*/
/**
* Implements hook_hal_type_uri_alter().
*/
function hal_test_hal_type_uri_alter(&$uri, $context = []) {
if (!empty($context['hal_test'])) {
$uri = 'hal_test_type';
}
}
/**
* Implements hook_hal_relation_uri_alter().
*/
function hal_test_hal_relation_uri_alter(&$uri, $context = []) {
if (!empty($context['hal_test'])) {
$uri = 'hal_test_relation';
}
}
/**
* Implements hook_rest_type_uri_alter().
*
* @deprecated Kept only for BC test coverage, see \Drupal\Tests\hal\Kernel\HalLinkManagerTest::testGetTypeUri().
*
* @see https://www.drupal.org/node/2830467
*/
function hal_test_rest_type_uri_alter(&$uri, $context = []) {
if (!empty($context['rest_test'])) {
$uri = 'rest_test_type';
}
}
/**
* Implements hook_rest_relation_uri_alter().
*
* @deprecated Kept only for BC test coverage, see \Drupal\Tests\hal\Kernel\HalLinkManagerTest::testGetRelationUri().
*
* @see https://www.drupal.org/node/2830467
*/
function hal_test_rest_relation_uri_alter(&$uri, $context = []) {
if (!empty($context['rest_test'])) {
$uri = 'rest_test_relation';
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Drupal\Tests\hal\Functional\EntityResource\Comment;
@trigger_error('The ' . __NAMESPACE__ . '\CommentHalJsonTestBase is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Instead, use Drupal\Tests\comment\Functional\Hal\CommentHalJsonTestBase. See https://www.drupal.org/node/2971931.', E_USER_DEPRECATED);
use Drupal\Tests\comment\Functional\Hal\CommentHalJsonTestBase as CommentHalJsonTestBaseReal;
/**
* @deprecated in Drupal 8.6.x. Will be removed before Drupal 9.0.0. Use
* Drupal\Tests\comment\Functional\Hal\CommentHalJsonTestBase instead.
*
* @see https://www.drupal.org/node/2971931
*/
abstract class CommentHalJsonTestBase extends CommentHalJsonTestBaseReal {
}

View file

@ -0,0 +1,16 @@
<?php
namespace Drupal\Tests\hal\Functional\EntityResource\Feed;
@trigger_error('The ' . __NAMESPACE__ . '\FeedHalJsonTestBase is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Instead, use Drupal\Tests\aggregator\Functional\Hal\FeedHalJsonTestBase. See https://www.drupal.org/node/2971931.', E_USER_DEPRECATED);
use Drupal\Tests\aggregator\Functional\Hal\FeedHalJsonTestBase as FeedHalJsonTestBaseReal;
/**
* @deprecated in Drupal 8.6.x. Will be removed before Drupal 9.0.0. Use
* Drupal\Tests\aggregator\Functional\Hal\FeedHalJsonTestBase instead.
*
* @see https://www.drupal.org/node/2971931
*/
abstract class FeedHalJsonTestBase extends FeedHalJsonTestBaseReal {
}

View file

@ -0,0 +1,16 @@
<?php
namespace Drupal\Tests\hal\Functional\EntityResource\File;
@trigger_error('The ' . __NAMESPACE__ . '\FileUploadHalJsonTestBase is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Instead, use Drupal\Tests\file\Functional\Hal\FileUploadHalJsonTestBase. See https://www.drupal.org/node/2971931.', E_USER_DEPRECATED);
use Drupal\Tests\file\Functional\Hal\FileUploadHalJsonTestBase as FileUploadHalJsonTestBaseReal;
/**
* @deprecated in Drupal 8.6.x. Will be removed before Drupal 9.0.0. Use
* Drupal\Tests\file\Functional\Hal\FileUploadHalJsonTestBase instead.
*
* @see https://www.drupal.org/node/2971931
*/
abstract class FileUploadHalJsonTestBase extends FileUploadHalJsonTestBaseReal {
}

View file

@ -0,0 +1,98 @@
<?php
namespace Drupal\Tests\hal\Functional\EntityResource;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Url;
use GuzzleHttp\RequestOptions;
/**
* Trait for EntityResourceTestBase subclasses testing formats using HAL.
*/
trait HalEntityNormalizationTrait {
/**
* Applies the HAL entity field normalization to an entity normalization.
*
* The HAL normalization:
* - adds a 'lang' attribute to every translatable field
* - omits reference fields, since references are stored in _links & _embedded
* - omits empty fields (fields without value)
*
* @param array $normalization
* An entity normalization.
*
* @return array
* The updated entity normalization.
*/
protected function applyHalFieldNormalization(array $normalization) {
if (!$this->entity instanceof FieldableEntityInterface) {
throw new \LogicException('This trait should only be used for fieldable entity types.');
}
// In the HAL normalization, all translatable fields get a 'lang' attribute.
$translatable_non_reference_fields = array_keys(array_filter($this->entity->getTranslatableFields(), function (FieldItemListInterface $field) {
return !$field instanceof EntityReferenceFieldItemListInterface;
}));
foreach ($translatable_non_reference_fields as $field_name) {
if (isset($normalization[$field_name])) {
$normalization[$field_name][0]['lang'] = 'en';
}
}
// In the HAL normalization, reference fields are omitted, except for the
// bundle field.
$bundle_key = $this->entity->getEntityType()->getKey('bundle');
$reference_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) use ($bundle_key) {
return $field instanceof EntityReferenceFieldItemListInterface && $field->getName() !== $bundle_key;
}));
foreach ($reference_fields as $field_name) {
unset($normalization[$field_name]);
}
// In the HAL normalization, the bundle field omits the 'target_type' and
// 'target_uuid' properties, because it's encoded in the '_links' section.
if ($bundle_key) {
unset($normalization[$bundle_key][0]['target_type']);
unset($normalization[$bundle_key][0]['target_uuid']);
}
// In the HAL normalization, empty fields are omitted.
$empty_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) {
return $field->isEmpty();
}));
foreach ($empty_fields as $field_name) {
unset($normalization[$field_name]);
}
return $normalization;
}
/**
* {@inheritdoc}
*/
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
// \Drupal\hal\Normalizer\EntityNormalizer::denormalize(): entity
// types with bundles MUST send their bundle field to be denormalizable.
if ($this->entity->getEntityType()->hasKey('bundle')) {
$normalization = $this->getNormalizedPostEntity();
$normalization['_links']['type'] = Url::fromUri('base:rest/type/' . static::$entityTypeId . '/bad_bundle_name');
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when incorrect entity type bundle is specified.
$response = $this->request($method, $url, $request_options);
$this->assertResourceErrorResponse(422, 'No entity type(s) specified', $response);
unset($normalization['_links']['type']);
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when no entity type bundle is specified.
$response = $this->request($method, $url, $request_options);
$this->assertResourceErrorResponse(422, 'The type link relation must be specified.', $response);
}
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Drupal\Tests\hal\Functional\EntityResource\Item;
@trigger_error('The ' . __NAMESPACE__ . '\ItemHalJsonTestBase is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Instead, use Drupal\Tests\aggregator\Functional\Hal\ItemHalJsonTestBase. See https://www.drupal.org/node/2971931.', E_USER_DEPRECATED);
use Drupal\Tests\aggregator\Functional\Hal\ItemHalJsonTestBase as ItemHalJsonTestBaseReal;
/**
* @deprecated in Drupal 8.6.x. Will be removed before Drupal 9.0.0. Use
* Drupal\Tests\aggregator\Functional\Hal\ItemHalJsonTestBase instead.
*
* @see https://www.drupal.org/node/2971931
*/
abstract class ItemHalJsonTestBase extends ItemHalJsonTestBaseReal {
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\Tests\hal\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests that 'hal.settings' is created, to store 'link_domain'.
*
* @see https://www.drupal.org/node/2758897
*
* @group hal
* @group legacy
*/
class CreateHalSettingsForLinkDomainUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/drupal-8.hal-hal_update_8301.php',
];
}
/**
* Tests hal_update_8301().
*/
public function testHalSettingsCreated() {
// Make sure we have the expected values before the update.
$hal_settings = $this->config('hal.settings');
$this->assertIdentical([], $hal_settings->getRawData());
$this->runUpdates();
// Make sure we have the expected values after the update.
$hal_settings = \Drupal::configFactory()->get('hal.settings');
$this->assertTrue(array_key_exists('link_domain', $hal_settings->getRawData()));
$this->assertIdentical(NULL, $hal_settings->getRawData()['link_domain']);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\hal\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* 'link_domain' is migrated from 'rest.settings' to 'hal.settings'.
*
* @see https://www.drupal.org/node/2758897
*
* @group hal
* @group legacy
*/
class MigrateLinkDomainSettingFromRestToHalUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/drupal-8.hal-hal_update_8301.php',
__DIR__ . '/../../../fixtures/update/drupal-8.rest-hal_update_8301.php',
];
}
/**
* Tests hal_update_8301().
*/
public function testLinkDomainMigratedFromRestSettingsToHalSettings() {
// Make sure we have the expected values before the update.
$hal_settings = $this->config('hal.settings');
$this->assertIdentical([], $hal_settings->getRawData());
$rest_settings = $this->config('rest.settings');
$this->assertTrue(array_key_exists('link_domain', $rest_settings->getRawData()));
$this->assertIdentical('http://example.com', $rest_settings->getRawData()['link_domain']);
$this->runUpdates();
// Make sure we have the expected values after the update.
$hal_settings = \Drupal::configFactory()->get('hal.settings');
$this->assertTrue(array_key_exists('link_domain', $hal_settings->getRawData()));
$this->assertIdentical('http://example.com', $hal_settings->getRawData()['link_domain']);
$rest_settings = $this->config('rest.settings');
$this->assertFalse(array_key_exists('link_domain', $rest_settings->getRawData()));
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Tests HAL denormalization edge cases for EntityResource.
*
* @group hal
*/
class DenormalizeTest extends NormalizerTestBase {
/**
* Tests that the type link relation in incoming data is handled correctly.
*/
public function testTypeHandling() {
// Valid type.
$data_with_valid_type = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/type/entity_test/entity_test', ['absolute' => TRUE])->toString(),
],
],
];
$denormalized = $this->serializer->denormalize($data_with_valid_type, $this->entityClass, $this->format);
$this->assertEqual(get_class($denormalized), $this->entityClass, 'Request with valid type results in creation of correct bundle.');
// Multiple types.
$data_with_multiple_types = [
'_links' => [
'type' => [
[
'href' => Url::fromUri('base:rest/types/foo', ['absolute' => TRUE])->toString(),
],
[
'href' => Url::fromUri('base:rest/type/entity_test/entity_test', ['absolute' => TRUE])->toString(),
],
],
],
];
$denormalized = $this->serializer->denormalize($data_with_multiple_types, $this->entityClass, $this->format);
$this->assertEqual(get_class($denormalized), $this->entityClass, 'Request with multiple types results in creation of correct bundle.');
// Invalid type.
$data_with_invalid_type = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/types/foo', ['absolute' => TRUE])->toString(),
],
],
];
try {
$this->serializer->denormalize($data_with_invalid_type, $this->entityClass, $this->format);
$this->fail('Exception should be thrown when type is invalid.');
}
catch (UnexpectedValueException $e) {
$this->pass('Exception thrown when type is invalid.');
}
// No type.
$data_with_no_type = [
'_links' => [],
];
try {
$this->serializer->denormalize($data_with_no_type, $this->entityClass, $this->format);
$this->fail('Exception should be thrown when no type is provided.');
}
catch (UnexpectedValueException $e) {
$this->pass('Exception thrown when no type is provided.');
}
}
/**
* Tests link relation handling with an invalid type.
*/
public function testTypeHandlingWithInvalidType() {
$data_with_invalid_type = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/type/entity_test/entity_test_invalid', ['absolute' => TRUE])->toString(),
],
],
];
$this->setExpectedException(UnexpectedValueException::class);
$this->serializer->denormalize($data_with_invalid_type, $this->entityClass, $this->format);
}
/**
* Tests link relation handling with no types.
*/
public function testTypeHandlingWithNoTypes() {
$data_with_no_types = [
'_links' => [
'type' => [],
],
];
$this->setExpectedException(UnexpectedValueException::class);
$this->serializer->denormalize($data_with_no_types, $this->entityClass, $this->format);
}
/**
* Test that a field set to an empty array is different than an absent field.
*/
public function testMarkFieldForDeletion() {
// Add a default value for a field.
$field = FieldConfig::loadByName('entity_test', 'entity_test', 'field_test_text');
$field->setDefaultValue([['value' => 'Llama']]);
$field->save();
// Denormalize data that contains no entry for the field, and check that
// the default value is present in the resulting entity.
$data = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/type/entity_test/entity_test', ['absolute' => TRUE])->toString(),
],
],
];
$entity = $this->serializer->denormalize($data, $this->entityClass, $this->format);
$this->assertEqual($entity->field_test_text->count(), 1);
$this->assertEqual($entity->field_test_text->value, 'Llama');
// Denormalize data that contains an empty entry for the field, and check
// that the field is empty in the resulting entity.
$data = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/type/entity_test/entity_test', ['absolute' => TRUE])->toString(),
],
],
'field_test_text' => [],
];
$entity = $this->serializer->denormalize($data, get_class($entity), $this->format, ['target_instance' => $entity]);
$this->assertEqual($entity->field_test_text->count(), 0);
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
use Drupal\node\Entity\NodeType;
/**
* Tests that translated nodes are correctly (de-)normalized.
*
* @group hal
*/
class EntityTranslationNormalizeTest extends NormalizerTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'content_translation'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['sequences']);
$this->installConfig(['node', 'content_translation']);
}
/**
* Tests the normalization of node translations.
*/
public function testNodeTranslation() {
$node_type = NodeType::create(['type' => 'example_type']);
$node_type->save();
$this->container->get('content_translation.manager')->setEnabled('node', 'example_type', TRUE);
$user = User::create(['name' => $this->randomMachineName()]);
$user->save();
$node = Node::create([
'title' => $this->randomMachineName(),
'uid' => (int) $user->id(),
'type' => $node_type->id(),
'status' => NodeInterface::PUBLISHED,
'langcode' => 'en',
'promote' => 1,
'sticky' => 0,
'body' => [
'value' => $this->randomMachineName(),
'format' => $this->randomMachineName(),
],
'revision_log' => $this->randomString(),
]);
$node->addTranslation('de', [
'title' => 'German title',
'body' => [
'value' => $this->randomMachineName(),
'format' => $this->randomMachineName(),
],
]);
$node->save();
$original_values = $node->toArray();
$translation = $node->getTranslation('de');
$original_translation_values = $node->getTranslation('en')->toArray();
$normalized = $this->serializer->normalize($node, $this->format);
$this->assertContains(['lang' => 'en', 'value' => $node->getTitle()], $normalized['title'], 'Original language title has been normalized.');
$this->assertContains(['lang' => 'de', 'value' => $translation->getTitle()], $normalized['title'], 'Translation language title has been normalized.');
/** @var \Drupal\node\NodeInterface $denormalized_node */
$denormalized_node = $this->serializer->denormalize($normalized, 'Drupal\node\Entity\Node', $this->format);
$this->assertSame($denormalized_node->language()->getId(), $denormalized_node->getUntranslated()->language()->getId(), 'Untranslated object is returned from serializer.');
$this->assertSame('en', $denormalized_node->language()->getId());
$this->assertTrue($denormalized_node->hasTranslation('de'));
$this->assertSame($node->getTitle(), $denormalized_node->getTitle());
$this->assertSame($translation->getTitle(), $denormalized_node->getTranslation('de')->getTitle());
$original_values['revision_default'] = [];
$original_translation_values['revision_default'] = [];
$this->assertEquals($original_values, $denormalized_node->toArray(), 'Node values are restored after normalizing and denormalizing.');
$this->assertEquals($original_translation_values, $denormalized_node->getTranslation('en')->toArray(), 'Node values are restored after normalizing and denormalizing.');
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\file\Entity\File;
/**
* Tests that file entities can be normalized in HAL.
*
* @group hal
*/
class FileNormalizeTest extends NormalizerTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['file'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('file');
}
/**
* Tests the normalize function.
*/
public function testNormalize() {
$file_params = [
'filename' => 'test_1.txt',
'uri' => 'public://test_1.txt',
'filemime' => 'text/plain',
'status' => FILE_STATUS_PERMANENT,
];
// Create a new file entity.
$file = File::create($file_params);
file_put_contents($file->getFileUri(), 'hello world');
$file->save();
$expected_array = [
'uri' => [
[
'value' => $file->getFileUri(),
'url' => file_url_transform_relative(file_create_url($file->getFileUri())),
],
],
];
$normalized = $this->serializer->normalize($file, $this->format);
$this->assertEqual($normalized['uri'], $expected_array['uri'], 'URI is normalized.');
}
}

View file

@ -0,0 +1,278 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
/**
* @coversDefaultClass \Drupal\hal\LinkManager\LinkManager
* @group hal
* @group legacy
*/
class HalLinkManagerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'hal_test', 'serialization', 'system', 'node', 'user', 'field'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
NodeType::create([
'type' => 'page',
])->save();
FieldStorageConfig::create([
'entity_type' => 'node',
'type' => 'entity_reference',
'field_name' => 'field_ref',
])->save();
FieldConfig::create([
'entity_type' => 'node',
'bundle' => 'page',
'field_name' => 'field_ref',
])->save();
\Drupal::service('router.builder')->rebuild();
}
/**
* @covers ::getTypeUri
* @dataProvider providerTestGetTypeUri
* @expectedDeprecation The deprecated alter hook hook_rest_type_uri_alter() is implemented in these functions: hal_test_rest_type_uri_alter. This hook is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Implement hook_hal_type_uri_alter() instead.
*/
public function testGetTypeUri($link_domain, $entity_type, $bundle, array $context, $expected_return, array $expected_context) {
$hal_settings = \Drupal::configFactory()->getEditable('hal.settings');
if ($link_domain === NULL) {
$hal_settings->clear('link_domain');
}
else {
$hal_settings->set('link_domain', $link_domain)->save(TRUE);
}
/* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_manager */
$type_manager = \Drupal::service('hal.link_manager.type');
$link = $type_manager->getTypeUri($entity_type, $bundle, $context);
$this->assertSame($link, str_replace('BASE_URL/', Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(), $expected_return));
$this->assertEquals($context, $expected_context);
}
public function providerTestGetTypeUri() {
$serialization_context_collecting_cacheability = [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),
];
$expected_serialization_context_cacheability_url_site = [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => (new CacheableMetadata())->setCacheContexts(['url.site']),
];
$base_test_case = [
'link_domain' => NULL,
'entity_type' => 'node',
'bundle' => 'page',
];
return [
'site URL' => $base_test_case + [
'context' => [],
'link_domain' => NULL,
'expected return' => 'BASE_URL/rest/type/node/page',
'expected context' => [],
],
'site URL, with optional context to collect cacheability metadata' => $base_test_case + [
'context' => $serialization_context_collecting_cacheability,
'expected return' => 'BASE_URL/rest/type/node/page',
'expected context' => $expected_serialization_context_cacheability_url_site,
],
// Test hook_hal_type_uri_alter().
'site URL, with optional context, to test hook_hal_type_uri_alter()' => $base_test_case + [
'context' => ['hal_test' => TRUE],
'expected return' => 'hal_test_type',
'expected context' => ['hal_test' => TRUE],
],
'site URL, with optional context, to test hook_hal_type_uri_alter(), and collecting cacheability metadata' => $base_test_case + [
'context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability,
'expected return' => 'hal_test_type',
// No cacheability metadata bubbled.
'expected context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability,
],
// Test hook_rest_type_uri_alter() — for backwards compatibility.
'site URL, with optional context, to test hook_rest_type_uri_alter()' => $base_test_case + [
'context' => ['rest_test' => TRUE],
'expected return' => 'rest_test_type',
'expected context' => ['rest_test' => TRUE],
],
'site URL, with optional context, to test hook_rest_type_uri_alter(), and collecting cacheability metadata' => $base_test_case + [
'context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability,
'expected return' => 'rest_test_type',
// No cacheability metadata bubbled.
'expected context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability,
],
'configured URL' => [
'link_domain' => 'http://llamas-rock.com/for-real/',
'entity_type' => 'node',
'bundle' => 'page',
'context' => [],
'expected return' => 'http://llamas-rock.com/for-real/rest/type/node/page',
'expected context' => [],
],
'configured URL, with optional context to collect cacheability metadata' => [
'link_domain' => 'http://llamas-rock.com/for-real/',
'entity_type' => 'node',
'bundle' => 'page',
'context' => $serialization_context_collecting_cacheability,
'expected return' => 'http://llamas-rock.com/for-real/rest/type/node/page',
'expected context' => [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => (new CacheableMetadata())->setCacheTags(['config:hal.settings']),
],
],
];
}
/**
* @covers ::getRelationUri
* @dataProvider providerTestGetRelationUri
* @expectedDeprecation The deprecated alter hook hook_rest_relation_uri_alter() is implemented in these functions: hal_test_rest_relation_uri_alter. This hook is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Implement hook_hal_relation_uri_alter() instead.
*/
public function testGetRelationUri($link_domain, $entity_type, $bundle, $field_name, array $context, $expected_return, array $expected_context) {
$hal_settings = \Drupal::configFactory()->getEditable('hal.settings');
if ($link_domain === NULL) {
$hal_settings->clear('link_domain');
}
else {
$hal_settings->set('link_domain', $link_domain)->save(TRUE);
}
/* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_manager */
$relation_manager = \Drupal::service('hal.link_manager.relation');
$link = $relation_manager->getRelationUri($entity_type, $bundle, $field_name, $context);
$this->assertSame($link, str_replace('BASE_URL/', Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(), $expected_return));
$this->assertEquals($context, $expected_context);
}
public function providerTestGetRelationUri() {
$serialization_context_collecting_cacheability = [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),
];
$expected_serialization_context_cacheability_url_site = [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => (new CacheableMetadata())->setCacheContexts(['url.site']),
];
$field_name = $this->randomMachineName();
$base_test_case = [
'link_domain' => NULL,
'entity_type' => 'node',
'bundle' => 'page',
'field_name' => $field_name,
];
return [
'site URL' => $base_test_case + [
'context' => [],
'link_domain' => NULL,
'expected return' => 'BASE_URL/rest/relation/node/page/' . $field_name,
'expected context' => [],
],
'site URL, with optional context to collect cacheability metadata' => $base_test_case + [
'context' => $serialization_context_collecting_cacheability,
'expected return' => 'BASE_URL/rest/relation/node/page/' . $field_name,
'expected context' => $expected_serialization_context_cacheability_url_site,
],
// Test hook_hal_relation_uri_alter().
'site URL, with optional context, to test hook_hal_relation_uri_alter()' => $base_test_case + [
'context' => ['hal_test' => TRUE],
'expected return' => 'hal_test_relation',
'expected context' => ['hal_test' => TRUE],
],
'site URL, with optional context, to test hook_hal_relation_uri_alter(), and collecting cacheability metadata' => $base_test_case + [
'context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability,
'expected return' => 'hal_test_relation',
// No cacheability metadata bubbled.
'expected context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability,
],
// Test hook_rest_relation_uri_alter() — for backwards compatibility.
'site URL, with optional context, to test hook_rest_relation_uri_alter()' => $base_test_case + [
'context' => ['rest_test' => TRUE],
'expected return' => 'rest_test_relation',
'expected context' => ['rest_test' => TRUE],
],
'site URL, with optional context, to test hook_rest_relation_uri_alter(), and collecting cacheability metadata' => $base_test_case + [
'context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability,
'expected return' => 'rest_test_relation',
// No cacheability metadata bubbled.
'expected context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability,
],
'configured URL' => [
'link_domain' => 'http://llamas-rock.com/for-real/',
'entity_type' => 'node',
'bundle' => 'page',
'field_name' => $field_name,
'context' => [],
'expected return' => 'http://llamas-rock.com/for-real/rest/relation/node/page/' . $field_name,
'expected context' => [],
],
'configured URL, with optional context to collect cacheability metadata' => [
'link_domain' => 'http://llamas-rock.com/for-real/',
'entity_type' => 'node',
'bundle' => 'page',
'field_name' => $field_name,
'context' => $serialization_context_collecting_cacheability,
'expected return' => 'http://llamas-rock.com/for-real/rest/relation/node/page/' . $field_name,
'expected context' => [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => (new CacheableMetadata())->setCacheTags(['config:hal.settings']),
],
],
];
}
/**
* @covers ::getRelationInternalIds
*/
public function testGetRelationInternalIds() {
/* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_manager */
$relation_manager = \Drupal::service('hal.link_manager.relation');
$link = $relation_manager->getRelationUri('node', 'page', 'field_ref');
$internal_ids = $relation_manager->getRelationInternalIds($link);
$this->assertEquals([
'entity_type_id' => 'node',
'entity_type' => \Drupal::entityTypeManager()->getDefinition('node'),
'bundle' => 'page',
'field_name' => 'field_ref',
], $internal_ids);
}
/**
* @covers ::setLinkDomain
*/
public function testHalLinkManagersSetLinkDomain() {
$serialization_context = [
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),
];
/* @var \Drupal\rest\LinkManager\LinkManager $link_manager */
$link_manager = \Drupal::service('hal.link_manager');
$link_manager->setLinkDomain('http://example.com/');
$link = $link_manager->getTypeUri('node', 'page', $serialization_context);
$this->assertEqual($link, 'http://example.com/rest/type/node/page');
$this->assertEqual(new CacheableMetadata(), $serialization_context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]);
$link = $link_manager->getRelationUri('node', 'page', 'field_ref', $serialization_context);
$this->assertEqual($link, 'http://example.com/rest/relation/node/page/field_ref');
$this->assertEqual(new CacheableMetadata(), $serialization_context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]);
}
}

View file

@ -0,0 +1,208 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests HAL normalization edge cases for EntityResource.
*
* @group hal
*/
class NormalizeTest extends NormalizerTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My Text Format',
'filters' => [
'filter_html' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [
'allowed_html' => '<p>',
],
],
'filter_autop' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [],
],
],
])->save();
\Drupal::service('router.builder')->rebuild();
}
/**
* Tests the normalize function.
*/
public function testNormalize() {
$target_entity_de = EntityTest::create((['langcode' => 'de', 'field_test_entity_reference' => NULL]));
$target_entity_de->save();
$target_entity_en = EntityTest::create((['langcode' => 'en', 'field_test_entity_reference' => NULL]));
$target_entity_en->save();
// Create a German entity.
$values = [
'langcode' => 'de',
'name' => $this->randomMachineName(),
'field_test_text' => [
'value' => $this->randomMachineName(),
'format' => 'my_text_format',
],
'field_test_entity_reference' => [
'target_id' => $target_entity_de->id(),
],
];
// Array of translated values.
$translation_values = [
'name' => $this->randomMachineName(),
'field_test_entity_reference' => [
'target_id' => $target_entity_en->id(),
],
];
$entity = EntityTest::create($values);
$entity->save();
// Add an English value for name and entity reference properties.
$entity->addTranslation('en')->set('name', [0 => ['value' => $translation_values['name']]]);
$entity->getTranslation('en')->set('field_test_entity_reference', [0 => $translation_values['field_test_entity_reference']]);
$entity->save();
$type_uri = Url::fromUri('base:rest/type/entity_test/entity_test', ['absolute' => TRUE])->toString();
$relation_uri = Url::fromUri('base:rest/relation/entity_test/entity_test/field_test_entity_reference', ['absolute' => TRUE])->toString();
$expected_array = [
'_links' => [
'curies' => [
[
'href' => '/relations',
'name' => 'site',
'templated' => TRUE,
],
],
'self' => [
'href' => $this->getEntityUri($entity),
],
'type' => [
'href' => $type_uri,
],
$relation_uri => [
[
'href' => $this->getEntityUri($target_entity_de),
'lang' => 'de',
],
[
'href' => $this->getEntityUri($target_entity_en),
'lang' => 'en',
],
],
],
'_embedded' => [
$relation_uri => [
[
'_links' => [
'self' => [
'href' => $this->getEntityUri($target_entity_de),
],
'type' => [
'href' => $type_uri,
],
],
'uuid' => [
[
'value' => $target_entity_de->uuid(),
],
],
'lang' => 'de',
],
[
'_links' => [
'self' => [
'href' => $this->getEntityUri($target_entity_en),
],
'type' => [
'href' => $type_uri,
],
],
'uuid' => [
[
'value' => $target_entity_en->uuid(),
],
],
'lang' => 'en',
],
],
],
'id' => [
[
'value' => $entity->id(),
],
],
'uuid' => [
[
'value' => $entity->uuid(),
],
],
'langcode' => [
[
'value' => 'de',
],
],
'name' => [
[
'value' => $values['name'],
'lang' => 'de',
],
[
'value' => $translation_values['name'],
'lang' => 'en',
],
],
'field_test_text' => [
[
'value' => $values['field_test_text']['value'],
'format' => $values['field_test_text']['format'],
'processed' => "<p>{$values['field_test_text']['value']}</p>",
],
],
];
$normalized = $this->serializer->normalize($entity, $this->format);
$this->assertEqual($normalized['_links']['self'], $expected_array['_links']['self'], 'self link placed correctly.');
// @todo Test curies.
// @todo Test type.
$this->assertEqual($normalized['id'], $expected_array['id'], 'Internal id is exposed.');
$this->assertEqual($normalized['uuid'], $expected_array['uuid'], 'Non-translatable fields is normalized.');
$this->assertEqual($normalized['name'], $expected_array['name'], 'Translatable field with multiple language values is normalized.');
$this->assertEqual($normalized['field_test_text'], $expected_array['field_test_text'], 'Field with properties is normalized.');
$this->assertEqual($normalized['_embedded'][$relation_uri], $expected_array['_embedded'][$relation_uri], 'Entity reference field is normalized.');
$this->assertEqual($normalized['_links'][$relation_uri], $expected_array['_links'][$relation_uri], 'Links are added for entity reference field.');
}
/**
* Constructs the entity URI.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return string
* The entity URI.
*/
protected function getEntityUri(EntityInterface $entity) {
$url = $entity->urlInfo('canonical', ['absolute' => TRUE]);
return $url->setRouteParameter('_format', 'hal_json')->toString();
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace Drupal\Tests\hal\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\KernelTests\KernelTestBase;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Test the HAL normalizer.
*/
abstract class NormalizerTestBase extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['entity_test', 'field', 'hal', 'language', 'serialization', 'system', 'text', 'user', 'filter'];
/**
* The mock serializer.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The format being tested.
*
* @var string
*/
protected $format = 'hal_json';
/**
* The class name of the test class.
*
* @var string
*/
protected $entityClass = 'Drupal\entity_test\Entity\EntityTest';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
// If the concrete test sub-class installs the Node or Comment modules,
// ensure that the node and comment entity schema are created before the
// field configurations are installed. This is because the entity tables
// need to be created before the body field storage tables. This prevents
// trying to create the body field tables twice.
$class = get_class($this);
while ($class) {
if (property_exists($class, 'modules')) {
// Only check the modules, if the $modules property was not inherited.
$rp = new \ReflectionProperty($class, 'modules');
if ($rp->class == $class) {
foreach (array_intersect(['node', 'comment'], $class::$modules) as $module) {
$this->installEntitySchema($module);
}
}
}
$class = get_parent_class($class);
}
$this->installConfig(['field', 'language']);
\Drupal::service('router.builder')->rebuild();
// Add German as a language.
ConfigurableLanguage::create([
'id' => 'de',
'label' => 'Deutsch',
'weight' => -1,
])->save();
// Create the test text field.
FieldStorageConfig::create([
'field_name' => 'field_test_text',
'entity_type' => 'entity_test',
'type' => 'text',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test_text',
'bundle' => 'entity_test',
'translatable' => FALSE,
])->save();
// Create the test translatable field.
FieldStorageConfig::create([
'field_name' => 'field_test_translatable_text',
'entity_type' => 'entity_test',
'type' => 'text',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test_translatable_text',
'bundle' => 'entity_test',
'translatable' => TRUE,
])->save();
// Create the test entity reference field.
FieldStorageConfig::create([
'field_name' => 'field_test_entity_reference',
'entity_type' => 'entity_test',
'type' => 'entity_reference',
'settings' => [
'target_type' => 'entity_test',
],
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'field_name' => 'field_test_entity_reference',
'bundle' => 'entity_test',
'translatable' => TRUE,
])->save();
$this->serializer = $this->container->get('serializer');
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\hal\Unit;
use Drupal\hal\Normalizer\FieldItemNormalizer;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* @coversDefaultClass \Drupal\hal\Normalizer\FieldItemNormalizer
* @group hal
*/
class FieldItemNormalizerDenormalizeExceptionsUnitTest extends NormalizerDenormalizeExceptionsUnitTestBase {
/**
* Tests that the FieldItemNormalizer::denormalize() throws proper exceptions.
*
* @param array $context
* Context for FieldItemNormalizer::denormalize().
*
* @dataProvider providerNormalizerDenormalizeExceptions
*/
public function testFieldItemNormalizerDenormalizeExceptions($context) {
$field_item_normalizer = new FieldItemNormalizer();
$data = [];
$class = [];
$this->setExpectedException(InvalidArgumentException::class);
$field_item_normalizer->denormalize($data, $class, NULL, $context);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\hal\Unit;
use Drupal\hal\Normalizer\FieldNormalizer;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* @coversDefaultClass \Drupal\hal\Normalizer\FieldNormalizer
* @group hal
*/
class FieldNormalizerDenormalizeExceptionsUnitTest extends NormalizerDenormalizeExceptionsUnitTestBase {
/**
* Tests that the FieldNormalizer::denormalize() throws proper exceptions.
*
* @param array $context
* Context for FieldNormalizer::denormalize().
*
* @dataProvider providerNormalizerDenormalizeExceptions
*/
public function testFieldNormalizerDenormalizeExceptions($context) {
$field_item_normalizer = new FieldNormalizer();
$data = [];
$class = [];
$this->setExpectedException(InvalidArgumentException::class);
$field_item_normalizer->denormalize($data, $class, NULL, $context);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\hal\Unit;
use Drupal\Tests\UnitTestCase;
/**
* Common ancestor for FieldItemNormalizerDenormalizeExceptionsUnitTest and
* FieldNormalizerDenormalizeExceptionsUnitTest as they have the same
* dataProvider.
*/
abstract class NormalizerDenormalizeExceptionsUnitTestBase extends UnitTestCase {
/**
* Provides data for FieldItemNormalizerDenormalizeExceptionsUnitTest::testFieldItemNormalizerDenormalizeExceptions()
* and for FieldNormalizerDenormalizeExceptionsUnitTest::testFieldNormalizerDenormalizeExceptions().
*
* @return array Test data.
*/
public function providerNormalizerDenormalizeExceptions() {
$mock = $this->getMock('\Drupal\Core\Field\Plugin\DataType\FieldItem', ['getParent']);
$mock->expects($this->any())
->method('getParent')
->will($this->returnValue(NULL));
return [
[[]],
[['target_instance' => $mock]],
];
}
}