Integrity::cacheStorages()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Drupal\qa\Plugin\QaCheck\References;
6
7
use Drupal\Core\Config\ConfigFactoryInterface;
8
use Drupal\Core\Entity\EntityTypeManagerInterface;
9
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
10
use Drupal\qa\Pass;
11
use Drupal\qa\Plugin\QaCheckBase;
12
use Drupal\qa\Plugin\QaCheckInterface;
13
use Drupal\qa\Result;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
16
/**
17
 * Integrity checks for broken entity references.
18
 *
19
 * It covers core entity_reference only.
20
 *
21
 * @QaCheck(
22
 *   id = "references.integrity",
23
 *   label = @Translation("Referential integrity"),
24
 *   details = @Translation("This check finds broken entity references. Missing nodes or references mean broken links and a bad user experience. These should usually be edited."),
25
 *   usesBatch = false,
26
 *   steps = 3,
27
 * )
28
 */
29
class Integrity extends QaCheckBase implements QaCheckInterface {
30
31
  const NAME = 'references.integrity';
32
33
  const STEP_ER = 'entity_reference';
34
35
  const STEP_FILE = 'file';
36
37
  const STEP_IMAGE = 'image';
38
39
  const STEP_ERR = 'entity_reference_revisions';
40
41
  const STEP_DER = 'dynamic_entity_reference';
42
43
  /**
44
   * The config.factory service.
45
   *
46
   * @var \Drupal\Core\Config\ConfigFactoryInterface
47
   */
48
  protected $config;
49
50
  /**
51
   * The entity_type.manager service.
52
   *
53
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
54
   */
55
  protected $etm;
56
57
  /**
58
   * A map of storage handler by entity_type ID.
59
   *
60
   * @var array
61
   */
62
  protected $storages;
63
64
  /**
65
   * SystemUnusedExtensions constructor.
66
   *
67
   * @param array $configuration
68
   *   The plugin configuration.
69
   * @param string $id
70
   *   The plugin ID.
71
   * @param array $definition
72
   *   The plugin definition.
73
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $etm
74
   *   The entity_type.manager service.
75
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
76
   *   The config.factory service.
77
   */
78
  public function __construct(
79
    array $configuration,
80
    string $id,
81
    array $definition,
82
    EntityTypeManagerInterface $etm,
83
    ConfigFactoryInterface $config
84
  ) {
85
    parent::__construct($configuration, $id, $definition);
86
    $this->config = $config;
87
    $this->etm = $etm;
88
89
    $this->cacheStorages();
90
  }
91
92
  /**
93
   * {@inheritdoc}
94
   */
95
  public static function create(
96
    ContainerInterface $container,
97
    array $configuration,
98
    $id,
99
    $definition
100
  ) {
101
    $etm = $container->get('entity_type.manager');
102
    $config = $container->get('config.factory');
103
    return new static($configuration, $id, $definition, $etm, $config);
0 ignored issues
show
Bug introduced by
It seems like $etm can also be of type null; however, parameter $etm of Drupal\qa\Plugin\QaCheck...ntegrity::__construct() does only seem to accept Drupal\Core\Entity\EntityTypeManagerInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
    return new static($configuration, $id, $definition, /** @scrutinizer ignore-type */ $etm, $config);
Loading history...
Bug introduced by
It seems like $config can also be of type null; however, parameter $config of Drupal\qa\Plugin\QaCheck...ntegrity::__construct() does only seem to accept Drupal\Core\Config\ConfigFactoryInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
    return new static($configuration, $id, $definition, $etm, /** @scrutinizer ignore-type */ $config);
Loading history...
104
  }
105
106
  /**
107
   * Fetch and cache the storage handlers per entity type for repeated use.
108
   *
109
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
110
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
111
   */
112
  protected function cacheStorages(): void {
113
    $ets = array_keys($this->etm->getDefinitions());
114
    $handlers = [];
115
    foreach ($ets as $et) {
116
      $handlers[$et] = $this->etm->getStorage($et);
117
    }
118
    $this->storages = $handlers;
119
  }
120
121
  /**
122
   * Check entity references in the passed reference map.
123
   *
124
   * @param array $fieldMap
125
   *   A map of fields by entity type.
126
   *
127
   * @return array
128
   *   A map of broken references.
129
   *
130
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
131
   */
132
  protected function checkForward(array $fieldMap): array {
133
    $checks = [];
134
    foreach ($fieldMap as $et => $fields) {
135
      $checks[$et] = [
136
        // Eventual result of a broken reference:
137
        // <id> => [ <field_name> => <target_id> ].
138
      ];
139
      $entities = $this->storages[$et]->loadMultiple();
140
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
141
      foreach ($entities as $entity) {
142
        $checks[$et][$entity->id()] = [];
143
        foreach ($fields as $name => $targetET) {
144
          if (!$entity->hasField($name)) {
145
            continue;
146
          }
147
          $target = $entity->get($name);
148
          if ($target->isEmpty()) {
149
            continue;
150
          }
151
          $checks[$et][$entity->id()][$name] = [];
152
          /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $value */
153
          foreach ($target as $delta => $value) {
154
            // Happens with DER.
155
            if (is_array($targetET)) {
156
              $targetType = $value->toArray()['target_type'];
157
              // A fail here would be a severe case where content was not
158
              // migrated after a schema change.
159
              $deltaTargetET = in_array($targetType,
160
                $targetET) ? $targetType : '';
161
            }
162
            else {
163
              $deltaTargetET = $targetET;
164
            }
165
166
            $targetID = $value->toArray()[EntityReferenceItem::mainPropertyName()];
167
            if (!empty($deltaTargetET)) {
168
              foreach ($entity->referencedEntities() as $targetEntity) {
169
                $x = $targetEntity->getEntityTypeId();
170
                if ($x != $deltaTargetET) {
171
                  continue;
172
                }
173
                // Target found, next delta.
174
                $x = $targetEntity->id();
175
                if ($x === $targetID) {
176
                  continue 2;
177
                }
178
              }
179
            }
180
            // Target not found: broken reference.
181
            $checks[$et][$entity->id()][$name][$delta] = $targetID;
182
          }
183
          if (empty($checks[$et][$entity->id()][$name])) {
184
            unset($checks[$et][$entity->id()][$name]);
185
          }
186
        }
187
        if (empty($checks[$et][$entity->id()])) {
188
          unset($checks[$et][$entity->id()]);
189
        }
190
      }
191
      if (empty($checks[$et])) {
192
        unset($checks[$et]);
193
      }
194
    }
195
    return $checks;
196
  }
197
198
  /**
199
   * Perform a reference integrity check of the specified kind.
200
   *
201
   * @param string $kind
202
   *   The reference kind.
203
   *
204
   * @return \Drupal\qa\Result
205
   *   The check result.
206
   *
207
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
208
   */
209
  public function checkReferenceType(string $kind): Result {
210
    $fieldMap = $this->getFields($kind);
211
    $checks = $this->checkForward($fieldMap);
212
    return new Result($kind, empty($checks), $checks);
213
  }
214
215
  /**
216
   * Get reference fields of the selected type.
217
   *
218
   * @param string $refType
219
   *   The field type.
220
   *
221
   * @return array
222
   *   A field by entity type map.
223
   */
224
  protected function getFields(string $refType): array {
225
    $fscStorage = $this->storages['field_storage_config'];
226
    $defs = $fscStorage->loadMultiple();
227
    $fields = [];
228
    /** @var \Drupal\field\FieldStorageConfigInterface $fsc */
229
    foreach ($defs as $fsc) {
230
      if ($fsc->getType() !== $refType) {
231
        continue;
232
      }
233
      $et = $fsc->getTargetEntityTypeId();
234
      $name = $fsc->getName();
235
      $target = $fsc->getSetting('target_type');
236
      if (empty($target)) {
237
        // Dynamic Entity Reference allows multiple target entity types.
238
        $target = array_values($fsc->getSetting('entity_type_ids'));
239
      }
240
      // XXX hard-coded knowledge. Maybe refactor once multiple types are used.
241
      // $prop = $fsc->getMainPropertyName();
242
      if (!isset($fields[$et])) {
243
        $fields[$et] = [];
244
      }
245
      $fields[$et][$name] = $target;
246
    }
247
    return $fields;
248
  }
249
250
  /**
251
   * {@inheritdoc}
252
   */
253
  public function run(): Pass {
254
    $pass = parent::run();
255
256
    $steps = [
257
      self::STEP_ER,
258
      self::STEP_ERR,
259
      self::STEP_DER,
260
      self::STEP_FILE,
261
      self::STEP_IMAGE,
262
    ];
263
    foreach ($steps as $step) {
264
      $pass->record($this->checkReferenceType($step));
265
      $pass->life->modify();
266
    }
267
    $pass->life->end();
268
    return $pass;
269
  }
270
271
}
272