Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
45.68% covered (danger)
45.68%
74 / 162
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RelationshipStatisticsFormatter
45.68% covered (danger)
45.68%
74 / 162
50.00% covered (danger)
50.00%
4 / 8
207.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 defaultSettings
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 settingsForm
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
2
 settingsSummary
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
6
 viewElements
88.89% covered (warning)
88.89%
32 / 36
0.00% covered (danger)
0.00%
0 / 1
13.23
 getLabel
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
13.99
 getRelationshipTypes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\crm\Plugin\Field\FieldFormatter;
6
7use Drupal\Core\Entity\EntityTypeManagerInterface;
8use Drupal\Core\Field\Attribute\FieldFormatter;
9use Drupal\Core\Field\FieldDefinitionInterface;
10use Drupal\Core\Field\FieldItemListInterface;
11use Drupal\Core\Field\FormatterBase;
12use Drupal\Core\Form\FormStateInterface;
13use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
14use Drupal\Core\StringTranslation\TranslatableMarkup;
15use Symfony\Component\DependencyInjection\ContainerInterface;
16
17/**
18 * Formatter for the relationship statistics field.
19 */
20#[FieldFormatter(
21  id: "crm_relationship_statistics_default",
22  label: new TranslatableMarkup("Relationship Statistics"),
23  description: new TranslatableMarkup("Display relationship type labels with counts."),
24  field_types: ["crm_relationship_statistics"],
25)]
26class RelationshipStatisticsFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
27
28  /**
29   * The entity type manager.
30   *
31   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
32   */
33  protected EntityTypeManagerInterface $entityTypeManager;
34
35  /**
36   * Cached relationship types.
37   *
38   * @var array|null
39   */
40  protected ?array $relationshipTypes = NULL;
41
42  /**
43   * {@inheritdoc}
44   */
45  public function __construct(
46    $plugin_id,
47    $plugin_definition,
48    FieldDefinitionInterface $field_definition,
49    array $settings,
50    $label,
51    $view_mode,
52    array $third_party_settings,
53    EntityTypeManagerInterface $entity_type_manager,
54  ) {
55    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
56    $this->entityTypeManager = $entity_type_manager;
57  }
58
59  /**
60   * {@inheritdoc}
61   */
62  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
63    return new static(
64      $plugin_id,
65      $plugin_definition,
66      $configuration['field_definition'],
67      $configuration['settings'],
68      $configuration['label'],
69      $configuration['view_mode'],
70      $configuration['third_party_settings'],
71      $container->get('entity_type.manager'),
72    );
73  }
74
75  /**
76   * {@inheritdoc}
77   */
78  public static function defaultSettings() {
79    return [
80      'format' => 'label_count',
81      'label_position' => 'opposite',
82      'sort_by' => 'none',
83      'sort_order' => 'asc',
84    ] + parent::defaultSettings();
85  }
86
87  /**
88   * {@inheritdoc}
89   */
90  public function settingsForm(array $form, FormStateInterface $form_state) {
91    $elements = [];
92
93    $elements['format'] = [
94      '#type' => 'select',
95      '#title' => $this->t('Display format'),
96      '#options' => [
97        'label_count' => $this->t('Label (count) - e.g., "Parent (3)"'),
98        'label_colon_count' => $this->t('Label: count - e.g., "Parent: 3"'),
99        'count_label' => $this->t('count Label - e.g., "3 Parents"'),
100        'label_only' => $this->t('Label only'),
101      ],
102      '#default_value' => $this->getSetting('format'),
103    ];
104
105    $elements['label_position'] = [
106      '#type' => 'select',
107      '#title' => $this->t('Label position'),
108      '#description' => $this->t('For asymmetric relationships, choose which label to display.'),
109      '#options' => [
110        'opposite' => $this->t('Opposite position (show what the related contacts are)'),
111        'same' => $this->t('Same position (show what this contact is in the relationship)'),
112      ],
113      '#default_value' => $this->getSetting('label_position'),
114    ];
115
116    $elements['sort_by'] = [
117      '#type' => 'select',
118      '#title' => $this->t('Sort by'),
119      '#options' => [
120        'none' => $this->t('No sorting (maintain original order)'),
121        'label' => $this->t('Label'),
122        'count' => $this->t('Count'),
123      ],
124      '#default_value' => $this->getSetting('sort_by'),
125    ];
126
127    $elements['sort_order'] = [
128      '#type' => 'select',
129      '#title' => $this->t('Sort order'),
130      '#options' => [
131        'asc' => $this->t('Ascending (A-Z or lowest first)'),
132        'desc' => $this->t('Descending (Z-A or highest first)'),
133      ],
134      '#default_value' => $this->getSetting('sort_order'),
135      '#states' => [
136        'invisible' => [
137          ':input[name="fields[relationship_statistics][settings_edit_form][settings][sort_by]"]' => ['value' => 'none'],
138        ],
139      ],
140    ];
141
142    return $elements;
143  }
144
145  /**
146   * {@inheritdoc}
147   */
148  public function settingsSummary() {
149    $summary = [];
150
151    $format = $this->getSetting('format');
152    $formats = [
153      'label_count' => $this->t('Label (count)'),
154      'label_colon_count' => $this->t('Label: count'),
155      'count_label' => $this->t('count Label'),
156      'label_only' => $this->t('Label only'),
157    ];
158    $summary[] = $this->t('Format: @format', ['@format' => $formats[$format] ?? $format]);
159
160    $label_position = $this->getSetting('label_position');
161    $label_positions = [
162      'opposite' => $this->t('Opposite position'),
163      'same' => $this->t('Same position'),
164    ];
165    $summary[] = $this->t('Label: @position', ['@position' => $label_positions[$label_position] ?? $label_position]);
166
167    $sort_by = $this->getSetting('sort_by');
168    if ($sort_by !== 'none') {
169      $sort_order = $this->getSetting('sort_order');
170      $sort_options = [
171        'label' => $this->t('label'),
172        'count' => $this->t('count'),
173      ];
174      $order_options = [
175        'asc' => $this->t('ascending'),
176        'desc' => $this->t('descending'),
177      ];
178      $summary[] = $this->t('Sort by @sort_by (@order)', [
179        '@sort_by' => $sort_options[$sort_by] ?? $sort_by,
180        '@order' => $order_options[$sort_order] ?? $sort_order,
181      ]);
182    }
183
184    return $summary;
185  }
186
187  /**
188   * {@inheritdoc}
189   */
190  public function viewElements(FieldItemListInterface $items, $langcode) {
191    $format = $this->getSetting('format');
192    $label_position = $this->getSetting('label_position');
193    $sort_by = $this->getSetting('sort_by');
194    $sort_order = $this->getSetting('sort_order');
195    $use_opposite = $label_position === 'opposite';
196
197    // Build an array of items with their labels and counts for sorting.
198    $processed_items = [];
199    foreach ($items as $delta => $item) {
200      $type_key = $item->value;
201      $count = (int) $item->count;
202      // For 'label_only' format, always use singular label since count is not
203      // displayed. For other formats, use singular/plural based on count.
204      $label_count = $format === 'label_only' ? 1 : $count;
205      $label = $this->getLabel($type_key, $use_opposite, $label_count);
206
207      $processed_items[] = [
208        'delta' => $delta,
209        'label' => $label,
210        'count' => $count,
211      ];
212    }
213
214    // Apply sorting if configured.
215    if ($sort_by !== 'none' && count($processed_items) > 1) {
216      usort($processed_items, function ($a, $b) use ($sort_by, $sort_order) {
217        if ($sort_by === 'label') {
218          $comparison = strcasecmp($a['label'], $b['label']);
219        }
220        else {
221          $comparison = $a['count'] <=> $b['count'];
222        }
223
224        return $sort_order === 'desc' ? -$comparison : $comparison;
225      });
226    }
227
228    // Build the render elements.
229    $elements = [];
230    foreach ($processed_items as $index => $item) {
231      $label = $item['label'];
232      $count = $item['count'];
233
234      $output = match ($format) {
235        'label_count' => $this->t('@label (@count)', ['@label' => $label, '@count' => $count]),
236        'label_colon_count' => $this->t('@label: @count', ['@label' => $label, '@count' => $count]),
237        'count_label' => $this->t('@count @label', ['@count' => $count, '@label' => $label]),
238        'label_only' => $label,
239        default => $this->t('@label (@count)', ['@label' => $label, '@count' => $count]),
240      };
241
242      $elements[$index] = ['#markup' => $output];
243    }
244
245    return $elements;
246  }
247
248  /**
249   * Gets the human-readable label for a relationship type key.
250   *
251   * @param string $type_key
252   *   The relationship type key (e.g., "friends" or "parent_child:a").
253   * @param bool $use_opposite
254   *   If TRUE, return the opposite position's label (what the related contacts
255   *   are). If FALSE, return the same position's label (what this contact is
256   *   in the relationship). Defaults to TRUE.
257   * @param int $count
258   *   The count of relationships. Used to determine singular vs plural label.
259   *   Defaults to 1 (singular).
260   *
261   * @return string
262   *   The human-readable label.
263   */
264  protected function getLabel(string $type_key, bool $use_opposite = TRUE, int $count = 1): string {
265    // Parse the type key to get the relationship type ID and position.
266    $parts = explode(':', $type_key);
267    $type_id = $parts[0];
268    $position = $parts[1] ?? NULL;
269
270    $relationship_types = $this->getRelationshipTypes();
271
272    if (!isset($relationship_types[$type_id])) {
273      // Fallback to the raw key if type not found.
274      return $type_key;
275    }
276
277    $type = $relationship_types[$type_id];
278
279    // Determine if we should use plural (count != 1).
280    $use_plural = $count !== 1;
281
282    // For asymmetric relationships, get the appropriate label based on
283    // the use_opposite setting.
284    if ($position === 'a') {
285      // Position A: opposite means label_b, same means label_a.
286      $label_key = $use_opposite ? 'label_b' : 'label_a';
287      $plural_key = $use_opposite ? 'label_b_plural' : 'label_a_plural';
288      $singular_label = $type->get($label_key) ?? $type->label();
289      if ($use_plural) {
290        $plural_label = $type->get($plural_key);
291        return !empty($plural_label) ? $plural_label : $singular_label;
292      }
293      return $singular_label;
294    }
295    elseif ($position === 'b') {
296      // Position B: opposite means label_a, same means label_b.
297      $label_key = $use_opposite ? 'label_a' : 'label_b';
298      $plural_key = $use_opposite ? 'label_a_plural' : 'label_b_plural';
299      $singular_label = $type->get($label_key) ?? $type->label();
300      if ($use_plural) {
301        $plural_label = $type->get($plural_key);
302        return !empty($plural_label) ? $plural_label : $singular_label;
303      }
304      return $singular_label;
305    }
306
307    // For symmetric relationships, use the main label.
308    return $type->label();
309  }
310
311  /**
312   * Gets all relationship types, cached for the request.
313   *
314   * @return array
315   *   An array of relationship type entities keyed by ID.
316   */
317  protected function getRelationshipTypes(): array {
318    if ($this->relationshipTypes === NULL) {
319      $this->relationshipTypes = $this->entityTypeManager
320        ->getStorage('crm_relationship_type')
321        ->loadMultiple();
322    }
323    return $this->relationshipTypes;
324  }
325
326}