ResourceType.php

Same filename in other branches
  1. 9 core/modules/jsonapi/src/ResourceType/ResourceType.php
  2. 10 core/modules/jsonapi/src/ResourceType/ResourceType.php
  3. 11.x core/modules/jsonapi/src/ResourceType/ResourceType.php

Namespace

Drupal\jsonapi\ResourceType

File

core/modules/jsonapi/src/ResourceType/ResourceType.php

View source
<?php

namespace Drupal\jsonapi\ResourceType;

use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceTargetDefinition;

/**
 * Value object containing all metadata for a JSON:API resource type.
 *
 * Used to generate routes (collection, individual, etcetera), generate
 * relationship links, and so on.
 *
 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
 *   class may change at any time and this will break any dependencies on it.
 *
 * @see https://www.drupal.org/project/drupal/issues/3032787
 * @see jsonapi.api.php
 *
 * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository
 */
class ResourceType {
    
    /**
     * The entity type ID.
     *
     * @var string
     */
    protected $entityTypeId;
    
    /**
     * The bundle ID.
     *
     * @var string
     */
    protected $bundle;
    
    /**
     * The type name.
     *
     * @var string
     */
    protected $typeName;
    
    /**
     * The class to which a payload converts to.
     *
     * @var string
     */
    protected $deserializationTargetClass;
    
    /**
     * Whether this resource type is internal.
     *
     * @var bool
     */
    protected $internal;
    
    /**
     * Whether this resource type's resources are locatable.
     *
     * @var bool
     */
    protected $isLocatable;
    
    /**
     * Whether this resource type's resources are mutable.
     *
     * @var bool
     */
    protected $isMutable;
    
    /**
     * Whether this resource type's resources are versionable.
     *
     * @var bool
     */
    protected $isVersionable;
    
    /**
     * The list of fields on the underlying entity type + bundle.
     *
     * @var string[]
     */
    protected $fields;
    
    /**
     * An array of arrays of relatable resource types, keyed by public field name.
     *
     * @var array
     */
    protected $relatableResourceTypesByField;
    
    /**
     * The mapping for field aliases: keys=public names, values=internal names.
     *
     * @var string[]
     */
    protected $fieldMapping;
    
    /**
     * Gets the entity type ID.
     *
     * @return string
     *   The entity type ID.
     *
     * @see \Drupal\Core\Entity\EntityInterface::getEntityTypeId
     */
    public function getEntityTypeId() {
        return $this->entityTypeId;
    }
    
    /**
     * Gets the type name.
     *
     * @return string
     *   The type name.
     */
    public function getTypeName() {
        return $this->typeName;
    }
    
    /**
     * Gets the bundle.
     *
     * @return string
     *   The bundle of the entity. Defaults to the entity type ID if the entity
     *   type does not make use of different bundles.
     *
     * @see \Drupal\Core\Entity\EntityInterface::bundle
     */
    public function getBundle() {
        return $this->bundle;
    }
    
    /**
     * Gets the deserialization target class.
     *
     * @return string
     *   The deserialization target class.
     */
    public function getDeserializationTargetClass() {
        return $this->deserializationTargetClass;
    }
    
    /**
     * Translates the entity field name to the public field name.
     *
     * This is only here so we can allow polymorphic implementations to take a
     * greater control on the field names.
     *
     * @return string
     *   The public field name.
     */
    public function getPublicName($field_name) {
        // By default the entity field name is the public field name.
        return isset($this->fields[$field_name]) ? $this->fields[$field_name]
            ->getPublicName() : $field_name;
    }
    
    /**
     * Translates the public field name to the entity field name.
     *
     * This is only here so we can allow polymorphic implementations to take a
     * greater control on the field names.
     *
     * @return string
     *   The internal field name as defined in the entity.
     */
    public function getInternalName($field_name) {
        // By default the entity field name is the public field name.
        return $this->fieldMapping[$field_name] ?? $field_name;
    }
    
    /**
     * Gets the attribute and relationship fields of this resource type.
     *
     * @return \Drupal\jsonapi\ResourceType\ResourceTypeField[]
     *   The field objects on this resource type.
     */
    public function getFields() {
        return $this->fields;
    }
    
    /**
     * Gets a particular attribute or relationship field by public field name.
     *
     * @param string $public_field_name
     *   The public field name of the desired field.
     *
     * @return \Drupal\jsonapi\ResourceType\ResourceTypeField|null
     *   A resource type field object or NULL if the field does not exist on this
     *   resource type.
     */
    public function getFieldByPublicName($public_field_name) {
        return isset($this->fieldMapping[$public_field_name]) ? $this->getFieldByInternalName($this->fieldMapping[$public_field_name]) : NULL;
    }
    
    /**
     * Gets a particular attribute or relationship field by internal field name.
     *
     * @param string $internal_field_name
     *   The internal field name of the desired field.
     *
     * @return \Drupal\jsonapi\ResourceType\ResourceTypeField|null
     *   A resource type field object or NULL if the field does not exist on this
     *   resource type.
     */
    public function getFieldByInternalName($internal_field_name) {
        return $this->fields[$internal_field_name] ?? NULL;
    }
    
    /**
     * Checks if the field exists.
     *
     * Note: a minority of config entity types which do not define a
     * `config_export` in their entity type annotation will not have their fields
     * represented here because it is impossible to determine them without an
     * instance of config available.
     *
     * @todo Refactor this in Drupal 9, because thanks to https://www.drupal.org/project/drupal/issues/2949021, `config_export` will be guaranteed to exist, and this won't need an instance anymore.
     *
     * @param string $field_name
     *   The internal field name.
     *
     * @return bool
     *   TRUE if the field is known to exist on the resource type; FALSE
     *   otherwise.
     */
    public function hasField($field_name) {
        return array_key_exists($field_name, $this->fields);
    }
    
    /**
     * Checks if a field is enabled or not.
     *
     * This is only here so we can allow polymorphic implementations to take a
     * greater control on the data model.
     *
     * @param string $field_name
     *   The internal field name.
     *
     * @return bool
     *   TRUE if the field exists and is enabled and should be considered as part
     *   of the data model. FALSE otherwise.
     */
    public function isFieldEnabled($field_name) {
        return $this->hasField($field_name) && $this->fields[$field_name]
            ->isFieldEnabled();
    }
    
    /**
     * Determine whether to include a collection count.
     *
     * @return bool
     *   Whether to include a collection count.
     */
    public function includeCount() {
        // By default, do not return counts in collection queries.
        return FALSE;
    }
    
    /**
     * Whether this resource type is internal.
     *
     * This must not be used as an access control mechanism.
     *
     * Internal resource types are not available via the HTTP API. They have no
     * routes and cannot be used for filtering or sorting. They cannot be included
     * in the response using the `include` query parameter.
     *
     * However, relationship fields on public resources *will include* a resource
     * identifier for the referenced internal resource.
     *
     * This method exists to remove data that should not logically be exposed by
     * the HTTP API. For example, read-only data from an internal resource might
     * be embedded in a public resource using computed fields. Therefore,
     * including the internal resource as a relationship with distinct routes
     * might unnecessarily expose internal implementation details.
     *
     * @return bool
     *   TRUE if the resource type is internal. FALSE otherwise.
     */
    public function isInternal() {
        return $this->internal;
    }
    
    /**
     * Whether resources of this resource type are locatable.
     *
     * A resource type may for example not be locatable when it is not stored.
     *
     * @return bool
     *   TRUE if the resource type's resources are locatable. FALSE otherwise.
     */
    public function isLocatable() {
        return $this->isLocatable;
    }
    
    /**
     * Whether resources of this resource type are mutable.
     *
     * Indicates that resources of this type may not be created, updated or
     * deleted (POST, PATCH or DELETE, respectively).
     *
     * @return bool
     *   TRUE if the resource type's resources are mutable. FALSE otherwise.
     */
    public function isMutable() {
        return $this->isMutable;
    }
    
    /**
     * Whether resources of this resource type are versionable.
     *
     * @return bool
     *   TRUE if the resource type's resources are versionable. FALSE otherwise.
     */
    public function isVersionable() {
        return $this->isVersionable;
    }
    
    /**
     * Instantiates a ResourceType object.
     *
     * @param string $entity_type_id
     *   An entity type ID.
     * @param string $bundle
     *   A bundle.
     * @param string $deserialization_target_class
     *   The deserialization target class.
     * @param bool $internal
     *   (optional) Whether the resource type should be internal.
     * @param bool $is_locatable
     *   (optional) Whether the resource type is locatable.
     * @param bool $is_mutable
     *   (optional) Whether the resource type is mutable.
     * @param bool $is_versionable
     *   (optional) Whether the resource type is versionable.
     * @param \Drupal\jsonapi\ResourceType\ResourceTypeField[] $fields
     *   (optional) The resource type fields, keyed by internal field name.
     */
    public function __construct($entity_type_id, $bundle, $deserialization_target_class, $internal = FALSE, $is_locatable = TRUE, $is_mutable = TRUE, $is_versionable = FALSE, array $fields = []) {
        if (!empty($fields) && !reset($fields) instanceof ResourceTypeField) {
            $fields = $this->updateDeprecatedFieldMapping($fields, $entity_type_id, $bundle);
        }
        $this->entityTypeId = $entity_type_id;
        $this->bundle = $bundle;
        $this->deserializationTargetClass = $deserialization_target_class;
        $this->internal = $internal;
        $this->isLocatable = $is_locatable;
        $this->isMutable = $is_mutable;
        $this->isVersionable = $is_versionable;
        $this->fields = $fields;
        $this->typeName = $this->bundle === '?' ? 'unknown' : sprintf('%s--%s', $this->entityTypeId, $this->bundle);
        $this->fieldMapping = array_flip(array_map(function (ResourceTypeField $field) {
            return $field->getPublicName();
        }, $this->fields));
    }
    
    /**
     * Sets the relatable resource types.
     *
     * @param array $relatable_resource_types
     *   The resource types with which this resource type may have a relationship.
     *   The array should be a multi-dimensional array keyed by public field name
     *   whose values are an array of resource types. There may be duplicate
     *   across resource types across fields, but not within a field.
     */
    public function setRelatableResourceTypes(array $relatable_resource_types) {
        $this->fields = array_reduce(array_keys($relatable_resource_types), function ($fields, $public_field_name) use ($relatable_resource_types) {
            if (!isset($this->fieldMapping[$public_field_name])) {
                throw new \LogicException('A field must exist for relatable resource types to be set on it.');
            }
            $internal_field_name = $this->fieldMapping[$public_field_name];
            $field = $fields[$internal_field_name];
            assert($field instanceof ResourceTypeRelationship);
            $fields[$internal_field_name] = $field->withRelatableResourceTypes($relatable_resource_types[$public_field_name]);
            return $fields;
        }, $this->fields);
    }
    
    /**
     * Get all resource types with which this type may have a relationship.
     *
     * @return array
     *   The relatable resource types, keyed by relationship field names.
     *
     * @see self::setRelatableResourceTypes()
     */
    public function getRelatableResourceTypes() {
        if (!isset($this->relatableResourceTypesByField)) {
            $this->relatableResourceTypesByField = array_reduce(array_map(function (ResourceTypeRelationship $field) {
                return [
                    $field->getPublicName() => $field->getRelatableResourceTypes(),
                ];
            }, array_filter($this->fields, function (ResourceTypeField $field) {
                return $field instanceof ResourceTypeRelationship && $field->isFieldEnabled();
            })), 'array_merge', []);
        }
        return $this->relatableResourceTypesByField;
    }
    
    /**
     * Get all resource types with which the given field may have a relationship.
     *
     * @param string $field_name
     *   The public field name.
     *
     * @return \Drupal\jsonapi\ResourceType\ResourceType[]
     *   The relatable JSON:API resource types.
     *
     * @see self::getRelatableResourceTypes()
     */
    public function getRelatableResourceTypesByField($field_name) {
        return ($field = $this->getFieldByPublicName($field_name)) && $field instanceof ResourceTypeRelationship && $field->isFieldEnabled() ? $field->getRelatableResourceTypes() : [];
    }
    
    /**
     * Get the resource path.
     *
     * @return string
     *   The path to access this resource type. Default: /entity_type_id/bundle.
     *
     * @see jsonapi.base_path
     */
    public function getPath() {
        return sprintf('/%s/%s', $this->getEntityTypeId(), $this->getBundle());
    }
    
    /**
     * {@inheritdoc}
     */
    public function __get($name) {
        $class_name = self::class;
        @trigger_error("Using the {${$name}} protected property of a {$class_name} is deprecated in Drupal 8.8.0 and will not be allowed in Drupal 9.0.0. Use {$class_name}::getFields() instead. See https://www.drupal.org/node/3084721.", E_USER_DEPRECATED);
        if ($name === 'disabledFields') {
            return array_map(function (ResourceTypeField $field) {
                return $field->getInternalName();
            }, array_filter($this->getFields(), function (ResourceTypeField $field) {
                return !$field->isFieldEnabled();
            }));
        }
        if ($name === 'invertedFieldMapping') {
            return array_reduce($this->getFields(), function ($inverted_field_mapping, ResourceTypeField $field) {
                $internal_field_name = $field->getInternalName();
                $public_field_name = $field->getPublicName();
                if ($field->isFieldEnabled() && $internal_field_name !== $public_field_name) {
                    $inverted_field_mapping[$public_field_name] = $internal_field_name;
                }
                return $inverted_field_mapping;
            }, []);
        }
    }
    
    /**
     * Takes a deprecated field mapping and converts it to ResourceTypeFields.
     *
     * @param array $field_mapping
     *   The deprecated field mapping.
     * @param string $entity_type_id
     *   The entity type ID of the field mapping.
     * @param string $bundle
     *   The bundle ID of the field mapping or the entity type ID if the entity
     *   type does not have bundles.
     *
     * @return \Drupal\jsonapi\ResourceType\ResourceTypeField[]
     *   The updated field mapping objects.
     *
     * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
     *   self::getFields() instead.
     *
     * @see https://www.drupal.org/project/drupal/issues/3014277
     */
    private function updateDeprecatedFieldMapping(array $field_mapping, $entity_type_id, $bundle) {
        $class_name = self::class;
        @trigger_error("Passing an array with strings or booleans as a field mapping to {$class_name}::__construct() is deprecated in Drupal 8.8.0 and will not be allowed in Drupal 9.0.0. See \\Drupal\\jsonapi\\ResourceTypeRepository::getFields(). See https://www.drupal.org/node/3084746.", E_USER_DEPRECATED);
        // See \Drupal\jsonapi\ResourceType\ResourceTypeRepository::isReferenceFieldDefinition().
        $is_reference_field_definition = function (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()];
            }
            
            /* @var \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition */
            $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;
        };
        
        /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
        $entity_type_manager = \Drupal::service('entity_type.manager');
        $is_fieldable = $entity_type_manager->getDefinition($entity_type_id)
            ->entityClassImplements(FieldableEntityInterface::class);
        $field_definitions = $is_fieldable ? \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle) : [];
        $fields = [];
        foreach ($field_mapping as $internal_field_name => $public_field_name) {
            assert(is_bool($public_field_name) || is_string($public_field_name));
            $field_definition = $is_fieldable && !empty($field_definitions[$internal_field_name]) ? $field_definitions[$internal_field_name] : NULL;
            $is_relationship_field = $field_definition && $is_reference_field_definition($field_definition);
            $has_one = !$field_definition || $field_definition->getFieldStorageDefinition()
                ->getCardinality() === 1;
            $alias = is_string($public_field_name) ? $public_field_name : NULL;
            $fields[$internal_field_name] = $is_relationship_field ? new ResourceTypeRelationship($internal_field_name, $alias, $public_field_name !== FALSE, $has_one) : new ResourceTypeAttribute($internal_field_name, $alias, $public_field_name !== FALSE, $has_one);
        }
        return $fields;
    }

}

Classes

Title Deprecated Summary
ResourceType Value object containing all metadata for a JSON:API resource type.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.