clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\jsonapi_extras\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Plugin annotation object for resource field enhancers.
*
* @see \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface
*
* @Annotation
*/
class ResourceFieldEnhancer extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the formatter type.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A short description of the formatter type.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
/**
* The name of the field formatter class.
*
* This is not provided manually, it will be added by the discovery mechanism.
*
* @var string
*/
public $class;
/**
* The name of modules that are required for this Field Enhancer to be usable.
*
* @var array
*/
public $dependencies;
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Drupal\jsonapi_extras\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\jsonapi\Routing\Routes;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
/**
* Defines the JSON:API Resource Config entity.
*
* @ConfigEntityType(
* id = "jsonapi_resource_config",
* label = @Translation("JSON:API Resource override"),
* label_collection = @Translation("JSON:API Resource overrides"),
* label_singular = @Translation("JSON:API resource override"),
* label_plural = @Translation("JSON:API resource overrides"),
* label_count = @PluralTranslation(
* singular = "@count JSON:API resource override",
* plural = "@count JSON:API resource overrides",
* ),
* handlers = {
* "list_builder" = "Drupal\jsonapi_extras\JsonapiResourceConfigListBuilder",
* "form" = {
* "add" = "Drupal\jsonapi_extras\Form\JsonapiResourceConfigForm",
* "edit" = "Drupal\jsonapi_extras\Form\JsonapiResourceConfigForm",
* "delete" = "Drupal\jsonapi_extras\Form\JsonapiResourceConfigDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* },
* },
* config_prefix = "jsonapi_resource_config",
* admin_permission = "administer site configuration",
* static_cache = TRUE,
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
* },
* config_export = {
* "id",
* "disabled",
* "path",
* "resourceType",
* "resourceFields",
* },
* links = {
* "add-form" = "/admin/config/services/jsonapi/add/resource_types/{entity_type_id}/{bundle}",
* "edit-form" = "/admin/config/services/jsonapi/resource_types/{jsonapi_resource_config}/edit",
* "delete-form" = "/admin/config/services/jsonapi/resource_types/{jsonapi_resource_config}/delete",
* "collection" = "/admin/config/services/jsonapi/resource_types"
* }
* )
*/
class JsonapiResourceConfig extends ConfigEntityBase {
/**
* The JSON:API Resource Config ID.
*
* @var string
*/
protected $id;
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
static::rebuildRoutes();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
static::rebuildRoutes();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
$id = explode('--', $this->id);
$typeManager = $this->entityTypeManager();
$dependency = $typeManager->getDefinition($id[0])->getBundleConfigDependency($id[1]);
$this->addDependency($dependency['type'], $dependency['name']);
return $this;
}
/**
* {@inheritdoc}
*/
protected function urlRouteParameters($rel) {
$uri_route_parameters = parent::urlRouteParameters($rel);
// The add-form route depends on entity_type_id and bundle.
if (in_array($rel, ['add-form'])) {
$parameters = explode('--', $this->id);
$uri_route_parameters['entity_type_id'] = $parameters[0];
$uri_route_parameters['bundle'] = $parameters[1];
}
return $uri_route_parameters;
}
/**
* Triggers rebuilding of JSON:API routes.
*/
protected static function rebuildRoutes() {
try {
ConfigurableResourceTypeRepository::reset();
Routes::rebuild();
}
catch (ServiceNotFoundException $exception) {
// This is intentionally empty.
}
}
/**
* Returns a field mapping as expected by JSON:API 2.x' ResourceType class.
*
* @see \Drupal\jsonapi\ResourceType\ResourceType::__construct()
*/
public function getFieldMapping() {
$resource_fields = $this->get('resourceFields') ?: [];
$mapping = [];
foreach ($resource_fields as $resource_field) {
$field_name = $resource_field['fieldName'];
if ($resource_field['disabled'] === TRUE) {
$mapping[$field_name] = FALSE;
continue;
}
if (($alias = $resource_field['publicName']) && $alias !== $field_name) {
$mapping[$field_name] = $alias;
continue;
}
$mapping[$field_name] = TRUE;
}
return $mapping;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\jsonapi_extras;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Url;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Simplifies the process of generating a JSON:API version of an entity.
*
* @api
*/
class EntityToJsonApi {
/**
* The HTTP kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* The JSON:API Resource Type Repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
*/
protected $resourceTypeRepository;
/**
* A Session object.
*
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request|null
*/
protected $currentRequest;
/**
* EntityToJsonApi constructor.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* The HTTP kernel.
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* The resource type repository.
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The session object.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The stack of requests.
*/
public function __construct(
HttpKernelInterface $http_kernel,
ResourceTypeRepositoryInterface $resource_type_repository,
SessionInterface $session,
RequestStack $request_stack,
) {
$this->httpKernel = $http_kernel;
$this->resourceTypeRepository = $resource_type_repository;
$this->currentRequest = $request_stack->getCurrentRequest();
$this->session = $this->currentRequest->hasPreviousSession()
? $this->currentRequest->getSession()
: $session;
}
/**
* Return the requested entity as a raw string.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to generate the JSON from.
* @param string[] $includes
* The list of includes.
*
* @return string
* The raw JSON string of the requested resource.
*
* @throws \Exception
*/
public function serialize(EntityInterface $entity, array $includes = []) {
$resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
$route_name = sprintf('jsonapi.%s.individual', $resource_type->getTypeName());
$route_options = [];
if ($resource_type->isVersionable() && $entity instanceof RevisionableInterface && $revision_id = $entity->getRevisionId()) {
$route_options['query']['resourceVersion'] = 'id:' . $revision_id;
}
$jsonapi_url = Url::fromRoute($route_name, ['entity' => $entity->uuid()], $route_options)
->toString(TRUE)
->getGeneratedUrl();
$query = [];
if ($includes) {
$query = ['include' => implode(',', $includes)];
}
$request = Request::create(
$jsonapi_url,
'GET',
$query,
$this->currentRequest->cookies->all(),
[],
$this->currentRequest->server->all()
);
if ($this->session) {
$request->setSession($this->session);
}
$response = $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// Get contents and terminate response before returning results.
$content = $response->getContent();
$this->httpKernel->terminate($request, $response);
return $content;
}
/**
* Return the requested entity as an structured array.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to generate the JSON from.
* @param string[] $includes
* The list of includes.
*
* @return array
* The JSON structure of the requested resource.
*
* @throws \Exception
*/
public function normalize(EntityInterface $entity, array $includes = []) {
return Json::decode($this->serialize($entity, $includes));
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Drupal\jsonapi_extras\EventSubscriber;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Associates config cache tag and rebuilds container + routes when necessary.
*/
class ConfigSubscriber implements EventSubscriberInterface {
/**
* The Drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $drupalKernel;
/**
* The route building service.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* The route builder.
*
* @var Drupal\Core\Routing\RouteBuilder
*/
protected $service;
/**
* Constructs a ConfigSubscriber object.
*
* @param \Drupal\Core\DrupalKernelInterface $drupal_kernel
* The Drupal kernel.
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route building service.
*/
public function __construct(DrupalKernelInterface $drupal_kernel, RouteBuilderInterface $route_builder) {
$this->drupalKernel = $drupal_kernel;
$this->routeBuilder = $route_builder;
}
/**
* Rebuilds container and routes when 'path_prefix' configuration is changed.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
$container = \Drupal::getContainer();
// It is problematic to rebuild the container during the installation.
$should_process = $container->getParameter('kernel.environment') !== 'install'
&& (!$container->hasParameter('jsonapi_extras.base_path_override_disabled') || !$container->getParameter('jsonapi_extras.base_path_override_disabled'))
&& $event->getConfig()->getName() === 'jsonapi_extras.settings';
if ($should_process) {
// @see \Drupal\jsonapi_extras\JsonapiExtrasServiceProvider::alter()
if ($event->isChanged('path_prefix')) {
$this->drupalKernel->rebuildContainer();
// Because \Drupal\jsonapi\Routing\Routes::routes() uses a container
// parameter, we need to ensure that it uses the freshly rebuilt
// container. Due to that, it's impossible to use an injected route
// builder service, at least until core updates it to support
// \Drupal\Core\DrupalKernelInterface::CONTAINER_INITIALIZE_SUBREQUEST_FINISHED.
$this->service = $container->get('router.builder');
$container->get('router.builder')->rebuild();
}
}
}
/**
* Associates JSON:API Extras' config cache tag with all JSON:API responses.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The response event.
*/
public function onResponse(ResponseEvent $event) {
if ($event->getRequest()->getRequestFormat() !== 'api_json') {
return;
}
$response = $event->getResponse();
if (!$response instanceof CacheableResponseInterface) {
return;
}
$response->getCacheableMetadata()
->addCacheTags([
'config:jsonapi_extras.settings',
'config:jsonapi_resource_config_list',
]);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[ConfigEvents::SAVE][] = ['onSave'];
// Run before
// \Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber::onResponse()
// (priority 128), so we can add JSON:API's config cache tag.
$events[KernelEvents::RESPONSE][] = ['onResponse', 150];
return $events;
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Drupal\jsonapi_extras\EventSubscriber;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Drupal\jsonapi_extras\ResourceType\NullJsonapiResourceConfig;
/**
* Makes sure that all resource config entities contain settings for all fields.
*
* This will avoid the use of default behavior when a field exists in an entity
* but there is no config about it. This typically happens when the field is
* added after the resource config was initially saved.
*/
class FieldConfigIntegrityValidation extends ConfigImportValidateEventSubscriberBase {
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
private ConfigManagerInterface $configManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private EntityTypeManagerInterface $entityTypeManager;
/**
* The resource type repository.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository
*/
private ConfigurableResourceTypeRepository $resourceTypeRepository;
/**
* Creates a new validator.
*
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository $resource_type_repository
* The resource type repository.
*/
public function __construct(ConfigManagerInterface $config_manager, EntityTypeManagerInterface $entity_type_manager, ConfigurableResourceTypeRepository $resource_type_repository) {
$this->configManager = $config_manager;
$this->entityTypeManager = $entity_type_manager;
$this->resourceTypeRepository = $resource_type_repository;
}
/**
* {@inheritDoc}
*/
public function onConfigImporterValidate(ConfigImporterEvent $event) {
$jsonapi_extras_settings = $this->configManager
->getConfigFactory()
->get('jsonapi_extras.settings');
if (!$jsonapi_extras_settings->get('validate_configuration_integrity')) {
// Nothing to do.
return;
}
$config_importer = $event->getConfigImporter();
// Get the configuration ready to be imported. Future configuration.
$changelist = $event->getChangelist();
// Determine if any fields are being updated, if so grab their entity type
// ID and bundle.
$changed_info = $this->getChangedFields($changelist, $config_importer);
array_map(
function (array $info) use ($config_importer) {
$entity_type_id = $info['entity_type'];
$bundle = $info['bundle'] ?? $entity_type_id;
$field_name = $info['field_name'];
// First check if the configuration for JSON:API Extras will be
// installed.
$resource_config_name = sprintf(
'%s.%s--%s',
$this->entityTypeManager->getDefinition('jsonapi_resource_config')->getConfigPrefix(),
$entity_type_id,
$bundle
);
// Check weather the field changes are accompanied by a resource change.
$new_resource_config = $config_importer->getStorageComparer()->getSourceStorage()->read($resource_config_name);
if ($new_resource_config && !empty($new_resource_config['resourceFields'][$field_name])) {
// All good. There are new fields, but they are coming in with the
// resource config as well.
return;
}
// Next let's grab the current configuration to see if there was
// configuration for that field already.
$resource_type = $this->resourceTypeRepository->get(
$entity_type_id,
$bundle,
);
if (!$resource_type instanceof ConfigurableResourceType) {
return;
}
// Make sure there is configuration associated to the resource type,
// otherwise there is nothing to do.
$current_config = $resource_type->getJsonapiResourceConfig();
if ($current_config instanceof NullJsonapiResourceConfig) {
return;
}
$missing = !isset($current_config->get('resourceFields')[$field_name]);
if ($missing) {
$config_importer->logError($this->t(
'Integrity check failed for the JSON:API Extras configuration. There is no configuration set for the field "@field_name" on the resource "@entity_type--@bundle". To fix this, disable the configuration integrity check (in the JSON:API Extras settings page), so you can import these fields locally. After that configure and re-save this resource type in the JSON:API Extras configuration page (@url). Finally, re-enable the configuration integrity checks and export the configuration again.',
[
'@field_name' => $field_name,
'@entity_type' => $entity_type_id,
'@bundle' => $bundle,
'@url' => $current_config->toUrl('edit-form', ['absolute' => TRUE])->toString(TRUE)->getGeneratedUrl(),
],
));
}
},
$changed_info,
);
}
/**
* Get information about the fields being changed, if any.
*
* @param array $changes_per_operation
* The list of changed config names grouped by operation.
* @param \Drupal\Core\Config\ConfigImporter $importer
* The configuration importer.
*
* @return array[]
* A list of associative arrays, each one containing the field name, bundle,
* and entity type of the fields being changed.
*/
private function getChangedFields(array $changes_per_operation, ConfigImporter $importer): array {
// We only care about create and update operations.
$changes_per_operation = array_intersect_key(
$changes_per_operation,
array_flip(['create', 'update'])
);
// Filter sub-arrays to get config names that correspond to field_config.
$field_config_names_per_operation = array_map(
fn(array $config_names) => array_filter(
$config_names,
fn(string $config_name) => $this->configManager->getEntityTypeIdByName($config_name) === 'field_config'
),
$changes_per_operation
);
// Flatten the array.
$field_config_names = array_reduce(
$field_config_names_per_operation,
static fn(array $carry, array $names) => array_unique([...$carry, ...$names]),
[]
);
// Read the configuration object for the field_config coming in, and collect
// the field name, bundle, and entity type.
return array_map(
static fn(string $config_name) => array_intersect_key(
$importer->getStorageComparer()->getSourceStorage()->read($config_name),
array_flip(['entity_type', 'bundle', 'field_name'])),
$field_config_names,
);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Drupal\jsonapi_extras\EventSubscriber;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent;
use Drupal\jsonapi\ResourceType\ResourceTypeBuildEvents;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Drupal\jsonapi_extras\ResourceType\NullJsonapiResourceConfig;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* JSON API build subscriber that applies all changes from extra's to the API.
*/
class JsonApiBuildSubscriber implements EventSubscriberInterface {
/**
* The extra's resource repository.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository
*/
private $repository;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
private $configFactory;
/**
* JsonApiBuildSubscriber constructor.
*
* @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository $repository
* Repository from jsonapi_extras is needed to apply configuration.
* @param \Drupal\Core\Config\ConfigFactoryInterface|null $configFactory
* Config factory.
*/
public function __construct(ConfigurableResourceTypeRepository $repository, ConfigFactoryInterface $configFactory = NULL) {
$this->repository = $repository;
if ($configFactory === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $configFactory argument is deprecated in jsonapi_extras:8.x-3.20 and will be required in jsonapi_extras:8.x-4.0. See https://www.drupal.org/node/3242191', E_USER_DEPRECATED);
$configFactory = \Drupal::configFactory();
}
$this->configFactory = $configFactory;
}
/**
* What events to subscribe to.
*/
public static function getSubscribedEvents(): array {
$events[ResourceTypeBuildEvents::BUILD][] = ['applyResourceConfig'];
return $events;
}
/**
* Apply resource config through the event.
*
* @param \Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent $event
* The build event used to change the resources and fields.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function applyResourceConfig(ResourceTypeBuildEvent $event) {
$resource_config = $this->getResourceConfig($event->getResourceTypeName());
if ($resource_config instanceof NullJsonapiResourceConfig && $this->configFactory->get('jsonapi_extras.settings')->get('default_disabled')) {
$event->disableResourceType();
return;
}
if ($resource_config->get('disabled')) {
$event->disableResourceType();
}
$this->overrideFields($resource_config, $event);
}
/**
* Get a single resource configuration entity by its ID.
*
* @param string $resource_config_id
* The configuration entity ID.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig
* The configuration entity for the resource type.
*/
protected function getResourceConfig($resource_config_id) {
$null_resource = new NullJsonapiResourceConfig(
['id' => $resource_config_id],
'jsonapi_resource_config'
);
try {
$resource_configs = $this->repository->getResourceConfigs();
return $resource_configs[$resource_config_id] ?? $null_resource;
}
catch (PluginException $e) {
return $null_resource;
}
}
/**
* Gets the fields for the given field names and entity type + bundle.
*
* @param \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $resource_config
* The associated resource config.
* @param \Drupal\jsonapi\ResourceType\ResourceTypeBuildEvent $event
* The associated resource config.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function overrideFields(JsonapiResourceConfig $resource_config, ResourceTypeBuildEvent $event) {
// Use the base class to fetch the non-configurable field mappings.
$mappings = $resource_config->getFieldMapping();
// Ignore all the fields that don't have aliases.
$mappings = array_filter($mappings, function ($field_info) {
return $field_info !== TRUE;
});
$fields = $event->getFields();
foreach ($mappings as $internal_name => $mapping) {
if (!isset($fields[$internal_name])) {
continue;
}
if (is_string($mapping)) {
$event->setPublicFieldName($fields[$internal_name], $mapping);
}
if ($mapping === FALSE) {
$event->disableField($fields[$internal_name]);
}
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Drupal\jsonapi_extras\Form;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure JSON:API settings for this site.
*/
class JsonapiExtrasSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected $routerBuilder;
/**
* Resource type repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
*/
protected $jsonApiResourceRepository;
/**
* The dependency injection container.
*
* @var \Drupal\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->routerBuilder = $container->get('router.builder');
$instance->jsonApiResourceRepository = $container->get('jsonapi.resource_type.repository');
$instance->container = $container;
return $instance;
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['jsonapi_extras.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'jsonapi_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('jsonapi_extras.settings');
$path_prefix_default_value = $config->get('path_prefix');
if ($this->isJsonApiBasePathParameterOverrideActive()) {
$path_prefix_default_value = ltrim($this->container->getParameter('jsonapi.base_path'), '/');
}
$form['path_prefix'] = [
'#title' => $this->t('Path prefix'),
'#type' => 'textfield',
'#required' => TRUE,
'#field_prefix' => '/',
'#description' => $this->t('The path prefix for JSON:API.'),
'#disabled' => $this->isJsonApiBasePathParameterOverrideActive(),
'#default_value' => $path_prefix_default_value,
];
if ($this->isJsonApiBasePathParameterOverrideActive()) {
$form['path_prefix']['#description'] = $this->t('@original <strong>This configuration option is disabled because the JSON:API base path is overridden via the <em>jsonapi.base_path</em> container parameter.</strong>', ['@original' => $form['path_prefix']['#description']]);
}
$form['include_count'] = [
'#title' => $this->t('Include count in collection queries'),
'#type' => 'checkbox',
'#description' => $this->t('If activated, all collection responses will return a total record count for the provided query.'),
'#default_value' => $config->get('include_count'),
];
$form['default_disabled'] = [
'#title' => $this->t('Disabled by default'),
'#type' => 'checkbox',
'#description' => $this->t("If activated, all resource types that don't have a matching enabled resource config will be disabled."),
'#default_value' => $config->get('default_disabled'),
];
$form['validate_configuration_integrity'] = [
'#title' => $this->t('Validate config integrity'),
'#type' => 'checkbox',
'#description' => $this->t("Enable a configuration validation step for the fields in your resources. This will ensure that new (and updated) fields also contain configuration for the corresponding resources.<br /><strong>IMPORTANT:</strong> disable this <em>temporarily</em> to allow importing incomplete configuration, so you can fix it locally and export complete configuration. Remember to re-enable this after the configuration has been fixed."),
'#default_value' => $config->get('validate_configuration_integrity'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if (!$this->isJsonApiBasePathParameterOverrideActive() && ($path_prefix = $form_state->getValue('path_prefix'))) {
$this->config('jsonapi_extras.settings')
->set('path_prefix', trim($path_prefix, '/'))
->save();
}
$this->config('jsonapi_extras.settings')
->set('include_count', $form_state->getValue('include_count'))
->set('default_disabled', $form_state->getValue('default_disabled'))
->set('validate_configuration_integrity', $form_state->getValue('validate_configuration_integrity'))
->save();
// Rebuild the router.
$this->routerBuilder->setRebuildNeeded();
// And the resource-type repository.
$this->jsonApiResourceRepository->reset();
Cache::invalidateTags(['jsonapi_resource_types']);
parent::submitForm($form, $form_state);
}
/**
* Checks if jsonapi.base_path container parameter override is active.
*
* @return bool
* TRUE if it is active, FALSE otherwise.
*/
private function isJsonApiBasePathParameterOverrideActive(): bool {
return $this->container->hasParameter('jsonapi_extras.base_path_override_disabled') && $this->container->getParameter('jsonapi_extras.base_path_override_disabled');
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\jsonapi_extras\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Builds the form to delete JSON:API Resource Config entities.
*/
class JsonapiResourceConfigDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to revert %id to default?', ['%id' => $this->entity->id()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.jsonapi_resource_config.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Revert');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
$this->messenger()->addStatus($this->t('Resource %id has been reverted to default.', [
'%id' => $this->entity->id(),
]));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,515 @@
<?php
namespace Drupal\jsonapi_extras\Form;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for jsonapi_resource_config.
*/
class JsonapiResourceConfigForm extends EntityForm {
/**
* The bundle information service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* The JSON:API resource type repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
*/
protected $resourceTypeRepository;
/**
* The field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManager
*/
protected $fieldManager;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* The field enhancer manager.
*
* @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
*/
protected $enhancerManager;
/**
* The JSON:API extras config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The current route match.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->bundleInfo = $container->get('entity_type.bundle.info');
$instance->resourceTypeRepository = $container->get('jsonapi.resource_type.repository');
$instance->fieldManager = $container->get('entity_field.manager');
$instance->entityTypeRepository = $container->get('entity_type.repository');
$instance->enhancerManager = $container->get('plugin.manager.resource_field_enhancer');
$instance->config = $container->get('config.factory')->get('jsonapi_extras.settings');
$instance->request = $container->get('request_stack')->getCurrentRequest();
$instance->typedConfigManager = $container->get('config.typed');
$instance->logger = $container->get('logger.channel.jsonapi_extras');
return $instance;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
// Disable caching on this form.
$form_state->setCached(FALSE);
$entity_type_id = $this->request->get('entity_type_id');
$bundle = $this->request->get('bundle');
/** @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity */
$entity = $this->getEntity();
$resource_id = $entity->get('id');
// If we are editing an entity we don't want the Entity Type and Bundle
// picker, that info is locked.
if (!$entity_type_id || !$bundle) {
if (!$resource_id) {
// We can't build the form without an entity type and bundle.
throw new \InvalidArgumentException('Unable to load entity type or bundle for the overrides form.');
}
[$entity_type_id, $bundle] = explode('--', $resource_id);
$form['#title'] = $this->t('Edit %label resource config', ['%label' => $resource_id]);
}
if ($entity_type_id && $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle)) {
// Get the JSON:API resource type.
$resource_config_id = sprintf('%s--%s', $entity_type_id, $bundle);
$existing_entity = $this->entityTypeManager
->getStorage('jsonapi_resource_config')->load($resource_config_id);
if ($existing_entity && $entity->isNew()) {
$this->messenger()->addStatus($this->t('This override already exists, please edit it instead.'));
return $form;
}
try {
$fields_wrapper = $this->buildOverridesForm($resource_type, $entity);
$form['bundle_wrapper']['fields_wrapper'] = $fields_wrapper;
}
catch (PluginNotFoundException $exception) {
$this->logger->error($exception);
}
$form['id'] = ['#type' => 'hidden', '#value' => $resource_config_id];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if (!method_exists($this->typedConfigManager, 'createFromNameAndData')) {
// Versions of Drupal before 8.4 have poor support for constraints. In
// those scenarios we don't validate the form submission.
return;
}
$typed_config = $this->typedConfigManager
->createFromNameAndData($this->entity->id(), $this->entity->toArray());
$constraints = $typed_config->validate();
/** @var \Symfony\Component\Validator\ConstraintViolation $violation */
foreach ($constraints as $violation) {
$form_path = str_replace('.', '][', $violation->getPropertyPath());
$form_state->setErrorByName($form_path, $violation->getMessage());
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$resource_config = $this->entity;
$status = $resource_config->save();
$type = $resource_config->get('resourceType') ?? '';
switch ($status) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created the %type JSON:API Resource overwrites.', [
'%type' => $type,
]));
break;
default:
$this->messenger()->addStatus($this->t('Saved the %type JSON:API Resource overwrites.', [
'%type' => $type,
]));
}
$form_state->setRedirectUrl($resource_config->toUrl('collection'));
}
/**
* Builds the part of the form that contains the overrides.
*
* @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
* The resource type being overridden.
* @param \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity
* The configuration entity backing this form.
*
* @return array
* The partial form.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function buildOverridesForm(ResourceType $resource_type, JsonapiResourceConfig $entity) {
$entity_type_id = $resource_type->getEntityTypeId();
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $resource_type->getBundle();
$field_names = $this->getAllFieldNames($entity_type, $bundle);
$overrides_form['overrides']['entity'] = [
'#type' => 'fieldset',
'#title' => $this->t('Resource'),
'#description' => $this->t('Override configuration for the resource entity.'),
'#open' => !$entity->get('resourceType') || !$entity->get('path'),
'#weight' => 0,
];
$overrides_form['overrides']['entity']['disabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disabled'),
'#description' => $this->t('Check this if you want to disable this resource. Disabling a resource can have unexpected results when following relationships belonging to that resource.'),
'#default_value' => $entity->get('disabled'),
];
$resource_type_name = $entity->get('resourceType');
if (!$resource_type_name) {
$resource_type_name = sprintf('%s--%s', $entity_type_id, $bundle);
}
$overrides_form['overrides']['entity']['resourceType'] = [
'#type' => 'textfield',
'#title' => $this->t('Resource Type'),
'#description' => $this->t('Overrides the type of the resource. Example: Change "node--article" to "articles".'),
'#default_value' => $resource_type_name,
'#states' => [
'visible' => [
':input[name="disabled"]' => ['checked' => FALSE],
],
],
];
$path = $entity->get('path');
if (!$path) {
$path = sprintf('%s/%s', $entity_type_id, $bundle);
}
$prefix = $this->config->get('path_prefix');
$overrides_form['overrides']['entity']['path'] = [
'#type' => 'textfield',
'#title' => $this->t('Resource Path'),
'#field_prefix' => sprintf('/%s/', $prefix),
'#description' => $this->t('Overrides the path of the resource. Example: Use "articles" to change "/@prefix/node/article" to "/@prefix/articles".', [
'@prefix' => $prefix,
]),
'#default_value' => $path,
'#required' => TRUE,
'#states' => [
'visible' => [
':input[name="disabled"]' => ['checked' => FALSE],
],
],
];
$overrides_form['overrides']['fields'] = [
'#type' => 'details',
'#title' => $this->t('Fields'),
'#open' => TRUE,
'#weight' => 1,
];
$markup = '';
$markup .= '<dl>';
$markup .= '<dt>' . $this->t('Disabled') . '</dt>';
$markup .= '<dd>' . $this->t('Check this if you want to disable this field completely. Disabling required fields will cause problems when writing to the resource.') . '</dd>';
$markup .= '<dt>' . $this->t('Alias') . '</dt>';
$markup .= '<dd>' . $this->t('Overrides the field name with a custom name. Example: Change "field_tags" to "tags".') . '</dd>';
$markup .= '<dt>' . $this->t('Enhancer') . '</dt>';
$markup .= '<dd>' . $this->t('Select an enhancer to manipulate the public output coming in and out.') . '</dd>';
$markup .= '</dl>';
$overrides_form['overrides']['fields']['info'] = [
'#markup' => $markup,
];
$overrides_form['overrides']['fields']['resourceFields'] = [
'#type' => 'table',
'#theme' => 'expandable_rows_table',
'#header' => [
'disabled' => $this->t('Disabled'),
'fieldName' => $this->t('Field name'),
'publicName' => $this->t('Alias'),
'advancedOptions' => '',
],
'#empty' => $this->t('No fields available.'),
'#states' => [
'visible' => [
':input[name="disabled"]' => ['checked' => FALSE],
],
],
'#attached' => [
'library' => [
'jsonapi_extras/expandable_rows_table',
],
],
];
foreach ($field_names as $field_name) {
try {
$overrides = $this->buildOverridesField($field_name, $entity);
}
catch (PluginException $exception) {
// Log exception and continue.
$this->logger->error($exception);
continue;
}
NestedArray::setValue(
$overrides_form,
['overrides', 'fields', 'resourceFields', $field_name],
$overrides
);
}
return $overrides_form;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity */
$entity = parent::buildEntity($form, $form_state);
// Trim slashes from path.
$path = trim($form_state->getValue('path'), '/');
if (strlen($path) > 0) {
$entity->set('path', $path);
}
return $entity;
}
/**
* Builds the part of the form that overrides the field.
*
* @param string $field_name
* The field name of the field being overridden.
* @param \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity
* The config entity backed by this form.
*
* @return array
* The partial form.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
protected function buildOverridesField($field_name, JsonapiResourceConfig $entity) {
$rfs = $entity->get('resourceFields') ?: [];
$resource_fields = array_filter($rfs, function (array $resource_field) use ($field_name) {
return $resource_field['fieldName'] == $field_name;
});
$resource_field = array_shift($resource_fields);
$overrides_form = [];
$overrides_form['disabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disabled'),
'#title_display' => 'invisible',
'#default_value' => empty($resource_field['disabled']) ? NULL : $resource_field['disabled'],
];
$overrides_form['fieldName'] = [
'#type' => 'hidden',
'#value' => $field_name,
'#prefix' => $field_name,
];
$overrides_form['publicName'] = [
'#type' => 'textfield',
'#title' => $this->t('Override Public Name'),
'#title_display' => 'hidden',
'#default_value' => empty($resource_field['publicName']) ? $field_name : $resource_field['publicName'],
'#states' => [
'visible' => [
':input[name="resourceFields[' . $field_name . '][disabled]"]' => [
'checked' => FALSE,
],
],
],
];
$overrides_form['advancedOptions'] = [
'#markup' => t('Advanced'),
];
$overrides_form['advancedOptionsIcon'] = [
// Here we are just printing an arrow.
'#markup' => '&#x21B3;',
];
$overrides_form['enhancer_label'] = [
'#markup' => $this->t('Enhancer for: %name', ['%name' => $field_name]),
];
// Build the select field for the list of enhancers.
$overrides_form['enhancer'] = [
'#wrapper_attributes' => ['colspan' => 2],
'#type' => 'fieldgroup',
'#states' => [
'visible' => [
':input[name="resourceFields[' . $field_name . '][disabled]"]' => [
'checked' => FALSE,
],
],
],
];
$options = array_reduce(
$this->enhancerManager->getDefinitions(),
function (array $carry, array $definition) {
$carry[$definition['id']] = $definition['label'];
return $carry;
},
['' => $this->t('- None -')]
);
$id = empty($resource_field['enhancer']['id'])
? ''
: $resource_field['enhancer']['id'];
$overrides_form['enhancer']['id'] = [
'#type' => 'select',
'#options' => $options,
'#ajax' => [
'callback' => '::getEnhancerSettings',
'wrapper' => $field_name . '-settings-wrapper',
],
'#default_value' => $id,
];
$overrides_form['enhancer']['settings'] = [
'#type' => 'container',
'#attributes' => ['id' => $field_name . '-settings-wrapper'],
];
if (!empty($resource_field['enhancer']['id'])) {
/** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
$enhancer = $this->enhancerManager
->createInstance($resource_field['enhancer']['id'], []);
$overrides_form['enhancer']['settings'] += $enhancer
->getSettingsForm($resource_field);
}
return $overrides_form;
}
/**
* AJAX callback to get the form settings for the enhancer for a field.
*
* @param array $form
* The reference to the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return mixed
* The specific form sub-tree in the form.
*/
public static function getEnhancerSettings(array &$form, FormStateInterface $form_state) {
// Find what is the field name that triggered the AJAX request.
$user_input = $form_state->getUserInput();
$parts = explode('[', $user_input['_triggering_element_name']);
$field_name = rtrim($parts[1], ']');
// Now return the sub-tree for the settings on the enhancer plugin.
return $form['bundle_wrapper']['fields_wrapper']['overrides']['fields']['resourceFields'][$field_name]['enhancer']['settings'];
}
/**
* {@inheritdoc}
*/
protected function actionsElement(array $form, FormStateInterface $form_state) {
// We want to display "Revert" instead of "Delete" on the Resource Config
// Form.
$element = parent::actionsElement($form, $form_state);
if (isset($element['delete'])) {
$element['delete']['#title'] = $this->t('Revert');
}
return $element;
}
/**
* Gets all field names for a given entity type and bundle.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type for which to get all field names.
* @param string $bundle
* The bundle for which to get all field names.
*
* @todo This is a copy of ResourceTypeRepository::getAllFieldNames. We can't
* reuse that code because it's protected.
*
* @return string[]
* All field names.
*/
protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) {
if (is_a($entity_type->getClass(), FieldableEntityInterface::class, TRUE)) {
$field_definitions = $this->fieldManager->getFieldDefinitions(
$entity_type->id(),
$bundle
);
return array_keys($field_definitions);
}
elseif (is_a($entity_type->getClass(), ConfigEntityInterface::class, TRUE)) {
// @todo Uncomment the first line, remove everything else once https://www.drupal.org/project/drupal/issues/2483407 lands.
// return array_keys($entity_type->getPropertiesToExport());
$export_properties = $entity_type->getPropertiesToExport();
if ($export_properties !== NULL) {
return array_keys($export_properties);
}
else {
return ['id', 'type', 'uuid', '_core'];
}
}
return [];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\jsonapi_extras;
use Drupal\Core\Config\BootstrapConfigStorageFactory;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Replace the resource type repository for our own configurable version.
*/
class JsonapiExtrasServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$settings = BootstrapConfigStorageFactory::get()
->read('jsonapi_extras.settings');
if ($settings !== FALSE) {
if ($container->getParameter('jsonapi.base_path') !== '/jsonapi') {
$container->setParameter('jsonapi_extras.base_path_override_disabled', TRUE);
}
else {
$container->setParameter('jsonapi.base_path', '/' . $settings['path_prefix']);
$container->setParameter('jsonapi_extras.base_path_override_disabled', FALSE);
}
}
// Enable normalizers in the "src-impostor-normalizers" directory to be
// within the \Drupal\jsonapi\Normalizer namespace in order to circumvent
// the encapsulation enforced by
// \Drupal\jsonapi\Serializer\Serializer::__construct().
$container_namespaces = $container->getParameter('container.namespaces');
$container_modules = $container->getParameter('container.modules');
$jsonapi_impostor_path = dirname($container_modules['jsonapi_extras']['pathname']) . '/src-impostor-normalizers';
$container_namespaces['Drupal\jsonapi\Normalizer\ImpostorFrom\jsonapi_extras'][] = $jsonapi_impostor_path;
// Manually include the impostor definitions to avoid class not found error
// during compilation, which gets triggered though cache-clear.
$container->getDefinition('serializer.normalizer.field_item.jsonapi_extras')
->setFile($jsonapi_impostor_path . '/FieldItemNormalizerImpostor.php');
$container->getDefinition('serializer.normalizer.resource_identifier.jsonapi_extras')
->setFile($jsonapi_impostor_path . '/ResourceIdentifierNormalizerImpostor.php');
$container->getDefinition('serializer.normalizer.resource_object.jsonapi_extras')
->setFile($jsonapi_impostor_path . '/ResourceObjectNormalizerImpostor.php');
$container->getDefinition('serializer.normalizer.content_entity.jsonapi_extras')
->setFile($jsonapi_impostor_path . '/ContentEntityDenormalizerImpostor.php');
$container->getDefinition('serializer.normalizer.config_entity.jsonapi_extras')
->setFile($jsonapi_impostor_path . '/ConfigEntityDenormalizerImpostor.php');
$container->setParameter('container.namespaces', $container_namespaces);
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$modules = $container->getParameter(('container.modules'));
if (isset($modules['schemata_json_schema'])) {
// Register field definition schema override.
$container
->register('serializer.normalizer.field_definition.schema_json.jsonapi_extras', 'Drupal\jsonapi_extras\Normalizer\SchemaFieldDefinitionNormalizer')
->addTag('normalizer', ['priority' => 32])
->addArgument(new Reference('jsonapi.resource_type.repository'));
// Register top-level schema override.
$container
->register('serializer.normalizer.schemata_schema_normalizer.schema_json.jsonapi_extras', 'Drupal\jsonapi_extras\Normalizer\SchemataSchemaNormalizer')
->addTag('normalizer', ['priority' => 100])
->addArgument(new Reference('jsonapi.resource_type.repository'));
}
}
}

View File

@@ -0,0 +1,239 @@
<?php
namespace Drupal\jsonapi_extras;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\jsonapi_extras\ResourceType\NullJsonapiResourceConfig;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a listing of JSON:API Resource Config entities.
*/
class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
/**
* The JSON:API configurable resource type repository.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository
*/
protected $resourceTypeRepository;
/**
* The JSON:API extras config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected $entityTypeManager;
/**
* Constructs new JsonapiResourceConfigListBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage.
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* The JSON:API resource type repository.
* @param \Drupal\Core\Config\ImmutableConfig $config
* The config instance.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entityTypeManager
* Entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, ResourceTypeRepositoryInterface $resource_type_repository, ImmutableConfig $config, EntityTypeManagerInterface $entityTypeManager = NULL) {
parent::__construct($entity_type, $storage);
$this->resourceTypeRepository = $resource_type_repository;
$this->config = $config;
if ($entityTypeManager === NULL) {
$entityTypeManager = \Drupal::entityTypeManager();
@trigger_error('Calling ' . __METHOD__ . ' without the $entityTypeManager argument is deprecated in jsonapi_extras:8.x-3.20 and will be required in jsonapi_extras:8.x-4.0. See https://www.drupal.org/node/3242791', E_USER_DEPRECATED);
}
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('jsonapi.resource_type.repository'),
$container->get('config.factory')->get('jsonapi_extras.settings'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [
'name' => $this->t('Name'),
'path' => $this->t('Path'),
'state' => $this->t('State'),
'operations' => $this->t('Operations'),
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function render() {
$list = [];
$resource_status = [
'enabled' => t('Enabled Resources'),
'disabled' => t('Disabled resources'),
];
$title = $this->t('Filter resources by name, entity type, bundle or path.');
$list['status']['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Filter'),
'#title_display' => 'invisible',
'#size' => 60,
'#placeholder' => $title,
'#attributes' => [
'class' => ['jsonapi-resources-filter-text'],
'data-table' => '.jsonapi-resources-table',
'autocomplete' => 'off',
'title' => $title,
],
];
foreach ($resource_status as $status => $label) {
$list[$status] = [
'#type' => 'details',
'#title' => $label,
'#open' => $status === 'enabled',
'#attributes' => [
'id' => 'jsonapi-' . $status . '-resources-list',
],
'#attached' => [
'library' => [
'jsonapi_extras/admin',
],
],
];
$list[$status]['table'] = [
'#type' => 'table',
'#header' => [
'name' => $this->t('Name'),
'path' => $this->t('Path'),
'state' => $this->t('State'),
'operations' => $this->t('Operations'),
],
'#attributes' => [
'class' => [
'jsonapi-resources-table',
],
],
'#attached' => [
'library' => [
'jsonapi_extras/admin',
],
],
];
}
$prefix = $this->config->get('path_prefix');
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType[] $resource_types */
$resource_types = $this->resourceTypeRepository->all();
$default_disabled = $this->config->get('default_disabled');
foreach ($resource_types as $resource_type) {
// Other modules may create resource types, e.g. jsonapi_cross_bundles.
$resource_config = $resource_type instanceof ConfigurableResourceType
? $resource_type->getJsonapiResourceConfig()
: NULL;
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
$entity_type_id = $resource_type->getEntityTypeId();
$bundle = $resource_type->getBundle();
$default_group = 'enabled';
if ($resource_config && $resource_type->isInternal() && !$resource_config->get('disabled')) {
// Either this item is marked internal by the entity-type OR the default
// disabled setting is active.
if (!$default_disabled) {
// If default disabled is inactive, this entity-type is marked as
// internal.
continue;
}
// If default disabled is active, we need to make sure that the entity
// type isn't marked internal before we present the option to edit and
// therefore enable the resource type.
$entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id);
if ($entity_type_definition->isInternal()) {
continue;
}
$default_group = 'disabled';
}
elseif (!$resource_config && $resource_type->isInternal()) {
continue;
}
$group = ($resource_config && $resource_config->get('disabled')) || (!$resource_config && !$resource_type->isLocatable())
? 'disabled'
: $default_group;
$row = [
'name' => ['#plain_text' => $resource_type->getTypeName()],
'path' => [
'#type' => 'html_tag',
'#tag' => 'code',
'#value' => sprintf('/%s/%s', $prefix, ltrim($resource_type->getPath(), '/')),
],
'state' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $this->t('Default'),
'#attributes' => [
'class' => [
'label',
],
],
],
'operations' => $resource_config ? [
'#type' => 'operations',
'#links' => [
'overwrite' => [
'title' => $group === 'disabled' ? $this->t('Enable') : $this->t('Overwrite'),
'weight' => -10,
'url' => Url::fromRoute('entity.jsonapi_resource_config.add_form', [
'entity_type_id' => $entity_type_id,
'bundle' => $bundle,
]),
],
],
] : [],
];
if ($resource_config && !($resource_config instanceof NullJsonapiResourceConfig)) {
$row['state']['#value'] = $this->t('Overwritten');
$row['state']['#attributes']['class'][] = 'label--overwritten';
$row['operations']['#links'] = $this->getDefaultOperations($resource_config);
$row['operations']['#links']['delete']['title'] = $this->t('Revert');
}
$list[$group]['table'][] = $row;
}
$list['#cache']['tags'][] = 'jsonapi_resource_types';
return $list;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\jsonapi\ResourceType\ResourceType;
/**
* Override ConfigEntityNormalizer to prepare input.
*/
class ConfigEntityDenormalizer extends JsonApiNormalizerDecoratorBase {
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
return parent::denormalize($this->prepareInput($data, $context['resource_type']), $class, $format, $context);
}
/**
* {@inheritdoc}
*/
protected function prepareInput(array $data, ResourceType $resource_type) {
foreach ($data as $public_field_name => &$field_value) {
/** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
$enhancer = $resource_type->getFieldEnhancer($public_field_name);
if (!$enhancer) {
continue;
}
$field_value = $enhancer->transform($field_value);
}
return $data;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\jsonapi\ResourceType\ResourceType;
/**
* Override ContentEntityNormalizer to prepare input.
*/
class ContentEntityDenormalizer extends JsonApiNormalizerDecoratorBase {
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
return parent::denormalize($this->prepareInput($data, $context['resource_type']), $class, $format, $context);
}
/**
* Prepares the input data to create the entity.
*
* @param array $data
* The input data to modify.
* @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
* Contains the info about the resource type.
*
* @return array
* The modified input data.
*/
protected function prepareInput(array $data, ResourceType $resource_type) {
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions */
$field_storage_definitions = \Drupal::service('entity_field.manager')
->getFieldStorageDefinitions(
$resource_type->getEntityTypeId()
);
$data_internal = [];
// Translate the public fields into the entity fields.
foreach ($data as $public_field_name => $field_value) {
// Skip any disabled field.
$internal_name = $resource_type->getInternalName($public_field_name);
$entity_type_id = $resource_type->getEntityTypeId();
$entity_type_definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$uuid_key = $entity_type_definition->getKey('uuid');
if (!$resource_type->isFieldEnabled($internal_name) && $uuid_key !== $internal_name) {
continue;
}
$enhancer = $resource_type->getFieldEnhancer($public_field_name, 'publicName');
if (isset($field_storage_definitions[$internal_name])) {
$field_storage_definition = $field_storage_definitions[$internal_name];
if ($field_storage_definition->getCardinality() === 1) {
try {
$field_value = $enhancer ? $enhancer->transform($field_value) : $field_value;
}
catch (\TypeError $exception) {
$field_value = NULL;
}
}
elseif (is_array($field_value)) {
foreach ($field_value as $key => $individual_field_value) {
try {
$field_value[$key] = $enhancer ? $enhancer->transform($individual_field_value) : $individual_field_value;
}
catch (\TypeError $exception) {
$field_value[$key] = NULL;
}
}
}
}
$data_internal[$public_field_name] = $field_value;
}
return $data_internal;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\jsonapi\Normalizer\FieldItemNormalizer as JsonapiFieldItemNormalizer;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
use Shaper\Util\Context;
/**
* Converts the Drupal field structure to a JSON:API array structure.
*/
class FieldItemNormalizer extends JsonApiNormalizerDecoratorBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The field enhancer manager.
*
* @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
*/
protected $enhancerManager;
/**
* Constructs a new FieldItemNormalizer.
*
* @param \Drupal\jsonapi\Normalizer\FieldItemNormalizer $inner
* The JSON:API field normalizer entity.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager $enhancer_manager
* The field enhancer manager.
*/
public function __construct(JsonapiFieldItemNormalizer $inner, EntityTypeManagerInterface $entity_type_manager, ResourceFieldEnhancerManager $enhancer_manager) {
parent::__construct($inner);
$this->entityTypeManager = $entity_type_manager;
$this->enhancerManager = $enhancer_manager;
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
// First get the regular output.
$normalized_output = parent::normalize($object, $format, $context);
// Then detect if there is any enhancer to be applied here.
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
$resource_type = $context['resource_object']->getResourceType();
$enhancer = $resource_type->getFieldEnhancer($object->getParent()->getName());
if (!$enhancer) {
return $normalized_output;
}
$cacheability = CacheableMetadata::createFromObject($normalized_output)
->addCacheTags(['config:jsonapi_resource_config_list']);
// Apply any enhancements necessary.
$context = new Context($context);
$context->offsetSet('field_item_object', $object);
$context->offsetSet(CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY, $cacheability);
$processed = $enhancer->undoTransform(
$normalized_output->getNormalization(),
$context
);
$normalized_output = new CacheableNormalization(
// This was passed by reference but often, merging creates a new object.
$context->offsetGet(CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY),
$processed
);
return $normalized_output;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Base class for decorated normalizers.
*/
class JsonApiNormalizerDecoratorBase implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface {
/**
* The decorated (de)normalizer.
*
* @var \Symfony\Component\Serializer\SerializerAwareInterface|\Symfony\Component\Serializer\Normalizer\NormalizerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface
*/
protected $inner;
/**
* JsonApiNormalizerDecoratorBase constructor.
*
* @param \Symfony\Component\Serializer\SerializerAwareInterface|\Symfony\Component\Serializer\Normalizer\NormalizerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $inner
* The decorated normalizer or denormalizer.
*/
public function __construct($inner) {
assert($inner instanceof NormalizerInterface || $inner instanceof DenormalizerInterface);
assert($inner instanceof SerializerAwareInterface);
$this->inner = $inner;
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
return $this->inner->normalize($object, $format, $context);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
return $this->inner->denormalize($data, $class, $format, $context);
}
/**
* {@inheritdoc}
*/
public function setSerializer(SerializerInterface $serializer): void {
$this->inner->setSerializer($serializer);
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = NULL, $context = []): bool {
return $this->inner instanceof NormalizerInterface && $this->inner->supportsNormalization($data, $format);
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = NULL, array $context = []): bool {
return $this->inner instanceof DenormalizerInterface && $this->inner->supportsDenormalization($data, $type, $format);
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return ['object' => TRUE];
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\jsonapi\JsonApiResource\ResourceIdentifier;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
use Shaper\Util\Context;
/**
* Converts the Drupal entity reference item object to a JSON:API structure.
*
* @internal
*/
class ResourceIdentifierNormalizer extends JsonApiNormalizerDecoratorBase {
/**
* The resource type repository for changes on the target resource type.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
*/
protected $resourceTypeRepository;
/**
* Instantiates a ResourceIdentifierNormalizer object.
*
* @param \Symfony\Component\Serializer\SerializerAwareInterface|\Symfony\Component\Serializer\Normalizer\NormalizerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $inner
* The decorated normalizer.
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* The repository.
*/
public function __construct($inner, ResourceTypeRepositoryInterface $resource_type_repository) {
parent::__construct($inner);
$this->resourceTypeRepository = $resource_type_repository;
}
/**
* {@inheritdoc}
*/
public function normalize($field, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
assert($field instanceof ResourceIdentifier);
$normalized_output = parent::normalize($field, $format, $context);
assert($normalized_output instanceof CacheableNormalization);
if (!isset($context['resource_object'])) {
return $normalized_output;
}
$resource_object = $context['resource_object'];
// Find the name of the field being normalized. This is unreasonably more
// contrived than one could expect for ResourceIdentifiers.
$resource_type = $resource_object->getResourceType();
if (!($resource_type instanceof ConfigurableResourceType)) {
return $normalized_output;
}
$field_name = $this->guessFieldName($field->getId(), $resource_object);
if (!$field_name) {
return $normalized_output;
}
$enhancer = $resource_type->getFieldEnhancer($field_name);
if (!$enhancer) {
return $normalized_output;
}
$cacheability = CacheableMetadata::createFromObject($normalized_output)
->addCacheTags(['config:jsonapi_resource_config_list']);
// Apply any enhancements necessary.
$context = new Context($context);
$context->offsetSet('field_resource_identifier', $field);
$context->offsetSet(CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY, $cacheability);
$transformed = $enhancer->undoTransform(
$normalized_output->getNormalization(),
$context
);
return new CacheableNormalization(
// This was passed by reference but often, merging creates a new object.
$context->offsetGet(CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY),
array_intersect_key($transformed, array_flip(['id', 'type', 'meta']))
);
}
/**
* Guesses the field name of a resource identifier pointing to a UUID.
*
* @param string $uuid
* The uuid being referenced.
* @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object
* The object being normalized.
*
* @return string|null
* The field name.
*/
protected function guessFieldName($uuid, ResourceObject $resource_object) {
$resource_type = $resource_object->getResourceType();
assert($resource_type instanceof ConfigurableResourceType);
// From the resource object get all the reference fields.
$reference_field_names = array_keys($resource_type->getRelatableResourceTypes());
// Only consider the fields that contain enhancers. This is to improve
// performance. Discard the candidates that will not have an enhancer.
$ref_enhancers = array_filter(array_map(function ($public_field_name) use ($resource_type) {
return $resource_type->getFieldEnhancer($public_field_name, 'publicName');
}, array_combine($reference_field_names, $reference_field_names)));
// Get the field objects of the reference fields that have enhancers.
$reference_fields = array_intersect_key(
$resource_object->getFields(),
array_flip(array_keys($ref_enhancers))
);
$reference_fields = array_filter($reference_fields, function ($reference_field) {
// This is certainly a limitation.
return $reference_field instanceof EntityReferenceFieldItemListInterface;
});
return array_reduce(
$reference_fields,
function ($field_name, EntityReferenceFieldItemListInterface $object_field) use ($uuid) {
if ($field_name) {
return $field_name;
}
$referenced_entities = $object_field->referencedEntities();
// If any of the referenced entities contains the UUID of the field
// being normalized, then we have our field name.
$matches = array_filter(
$referenced_entities,
function (EntityInterface $referenced_entity) use ($uuid) {
return $uuid === $referenced_entity->uuid();
}
);
return empty($matches) ? NULL : $object_field->getName();
}
);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Shaper\Util\Context;
/**
* Decorates the JSON:API ResourceObjectNormalizer.
*
* @internal
*/
class ResourceObjectNormalizer extends JsonApiNormalizerDecoratorBase {
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
assert($object instanceof ResourceObject);
$resource_type = $object->getResourceType();
$cacheable_normalization = parent::normalize($object, $format, $context);
assert($cacheable_normalization instanceof CacheableNormalization);
if (is_subclass_of($resource_type->getDeserializationTargetClass(), ConfigEntityInterface::class)) {
return new CacheableNormalization(
$cacheable_normalization,
static::enhanceConfigFields($object, $cacheable_normalization->getNormalization(), $resource_type)
);
}
return $cacheable_normalization;
}
/**
* Applies field enhancers to a config entity normalization.
*
* @param mixed $object
* The parent object.
* @param array $normalization
* The normalization to be enhanced.
* @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type
* The resource type of the normalized resource object.
*
* @return array
* The enhanced field data.
*/
protected static function enhanceConfigFields($object, array $normalization, ConfigurableResourceType $resource_type) {
if (!empty($normalization['attributes'])) {
foreach ($normalization['attributes'] as $field_name => $field_value) {
$enhancer = $resource_type->getFieldEnhancer($field_name);
if (!$enhancer) {
continue;
}
$context['field_item_object'] = $object;
$normalization['attributes'][$field_name] = $enhancer->undoTransform($field_value, new Context($context));
}
}
return $normalization;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\schemata_json_schema\Normalizer\jsonapi\FieldDefinitionNormalizer as SchemataJsonSchemaFieldDefinitionNormalizer;
/**
* Applies field enhancer schema changes to field schema.
*/
class SchemaFieldDefinitionNormalizer extends SchemataJsonSchemaFieldDefinitionNormalizer {
/**
* The JSON:API resource type repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
*/
protected $resourceTypeRepository;
/**
* Constructs a SchemaFieldDefinitionNormalizer object.
*
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* A resource type repository.
*/
public function __construct(ResourceTypeRepositoryInterface $resource_type_repository) {
$this->resourceTypeRepository = $resource_type_repository;
}
/**
* {@inheritdoc}
*/
public function normalize($field_definition, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
assert($field_definition instanceof FieldDefinitionInterface);
$normalized = parent::normalize($field_definition, $format, $context);
// Load the resource type for this entity type and bundle.
$bundle = empty($context['bundleId'])
? $context['entityTypeId']
: $context['bundleId'];
$resource_type = $this->resourceTypeRepository->get($context['entityTypeId'], $bundle);
if (!$resource_type || !$resource_type instanceof ConfigurableResourceType) {
return $normalized;
}
$field_name = $context['name'];
$enhancer = $resource_type->getFieldEnhancer($field_definition->getName());
if (!$enhancer) {
return $normalized;
}
$parents = ['properties', 'attributes', 'properties', $field_name];
$original_field_schema = NestedArray::getValue($normalized, $parents);
$to_copy = ['title', 'description'];
$field_schema = array_merge(
$enhancer->getOutputJsonSchema(),
// Copy *some* properties from the original.
array_intersect_key($original_field_schema, array_flip($to_copy))
);
NestedArray::setValue(
$normalized,
$parents,
$field_schema
);
return $normalized;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\schemata_json_schema\Normalizer\jsonapi\SchemataSchemaNormalizer as SchemataJsonSchemaSchemataSchemaNormalizer;
/**
* Applies JSONAPI Extras attribute overrides to entity schemas.
*/
class SchemataSchemaNormalizer extends SchemataJsonSchemaSchemataSchemaNormalizer {
/**
* The JSON:API resource type repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
*/
protected $resourceTypeRepository;
/**
* Constructs a SchemataSchemaNormalizer object.
*
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* A resource repository.
*/
public function __construct(ResourceTypeRepositoryInterface $resource_type_repository) {
$this->resourceTypeRepository = $resource_type_repository;
}
/**
* {@inheritdoc}
*/
public function normalize($entity, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
$normalized = parent::normalize($entity, $format, $context);
// Load the resource type for this entity type and bundle.
$bundle = $entity->getBundleId();
$bundle = $bundle ?: $entity->getEntityTypeId();
$resource_type = $this->resourceTypeRepository->get(
$entity->getEntityTypeId(),
$bundle
);
if (!$resource_type || !$resource_type instanceof ConfigurableResourceType) {
return $normalized;
}
// Alter the attributes according to the resource config.
if (!empty($normalized['definitions'])) {
$root = &$normalized['definitions'];
}
else {
$root = &$normalized['properties']['data']['properties'];
}
foreach (['attributes', 'relationships'] as $property_type) {
if (!isset($root[$property_type]['required'])) {
$root[$property_type]['required'] = [];
}
$properties = NestedArray::getValue($root, [$property_type, 'properties']) ?: [];
foreach ($properties as $fieldname => $schema) {
if ($enhancer = $resource_type->getFieldEnhancer($resource_type->getFieldByPublicName($fieldname)->getInternalName())) {
$root[$property_type]['properties'][$fieldname] = array_merge(
array_intersect_key($root[$property_type]['properties'][$fieldname],
array_flip(['title', 'description'])),
$enhancer->getOutputJsonSchema()
);
}
}
}
return $normalized;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Drupal\jsonapi_extras\Plugin;
/**
* Base class for date and time based resourceFieldEnhancer plugins.
*/
abstract class DateTimeEnhancerBase extends ResourceFieldEnhancerBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'dateTimeFormat' => \DateTime::ISO8601,
];
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
return [
'type' => 'string',
];
}
/**
* {@inheritdoc}
*/
public function getSettingsForm(array $resource_field_info) {
$settings = empty($resource_field_info['enhancer']['settings'])
? $this->getConfiguration()
: $resource_field_info['enhancer']['settings'];
return [
'dateTimeFormat' => [
'#type' => 'textfield',
'#title' => $this->t('Format'),
'#description' => $this->t('Use a valid date format.'),
'#default_value' => $settings['dateTimeFormat'],
],
];
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\jsonapi_extras\Plugin;
use Drupal\Core\Plugin\PluginBase;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Validator;
use Shaper\DataAdaptor\DataAdaptorTransformerTrait;
use Shaper\DataAdaptor\DataAdaptorValidatorTrait;
use Shaper\Validator\AcceptValidator;
use Shaper\Validator\JsonSchemaValidator;
/**
* Common base class for resourceFieldEnhancer plugins.
*
* @see \Drupal\jsonapi_extras\Annotation\ResourceFieldEnhancer
* @see \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
* @see \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface
* @see plugin_api
*
* @ingroup third_party
*/
abstract class ResourceFieldEnhancerBase extends PluginBase implements ResourceFieldEnhancerInterface {
use DataAdaptorValidatorTrait;
use DataAdaptorTransformerTrait;
/**
* Holds the plugin configuration.
*
* @var array
*/
protected $configuration;
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// @todo This should have a dependency on the resource_config entity.
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration
? $this->configuration
: $this->setConfiguration([]);
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration + $this->defaultConfiguration();
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function getSettingsForm(array $resource_field_info) {
return [];
}
/**
* {@inheritdoc}
*/
public function getInternalValidator() {
return new AcceptValidator();
}
/**
* {@inheritdoc}
*/
public function getInputValidator() {
// @todo Implement a getInputJsonSchema method.
return new AcceptValidator();
}
/**
* {@inheritdoc}
*/
public function getOutputValidator() {
return new JsonSchemaValidator($this->getOutputJsonSchema(), new Validator(), Constraint::CHECK_MODE_TYPE_CAST);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\jsonapi_extras\Plugin;
use Shaper\DataAdaptor\ReversibleTransformationInterface;
use Shaper\DataAdaptor\ReversibleTransformationValidationInterface;
use Shaper\Transformation\TransformationInterface;
use Shaper\Transformation\TransformationValidationInterface;
/**
* Provides an interface defining a ResourceFieldEnhancer entity.
*/
interface ResourceFieldEnhancerInterface extends TransformationInterface, ReversibleTransformationInterface, TransformationValidationInterface, ReversibleTransformationValidationInterface {
/**
* Get the JSON Schema for the new output.
*
* @return array
* An structured array representing the JSON Schema of the new output.
*/
public function getOutputJsonSchema();
/**
* Get a form element to render the settings.
*
* @param array $resource_field_info
* The resource field info.
*
* @return array
* The form element array.
*/
public function getSettingsForm(array $resource_field_info);
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\jsonapi_extras\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages discovery and instantiation of resourceFieldEnhancer plugins.
*/
class ResourceFieldEnhancerManager extends DefaultPluginManager {
/**
* Constructs a new ResourceFieldEnhancerManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct(
'Plugin/jsonapi/FieldEnhancer',
$namespaces,
$module_handler,
'Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface',
'Drupal\jsonapi_extras\Annotation\ResourceFieldEnhancer'
);
$this->alterInfo('resource_field_enhancer_info');
$this->setCacheBackend($cache_backend, 'resource_field_enhancer_plugins');
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
// Loop through all definitions.
foreach ($definitions as $definition_key => $definition_info) {
// Check to see if dependencies key is set.
if (!empty($definition_info['dependencies'])) {
$definition_dependencies = $definition_info['dependencies'];
// Loop through dependencies to confirm if enabled.
foreach ($definition_dependencies as $dependency) {
// If dependency is not enabled removed from list of definitions.
if (!$this->moduleHandler->moduleExists($dependency)) {
unset($definitions[$definition_key]);
continue;
}
}
}
}
parent::alterDefinitions($definitions);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* The constraint object.
*
* @Constraint(
* id = "jsonapi_extras__duplicate_field",
* label = @Translation("Duplicate field", context = "Validation")
* )
*/
class DuplicateFieldConstraint extends Constraint {
/**
* The error message for the constraint.
*
* @var string
*/
public $message = 'The override must be unique.';
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\Validation\Constraint;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* The validator.
*/
class DuplicateFieldConstraintValidator extends ConstraintValidator {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* DuplicateFieldConstraintValidator constructor.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager = NULL) {
$this->entityTypeManager = $entityTypeManager ?: \Drupal::entityTypeManager();
}
/**
* {@inheritdoc}
*/
public function validate($entity_data, Constraint $constraint) {
$resourceFields = $entity_data['resourceFields'];
$overrides = [];
// Get the field values.
foreach ($resourceFields as $field => $data) {
// Only get the overridden fields.
if ($data['fieldName'] != $data['publicName']) {
// Store the publicName for comparison.
$overrides[$field] = $data['publicName'];
}
}
// Compare the overrides and find any duplicate values.
$deduped_overrides = array_unique($overrides);
$dupes = array_diff_assoc($overrides, $deduped_overrides);
// Set an error if there are duplicates.
if ($dupes) {
foreach ($dupes as $field => $value) {
$this->context->buildViolation($constraint->message)
->atPath("resourceFields.$field.publicName")
->addViolation();
}
}
// Now compare the overrides with the default names to validate no dupes
// exist.
foreach ($overrides as $field => $override) {
if (array_key_exists($override, $resourceFields)) {
$this->context->buildViolation($constraint->message)
->atPath("resourceFields.$field.publicName")
->addViolation();
}
}
// Validate URL and resource type.
$resource_types = $this->entityTypeManager
->getStorage('jsonapi_resource_config')
->loadByProperties(['disabled' => FALSE]);
foreach ($resource_types as $id => $resource_type) {
if ($entity_data['id'] == $id) {
continue;
}
if ($resource_type->get('resourceType') == $entity_data['resourceType']) {
$this->context->buildViolation(
'There is already resource (@name) with this resource type.',
['@name' => $resource_type->id()]
)
->atPath('resourceType')
->addViolation();
}
if ($resource_type->get('path') == $entity_data['path']) {
$this->context->buildViolation('There is already resource (@name) with this path.', ['@name' => $resource_type->id()])
->atPath('resourceType')
->addViolation();
}
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\jsonapi_extras\Plugin\DateTimeEnhancerBase;
use Shaper\Util\Context;
/**
* Perform additional manipulations to timestamp fields.
*
* @ResourceFieldEnhancer(
* id = "date_time",
* label = @Translation("Date Time (Timestamp field)"),
* description = @Translation("Formats a date based the configured date format for timestamp fields."),
* dependencies = {"datetime"}
* )
*/
class DateTimeEnhancer extends DateTimeEnhancerBase {
/**
* {@inheritdoc}
*/
protected function doUndoTransform($data, Context $context) {
$date = new \DateTime();
$date->setTimestamp(is_int($data)
? $data
: (new DrupalDateTime($data))->getTimestamp()
);
$configuration = $this->getConfiguration();
return $date->format($configuration['dateTimeFormat']);
}
/**
* {@inheritdoc}
*/
protected function doTransform($data, Context $context) {
$date = new \DateTime($data);
return (int) $date->format('U');
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\jsonapi_extras\Plugin\DateTimeEnhancerBase;
use Shaper\Util\Context;
/**
* Perform additional manipulations to datetime fields.
*
* @ResourceFieldEnhancer(
* id = "date_time_from_string",
* label = @Translation("Date Time (Date Time field)"),
* description = @Translation("Formats a date based the configured date format for date fields."),
* dependencies = {"datetime"}
* )
*/
class DateTimeFromStringEnhancer extends DateTimeEnhancerBase {
/**
* {@inheritdoc}
*/
protected function doUndoTransform($data, Context $context) {
$configuration = $this->getConfiguration();
$reformat = function ($input) use ($configuration) {
$storage_timezone = new \DateTimezone(DateTimeItemInterface::STORAGE_TIMEZONE);
$date = new \DateTime($input, $storage_timezone);
$output_timezone = new \DateTimezone(date_default_timezone_get());
$date->setTimezone($output_timezone);
$output = $date->format($configuration['dateTimeFormat']);
return $output;
};
$result = is_array($data) ? array_map($reformat, $data) : $reformat($data);
return $result;
}
/**
* {@inheritdoc}
*/
protected function doTransform($data, Context $context) {
$reformat = function ($input) {
$date = new \DateTime($input);
// Adjust the date for storage.
$storage_timezone = new \DateTimezone(DateTimeItemInterface::STORAGE_TIMEZONE);
$date->setTimezone($storage_timezone);
$output = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
return $output;
};
$result = is_array($data) ? array_map($reformat, $data) : $reformat($data);
return $result;
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
$baseType = parent::getOutputJsonSchema();
return [
"anyOf" => [
$baseType,
["type" => "array", "items" => $baseType],
[
"type" => "object",
"properties" => [
"value" => $baseType,
"end_value" => $baseType,
],
],
],
];
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerBase;
use Shaper\Util\Context;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Perform additional manipulations to JSON fields.
*
* @ResourceFieldEnhancer(
* id = "json",
* label = @Translation("JSON Field"),
* description = @Translation("Render JSON Field has real json")
* )
*/
class JSONFieldEnhancer extends ResourceFieldEnhancerBase implements ContainerFactoryPluginInterface {
/**
* The serialization json.
*
* @var Drupal\Component\serialization\Json
*/
protected $encoder;
/**
* Constructs a new JSONFieldEnhancer.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Component\Serialization\Json $encoder
* The serialization json.
*/
public function __construct(array $configuration, string $plugin_id, $plugin_definition, Json $encoder) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->encoder = $encoder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('serialization.json'));
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function doUndoTransform($data, Context $context) {
return $this->encoder->decode($data);
}
/**
* {@inheritdoc}
*/
protected function doTransform($data, Context $context) {
return $this->encoder->encode($data);
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
return [
'oneOf' => [
['type' => 'object'],
['type' => 'array'],
['type' => 'null'],
],
];
}
/**
* {@inheritdoc}
*/
public function getSettingsForm(array $resource_field_info) {
return [];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerBase;
use Drupal\options\Plugin\Field\FieldType\ListItemBase;
use Shaper\Util\Context;
/**
* Perform additional manipulations to list fields.
*
* @ResourceFieldEnhancer(
* id = "list",
* label = @Translation("List Field"),
* description = @Translation("Formats a list field based on labels and values.")
* )
*/
class ListFieldEnhancer extends ResourceFieldEnhancerBase {
/**
* {@inheritDoc}
*/
protected function doTransform($data, Context $context) {
return is_array($data) ? array_column($data, 'value') : $data;
}
/**
* {@inheritDoc}
*/
protected function doUndoTransform($data, Context $context) {
$field_context = $context->offsetGet('field_item_object');
assert($field_context instanceof ListItemBase);
$options = $field_context->getPossibleOptions();
$reformat = static function ($input) use ($options) {
return [
'value' => $input,
'label' => $options[(string) $input] ?? '',
];
};
return is_array($data) ? array_map($reformat, $data) : $reformat($data);
}
/**
* {@inheritDoc}
*/
public function getOutputJsonSchema(): array {
return [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'value' => [
'anyOf' => [
['type' => 'string'],
['type' => 'number'],
['type' => 'null'],
],
],
'label' => [
'anOf' => [
['type' => 'string'],
['type' => 'null'],
],
],
],
],
];
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerBase;
use Shaper\Util\Context;
/**
* Perform additional manipulations to date fields.
*
* @ResourceFieldEnhancer(
* id = "nested",
* label = @Translation("Single Nested Property"),
* description = @Translation("Extracts or wraps nested properties from an object.")
* )
*/
class SingleNestedEnhancer extends ResourceFieldEnhancerBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'path' => 'value',
];
}
/**
* {@inheritdoc}
*/
protected function doUndoTransform($data, Context $context) {
$output = $data;
$configuration = $this->getConfiguration();
$path = $configuration['path'];
$path_parts = explode('.', $path);
// Start drilling down until there are no more path parts.
while ($output && ($path_part = array_shift($path_parts))) {
$output = empty($output[$path_part])
? NULL
: $output[$path_part];
}
return $output;
}
/**
* {@inheritdoc}
*/
protected function doTransform($data, Context $context) {
$input = $data;
$configuration = $this->getConfiguration();
$path = $configuration['path'];
$path_parts = explode('.', $path);
// Start wrapping up until there are no more path parts.
while ($path_part = array_pop($path_parts)) {
$input = [$path_part => $input];
}
return $input;
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
return [
'oneOf' => [
['type' => 'string'],
['type' => 'null'],
],
];
}
/**
* {@inheritdoc}
*/
public function getSettingsForm(array $resource_field_info) {
$settings = empty($resource_field_info['enhancer']['settings'])
? $this->getConfiguration()
: $resource_field_info['enhancer']['settings'];
return [
'path' => [
'#type' => 'textfield',
'#title' => $this->t('Path'),
'#description' => $this->t('A dot separated path to extract the sub-property.'),
'#default_value' => $settings['path'],
],
];
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerBase;
use Shaper\Util\Context;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Add URL aliases to links.
*
* @ResourceFieldEnhancer(
* id = "url_link",
* label = @Translation("URL for link (link field only)"),
* description = @Translation("Use Url for link fields.")
* )
*/
class UrlLinkEnhancer extends ResourceFieldEnhancerBase implements ContainerFactoryPluginInterface {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The logger service.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs UrlLinkEnhancer.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger service.
*/
public function __construct(
array $configuration,
$plugin_id,
array $plugin_definition,
LanguageManagerInterface $language_manager,
LoggerChannelFactoryInterface $logger_factory,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->languageManager = $language_manager;
$this->logger = $logger_factory->get('jsonapi_extras');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager'),
$container->get('logger.factory')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'absolute_url' => 0,
];
}
/**
* {@inheritDoc}
*/
public function getSettingsForm(array $resource_field_info) {
$settings = empty($resource_field_info['enhancer']['settings'])
? $this->getConfiguration()
: $resource_field_info['enhancer']['settings'];
$form = parent::getSettingsForm($resource_field_info);
$form['absolute_url'] = [
'#type' => 'checkbox',
'#title' => $this->t('Get Absolute Urls'),
'#default_value' => $settings['absolute_url'],
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function doUndoTransform($data, Context $context) {
if (isset($data['uri'])) {
try {
$url = Url::fromUri($data['uri'], ['language' => $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)]);
// Use absolute urls if configured.
$configuration = $this->getConfiguration();
if ($configuration['absolute_url']) {
$url->setAbsolute(TRUE);
}
$data['url'] = $url->toString();
}
catch (\Exception $e) {
$this->logger->error('Failed to create a URL from uri @uri. Error: @error', [
'@uri' => $data['uri'],
'@error' => $e->getMessage(),
]);
}
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function doTransform($value, Context $context) {
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
return [
'type' => 'object',
'properties' => [
'uri' => ['type' => 'string'],
'title' => [
'anyOf' => [
['type' => 'null'],
['type' => 'string'],
],
],
'options' => [
'anyOf' => [
['type' => 'array'],
['type' => 'object'],
],
],
'url' => ['type' => 'string'],
],
];
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerBase;
use Shaper\Util\Context;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Use UUID for internal link field value.
*
* @ResourceFieldEnhancer(
* id = "uuid_link",
* label = @Translation("UUID for link (link field only)"),
* description = @Translation("Use UUID for internal link field.")
* )
*/
class UuidLinkEnhancer extends ResourceFieldEnhancerBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
protected function doUndoTransform($data, Context $context) {
if (isset($data['uri'])) {
// Check if it is a link to an entity.
preg_match("/entity:(.*)\/(.*)/", $data['uri'], $parsed_uri);
if (!empty($parsed_uri)) {
$entity_type = $parsed_uri[1];
$entity_id = $parsed_uri[2];
$entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id);
if (!is_null($entity)) {
$data['uri'] = 'entity:' . $entity_type . '/' . $entity->bundle() . '/' . $entity->uuid();
}
// Remove the value.
else {
$data = [
'uri' => '',
'title' => '',
'options' => [],
];
}
}
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function doTransform($value, Context $context) {
if (isset($value['uri'])) {
// Check if it is a link to an entity.
preg_match("/entity:(.*)\/(.*)\/(.*)/", $value['uri'], $parsed_uri);
if (!empty($parsed_uri)) {
$entity_type = $parsed_uri[1];
$entity_uuid = $parsed_uri[3];
$entities = $this->entityTypeManager->getStorage($entity_type)->loadByProperties(['uuid' => $entity_uuid]);
if (!empty($entities)) {
$entity = array_shift($entities);
$value['uri'] = 'entity:' . $entity_type . '/' . $entity->id();
}
else {
// If the entity has not been imported yet we unset the field value.
$value = [];
}
}
}
return $value;
}
/**
* {@inheritdoc}
*/
public function getOutputJsonSchema() {
return [
'type' => 'object',
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
/**
* Defines a configurable resource type.
*/
class ConfigurableResourceType extends ResourceType {
use DependencySerializationTrait;
/**
* The JsonapiResourceConfig entity.
*
* @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig
*/
protected $jsonapiResourceConfig;
/**
* Plugin manager for enhancers.
*
* @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
*/
protected $enhancerManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The static cache.
*
* @var array
*/
protected $cache = [];
/**
* Returns the jsonapi_resource_config.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig
* The jsonapi_resource_config entity.
*/
public function getJsonapiResourceConfig() {
return $this->jsonapiResourceConfig;
}
/**
* Sets the jsonapi_resource_config.
*
* @param \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $resource_config
* The jsonapi_resource_config entity.
*/
public function setJsonapiResourceConfig(JsonapiResourceConfig $resource_config) {
$this->jsonapiResourceConfig = $resource_config;
if ($name = $resource_config->get('resourceType')) {
// Set the type name.
$this->typeName = $name;
}
}
/**
* {@inheritdoc}
*/
public function includeCount() {
return $this->configFactory
->get('jsonapi_extras.settings')
->get('include_count');
}
/**
* {@inheritdoc}
*/
public function getPath() {
$resource_config = $this->getJsonapiResourceConfig();
if (!$resource_config) {
return parent::getPath();
}
$config_path = $resource_config->get('path');
if (!$config_path) {
return parent::getPath();
}
return '/' . ltrim($config_path, '/');
}
/**
* Get the resource field configuration.
*
* @todo https://www.drupal.org/node/3007820
*
* @param string $field_name
* The internal field name.
* @param string $from
* The realm of the provided field name.
*
* @return array
* The resource field definition. NULL if none can be found.
*/
public function getResourceFieldConfiguration($field_name, $from = 'fieldName') {
$cid = "$field_name:$from";
if (isset($this->cache[$cid]) || array_key_exists($cid, $this->cache)) {
return $this->cache[$cid];
}
$resource_fields = $this->getJsonapiResourceConfig()->get('resourceFields');
// Find the resource field in the config entity for the given field name.
$found = array_filter($resource_fields, function ($resource_field) use ($field_name, $from) {
return !empty($resource_field[$from]) &&
$field_name == $resource_field[$from];
});
$result = empty($found) ? NULL : reset($found);
$this->cache[$cid] = $result;
return $result;
}
/**
* Injects the config factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The field enhancer manager.
*/
public function setConfigFactory(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* Injects the field enhancer manager.
*
* @param \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager $enhancer_manager
* The field enhancer manager.
*/
public function setEnhancerManager(ResourceFieldEnhancerManager $enhancer_manager) {
$this->enhancerManager = $enhancer_manager;
}
/**
* Get the field enhancer plugin.
*
* @param string $field_name
* The internal field name.
* @param string $from
* The realm of the provided field name.
*
* @return \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface|null
* The enhancer plugin. NULL if not found.
*/
public function getFieldEnhancer($field_name, $from = 'fieldName') {
if (!$resource_field = $this->getResourceFieldConfiguration($field_name, $from)) {
return NULL;
}
if (empty($resource_field['enhancer']['id'])) {
return NULL;
}
try {
$enhancer_info = $resource_field['enhancer'];
// Ensure that the settings are in a suitable format.
$settings = [];
if (!empty($enhancer_info['settings']) && is_array($enhancer_info['settings'])) {
$settings = $enhancer_info['settings'];
}
// Get the enhancer instance.
/** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
$enhancer = $this->enhancerManager->createInstance(
$enhancer_info['id'],
$settings
);
return $enhancer;
}
catch (PluginException $exception) {
return NULL;
}
}
}

View File

@@ -0,0 +1,251 @@
<?php
namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
/**
* Provides a repository of JSON:API configurable resource types.
*/
class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Plugin manager for enhancers.
*
* @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
*/
protected $enhancerManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* A list of all resource types.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType[]
*/
protected $resourceTypes;
/**
* A list of only enabled resource types.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType[]
*/
protected $enabledResourceTypes;
/**
* A list of all resource configuration entities.
*
* @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[]
*/
protected static $resourceConfigs;
/**
* Builds the resource config ID from the entity type ID and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The entity bundle.
*
* @return string
* The ID of the associated ResourceConfig entity.
*/
protected static function buildResourceConfigId($entity_type_id, $bundle) {
return sprintf(
'%s--%s',
$entity_type_id,
$bundle
);
}
/**
* {@inheritdoc}
*/
public function __construct(...$arguments) {
parent::__construct(...$arguments);
$this->cacheTags = array_merge($this->cacheTags, [
'config:jsonapi_extras.settings',
'config:jsonapi_resource_config_list',
]);
}
/**
* Injects the entity repository.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function setEntityRepository(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* Injects the resource enhancer manager.
*
* @param \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager $enhancer_manager
* The resource enhancer manager.
*/
public function setEnhancerManager(ResourceFieldEnhancerManager $enhancer_manager) {
$this->enhancerManager = $enhancer_manager;
}
/**
* Injects the configuration factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function setConfigFactory(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*
* Mostly the same as the parent implementation, with three key differences:
* 1. Different resource type class.
* 2. Every resource type is assumed to be mutable.
* 2. Field mapping not based on logic, but on configuration.
*/
protected function createResourceType(EntityTypeInterface $entity_type, $bundle) {
$resource_type = parent::createResourceType($entity_type, $bundle);
$configurable_resource_type = new ConfigurableResourceType(
$resource_type->getEntityTypeId(),
$resource_type->getBundle(),
$resource_type->getDeserializationTargetClass(),
$resource_type->isInternal(),
$resource_type->isLocatable(),
$resource_type->isMutable(),
$resource_type->isVersionable(),
$resource_type->getFields()
);
$resource_config_id = static::buildResourceConfigId(
$entity_type->id(),
$bundle
);
$resource_config = $this->getResourceConfig($resource_config_id);
// Inject additional services through setters. By using setter injection
// rather that constructor injection, we prevent most future BC breaks.
$configurable_resource_type->setJsonapiResourceConfig($resource_config);
$configurable_resource_type->setEnhancerManager($this->enhancerManager);
$configurable_resource_type->setConfigFactory($this->configFactory);
return $configurable_resource_type;
}
/**
* Get a single resource configuration entity by its ID.
*
* @param string $resource_config_id
* The configuration entity ID.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig
* The configuration entity for the resource type.
*/
protected function getResourceConfig($resource_config_id) {
$null_resource = new NullJsonapiResourceConfig(
['id' => $resource_config_id],
'jsonapi_resource_config'
);
try {
$resource_configs = $this->getResourceConfigs();
return $resource_configs[$resource_config_id] ??
$null_resource;
}
catch (PluginException $e) {
return $null_resource;
}
}
/**
* Load all resource configuration entities.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[]
* The resource config entities.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function getResourceConfigs() {
if (!static::$resourceConfigs) {
$resource_config_ids = [];
foreach ($this->getEntityTypeBundleTuples() as $tuple) {
[$entity_type_id, $bundle] = $tuple;
$resource_config_ids[] = static::buildResourceConfigId(
$entity_type_id,
$bundle
);
}
static::$resourceConfigs = $this->entityTypeManager
->getStorage('jsonapi_resource_config')
->loadMultiple($resource_config_ids);
}
return static::$resourceConfigs;
}
/**
* Entity type ID and bundle iterator.
*
* @return array
* A list of entity type ID and bundle tuples.
*/
protected function getEntityTypeBundleTuples() {
$entity_type_ids = array_keys($this->entityTypeManager->getDefinitions());
// For each entity type return as many tuples as bundles.
return array_reduce($entity_type_ids, function ($carry, $entity_type_id) {
$bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
// Get all the tuples for the current entity type.
$tuples = array_map(function ($bundle) use ($entity_type_id) {
return [$entity_type_id, $bundle];
}, $bundles);
// Append the tuples to the aggregated list.
return array_merge($carry, $tuples);
}, []);
}
/**
* Resets the internal caches for resource types and resource configs.
*/
public static function reset() {
static::$resourceConfigs = [];
}
/**
* {@inheritdoc}
*/
public function getByTypeName($type_name) {
$resource_types = $this->all();
if (isset($resource_types[$type_name])) {
return $resource_types[$type_name];
}
if (strpos($type_name ?? '', '--') !== FALSE) {
[$entity_type_id, $bundle] = explode('--', $type_name);
return static::lookupResourceType($resource_types, $entity_type_id, $bundle);
}
return NULL;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
/**
* Null pattern class resources without overridden configuration.
*/
class NullJsonapiResourceConfig extends JsonapiResourceConfig {
/**
* {@inheritdoc}
*/
public function get($key) {
return $key == 'resourceFields' ? [] : NULL;
}
/**
* {@inheritdoc}
*/
public function getConfigDependencyName() {
return __CLASS__;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Drupal\jsonapi_extras;
use Drupal\jsonapi\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* A decorated JSON:API serializer, with lazily initialized fallback serializer.
*/
class SerializerDecorator implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface {
/**
* The decorated JSON:API serializer service.
*
* @var \Drupal\jsonapi\Serializer\Serializer
*/
protected $decoratedSerializer;
/**
* Whether the lazy dependency has been initialized.
*
* @var bool
*/
protected $isInitialized = FALSE;
/**
* Constructs a SerializerDecorator.
*
* @param \Drupal\jsonapi\Serializer\Serializer $serializer
* The decorated JSON:API serializer.
*/
public function __construct(Serializer $serializer) {
$this->decoratedSerializer = $serializer;
}
/**
* Lazily initializes the fallback serializer for the JSON:API serializer.
*
* Breaks circular dependency.
*/
protected function lazilyInitialize() {
if (!$this->isInitialized) {
$core_serializer = \Drupal::service('serializer');
$this->decoratedSerializer->setFallbackNormalizer($core_serializer);
$this->isInitialized = TRUE;
}
}
/**
* Relays a method call to the decorated service.
*
* @param string $method_name
* The method to invoke on the decorated serializer.
* @param array $args
* The arguments to pass to the invoked method on the decorated serializer.
*
* @return mixed
* The return value.
*/
protected function relay($method_name, array $args) {
$this->lazilyInitialize();
return call_user_func_array([$this->decoratedSerializer, $method_name], $args);
}
/**
* {@inheritdoc}
*/
public function decode($data, $format, array $context = []): mixed {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, array $context = []): mixed {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function encode($data, $format, array $context = []): string {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function supportsDecoding($format, array $context = []): bool {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, array $context = []): string {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, string $type, string $format = NULL, array $context = []): bool {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function supportsEncoding($format, array $context = []): bool {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, ?string $format = NULL, $context = []): bool {
return $this->relay(__FUNCTION__, func_get_args());
}
}