Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.29% covered (success)
94.29%
33 / 35
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrimaryEntityReferenceItemNormalizer
94.29% covered (success)
94.29%
33 / 35
66.67% covered (warning)
66.67%
4 / 6
16.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsNormalization
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normalize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsDenormalization
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 denormalize
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
10
 getSupportedTypes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\primary_entity_reference\Normalizer;
6
7use Drupal\Core\Entity\EntityRepositoryInterface;
8use Drupal\primary_entity_reference\Plugin\Field\FieldType\PrimaryEntityReferenceItem;
9use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
10use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
11
12/**
13 * Normalizer for PrimaryEntityReferenceItem field items.
14 *
15 * Supports the 'entity' key format (UUID-based) used by Drupal Recipes
16 * and Default Content during import. Converts 'entity: uuid' to
17 * 'target_id: numeric_id' for compatibility.
18 *
19 * Implements DenormalizerInterface directly (Drupal 11 pattern).
20 */
21class PrimaryEntityReferenceItemNormalizer implements DenormalizerInterface, NormalizerInterface {
22
23  /**
24   * Constructs a PrimaryEntityReferenceItemNormalizer object.
25   *
26   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
27   *   The entity repository service.
28   */
29  public function __construct(
30    protected EntityRepositoryInterface $entityRepository,
31  ) {
32  }
33
34  /**
35   * {@inheritdoc}
36   */
37  public function supportsNormalization(mixed $data, ?string $format = NULL, array $context = []): bool {
38    return $data instanceof PrimaryEntityReferenceItem;
39  }
40
41  /**
42   * {@inheritdoc}
43   */
44  public function normalize(mixed $object, ?string $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
45    // Default normalization: return field values as array.
46    return $object->getValue();
47  }
48
49  /**
50   * {@inheritdoc}
51   *
52   * CRITICAL: Symfony appends [] to the type when denormalizing arrays.
53   * We must explicitly support both the base class and array notation.
54   */
55  public function supportsDenormalization(
56    mixed $data,
57    string $type,
58    ?string $format = NULL,
59    array $context = [],
60  ): bool {
61    return $type === PrimaryEntityReferenceItem::class
62      || $type === PrimaryEntityReferenceItem::class . '[]';
63  }
64
65  /**
66   * {@inheritdoc}
67   *
68   * Handles both single field items and arrays of field items.
69   * Returns plain value arrays suitable for field assignment.
70   */
71  public function denormalize(
72    mixed $data,
73    string $type,
74    ?string $format = NULL,
75    array $context = [],
76  ): mixed {
77    // Handle array of field items (e.g., "ClassName[]").
78    if ($type === PrimaryEntityReferenceItem::class . '[]') {
79      $items = [];
80      foreach ($data as $delta => $value) {
81        $item = $this->denormalize(
82          $value,
83          PrimaryEntityReferenceItem::class,
84          $format,
85          $context
86        );
87        if ($item !== NULL) {
88          $items[$delta] = $item;
89        }
90      }
91      return $items;
92    }
93
94    // ---- Single item denormalization ----
95    $values = [];
96
97    // Backward compatibility: target_id format.
98    if (isset($data['target_id'])) {
99      $values['target_id'] = $data['target_id'];
100    }
101
102    // New format: entity UUID (for recipes/default content).
103    if (isset($data['entity']) && isset($context['field_definition'])) {
104      $uuid = $data['entity'];
105
106      // Get target entity type from field settings.
107      $target_type = $context['field_definition']
108        ->getFieldStorageDefinition()
109        ->getSetting('target_type');
110
111      // Load entity by UUID.
112      $entity = $this->entityRepository->loadEntityByUuid($target_type, $uuid);
113
114      if ($entity) {
115        $values['target_id'] = $entity->id();
116      }
117      // If entity not found, don't set target_id.
118      // Item will still be created if other properties (like primary) exist.
119    }
120
121    // Preserve primary flag (even if entity is missing).
122    if (array_key_exists('primary', $data)) {
123      $values['primary'] = $data['primary'];
124    }
125
126    // Return values array if it has any keys, otherwise NULL.
127    return !empty($values) ? $values : NULL;
128  }
129
130  /**
131   * {@inheritdoc}
132   */
133  public function getSupportedTypes(?string $format): array {
134    return [
135      PrimaryEntityReferenceItem::class       => TRUE,
136      PrimaryEntityReferenceItem::class . '[]' => TRUE,
137    ];
138  }
139
140}