class ResourceObjectNormalizer

Same name in other branches
  1. 9 core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer
  2. 8.9.x core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer
  3. 10 core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer

Converts the JSON:API module ResourceObject into a JSON:API array structure.

@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.

Hierarchy

Expanded class hierarchy of ResourceObjectNormalizer

See also

https://www.drupal.org/project/drupal/issues/3032787

jsonapi.api.php

1 string reference to 'ResourceObjectNormalizer'
jsonapi.services.yml in core/modules/jsonapi/jsonapi.services.yml
core/modules/jsonapi/jsonapi.services.yml
1 service uses ResourceObjectNormalizer
serializer.normalizer.resource_object.jsonapi in core/modules/jsonapi/jsonapi.services.yml
Drupal\jsonapi\Normalizer\ResourceObjectNormalizer

File

core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php, line 34

Namespace

Drupal\jsonapi\Normalizer
View source
class ResourceObjectNormalizer extends NormalizerBase {
    use SchematicNormalizerTrait;
    
    /**
     * The entity normalization cacher.
     *
     * @var \Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher
     */
    protected $cacher;
    
    /**
     * @var mixed|\Symfony\Component\EventDispatcher\EventDispatcherInterface|null
     */
    private EventDispatcherInterface $eventDispatcher;
    
    /**
     * @var mixed|\Drupal\Core\Entity\EntityFieldManagerInterface|null
     */
    private EntityFieldManagerInterface $entityFieldManager;
    
    /**
     * @var mixed|\Drupal\Core\Entity\EntityTypeManagerInterface|null
     */
    private EntityTypeManagerInterface $entityTypeManager;
    
    /**
     * Constructs a ResourceObjectNormalizer object.
     *
     * @param \Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher $cacher
     *   The entity normalization cacher.
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
     *   The event dispatcher.
     * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
     *   The entity field manager.
     * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
     *   The entity type manager.
     */
    public function __construct(ResourceObjectNormalizationCacher $cacher, ?EventDispatcherInterface $event_dispatcher = NULL, ?EntityFieldManagerInterface $entity_field_manager = NULL, ?EntityTypeManagerInterface $entity_type_manager = NULL) {
        $this->cacher = $cacher;
        if ($event_dispatcher === NULL) {
            @trigger_error('Calling ' . __METHOD__ . '() without the $event_dispatcher argument is deprecated in drupal:11.2.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3280569', E_USER_DEPRECATED);
            $event_dispatcher = \Drupal::service('event_dispatcher');
        }
        $this->eventDispatcher = $event_dispatcher;
        if ($entity_field_manager === NULL) {
            @trigger_error('Calling ' . __METHOD__ . '() without the $entity_field_manager argument is deprecated in drupal:11.2.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3031367', E_USER_DEPRECATED);
            $entity_field_manager = \Drupal::service('entity_field_manager');
        }
        $this->entityFieldManager = $entity_field_manager;
        if ($entity_type_manager === NULL) {
            @trigger_error('Calling ' . __METHOD__ . '() without the $entity_type_manager argument is deprecated in drupal:11.2.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3031367', E_USER_DEPRECATED);
            $entity_type_manager = \Drupal::service('entity_type_manager');
        }
        $this->entityTypeManager = $entity_type_manager;
    }
    
    /**
     * {@inheritdoc}
     */
    public function supportsDenormalization($data, string $type, ?string $format = NULL, array $context = []) : bool {
        return FALSE;
    }
    
    /**
     * {@inheritdoc}
     */
    public function doNormalize($object, $format = NULL, array $context = []) : array|string|int|float|bool|\ArrayObject|null {
        assert($object instanceof ResourceObject);
        // If the fields to use were specified, only output those field values.
        $context['resource_object'] = $object;
        $resource_type = $object->getResourceType();
        $resource_type_name = $resource_type->getTypeName();
        $fields = $object->getFields();
        // Get the bundle ID of the requested resource. This is used to determine if
        // this is a bundle level resource or an entity level resource.
        if (!empty($context['sparse_fieldset'][$resource_type_name])) {
            $field_names = $context['sparse_fieldset'][$resource_type_name];
        }
        else {
            $field_names = array_keys($fields);
        }
        $normalization_parts = $this->getNormalization($field_names, $object, $format, $context);
        // Keep only the requested fields (the cached normalization gradually grows
        // to the complete set of fields).
        $fields = $normalization_parts[ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_FIELDS];
        $field_normalizations = array_intersect_key($fields, array_flip($field_names));
        $relationship_field_names = array_keys($resource_type->getRelatableResourceTypes());
        $attributes = array_diff_key($field_normalizations, array_flip($relationship_field_names));
        $relationships = array_intersect_key($field_normalizations, array_flip($relationship_field_names));
        $event = new CollectResourceObjectMetaEvent($object, $context);
        $this->eventDispatcher
            ->dispatch($event);
        $entity_normalization = array_filter($normalization_parts[ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_BASE] + [
            'attributes' => CacheableNormalization::aggregate($attributes)->omitIfEmpty(),
            'relationships' => CacheableNormalization::aggregate($relationships)->omitIfEmpty(),
            'meta' => count($event->getMeta()) > 0 ? new CacheableNormalization($event, $event->getMeta()) : '',
        ]);
        return CacheableNormalization::aggregate($entity_normalization)->withCacheableDependency($object);
    }
    
    /**
     * Normalizes an entity using the given fieldset.
     *
     * @param string[] $field_names
     *   The field names to normalize (the sparse fieldset, if any).
     * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $object
     *   The resource object to partially normalize.
     * @param string $format
     *   The format in which the normalization will be encoded.
     * @param array $context
     *   Context options for the normalizer.
     *
     * @return array
     *   An array with two key-value pairs:
     *   - 'base': array, the base normalization of the entity, that does not
     *             depend on which sparse fieldset was requested.
     *   - 'fields': CacheableNormalization for all requested fields.
     *
     * @see ::normalize()
     */
    protected function getNormalization(array $field_names, ResourceObject $object, $format = NULL, array $context = []) {
        $cached_normalization_parts = $this->cacher
            ->get($object);
        $normalizer_values = $cached_normalization_parts !== FALSE ? $cached_normalization_parts : static::buildEmptyNormalization($object);
        $fields =& $normalizer_values[ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_FIELDS];
        $non_cached_fields = array_diff_key($object->getFields(), $fields);
        $non_cached_requested_fields = array_intersect_key($non_cached_fields, array_flip($field_names));
        foreach ($non_cached_requested_fields as $field_name => $field) {
            $fields[$field_name] = $this->serializeField($field, $context, $format);
        }
        // Add links if missing.
        $base =& $normalizer_values[ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_BASE];
        $base['links'] = $base['links'] ?? $this->serializer
            ->normalize($object->getLinks(), $format, $context)
            ->omitIfEmpty();
        if (!empty($non_cached_requested_fields)) {
            $this->cacher
                ->saveOnTerminate($object, $normalizer_values);
        }
        return $normalizer_values;
    }
    
    /**
     * Builds the empty normalization structure for cache misses.
     *
     * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $object
     *   The resource object being normalized.
     *
     * @return array
     *   The normalization structure as defined in ::getNormalization().
     *
     * @see ::getNormalization()
     */
    protected static function buildEmptyNormalization(ResourceObject $object) {
        return [
            ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_BASE => [
                'type' => CacheableNormalization::permanent($object->getResourceType()
                    ->getTypeName()),
                'id' => CacheableNormalization::permanent($object->getId()),
            ],
            ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_FIELDS => [],
        ];
    }
    
    /**
     * Serializes a given field.
     *
     * @param mixed $field
     *   The field to serialize.
     * @param array $context
     *   The normalization context.
     * @param string $format
     *   The serialization format.
     *
     * @return \Drupal\jsonapi\Normalizer\Value\CacheableNormalization
     *   The normalized value.
     */
    protected function serializeField($field, array $context, $format) {
        // Only content entities contain FieldItemListInterface fields. Since config
        // entities do not have "real" fields and therefore do not have field access
        // restrictions.
        if ($field instanceof FieldItemListInterface) {
            $field_access_result = $field->access('view', $context['account'] ?? NULL, TRUE);
            if (!$field_access_result->isAllowed()) {
                return new CacheableOmission(CacheableMetadata::createFromObject($field_access_result));
            }
            if ($field instanceof EntityReferenceFieldItemListInterface) {
                // Build the relationship object based on the entity reference and
                // normalize that object instead.
                assert(!empty($context['resource_object']) && $context['resource_object'] instanceof ResourceObject);
                $resource_object = $context['resource_object'];
                $resource_field_name = $resource_object->getResourceType()
                    ->getFieldByInternalName($field->getName())
                    ->getPublicName();
                $collect_meta_event = new CollectRelationshipMetaEvent($resource_object, $resource_field_name);
                $this->eventDispatcher
                    ->dispatch($collect_meta_event);
                $relationship = Relationship::createFromEntityReferenceField(context: $resource_object, field: $field, meta: $collect_meta_event->getMeta());
                $normalized_field = $this->serializer
                    ->normalize($relationship, $format, $context);
                $normalized_field = $normalized_field->withCacheableDependency($collect_meta_event);
            }
            else {
                $normalized_field = $this->serializer
                    ->normalize($field, $format, $context);
            }
            assert($normalized_field instanceof CacheableNormalization);
            return $normalized_field->withCacheableDependency(CacheableMetadata::createFromObject($field_access_result));
        }
        else {
            // @todo Replace this workaround after https://www.drupal.org/node/3043245
            //   or remove the need for this in https://www.drupal.org/node/2942975.
            //   See \Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer.
            if (is_a($context['resource_object']->getResourceType()
                ->getDeserializationTargetClass(), 'Drupal\\layout_builder\\Entity\\LayoutBuilderEntityViewDisplay', TRUE) && $context['resource_object']->getField('third_party_settings') === $field) {
                unset($field['layout_builder']['sections']);
            }
            // Config "fields" in this case are arrays or primitives and do not need
            // to be normalized.
            return CacheableNormalization::permanent($field);
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function getNormalizationSchema(mixed $object, array $context = []) : array {
        if (is_string($object)) {
            // Without a true object we can only provide a generic schema.
            return [
                '$ref' => JsonApiSpec::SUPPORTED_SPECIFICATION_JSON_SCHEMA . '#/definitions/resource',
            ];
        }
        // ResourceObject is usually instantiated from a specific entity, however
        // a placeholder can be created for purposes of schema generation which
        // would provide access to the resource type, but not contain any "live"
        // data.
        assert($object instanceof ResourceObject);
        $attributes_schema = [];
        $relationships_schema = [];
        $this->entityTypeManager
            ->getDefinition($object->getResourceType()
            ->getEntityTypeId())
            ->entityClassImplements(FieldableEntityInterface::class) ? $this->processContentEntitySchema($object, $context, $attributes_schema, $relationships_schema) : $this->processConfigEntitySchema($object->getResourceType(), $context, $attributes_schema);
        return [
            'allOf' => [
                [
                    '$ref' => JsonApiSpec::SUPPORTED_SPECIFICATION_JSON_SCHEMA . '#/definitions/resourceIdentification',
                ],
            ],
            'properties' => [
                'meta' => [
                    '$ref' => JsonApiSpec::SUPPORTED_SPECIFICATION_JSON_SCHEMA . '#/definitions/meta',
                ],
                'links' => [
                    '$ref' => JsonApiSpec::SUPPORTED_SPECIFICATION_JSON_SCHEMA . '#/definitions/resourceLinks',
                ],
                // If the array is empty we must return an object so it won't encode as
                // an array; we don't cast the value here so the returned value is still
                // traversable as an array when accessed in PHP.
'attributes' => $attributes_schema ?: new \ArrayObject(),
                'relationships' => $relationships_schema ?: new \ArrayObject(),
            ],
            'unevaluatedProperties' => FALSE,
        ];
    }
    protected function processConfigEntitySchema(ResourceType $resource_type, array $context, array &$attributes_schema) : void {
        // This is largely the same as in ResourceObject but without a real entity.
        $fields = $resource_type->getFields();
        // Filter the array based on the field names.
        $enabled_field_names = array_filter(array_keys($fields), static fn(string $internal_field_name) => $resource_type->isFieldEnabled($internal_field_name));
        // Return a sub-array of $output containing the keys in $enabled_fields.
        $input = array_intersect_key($fields, array_flip($enabled_field_names));
        foreach ($input as $field_name => $field_value) {
            $attributes_schema['properties'][$resource_type->getPublicName($field_name)] = [
                'title' => $field_name,
            ];
        }
    }
    protected function processContentEntitySchema(ResourceObject $resource_object, array $context, array &$attributes_schema, &$relationships_schema) : void {
        // Actual normalization supports sparse fieldsets, however we provide schema
        // for all possible fields that may be retrieved.
        $resource_type = $resource_object->getResourceType();
        $field_definitions = $this->entityFieldManager
            ->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle());
        $resource_fields = $resource_type->getFields();
        // User resource objects contain a read-only attribute that is not a real
        // field on the user entity type.
        // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields()
        // @todo Eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254.
        if ($resource_type->getEntityTypeId() === 'user') {
            $resource_fields = array_diff_key($resource_fields, array_flip([
                $resource_type->getPublicName('display_name'),
            ]));
        }
        
        /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
        $fields = array_reduce($resource_fields, function (array $carry, ResourceTypeField $resource_field) use ($field_definitions) {
            if (!$resource_field->isFieldEnabled()) {
                return $carry;
            }
            $carry[$resource_field->getPublicName()] = $field_definitions[$resource_field->getInternalName()];
            return $carry;
        }, []);
        assert($this->serializer instanceof Serializer);
        $relationship_field_names = array_keys($resource_type->getRelatableResourceTypes());
        $create_values = [];
        if ($bundle_key = $this->entityTypeManager
            ->getDefinition($resource_type->getEntityTypeId())
            ->getKey('bundle')) {
            $create_values = [
                $bundle_key => $resource_type->getBundle(),
            ];
        }
        $stub_entity = $this->entityTypeManager
            ->getStorage($resource_type->getEntityTypeId())
            ->create($create_values);
        foreach ($fields as $field_name => $field) {
            $stub_field = $stub_entity->get($field->getName());
            if ($stub_field instanceof EntityReferenceFieldItemListInterface) {
                // Build the relationship object based on the entity reference and
                // retrieve normalizer for that object instead.
                // @see ::serializeField()
                $relationship = Relationship::createFromEntityReferenceField($resource_object, $stub_field);
                $schema = $this->serializer
                    ->getJsonSchema($relationship, $context);
            }
            else {
                $schema = $this->serializer
                    ->getJsonSchema($stub_field, $context);
            }
            // Fallback basic annotations.
            if (empty($schema['title']) && ($title = $field->getLabel())) {
                $schema['title'] = (string) $title;
            }
            if (empty($schema['description']) && ($description = $field->getFieldStorageDefinition()
                ->getDescription())) {
                $schema['description'] = (string) $description;
            }
            if (in_array($field_name, $relationship_field_names, TRUE)) {
                $relationships_schema['properties'][$field_name] = $schema;
            }
            else {
                $attributes_schema['properties'][$field_name] = $schema;
            }
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function getSupportedTypes(?string $format) : array {
        return [
            ResourceObject::class => TRUE,
        ];
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY constant Name of key for bubbling cacheability metadata via serialization context.
JsonSchemaReflectionTrait::getJsonSchemaForMethod protected function Get a JSON Schema based on method reflection.
NormalizerBase::$format protected property Overrides NormalizerBase::$format
NormalizerBase::addCacheableDependency protected function Adds cacheability if applicable.
NormalizerBase::rasterizeValueRecursive protected static function Rasterizes a value recursively.
NormalizerBase::supportsNormalization public function 1
ResourceObjectNormalizer::$cacher protected property The entity normalization cacher.
ResourceObjectNormalizer::$entityFieldManager private property
ResourceObjectNormalizer::$entityTypeManager private property
ResourceObjectNormalizer::$eventDispatcher private property
ResourceObjectNormalizer::buildEmptyNormalization protected static function Builds the empty normalization structure for cache misses.
ResourceObjectNormalizer::doNormalize public function Overrides SchematicNormalizerTrait::doNormalize
ResourceObjectNormalizer::getNormalization protected function Normalizes an entity using the given fieldset.
ResourceObjectNormalizer::getNormalizationSchema public function Overrides SchematicNormalizerTrait::getNormalizationSchema
ResourceObjectNormalizer::getSupportedTypes public function Overrides NormalizerBase::getSupportedTypes
ResourceObjectNormalizer::processConfigEntitySchema protected function
ResourceObjectNormalizer::processContentEntitySchema protected function
ResourceObjectNormalizer::serializeField protected function Serializes a given field.
ResourceObjectNormalizer::supportsDenormalization public function Overrides NormalizerBase::supportsDenormalization
ResourceObjectNormalizer::__construct public function Constructs a ResourceObjectNormalizer object.
SchematicNormalizerFallbackTrait::generateNoSchemaAvailableMessage public static function
SchematicNormalizerHelperTrait::checkFormat protected function
SchematicNormalizerTrait::normalize public function 2

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