Passed
Push — stage ( 918a10...513532 )
by Jon
09:29
created

ConceptAttribute::createReciprocal()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 48
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 27
nc 6
nop 0
1
<?php
2
3
namespace App\Models;
4
5
use App\Exceptions\DuplicatePrefLabelException;
6
use App\Helpers\Macros\Traits\Languages;
7
use App\Models\Access\User\User;
8
use App\Models\Traits\BelongsToConcept;
9
use App\Models\Traits\BelongsToProfileProperty;
10
use App\Models\Traits\BelongsToRelatedConcept;
11
use App\Models\Traits\HasStatus;
12
use Carbon\Carbon;
13
use Culpa\Traits\Blameable;
14
use Culpa\Traits\CreatedBy;
15
use Culpa\Traits\DeletedBy;
16
use Culpa\Traits\UpdatedBy;
17
use Illuminate\Database\Eloquent\Model;
18
use Illuminate\Database\Eloquent\Relations\HasMany;
19
use Illuminate\Database\Eloquent\Relations\HasOne;
20
use Illuminate\Database\Eloquent\SoftDeletes;
21
use InvalidArgumentException;
22
use Laracasts\Matryoshka\Cacheable;
23
use Venturecraft\Revisionable\RevisionableTrait;
24
25
/**
26
 * App\Models\ConceptAttribute
27
 *
28
 * @property int $id
29
 * @property \Carbon\Carbon|null $created_at
30
 * @property \Carbon\Carbon|null $updated_at
31
 * @property \Carbon\Carbon|null $deleted_at
32
 * @property int $created_user_id
33
 * @property int $updated_user_id
34
 * @property int $concept_id
35
 * @property string $primary_pref_label
36
 * @property int $skos_property_id
37
 * @property string $object
38
 * @property int $scheme_id
39
 * @property int $related_concept_id
40
 * @property string $language
41
 * @property int $status_id
42
 * @property bool $is_concept_property
43
 * @property int $profile_property_id
44
 * @property int $last_import_id
45
 * @property bool $is_generated
46
 * @property int|null $created_by
47
 * @property int|null $updated_by
48
 * @property int|null $deleted_by
49
 * @property int|null $review_reciprocal
50
 * @property int|null $reciprocal_concept_property_id
51
 * @property-read \App\Models\Concept $concept
52
 * @property-read \App\Models\Access\User\User|null $creator
53
 * @property-read \App\Models\Access\User\User|null $eraser
54
 * @property mixed $languages
55
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ConceptAttributeHistory[] $history
56
 * @property-read \App\Models\ConceptAttribute $inverse
57
 * @property-read \App\Models\ProfileProperty|null $profile_property
58
 * @property-read \App\Models\ConceptAttribute $reciprocal
59
 * @property-read \App\Models\Concept|null $related_concept
60
 * @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
61
 * @property-read \App\Models\Status|null $status
62
 * @property-read \App\Models\Access\User\User|null $updater
63
 * @method static bool|null forceDelete()
64
 * @method static \Illuminate\Database\Query\Builder|\App\Models\ConceptAttribute onlyTrashed()
65
 * @method static bool|null restore()
66
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereConceptId($value)
67
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereCreatedAt($value)
68
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereCreatedBy($value)
69
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereCreatedUserId($value)
70
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereDeletedAt($value)
71
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereDeletedBy($value)
72
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereId($value)
73
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereIsConceptProperty($value)
74
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereIsGenerated($value)
75
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereLanguage($value)
76
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereLastImportId($value)
77
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereObject($value)
78
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute wherePrimaryPrefLabel($value)
79
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereProfilePropertyId($value)
80
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereReciprocalConceptPropertyId($value)
81
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereRelatedConceptId($value)
82
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereReviewReciprocal($value)
83
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereSchemeId($value)
84
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereSkosPropertyId($value)
85
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereStatusId($value)
86
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereUpdatedAt($value)
87
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereUpdatedBy($value)
88
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ConceptAttribute whereUpdatedUserId($value)
89
 * @method static \Illuminate\Database\Query\Builder|\App\Models\ConceptAttribute withTrashed()
90
 * @method static \Illuminate\Database\Query\Builder|\App\Models\ConceptAttribute withoutTrashed()
91
 * @mixin \Eloquent
92
 */
93
class ConceptAttribute extends Model
94
{
95
    const TABLE      = 'reg_concept_property';
96
    protected $table = self::TABLE;
97
    use SoftDeletes, Blameable, CreatedBy, UpdatedBy, DeletedBy;
98
    use RevisionableTrait;
99
    use Cacheable;
100
    use Languages, HasStatus, BelongsToProfileProperty, BelongsToConcept, BelongsToRelatedConcept;
101
    protected $blameable = [
102
        'created' => 'created_user_id',
103
        'updated' => 'updated_user_id',
104
        'deleted' => 'deleted_by',
105
    ];
106
    protected $dates                    = ['deleted_at'];
107
    protected $guarded                  = ['id'];
108
    protected $touches                  = ['concept'];
109
    protected $revisionCreationsEnabled = true;
110
    protected $casts                    = [
111
        'id'                  => 'integer',
112
        'created_user_id'     => 'integer',
113
        'updated_user_id'     => 'integer',
114
        'concept_id'          => 'integer',
115
        'is_concept_property' => 'bool',
116
        'is_generated'        => 'bool',
117
        'language'            => 'string',
118
        'last_import_id'      => 'integer',
119
        'object'              => 'string',
120
        'primary_pref_label'  => 'string',
121
        'profile_property_id' => 'integer',
122
        'related_concept_id'  => 'integer',
123
        'scheme_id'           => 'integer',
124
        'skos_property_id'    => 'integer',
125
        'status_id'           => 'integer',
126
        'vocabulary_id'       => 'integer',
127
    ];
128
129
    /**
130
     * Create the event listeners for the saving and saved events
131
     * This lets us save revisions whenever a save is made, no matter the
132
     * http method.
133
     */
134
    protected static function boot()
135
    {
136
        parent::boot();
137
138
        // static::creating(function(self $attribute) {
139
        //     self::checkForDuplicatePrefLabel($attribute);
140
        // });
141
        //
142
        // static::updating(function(self $attribute) {
143
        //     //make sure the update isn't part of a delete
144
        //     if (! array_key_exists('deleted_by', $attribute->getDirty())) {
145
        //         self::checkForDuplicatePrefLabel($attribute);
146
        //     }
147
        // });
148
149
        static::created(function (self $attribute) {
150
            //make sure we don't keep making new reciprocals
151
            if ($attribute->reciprocal_concept_property_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attribute->reciprocal_concept_property_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
152
                return;
153
            }
154
            $attribute->createHistory('added');
155
            if ($attribute->createReciprocal()) {
156
                //Sometimes we just update this attribute instead of creating a reciprocal.
157
                //This deletes the extra new history that was been added when we do that
158
                $attribute->history()->latest()->first()->delete();
159
            }
160
        });
161
162
        static::updated(function (self $attribute) {
163
            if (\count($attribute->dirtyData) === 1) {
164
                if ($attribute->isDirty('deleted_user_id') || $attribute->isDirty('deleted_by')) {
165
                    return;
166
                }
167
                if ($attribute->isDirty('related_concept_id')) {
168
                    $attribute->updateHistory();
169
170
                    return;
171
                }
172
                if ($attribute->isDirty('object')) {
173
                    if ($attribute->reciprocal_concept_property_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attribute->reciprocal_concept_property_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
174
                        $attribute->reciprocal->createHistory('deleted');
175
                        $attribute->reciprocal()->delete();
176
                    }
177
                    $attribute->createReciprocal();
178
179
                    return;
180
                }
181
            }
182
            $attribute->createHistory('updated');
183
        });
184
185
        static::deleted(function (self $attribute) {
186
            $attribute->createHistory('deleted');
187
            if ($attribute->reciprocal_concept_property_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attribute->reciprocal_concept_property_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
188
                $reciprocal = self::find($attribute->reciprocal_concept_property_id);
189
                if ($reciprocal) {
190
                    $reciprocal->delete();
191
                }
192
            }
193
        });
194
    }
195
196
    /*
197
    |--------------------------------------------------------------------------
198
    | FUNCTIONS
199
    |--------------------------------------------------------------------------
200
    */
201
202
    public function isPrefLabel()
203
    {
204
        return $this->profile_property->uri === 'skos:prefLabel';
205
    }
206
207
    /**
208
     * @param $vocabulary_id
209
     *
210
     * @return Carbon
211
     * @throws \InvalidArgumentException
212
     */
213
    public static function getLatestDateForVocabulary($vocabulary_id): Carbon
214
    {
215
        $created_at = self::getLatest($vocabulary_id, 'created_at');
216
        $updated_at = self::getLatest($vocabulary_id, 'updated_at');
217
        $deleted_at = self::getLatest($vocabulary_id, 'deleted_at');
218
219
        $date = collect([$created_at, $updated_at, $deleted_at])->max();
220
        try {
221
            return Carbon::createFromFormat(config('app.timestamp_format'), $date);
222
        } catch (InvalidArgumentException $e) {
223
            return null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return null returns the type null which is incompatible with the type-hinted return Carbon\Carbon.
Loading history...
224
        }
225
    }
226
227
    /**
228
     * @param int    $vocabulary_id
229
     * @param string $field
230
     *
231
     * @return string
232
     */
233
    private static function getLatest($vocabulary_id, $field)
234
    {
235
        return \DB::table(self::TABLE)
236
            ->join(Concept::TABLE, Concept::TABLE . '.id', '=', self::TABLE . '.concept_id')
237
            ->select(self::TABLE . '.' . $field)
238
            ->where(Concept::TABLE . '.vocabulary_id', $vocabulary_id)
239
            ->max(self::TABLE . '.' . $field);
240
    }
241
242
    public function createHistory(string $action): ConceptAttributeHistory
243
    {
244
        return ConceptAttributeHistory::create([
245
            'action'              => $action,
246
            'created_user_id'     => $this->updated_user_id,
247
            'concept_property_id' => $this->id,
248
            'concept_id'          => $this->concept_id,
249
            'vocabulary_id'       => $this->concept->vocabulary_id,
250
            'profile_property_id' => $this->profile_property_id,
251
            'object'              => $this->object,
252
            'language'            => $this->getAttributeFromArray('language'),
253
            'status_id'           => $this->status_id,
254
            //this should be set to null in the parent if there was no import
255
            'import_id'           => $this->last_import_id,
256
            //things we don't know yet. Must come from post-processing
257
            'scheme_id'           => null,
258
            'related_concept_id'  => null,
259
            //should be added to the concept model and not here
260
            'change_note'         => null,
261
        ]);
262
    }
263
264
    public function updateHistory()
265
    {
266
        $history = $this->history()->where('import_id', $this->last_import_id)->first();
267
        if ($history) {
268
            $history->update(['related_concept_id' => $this->related_concept_id]);
269
        }
270
    }
271
272
    public function createReciprocal()
273
    {
274
        //is the object a uri?
275
        if (! filter_var($this->object, FILTER_VALIDATE_URL)) {
276
            return false;
277
        }
278
279
        //does the profile_property have a reciprocal?
280
        if (! $this->profile_property->is_reciprocal && $this->profile_property->inverse_profile_property_id === null) {
281
            return false;
282
        }
283
284
        $reciprocalProperty = $this->profile_property->inverse_profile_property_id ?? $this->profile_property->id;
285
286
        //does it reference a known URI in a vocabulary I 'own'? (tricky -- what if it hasn't been created yet?)
287
        $relatedConcept = Concept::whereUri($this->object)->first();
288
        if (! $relatedConcept) {
289
            $this->update(['review_reciprocal' => true]);
290
291
            return true;
292
        }
293
294
        //todo: this should probably be findOrFail(), the error caught and added to error log
295
        $user = User::find($this->updated_user_id);
296
        if (! $user) {
297
            return false;
298
        }
299
300
        if ($user->cant('update', $relatedConcept)) {
301
            return false;
302
        }
303
304
        //todo: need a review reciprocals job
305
        //create the reciprocal
306
        $attribute = self::create([
307
            'related_concept_id'             => $this->concept->id,
308
            'object'                         => $this->concept->uri,
309
            'concept_id'                     => $relatedConcept->id,
310
            'status_id'                      => $this->status_id,
311
            'profile_property_id'            => $reciprocalProperty,
312
            'last_import_id'                 => $this->last_import_id,
313
            'is_generated'                   => true,
314
            'reciprocal_concept_property_id' => $this->id,
315
            'language'                       => null,
316
            'created_user_id'                => $this->updated_user_id,
317
            'updated_user_id'                => $this->updated_user_id,
318
        ]);
319
        $this->update(['reciprocal_concept_property_id' => $attribute->id]);
320
    }
321
322
    /**
323
     * @param ConceptAttribute $attribute
324
     *
325
     * @throws DuplicatePrefLabelException
326
     * @throws \App\Exceptions\DuplicatePrefLabelException
327
     */
328
    private static function checkForDuplicatePrefLabel(self $attribute): void
0 ignored issues
show
Unused Code introduced by
The method checkForDuplicatePrefLabel() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
329
    {
330
        //get the profileProperty and check if this is a skos:prefLabel
331
        //TODO make this check and see if the property subclasses skos:prefLabel
332
        if ($attribute->isPrefLabel()) {
333
            //if it's a prefLabel, then lookup the label+language combination and eager load the related concepts
334
            $vocabId                 = $attribute->concept->vocabulary_id;
335
            $matchingAttributesCount = self::join(Concept::TABLE,
336
                self::TABLE . '.concept_id',
337
                '=',
338
                Concept::TABLE . '.id')->where([
339
                ['object', '=', $attribute->object],
340
                [self::TABLE . '.language', '=', $attribute->getAttributeFromArray('language')],
341
                [Concept::TABLE . '.vocabulary_id', '=', $vocabId],
342
            ])->count();
343
            //check the vocabulary Ids and see if any of them are the same
344
            //if they are, then throw a DuplicatePrefLabel exception
345
            if ($matchingAttributesCount) {
346
                throw new DuplicatePrefLabelException('The skos:prefLabel combination of "' .
347
                    $attribute->object .
348
                    '" and "' .
349
                    $attribute->language .
350
                    '" already exists in this vocabulary');
351
            }
352
        }
353
    }
354
355
    /*
356
    |--------------------------------------------------------------------------
357
    | RELATIONS
358
    |--------------------------------------------------------------------------
359
    */
360
361
    public function history(): ?HasMany
362
    {
363
        return $this->hasMany(ConceptAttributeHistory::class, 'concept_property_id', 'id');
364
    }
365
366
    public function reciprocal(): ?HasOne
367
    {
368
        return $this->hasOne(self::class, 'reciprocal_concept_property_id', 'id');
369
    }
370
371
    public function inverse(): ?HasOne
372
    {
373
        return $this->hasOne(self::class, 'reciprocal_concept_property_id', 'id');
374
    }
375
376
    /*
377
    |--------------------------------------------------------------------------
378
    | SCOPES
379
    |--------------------------------------------------------------------------
380
    */
381
382
    /*
383
    |--------------------------------------------------------------------------
384
    | ACCESSORS
385
    |--------------------------------------------------------------------------
386
    */
387
388
    /*
389
    |--------------------------------------------------------------------------
390
    | MUTATORS
391
    |--------------------------------------------------------------------------
392
    */
393
}
394