Passed
Push — stage ( b80c43...225f24 )
by Jon
12:24
created

ConceptAttribute::project()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
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;
0 ignored issues
show
introduced by
The trait Venturecraft\Revisionable\RevisionableTrait requires some properties which are not provided by App\Models\ConceptAttribute: $revisionNullString, $revisionEnabled, $revisionCleanup, $revisionFormattedFields, $keepRevisionOf, $revisionUnknownString, $revisionFormattedFieldNames, $historyLimit, $softDelete
Loading history...
99
    use Cacheable;
0 ignored issues
show
Bug introduced by
The trait Laracasts\Matryoshka\Cacheable requires the property $timestamp which is not provided by App\Models\ConceptAttribute.
Loading history...
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
155
            $attribute->createHistory('added');
156
157
            if ($attribute->createReciprocal()) {
158
                //Sometimes we just update this attribute instead of creating a reciprocal.
159
                //This deletes the extra new history that was been added when we do that
160
                $attribute->history()->latest()->first()->delete();
161
            }
162
        });
163
164
        static::updated(function (self $attribute) {
165
            if (\count($attribute->dirtyData) === 1) {
166
                if ($attribute->isDirty('deleted_user_id') || $attribute->isDirty('deleted_by')) {
167
                    return;
168
                }
169
                if ($attribute->isDirty('related_concept_id')) {
170
                    $attribute->updateHistory();
171
172
                    return;
173
                }
174
                if ($attribute->isDirty('object')) {
175
                    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...
176
177
                        //only procede if we allow statement generation
178
                        if (! $attribute->project()->generate_statements) {
179
                            return;
180
                        }
181
182
                        $attribute->reciprocal->createHistory('deleted');
183
                        $attribute->reciprocal()->delete();
184
                    }
185
186
                    $attribute->createReciprocal();
187
188
                    return;
189
                }
190
            }
191
            $attribute->createHistory('updated');
192
        });
193
194
        static::deleted(function (self $attribute) {
195
            $attribute->createHistory('deleted');
196
197
            //only procede if we allow statement generation
198
            if (! $attribute->project()->generate_statements) {
199
                return;
200
            }
201
202
            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...
203
                $reciprocal = self::find($attribute->reciprocal_concept_property_id);
204
                if ($reciprocal) {
205
                    $reciprocal->delete();
206
                }
207
            }
208
        });
209
    }
210
211
    /*
212
    |--------------------------------------------------------------------------
213
    | FUNCTIONS
214
    |--------------------------------------------------------------------------
215
    */
216
217
    public function isPrefLabel()
218
    {
219
        return $this->profile_property->uri === 'skos:prefLabel';
220
    }
221
222
    /**
223
     * @param $vocabulary_id
224
     *
225
     * @return Carbon
226
     * @throws \InvalidArgumentException
227
     */
228
    public static function getLatestDateForVocabulary($vocabulary_id): Carbon
229
    {
230
        $created_at = self::getLatest($vocabulary_id, 'created_at');
231
        $updated_at = self::getLatest($vocabulary_id, 'updated_at');
232
        $deleted_at = self::getLatest($vocabulary_id, 'deleted_at');
233
234
        $date = collect([$created_at, $updated_at, $deleted_at])->max();
235
        try {
236
            return Carbon::createFromFormat(config('app.timestamp_format'), $date);
237
        } catch (InvalidArgumentException $e) {
238
            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...
239
        }
240
    }
241
242
    /**
243
     * @param int    $vocabulary_id
244
     * @param string $field
245
     *
246
     * @return string
247
     */
248
    private static function getLatest($vocabulary_id, $field)
249
    {
250
        return \DB::table(self::TABLE)
251
            ->join(Concept::TABLE, Concept::TABLE . '.id', '=', self::TABLE . '.concept_id')
252
            ->select(self::TABLE . '.' . $field)
253
            ->where(Concept::TABLE . '.vocabulary_id', $vocabulary_id)
254
            ->max(self::TABLE . '.' . $field);
255
    }
256
257
    public function createHistory(string $action): ConceptAttributeHistory
258
    {
259
        return ConceptAttributeHistory::create([
260
            'action'              => $action,
261
            'created_user_id'     => $this->updated_user_id,
262
            'concept_property_id' => $this->id,
263
            'concept_id'          => $this->concept_id,
264
            'vocabulary_id'       => $this->concept->vocabulary_id,
265
            'profile_property_id' => $this->profile_property_id,
266
            'object'              => $this->object,
267
            'language'            => $this->getAttributeFromArray('language'),
268
            'status_id'           => $this->status_id,
269
            //this should be set to null in the parent if there was no import
270
            'import_id'           => $this->last_import_id,
271
            //things we don't know yet. Must come from post-processing
272
            'scheme_id'           => null,
273
            'related_concept_id'  => null,
274
            //should be added to the concept model and not here
275
            'change_note'         => null,
276
        ]);
277
    }
278
279
    public function updateHistory()
280
    {
281
        $history = $this->history()->where('import_id', $this->last_import_id)->first();
282
        if ($history) {
283
            $history->update(['related_concept_id' => $this->related_concept_id]);
284
        }
285
    }
286
287
    public function createReciprocal(): ?bool
288
    {
289
        //only procede if we allow statement generation
290
        if (! $this->project()->generate_statements) {
291
            return false;
292
        }
293
        //is the object a uri?
294
        if (! filter_var($this->object, FILTER_VALIDATE_URL)) {
295
            return false;
296
        }
297
298
        //does the profile_property have a reciprocal?
299
        if (! $this->profile_property->is_reciprocal && $this->profile_property->inverse_profile_property_id === null) {
300
            return false;
301
        }
302
303
        $reciprocalProperty = $this->profile_property->inverse_profile_property_id ?? $this->profile_property->id;
304
305
        //does it reference a known URI in a vocabulary I 'own'? (tricky -- what if it hasn't been created yet?)
306
        $relatedConcept = Concept::whereUri($this->object)->first();
307
        if (! $relatedConcept) {
308
            $this->update(['review_reciprocal' => true]);
309
310
            return true;
311
        }
312
313
        //todo: this should probably be findOrFail(), the error caught and added to error log
314
        $user = User::find($this->updated_user_id);
315
        if (! $user) {
316
            return false;
317
        }
318
319
        if ($user->cant('update', $relatedConcept)) {
320
            return false;
321
        }
322
323
        //todo: need a review reciprocals job
324
        //create the reciprocal
325
        $attribute = self::create([
326
            'related_concept_id'             => $this->concept->id,
327
            'object'                         => $this->concept->uri,
328
            'concept_id'                     => $relatedConcept->id,
329
            'status_id'                      => $this->status_id,
330
            'profile_property_id'            => $reciprocalProperty,
331
            'last_import_id'                 => $this->last_import_id,
332
            'is_generated'                   => true,
333
            'reciprocal_concept_property_id' => $this->id,
334
            'language'                       => null,
335
            'created_user_id'                => $this->updated_user_id,
336
            'updated_user_id'                => $this->updated_user_id,
337
        ]);
338
        $this->update(['reciprocal_concept_property_id' => $attribute->id]);
339
340
        return null;
341
    }
342
343
    /**
344
     * @param self $attribute
345
     *
346
     * @throws \App\Exceptions\DuplicatePrefLabelException
347
     */
348
    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...
349
    {
350
        //get the profileProperty and check if this is a skos:prefLabel
351
        //TODO make this check and see if the property subclasses skos:prefLabel
352
        if ($attribute->isPrefLabel()) {
353
            //if it's a prefLabel, then lookup the label+language combination and eager load the related concepts
354
            $vocabId                 = $attribute->concept->vocabulary_id;
355
            $matchingAttributesCount = self::join(Concept::TABLE,
356
                self::TABLE . '.concept_id',
357
                '=',
358
                Concept::TABLE . '.id')->where([
359
                ['object', '=', $attribute->object],
360
                [self::TABLE . '.language', '=', $attribute->getAttributeFromArray('language')],
361
                [Concept::TABLE . '.vocabulary_id', '=', $vocabId],
362
            ])->count();
363
            //check the vocabulary Ids and see if any of them are the same
364
            //if they are, then throw a DuplicatePrefLabel exception
365
            if ($matchingAttributesCount) {
366
                throw new DuplicatePrefLabelException('The skos:prefLabel combination of "' .
367
                    $attribute->object .
368
                    '" and "' .
369
                    $attribute->language .
370
                    '" already exists in this vocabulary');
371
            }
372
        }
373
    }
374
375
    /*
376
    |--------------------------------------------------------------------------
377
    | RELATIONS
378
    |--------------------------------------------------------------------------
379
    */
380
381
    public function history(): ?HasMany
382
    {
383
        return $this->hasMany(ConceptAttributeHistory::class, 'concept_property_id', 'id');
384
    }
385
386
    public function reciprocal(): ?HasOne
387
    {
388
        return $this->hasOne(self::class, 'reciprocal_concept_property_id', 'id');
389
    }
390
391
    public function inverse(): ?HasOne
392
    {
393
        return $this->hasOne(self::class, 'reciprocal_concept_property_id', 'id');
394
    }
395
396
    public function project()
397
    {
398
        return $this->concept->vocabulary->project;
399
    }
400
401
    /*
402
    |--------------------------------------------------------------------------
403
    | SCOPES
404
    |--------------------------------------------------------------------------
405
    */
406
407
    /*
408
    |--------------------------------------------------------------------------
409
    | ACCESSORS
410
    |--------------------------------------------------------------------------
411
    */
412
413
    /*
414
    |--------------------------------------------------------------------------
415
    | MUTATORS
416
    |--------------------------------------------------------------------------
417
    */
418
}
419