Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.10% covered (danger)
32.10%
26 / 81
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RelationshipStatistics
32.10% covered (danger)
32.10%
26 / 81
50.00% covered (danger)
50.00%
3 / 6
292.29
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 defineOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 buildOptionsForm
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 render
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 getLabel
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
 getRelationshipTypes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\crm\Plugin\views\field;
6
7use Drupal\Core\Entity\EntityTypeManagerInterface;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\views\Attribute\ViewsField;
11use Drupal\views\Plugin\views\field\FieldPluginBase;
12use Drupal\views\ResultRow;
13use Symfony\Component\DependencyInjection\ContainerInterface;
14
15/**
16 * Field handler for relationship statistics count.
17 */
18#[ViewsField("crm_relationship_statistics")]
19class RelationshipStatistics extends FieldPluginBase {
20
21  /**
22   * The entity type manager.
23   *
24   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
25   */
26  protected EntityTypeManagerInterface $entityTypeManager;
27
28  /**
29   * Cached relationship types.
30   *
31   * @var array|null
32   */
33  protected ?array $relationshipTypes = NULL;
34
35  /**
36   * {@inheritdoc}
37   */
38  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
39    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
40    $instance->entityTypeManager = $container->get('entity_type.manager');
41    return $instance;
42  }
43
44  /**
45   * {@inheritdoc}
46   */
47  protected function defineOptions() {
48    $options = parent::defineOptions();
49    $options['format'] = ['default' => 'label_count'];
50    $options['label_position'] = ['default' => 'opposite'];
51    return $options;
52  }
53
54  /**
55   * {@inheritdoc}
56   */
57  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
58    parent::buildOptionsForm($form, $form_state);
59
60    $form['format'] = [
61      '#type' => 'select',
62      '#title' => $this->t('Display format'),
63      '#options' => [
64        'label_count' => $this->t('Label (count) - e.g., "Parent (3)"'),
65        'label_colon_count' => $this->t('Label: count - e.g., "Parent: 3"'),
66        'count_label' => $this->t('count Label - e.g., "3 Parents"'),
67        'label_only' => $this->t('Label only'),
68        'count_only' => $this->t('Count only'),
69      ],
70      '#default_value' => $this->options['format'],
71    ];
72
73    $form['label_position'] = [
74      '#type' => 'select',
75      '#title' => $this->t('Label position'),
76      '#description' => $this->t('For asymmetric relationships, choose which label to display.'),
77      '#options' => [
78        'opposite' => $this->t('Opposite position (show what the related contacts are)'),
79        'same' => $this->t('Same position (show what this contact is in the relationship)'),
80      ],
81      '#default_value' => $this->options['label_position'],
82    ];
83  }
84
85  /**
86   * {@inheritdoc}
87   */
88  public function render(ResultRow $values) {
89    $value = $this->getValue($values);
90    $count = $values->{$this->tableAlias . '_relationship_statistics_count'} ?? $values->relationship_statistics_count ?? 0;
91    $type_key = $values->{$this->tableAlias . '_relationship_statistics_value'} ?? $values->relationship_statistics_value ?? '';
92
93    if ($value === NULL && $count === 0) {
94      return '';
95    }
96
97    // Use count from the field value if available.
98    if (is_numeric($value)) {
99      $count = (int) $value;
100    }
101
102    $format = $this->options['format'];
103    $label_position = $this->options['label_position'] ?? 'opposite';
104    $use_opposite = $label_position === 'opposite';
105
106    // For 'label_only' format, always use singular label since count is not
107    // displayed. For other formats, use singular/plural based on count.
108    $label_count = $format === 'label_only' ? 1 : $count;
109    $label = $this->getLabel($type_key, $use_opposite, $label_count);
110
111    return match ($format) {
112      'label_count' => new TranslatableMarkup('@label (@count)', ['@label' => $label, '@count' => $count]),
113      'label_colon_count' => new TranslatableMarkup('@label: @count', ['@label' => $label, '@count' => $count]),
114      'count_label' => new TranslatableMarkup('@count @label', ['@count' => $count, '@label' => $label]),
115      'label_only' => $label,
116      'count_only' => (string) $count,
117      default => new TranslatableMarkup('@label (@count)', ['@label' => $label, '@count' => $count]),
118    };
119  }
120
121  /**
122   * Gets the human-readable label for a relationship type key.
123   *
124   * @param string $type_key
125   *   The relationship type key (e.g., "friends" or "parent_child:a").
126   * @param bool $use_opposite
127   *   If TRUE, return the opposite position's label (what the related contacts
128   *   are). If FALSE, return the same position's label (what this contact is
129   *   in the relationship). Defaults to TRUE.
130   * @param int $count
131   *   The count of relationships. Used to determine singular vs plural label.
132   *   Defaults to 1 (singular).
133   *
134   * @return string
135   *   The human-readable label.
136   */
137  protected function getLabel(string $type_key, bool $use_opposite = TRUE, int $count = 1): string {
138    if (empty($type_key)) {
139      return '';
140    }
141
142    // Parse the type key to get the relationship type ID and position.
143    $parts = explode(':', $type_key);
144    $type_id = $parts[0];
145    $position = $parts[1] ?? NULL;
146
147    $relationship_types = $this->getRelationshipTypes();
148
149    if (!isset($relationship_types[$type_id])) {
150      // Fallback to the raw key if type not found.
151      return $type_key;
152    }
153
154    $type = $relationship_types[$type_id];
155
156    // Determine if we should use plural (count != 1).
157    $use_plural = $count !== 1;
158
159    // For asymmetric relationships, get the appropriate label based on
160    // the use_opposite setting.
161    if ($position === 'a') {
162      // Position A: opposite means label_b, same means label_a.
163      $label_key = $use_opposite ? 'label_b' : 'label_a';
164      $plural_key = $use_opposite ? 'label_b_plural' : 'label_a_plural';
165      $singular_label = $type->get($label_key) ?? $type->label();
166      if ($use_plural) {
167        $plural_label = $type->get($plural_key);
168        return !empty($plural_label) ? $plural_label : $singular_label;
169      }
170      return $singular_label;
171    }
172    elseif ($position === 'b') {
173      // Position B: opposite means label_a, same means label_b.
174      $label_key = $use_opposite ? 'label_a' : 'label_b';
175      $plural_key = $use_opposite ? 'label_a_plural' : 'label_b_plural';
176      $singular_label = $type->get($label_key) ?? $type->label();
177      if ($use_plural) {
178        $plural_label = $type->get($plural_key);
179        return !empty($plural_label) ? $plural_label : $singular_label;
180      }
181      return $singular_label;
182    }
183
184    // For symmetric relationships, use the main label.
185    return $type->label();
186  }
187
188  /**
189   * Gets all relationship types, cached for the request.
190   *
191   * @return array
192   *   An array of relationship type entities keyed by ID.
193   */
194  protected function getRelationshipTypes(): array {
195    if ($this->relationshipTypes === NULL) {
196      $this->relationshipTypes = $this->entityTypeManager
197        ->getStorage('crm_relationship_type')
198        ->loadMultiple();
199    }
200    return $this->relationshipTypes;
201  }
202
203}