Completed
Push — 8.x-1.x ( 58d359...626d2d )
by Frédéric G.
28s queued 11s
created

Integrity   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 28
eloc 89
c 2
b 0
f 0
dl 0
loc 223
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getFields() 0 24 5
A checkReferenceType() 0 4 1
A cacheStorages() 0 7 2
A run() 0 16 2
A create() 0 9 1
C checkForward() 0 63 16
A __construct() 0 12 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Drupal\qa\Plugin\QaCheck\References;
6
7
use Drupal\Core\Config\ConfigFactoryInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Config\ConfigFactoryInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Drupal\Core\Entity\EntityTypeManagerInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Entity\EntityTypeManagerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Field\Plugin...ype\EntityReferenceItem was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Depend...tion\ContainerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
16
/**
17
 * Integrity checks for broken entity references.
18
 *
19
 * It covers core entity_reference only.
20
 *
21
 * Future versions are expected to cover
22
 * - core: file
23
 * - core: image
24
 * - contrib: entity_reference_revisions, notably used by paragraphs.
25
 * - contrib: dynamic_entity_reference
26
 *
27
 * For entity_reference_revisions, it will check references both up and down
28
 * the
29
 * parent/child chain.
30
 *
31
 * @QaCheck(
32
 *   id = "references.integrity",
33
 *   label=@Translation("Referential integrity"),
34
 *   details=@Translation("This check finds broken entity references. Missing
35
 *   nodes or references mean broken links and a bad user experience. These
36
 *   should usually be edited."), usesBatch=true, steps=3,
37
 * )
38
 */
39
class Integrity extends QaCheckBase implements QaCheckInterface {
40
41
  const NAME = 'references.integrity';
42
43
  const STEP_ER = 'entity_reference';
44
45
  const STEP_FILE = 'file';
46
47
  const STEP_IMAGE = 'image';
48
49
  const STEP_ERR = 'entity_reference_revisions';
50
51
  const STEP_DER = 'dynamic_entity_reference';
52
53
  /**
54
   * The config.factory service.
55
   *
56
   * @var \Drupal\Core\Config\ConfigFactoryInterface
57
   */
58
  protected $config;
59
60
  /**
61
   * The entity_type.manager service.
62
   *
63
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
64
   */
65
  protected $etm;
66
67
  /**
68
   * A map of storage handler by entity_type ID.
69
   *
70
   * @var array
71
   */
72
  protected $storages;
73
74
  /**
75
   * SystemUnusedExtensions constructor.
76
   *
77
   * @param array $configuration
78
   *   The plugin configuration.
79
   * @param string $id
80
   *   The plugin ID.
81
   * @param array $definition
82
   *   The plugin definition.
83
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $etm
84
   *   The entity_type.manager service.
85
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
86
   *   The config.factory service.
87
   */
88
  public function __construct(
89
    array $configuration,
90
    string $id,
91
    array $definition,
92
    EntityTypeManagerInterface $etm,
93
    ConfigFactoryInterface $config
94
  ) {
95
    parent::__construct($configuration, $id, $definition);
96
    $this->config = $config;
97
    $this->etm = $etm;
98
99
    $this->cacheStorages();
100
  }
101
102
  /**
103
   * {@inheritdoc}
104
   */
105
  public static function create(
106
    ContainerInterface $container,
107
    array $configuration,
108
    $id,
109
    $definition
110
  ) {
111
    $etm = $container->get('entity_type.manager');
112
    $config = $container->get('config.factory');
113
    return new static($configuration, $id, $definition, $etm, $config);
114
  }
115
116
  /**
117
   * Fetch and cache the storage handlers per entity type for repeated use.
118
   *
119
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
120
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
121
   */
122
  protected function cacheStorages(): void {
123
    $ets = array_keys($this->etm->getDefinitions());
124
    $handlers = [];
125
    foreach ($ets as $et) {
126
      $handlers[$et] = $this->etm->getStorage($et);
127
    }
128
    $this->storages = $handlers;
129
  }
130
131
  /**
132
   * @param array $fieldMap
133
   *
134
   * @return array
135
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
136
   */
137
  protected function checkForward(array $fieldMap): array {
138
    $checks = [];
139
    foreach ($fieldMap as $et => $fields) {
140
      $checks[$et] = [
141
        // Eventual result of a broken reference:
142
        // <id> => [ <field_name> => <target_id> ].
143
      ];
144
      $entities = $this->storages[$et]->loadMultiple();
145
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
146
      foreach ($entities as $entity) {
147
        $checks[$et][$entity->id()] = [];
148
        foreach ($fields as $name => $targetET) {
149
          if (!$entity->hasField($name)) {
150
            continue;
151
          }
152
          $target = $entity->get($name);
153
          if ($target->isEmpty()) {
154
            continue;
155
          }
156
          $checks[$et][$entity->id()][$name] = [];
157
          /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $value */
158
          foreach ($target as $delta => $value) {
159
            // Happens with DER.
160
            if (is_array($targetET)) {
161
              $targetType = $value->toArray()['target_type'];
162
              // A fail here would be a severe case where content was not migrated after a schema change.
163
              $deltaTargetET = in_array($targetType,
164
                $targetET) ? $targetType : '';
165
            }
166
            else {
167
              $deltaTargetET = $targetET;
168
            }
169
170
            $targetID = $value->toArray()[EntityReferenceItem::mainPropertyName()];
171
            if (!empty($deltaTargetET)) {
172
              foreach ($entity->referencedEntities() as $targetEntity) {
173
                $x = $targetEntity->getEntityTypeId();
174
                if ($x != $deltaTargetET) {
175
                  continue;
176
                }
177
                // Target found, next delta.
178
                $x = $targetEntity->id();
179
                if ($x === $targetID) {
180
                  continue 2;
181
                }
182
              }
183
            }
184
            // Target not found: broken reference.
185
            $checks[$et][$entity->id()][$name][$delta] = $targetID;
186
          }
187
          if (empty($checks[$et][$entity->id()][$name])) {
188
            unset($checks[$et][$entity->id()][$name]);
189
          }
190
        }
191
        if (empty($checks[$et][$entity->id()])) {
192
          unset($checks[$et][$entity->id()]);
193
        }
194
      }
195
      if (empty($checks[$et])) {
196
        unset($checks[$et]);
197
      }
198
    }
199
    return $checks;
200
  }
201
202
  public function checkReferenceType(string $step): Result {
203
    $fieldMap = $this->getFields($step);
204
    $checks = $this->checkForward($fieldMap);
205
    return new Result($step, empty($checks), $checks);
206
  }
207
208
  /**
209
   * Get reference fields of the selected type.
210
   *
211
   * @param string $refType
212
   *   The field type.
213
   *
214
   * @return array
215
   *   A field by entity type map.
216
   */
217
  protected function getFields(string $refType): array {
218
    $fscStorage = $this->storages['field_storage_config'];
219
    $defs = $fscStorage->loadMultiple();
220
    $fields = [];
221
    /** @var \Drupal\field\FieldStorageConfigInterface $fsc */
222
    foreach ($defs as $fsc) {
223
      if ($fsc->getType() !== $refType) {
224
        continue;
225
      }
226
      $et = $fsc->getTargetEntityTypeId();
227
      $name = $fsc->getName();
228
      $target = $fsc->getSetting('target_type');
229
      if (empty($target)) {
230
        // Dynamic Entity Reference allows multiple target entity types.
231
        $target = array_values($fsc->getSetting('entity_type_ids'));
232
      }
233
      // XXX hard-coded knowledge. Maybe refactor once multiple types are used.
234
      // $prop = $fsc->getMainPropertyName();
235
      if (!isset($fields[$et])) {
236
        $fields[$et] = [];
237
      }
238
      $fields[$et][$name] = $target;
239
    }
240
    return $fields;
241
  }
242
243
  /**
244
   * {@inheritdoc}
245
   */
246
  public function run(): Pass {
247
    $pass = parent::run();
248
249
    $steps = [
250
      self::STEP_ER,
251
      self::STEP_ERR,
252
      self::STEP_DER,
253
      self::STEP_FILE,
254
      self::STEP_IMAGE,
255
    ];
256
    foreach ($steps as $step) {
257
      $pass->record($this->checkReferenceType($step));
258
      $pass->life->modify();
259
    }
260
    $pass->life->end();
261
    return $pass;
262
  }
263
264
}
265