View source
<?php
namespace Drupal\jsonapi\ResourceType;
use Drupal\Component\Assertion\Inspector;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\ContentEntityNullStorage;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\TypedData\DataReferenceTargetDefinition;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
protected $entityTypeManager;
protected $entityTypeBundleInfo;
protected $entityFieldManager;
protected $cache;
protected $eventDispatcher;
protected $cacheTags = [
'jsonapi_resource_types',
'entity_field_info',
'entity_bundles',
'entity_types',
];
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_bundle_info, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, EventDispatcherInterface $dispatcher) {
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_bundle_info;
$this->entityFieldManager = $entity_field_manager;
$this->cache = $cache;
$this->eventDispatcher = $dispatcher;
}
public function all() {
$cached = $this->cache
->get('jsonapi.resource_types', FALSE);
if ($cached) {
return $cached->data;
}
$resource_types = [];
foreach ($this->entityTypeManager
->getDefinitions() as $entity_type) {
$bundles = array_keys($this->entityTypeBundleInfo
->getBundleInfo($entity_type
->id()));
$resource_types = array_reduce($bundles, function ($resource_types, $bundle) use ($entity_type) {
$resource_type = $this
->createResourceType($entity_type, (string) $bundle);
return array_merge($resource_types, [
$resource_type
->getTypeName() => $resource_type,
]);
}, $resource_types);
}
foreach ($resource_types as $resource_type) {
$relatable_resource_types = $this
->calculateRelatableResourceTypes($resource_type, $resource_types);
$resource_type
->setRelatableResourceTypes($relatable_resource_types);
}
$this->cache
->set('jsonapi.resource_types', $resource_types, Cache::PERMANENT, $this->cacheTags);
return $resource_types;
}
protected function createResourceType(EntityTypeInterface $entity_type, $bundle) {
$type_name = NULL;
$raw_fields = $this
->getAllFieldNames($entity_type, $bundle);
$internalize_resource_type = $entity_type
->isInternal();
$fields = static::getFields($raw_fields, $entity_type, $bundle);
if (!$internalize_resource_type) {
$event = ResourceTypeBuildEvent::createFromEntityTypeAndBundle($entity_type, $bundle, $fields);
$this->eventDispatcher
->dispatch($event, ResourceTypeBuildEvents::BUILD);
$internalize_resource_type = $event
->resourceTypeShouldBeDisabled();
$fields = $event
->getFields();
$type_name = $event
->getResourceTypeName();
}
return new ResourceType($entity_type
->id(), $bundle, $entity_type
->getClass(), $internalize_resource_type, static::isLocatableResourceType($entity_type, $bundle), static::isMutableResourceType($entity_type, $bundle), static::isVersionableResourceType($entity_type), $fields, $type_name);
}
public function get($entity_type_id, $bundle) {
assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.');
if (empty($entity_type_id)) {
throw new PreconditionFailedHttpException('Server error. The current route is malformed.');
}
$map_id = sprintf('jsonapi.resource_type.%s.%s', $entity_type_id, $bundle);
$cached = $this->cache
->get($map_id);
if ($cached) {
return $cached->data;
}
$resource_type = static::lookupResourceType($this
->all(), $entity_type_id, $bundle);
$this->cache
->set($map_id, $resource_type, Cache::PERMANENT, $this->cacheTags);
return $resource_type;
}
public function getByTypeName($type_name) {
$resource_types = $this
->all();
return isset($resource_types[$type_name]) ? $resource_types[$type_name] : NULL;
}
protected function getFields(array $field_names, EntityTypeInterface $entity_type, $bundle) {
assert(Inspector::assertAllStrings($field_names));
assert($entity_type instanceof ContentEntityTypeInterface || $entity_type instanceof ConfigEntityTypeInterface);
assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.');
$id_field_name = $entity_type
->getKey('id');
$uuid_field_name = $entity_type
->getKey('uuid');
if ($uuid_field_name && $uuid_field_name !== 'id') {
$fields[$uuid_field_name] = new ResourceTypeAttribute($uuid_field_name, NULL, FALSE);
}
$fields[$id_field_name] = new ResourceTypeAttribute($id_field_name, "drupal_internal__{$id_field_name}");
if ($entity_type
->isRevisionable() && ($revision_id_field_name = $entity_type
->getKey('revision'))) {
$fields[$revision_id_field_name] = new ResourceTypeAttribute($revision_id_field_name, "drupal_internal__{$revision_id_field_name}");
}
if ($entity_type instanceof ConfigEntityTypeInterface) {
$fields['_core'] = new ResourceTypeAttribute('_core', NULL, FALSE);
}
$is_fieldable = $entity_type
->entityClassImplements(FieldableEntityInterface::class);
if ($is_fieldable) {
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($entity_type
->id(), $bundle);
}
$reserved_field_names = [
'id',
'type',
];
foreach (array_diff($field_names, array_keys($fields)) as $field_name) {
$alias = $field_name;
if (in_array($field_name, $reserved_field_names, TRUE)) {
$alias = $entity_type
->id() . '_' . $field_name;
}
$field_definition = $is_fieldable && !empty($field_definitions[$field_name]) ? $field_definitions[$field_name] : NULL;
$is_relationship_field = $field_definition && static::isReferenceFieldDefinition($field_definition);
$has_one = !$field_definition || $field_definition
->getFieldStorageDefinition()
->getCardinality() === 1;
$fields[$field_name] = $is_relationship_field ? new ResourceTypeRelationship($field_name, $alias, TRUE, $has_one) : new ResourceTypeAttribute($field_name, $alias, TRUE, $has_one);
}
foreach (array_intersect($reserved_field_names, array_keys($fields)) as $reserved_field_name) {
$aliased_reserved_field = $fields[$reserved_field_name];
foreach (array_diff_key($fields, array_flip([
$reserved_field_name,
])) as $field) {
if ($aliased_reserved_field
->getPublicName() === $field
->getPublicName()) {
throw new \LogicException("The generated alias '{$aliased_reserved_field->getPublicName()}' for field name '{$aliased_reserved_field->getInternalName()}' conflicts with an existing field. Please report this in the JSON:API issue queue!");
}
}
}
if ($entity_type
->id() === 'user') {
$fields['display_name'] = new ResourceTypeAttribute('display_name');
}
return $fields;
}
protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) {
if ($entity_type instanceof ContentEntityTypeInterface) {
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($entity_type
->id(), $bundle);
return array_keys($field_definitions);
}
elseif ($entity_type instanceof ConfigEntityTypeInterface) {
$export_properties = $entity_type
->getPropertiesToExport();
if ($export_properties !== NULL) {
return array_keys($export_properties);
}
else {
return [
'id',
'type',
'uuid',
'_core',
];
}
}
else {
throw new \LogicException("Only content and config entity types are supported.");
}
}
protected static function isMutableResourceType(EntityTypeInterface $entity_type, $bundle) {
assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.');
return !$entity_type instanceof ConfigEntityTypeInterface;
}
protected static function isLocatableResourceType(EntityTypeInterface $entity_type, $bundle) {
assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.');
return $entity_type
->getStorageClass() !== ContentEntityNullStorage::class;
}
protected static function isVersionableResourceType(EntityTypeInterface $entity_type) {
return in_array($entity_type
->id(), [
'node',
'media',
]);
}
protected function calculateRelatableResourceTypes(ResourceType $resource_type, array $resource_types) {
$entity_type = $this->entityTypeManager
->getDefinition($resource_type
->getEntityTypeId());
if ($entity_type
->entityClassImplements(FieldableEntityInterface::class)) {
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($resource_type
->getEntityTypeId(), $resource_type
->getBundle());
$relatable_internal = array_map(function ($field_definition) use ($resource_types) {
return $this
->getRelatableResourceTypesFromFieldDefinition($field_definition, $resource_types);
}, array_filter($field_definitions, function ($field_definition) {
return $this
->isReferenceFieldDefinition($field_definition);
}));
$relatable_public = [];
foreach ($relatable_internal as $internal_field_name => $value) {
$relatable_public[$resource_type
->getPublicName($internal_field_name)] = $value;
}
return $relatable_public;
}
return [];
}
protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionInterface $field_definition, array $resource_types) {
$item_definition = $field_definition
->getItemDefinition();
$entity_type_id = $item_definition
->getSetting('target_type');
$handler_settings = $item_definition
->getSetting('handler_settings');
$target_bundles = empty($handler_settings['target_bundles']) ? $this
->getAllBundlesForEntityType($entity_type_id) : $handler_settings['target_bundles'];
$relatable_resource_types = [];
foreach ($target_bundles as $target_bundle) {
if ($resource_type = static::lookupResourceType($resource_types, $entity_type_id, $target_bundle)) {
$relatable_resource_types[] = $resource_type;
}
elseif (!InstallerKernel::installationAttempted()) {
trigger_error(sprintf('The "%s" at "%s:%s" references the "%s:%s" entity type that does not exist. Please take action.', $field_definition
->getName(), $field_definition
->getTargetEntityTypeId(), $field_definition
->getTargetBundle(), $entity_type_id, $target_bundle), E_USER_WARNING);
}
}
return $relatable_resource_types;
}
protected function isReferenceFieldDefinition(FieldDefinitionInterface $field_definition) {
static $field_type_is_reference = [];
if (isset($field_type_is_reference[$field_definition
->getType()])) {
return $field_type_is_reference[$field_definition
->getType()];
}
$item_definition = $field_definition
->getItemDefinition();
$main_property = $item_definition
->getMainPropertyName();
$property_definition = $item_definition
->getPropertyDefinition($main_property);
return $field_type_is_reference[$field_definition
->getType()] = $property_definition instanceof DataReferenceTargetDefinition;
}
protected function getAllBundlesForEntityType($entity_type_id) {
return array_map('strval', array_keys($this->entityTypeBundleInfo
->getBundleInfo($entity_type_id)));
}
protected static function lookupResourceType(array $resource_types, $entity_type_id, $bundle) {
if (isset($resource_types[$entity_type_id . ResourceType::TYPE_NAME_URI_PATH_SEPARATOR . $bundle])) {
return $resource_types[$entity_type_id . ResourceType::TYPE_NAME_URI_PATH_SEPARATOR . $bundle];
}
foreach ($resource_types as $resource_type) {
if ($resource_type
->getEntityTypeId() === $entity_type_id && $resource_type
->getBundle() === $bundle) {
return $resource_type;
}
}
return NULL;
}
}