Passed
Push — stage ( f5f9eb...941b8c )
by Jon
06:30
created

DataImporter   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 496
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 62
dl 0
loc 496
rs 3.8461
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
C getChangeset() 0 83 9
C getColumnProfileMap() 0 65 12
B __construct() 0 45 6
A makeErrorMessage() 0 3 1
A getHeaderFromMap() 0 7 2
A getAddRows() 0 6 1
A validateRequired() 0 8 3
B makeFqn() 0 16 6
A getDataForImport() 0 7 1
A getRowMap() 0 7 1
A makeCurie() 0 11 3
A getDeleteRows() 0 7 1
A getVocabularyStatements() 0 17 1
B setStats() 0 6 5
A getElementSetStatements() 0 23 3
A getUpdateRows() 0 6 1
A logRowError() 0 8 2
A getErrors() 0 3 1
A getStats() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like DataImporter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataImporter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/** Created by PhpStorm,  User: jonphipps,  Date: 2017-05-05,  Time: 5:49 PM */
4
5
namespace App\Services\Import;
6
7
use App\Exceptions\DuplicateAttributesException;
8
use App\Exceptions\MissingRequiredAttributeException;
9
use App\Exceptions\UnknownAttributeException;
10
use App\Models\Concept;
11
use App\Models\Element;
12
use App\Models\Export;
13
use App\Models\ProfileProperty;
14
use Illuminate\Database\Eloquent\Collection as DBCollection;
15
use Illuminate\Support\Collection;
16
use const null;
17
use function collect;
18
19
class DataImporter
20
{
21
    /** @var string $exportName */
22
    private $exportName;
0 ignored issues
show
introduced by
The private property $exportName is not used, and could be removed.
Loading history...
23
    /** @var Collection $data */
24
    private $data;
25
    /** @var Export */
26
    private $export;
27
    /** @var Collection $rowMap */
28
    private $rowMap;
29
    /** @var Collection $rows */
30
    private $rows;
31
    /** @var Collection $addRows */
32
    private $addRows;
33
    /** @var Collection $updateRows */
34
    private $updateRows;
35
    /** @var Collection $updateRows */
36
    private $deleteRows;
37
    /** @var Collection $errors */
38
    private $errors;
39
    /** @var DBCollection $statements */
40
    private $statements;
41
    private $prefixes = [];
42
    private $stats    = [];
43
    /** @var Collection $columnProfileMap */
44
    private $columnProfileMap;
45
    /** @var string */
46
    private $currentColumnName;
47
    /** @var string */
48
    private $currentRowName;
49
50
    public function __construct(Collection $data, Export $export = null)
51
    {
52
        $this->data    = $data;
53
        $columnHeaders = collect($data[0]);
54
55
        $this->rows    = $this->getDataForImport();
56
57
        $this->export = $export;
58
        if ($export) {
59
            $props = ProfileProperty::whereProfileId($export->profile_id)->get()->keyBy('id');
60
            $this->rowMap     = self::getRowMap($export->map,$props);
0 ignored issues
show
Bug introduced by
It seems like $export->map can also be of type null; however, parameter $map of App\Services\Import\DataImporter::getRowMap() does only seem to accept Illuminate\Support\Collection, 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

60
            $this->rowMap     = self::getRowMap(/** @scrutinizer ignore-type */ $export->map,$props);
Loading history...
61
            try {
62
                $this->columnProfileMap = self::getColumnProfileMap($export, $columnHeaders, $props);
63
            }
64
            //these are all fatal errors
65
            catch (DuplicateAttributesException $e) {
66
                $this->errors = collect(['fatal' => $e->getMessage()]);
67
                $this->setStats();
68
69
                return;
70
            } catch (MissingRequiredAttributeException $e) {
71
                $this->errors = collect(['fatal' => $e->getMessage()]);
72
                $this->setStats();
73
74
                return;
75
            } catch (UnknownAttributeException $e) {
76
                $this->errors = collect(['fatal' => $e->getMessage()]);
77
                $this->setStats();
78
79
                return;
80
            }
81
            $this->errors     = new Collection();
82
            $this->addRows    = $this->getAddRows(); //only gets data rows with no row_id
83
            $this->updateRows = $this->getUpdateRows(); //gets data rows with matching map
84
            $this->deleteRows = $this->getDeleteRows(); //gets map rows with no matching row
85
            if ($export->vocabulary_id) {
86
                $this->prefixes   = $export->vocabulary->prefixes;
87
                $this->statements = $this->getVocabularyStatements();
88
            } else {
89
                $this->prefixes   = $export->elementset->prefixes;
90
                $this->statements = $this->getElementSetStatements();
91
            }
92
        }
93
94
        $this->setStats();
95
    }
96
97
    public function setStats()
98
    {
99
        $this->stats['deleted'] = $this->deleteRows === null ? 0 : $this->deleteRows->count();
100
        $this->stats['updated'] = $this->updateRows === null ? 0 : $this->updateRows->count();
101
        $this->stats['added']   = $this->addRows === null ? 0 : $this->addRows->count();
102
        $this->stats['errors']  = $this->errors === null ? 0 : $this->errors->count();
103
    }
104
    /**
105
     * return a collection of rows that have no reg_id.
106
     *
107
     * @return Collection
108
     */
109
    public function getAddRows(): Collection
110
    {
111
        //only keep rows that have no reg_id
112
        return collect($this->rows->filter(function ($row, $key) {
113
            return empty($row['reg_id']);
114
        }))->values();
115
    }
116
117
    /**
118
     * @return Collection
119
     * @internal param Collection $map
120
     * @internal param array $rowMap
121
     */
122
    public function getChangeset(): Collection
123
    {
124
        $rows       = $this->updateRows;
125
        $rowMap     = $this->rowMap;
126
        $columnMap  = $this->columnProfileMap;
127
        $statements = $this->statements;
128
129
        if ($rows === null) {
130
            return collect([]);
131
        }
132
133
        $changes = $rows->map(function (Collection $row, $key) use ($rowMap, $columnMap, $statements) {
134
            $map          = $rowMap[$key];
135
            $statementRow = $statements[$key];
136
            $this->currentRowName = 'reg_id: ' . $key;
137
138
            return $row->map(function ($value, $column) use ($map, $columnMap, $statementRow) {
139
                // this is to correct for export maps that have '0' for a statement cell reference, but do have data in the statement row
140
                $statementId = ($map->get($column) !== 0) ? $map->get($column) : $column;
141
                $statement   = $statementId ? collect($statementRow->pull($statementId)) : null;
142
                $this->currentColumnName = $column;
143
144
                return [
145
                    'new value'    => $this->validateRequired($value, $columnMap[$column]),
146
                    'old value'    => $statement ? $statement->get('old value') : null,
147
                    'statement_id' => $statementId,
148
                    'language'     => $columnMap[$column]['language'],
149
                    'property_id'  => $columnMap[$column]['id'],
150
                    'updated_at'   => $statement ? $statement->get('updated_at') : null,
151
                    'required'     => $column[0] === '*',
152
                ];
153
            })->reject(function ($array) {
154
                return empty($array['new value']) && empty($array['statement_id']); //remove all of the items that have been, and continue to be, empty
155
            })->reject(function ($array, $arrayKey) {
156
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
157
            })->reject(function ($array) {
158
                return $array['new value'] === $array['old value']; //reject every item that has no changes
159
            })->reject(function ($array) {
160
                //reset the URI to be fully qualified
161
                if ($array['statement_id'] === '*uri') {
162
                    $array['new value'] = $this->makeFqn($this->prefixes, $array['new value']);
163
                }
164
165
                return $array['new value'] === $array['old value']; //reject if the URIs match
166
            });
167
        })->reject(function (Collection $items) {
168
            return $items->count() === 0; //reject every row that no longer has items
169
        });
170
171
        $rows      = $this->addRows;
172
        $additions = $rows->map(function (Collection $row, $key) use ($columnMap) {
173
            $this->currentRowName = 'new row: ' . $key;
174
175
            return $row->map(function ($value, $column) use ($columnMap) {
176
                $this->currentColumnName = $column;
177
                //reset the URI to be fully qualified
178
                if ($column === '*uri') {
179
                    $value = $this->makeFqn($this->prefixes, $value);
180
                }
181
182
                return [
183
                   'new value'    => $this->validateRequired($value, $columnMap[$column]),
184
                   'old value'    => null,
185
                   'statement_id' => null,
186
                   'language'     => $columnMap[$column]['language'],
187
                   'property_id'  => $columnMap[$column]['id'],
188
                   'updated_at'   => null,
189
                   'required'     => $column[0] === '*',
190
                ];
191
            })->reject(function ($array) {
192
                return empty($array['new value']); //remove all of the items that have been, and continue to be, empty
193
            })->reject(function ($array, $arrayKey) {
194
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
195
            });
196
        })->reject(function (Collection $items) {
197
            return $items->count() === 0; //reject every row that no longer has items
198
        });
199
200
        $changeset['update'] = $changes;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$changeset was never initialized. Although not strictly required by PHP, it is generally a good practice to add $changeset = array(); before regardless.
Loading history...
201
        $changeset['delete'] = $this->deleteRows;
202
        $changeset['add']    = $additions;
203
204
        return collect($changeset);
205
    }
206
207
    /**
208
     * Returns an associative array of data based on the data supplied for import.
209
     *
210
     * @return Collection
211
     */
212
    public function getDataForImport(): Collection
213
    {
214
        $h = $this->data->first();
215
216
        return $this->data->slice(1)->transform(function ($item, $key) use ($h) {
217
            return collect($item)->mapWithKeys(function ($item, $key) use ($h) {
218
                return [$h[$key] => $item];
219
            });
220
        });
221
    }
222
223
    /**
224
     * @return Collection
225
     */
226
    public function getDeleteRows(): Collection
227
    {
228
        //only keep rows that are in the rowmap but are missing from the supplied data
229
        $updateRows = $this->updateRows;
230
231
        return collect($this->rowMap->reject(function ($row, $key) use ($updateRows) {
232
            return isset($updateRows[$key]);
233
        }));
234
    }
235
236
    /**
237
     * @param Collection                     $map
238
     * @param \Illuminate\Support\Collection $props
239
     *
240
     * @return Collection
241
     */
242
    public static function getHeaderFromMap(Collection $map, Collection $props): Collection
243
    {
244
        //add the latest required attribute from the profile
245
        return collect($map->first())->map(function ($item, $key) use ($props) {
246
            $item['required'] = $item['id'] ? $props[$item['id']]->is_required : null;
247
248
            return $item;
249
        });
250
    }
251
252
253
    public function getStats(): Collection
254
    {
255
        $errorCount = 0;
256
        if ($this->errors !== null) {
257
            foreach ($this->errors as $error) {
258
                $errorCount += \count($error);
259
            }
260
        }
261
262
        $this->stats['errors'] = $errorCount;
263
264
        return collect($this->stats);
265
    }
266
267
    /**
268
     * @param Collection                     $map
269
     * @param \Illuminate\Support\Collection $props
270
     *
271
     * @return Collection
272
     */
273
    public static function getRowMap(Collection $map, Collection $props): Collection
274
    {
275
        $p = self::getHeaderFromMap($map, $props);
276
277
        return $map->slice(1)->transform(function ($item, $key) use ($p) {
278
            return collect($item)->mapWithKeys(function ($item, $key) use ($p) {
279
                return [$p[$key]['label'] => $item];
280
            });
281
        });
282
    }
283
284
    /**
285
     * @param Export                         $export
286
     * @param Collection                     $columnHeaders
287
     * @param \Illuminate\Support\Collection $props
288
     *
289
     * @return Collection
290
     * @throws \App\Exceptions\DuplicateAttributesException
291
     * @throws \App\Exceptions\MissingRequiredAttributeException
292
     * @throws \App\Exceptions\UnknownAttributeException
293
     */
294
    public static function getColumnProfileMap(Export $export, Collection $columnHeaders, Collection $props): Collection
295
    {
296
        $map        = $export->map;
297
        $profile    = $export->profile;
298
        $mapHeaders = self::getHeaderFromMap($map, $props)->keyBy('label');
0 ignored issues
show
Bug introduced by
It seems like $map can also be of type null; however, parameter $map of App\Services\Import\Data...ter::getHeaderFromMap() does only seem to accept Illuminate\Support\Collection, 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

298
        $mapHeaders = self::getHeaderFromMap(/** @scrutinizer ignore-type */ $map, $props)->keyBy('label');
Loading history...
299
        $keys       = $mapHeaders->keys()->toArray();
300
        //get the map for all new columns
301
        $newColumns = $columnHeaders->reject(function ($value, $key) use ($keys) {
302
            return \in_array($value, $keys, false);
303
        })->map(function ($column) use ($profile) {
304
            return $profile->getColumnMapFromHeader($column);
0 ignored issues
show
Bug introduced by
The method getColumnMapFromHeader() does not exist on null. ( Ignorable by Annotation )

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

304
            return $profile->/** @scrutinizer ignore-call */ getColumnMapFromHeader($column);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
305
        })->keyBy('label');
306
307
        //check for duplicate column headers
308
        $headers = [];
309
        foreach ($columnHeaders as $columnHeader) {
310
            if (isset($headers[$columnHeader])) {
311
                throw new DuplicateAttributesException('"' . $columnHeader . '" is a duplicate attribute column. Columns cannot be duplicated');
312
            }
313
314
            $headers[$columnHeader] = $columnHeader;
315
        }
316
317
        //check for unknown columns
318
        if (count($newColumns)) {
319
            if (count($newColumns) > 1) {
320
                $unknown = 'columns: ';
321
                foreach ($newColumns as $item) {
322
                    $unknown .= '"' . $item['label'] . '", ';
323
                }
324
                $unknown = rtrim($unknown, ', ') . ' ...are';
325
            } else {
326
                $unknown = 'column: ';
327
                foreach ($newColumns as $item) {
328
                    $unknown .= '"' . $item['label'] . '" ...is';
329
                }
330
            }
331
            throw new UnknownAttributeException('The ' . $unknown . ' unknown and need to be registered with the Profile');
332
        }
333
334
        //check for missing required columns
335
336
        $missingRequired = $mapHeaders->filter(function ($value, $key) use ($columnHeaders) {
337
            return $value['required'] && ! $columnHeaders->contains($key);
338
        })->map(function ($item, $key) {
339
            return $key;
340
        });
341
342
        if (count($missingRequired)) {
343
            if (count($missingRequired) > 1) {
344
                $missing = 'columns: ';
345
                foreach ($missingRequired as $item) {
346
                    $missing .= '"' . $item . '", ';
347
                }
348
                $missing = rtrim($missing, ', ') . ' ...are';
349
            } else {
350
                $missing = 'column: ';
351
                foreach ($missingRequired as $item) {
352
                    $missing .= '"' . $item . '" ...is';
353
                }
354
            }
355
            throw new MissingRequiredAttributeException('The required attribute ' . $missing . ' missing');
356
        }
357
358
        return $mapHeaders->merge($newColumns);
359
    }
360
361
    /**
362
     * @return Collection
363
     */
364
    public function getUpdateRows(): Collection
365
    {
366
        //only keep rows that have a non-empty reg_id
367
        return $this->rows->reject(function ($row) {
368
            return empty($row['reg_id']);
369
        })->keyBy('reg_id');
370
    }
371
372
    /**
373
     * @return Collection
374
     */
375
    public function getErrors(): Collection
376
    {
377
        return $this->errors;
378
    }
379
380
    /**
381
     * @return Collection
382
     */
383
    public function getVocabularyStatements(): Collection
384
    {
385
        return Concept::whereVocabularyId($this->export->vocabulary_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($concept, $key) {
386
            return $concept->statements->keyBy('id')->map(function ($property) {
387
                return [
388
                    'old value'  => $property->object,
389
                    'updated_at' => $property->updated_at,
390
                ];
391
            })->prepend([
392
                'old value'  => $concept->uri,
393
                'updated_at' => $concept->updated_at,
394
            ],
395
                '*uri')->prepend([
396
                    'old value'  => $concept->status->display_name,
397
                    'updated_at' => $concept->updated_at,
398
                ],
399
                    '*status');
400
        });
401
    }
402
403
    /**
404
     * @return Collection
405
     */
406
    public function getElementSetStatements(): Collection
407
    {
408
        return Element::whereSchemaId($this->export->schema_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($element, $key) {
409
            $status = $element->status->display_name;
410
            $thingy = $element->statements->keyBy('id')->map(function ($property) {
411
                return [
412
                    'old value'   => $property->object,
413
                    'updated_at'  => $property->updated_at,
414
                    'profile_uri' => $property->profile_property->uri,
415
                    'is_resource' => (bool) $property->profile_property->is_object_prop,
416
                ];
417
            })->map(function ($item) use ($status) {
418
                if ($item['profile_uri'] === 'reg:status') {
419
                    $item['old value'] = $status;
420
                }
421
                if ($item['is_resource']) {
422
                    $item['old value'] = self::makeCurie($this->prefixes, $item['old value']);
423
                }
424
425
                return $item;
426
            });
427
428
            return $thingy;
429
        });
430
    }
431
432
    /**
433
     * @param $message
434
     *
435
     * @return string
436
     */
437
    protected static function makeErrorMessage($message): string
438
    {
439
        return "[ERROR: $message]";
440
    }
441
442
    /**
443
     * @param $value
444
     * @param $column
445
     * @param $row
446
     * @param $level
447
     */
448
    private function logRowError($value, $column, $row, $level): void
449
    {
450
        //if this is the first error, initialize the row errors
451
        if (! $this->errors->get('row')) {
452
            $this->errors->put('row', collect());
453
        }
454
455
        $this->errors->get('row')->push(collect([$row, $column, $value, $level]));
456
    }
457
458
    /**
459
     * @param array  $prefixes
460
     * @param string $uri
461
     *
462
     * @return string
463
     */
464
    private function makeFqn($prefixes, $uri): string
465
    {
466
        $result = $uri;
467
        foreach ($prefixes as $prefix => $fullUri) {
468
            $result = preg_replace('#' . $prefix . ':#uis', $fullUri, $uri);
469
            if ($result !== $uri) {
470
                break;
471
            }
472
        }
473
        if ($uri === $result && strpos($uri, ':') && ! strpos($uri, '://')) {
474
            //we have an unregistered prefix
475
            $prefix = str_before($uri, ':');
476
            $this->logRowError(self::makeErrorMessage("'$prefix' is an unregistered prefix and cannot be expanded to form a full URI"), $this->currentColumnName, $this->currentRowName, 'warning');
477
        }
478
479
        return $result;
480
    }
481
482
    /**
483
     * @param array  $prefixes
484
     * @param string $uri
485
     *
486
     * @return string
487
     */
488
    private static function makeCurie($prefixes, $uri): string
489
    {
490
        $result = $uri;
491
        foreach ($prefixes as $prefix => $fullUri) {
492
            $result = preg_replace('!' . $fullUri . '!uis', $prefix . ':', $uri);
493
            if ($result !== $uri) {
494
                break;
495
            }
496
        }
497
498
        return $result;
499
    }
500
501
    /**
502
     * @param          $value
503
     * @param          $column
504
     *
505
     * @return string
506
     */
507
    private function validateRequired($value, array $column): string
508
    {
509
        if (empty($value) && $column['required']) {
510
            $error = self::makeErrorMessage('Empty required attribute');
511
            $this->logRowError($error, $column['label'], $this->currentRowName, 'Row Fatal');
512
        }
513
514
        return $value;
515
    }
516
}
517