VersionableTrait::defaultVersionSelect()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 8
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace JPNut\Versioning;
4
5
use Exception;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Relations\HasMany;
8
use Illuminate\Support\Str;
9
10
trait VersionableTrait
11
{
12
    /**
13
     * @return void
14
     */
15
    public static function bootVersionableTrait()
16
    {
17
        static::addGlobalScope(new VersioningScope());
18
    }
19
20
    /**
21
     * Get a new query builder that doesn't have any global scopes or eager loading.
22
     *
23
     * @return \Illuminate\Database\Eloquent\Builder|static
24
     */
25
    public function newModelQuery()
26
    {
27
        if (!$this->hasVersionJoin($builder = parent::newModelQuery(), $this->getVersionTable())) {
28
            $builder
29
                ->join(
30
                    $this->getVersionTable(),
31
                    function ($join) {
32
                        $join->on($this->getQualifiedKeyName(), '=', $this->getQualifiedVersionTableForeignKeyName())
0 ignored issues
show
Bug introduced by
The method getQualifiedKeyName() does not exist on JPNut\Versioning\VersionableTrait. Did you maybe mean getQualifiedVersionKeyName()? ( Ignorable by Annotation )

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

32
                        $join->on($this->/** @scrutinizer ignore-call */ getQualifiedKeyName(), '=', $this->getQualifiedVersionTableForeignKeyName())

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...
33
                            ->on($this->getQualifiedVersionTableKeyName(), '=', $this->getQualifiedVersionKeyName());
34
                    }
35
                )
36
                ->select(
37
                    $this->defaultVersionSelect()
38
                );
39
        }
40
41
        return $builder;
42
    }
43
44
    /**
45
     * @return array|string[]
46
     */
47
    public function defaultVersionSelect(): array
48
    {
49
        return array_merge(
50
            [
51
                $this->getTable().'.*',
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

51
                $this->/** @scrutinizer ignore-call */ 
52
                       getTable().'.*',
Loading history...
52
                $this->getQualifiedVersionTableKeyName(),
53
            ],
54
            $this->getQualifiedVersionableAttributes(),
55
        );
56
    }
57
58
    /**
59
     * Determine if the given builder contains a join with the given table.
60
     *
61
     * @param \Illuminate\Database\Eloquent\Builder $builder
62
     * @param string                                $table
63
     *
64
     * @return bool
65
     */
66
    protected function hasVersionJoin(Builder $builder, string $table)
67
    {
68
        return collect($builder->getQuery()->joins)->pluck('table')->contains($table);
69
    }
70
71
    /**
72
     * Perform a model insert operation.
73
     *
74
     * @param \Illuminate\Database\Eloquent\Builder $query
75
     *
76
     * @return bool
77
     */
78
    protected function performInsert(Builder $query)
79
    {
80
        if ($this->fireModelEvent('creating') === false) {
0 ignored issues
show
Bug introduced by
It seems like fireModelEvent() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

80
        if ($this->/** @scrutinizer ignore-call */ fireModelEvent('creating') === false) {
Loading history...
81
            return false;
82
        }
83
84
        // First we'll need to create a fresh query instance and touch the creation and
85
        // update timestamps on this model, which are maintained by us for developer
86
        // convenience. After, we will just continue saving these model instances.
87
        if ($this->usesTimestamps()) {
0 ignored issues
show
Bug introduced by
It seems like usesTimestamps() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

87
        if ($this->/** @scrutinizer ignore-call */ usesTimestamps()) {
Loading history...
88
            $this->updateTimestamps();
0 ignored issues
show
Bug introduced by
It seems like updateTimestamps() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

88
            $this->/** @scrutinizer ignore-call */ 
89
                   updateTimestamps();
Loading history...
89
        }
90
91
        // Ensure that the initial version number is set to 1. This sets the attribute,
92
        // and will be synced into storage later.
93
        $this->setVersionKey(1);
94
95
        // If the model has an incrementing key, we can use the "insertGetId" method on
96
        // the query builder, which will give us back the final inserted ID for this
97
        // table from the database. Not all tables have to be incrementing though.
98
        $attributes = $this->getAttributes();
0 ignored issues
show
Bug introduced by
It seems like getAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

98
        /** @scrutinizer ignore-call */ 
99
        $attributes = $this->getAttributes();
Loading history...
99
100
        // get version values & master attributes
101
        $versionAttributes = $this->getVersionAttributes($attributes);
102
        $masterAttributes = array_diff_key($attributes, $versionAttributes);
103
104
        if ($this->getIncrementing()) {
0 ignored issues
show
Bug introduced by
It seems like getIncrementing() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

104
        if ($this->/** @scrutinizer ignore-call */ getIncrementing()) {
Loading history...
105
            $this->insertAndSetId($query, $masterAttributes);
0 ignored issues
show
Bug introduced by
It seems like insertAndSetId() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

105
            $this->/** @scrutinizer ignore-call */ 
106
                   insertAndSetId($query, $masterAttributes);
Loading history...
106
        }
107
108
        // If the table isn't incrementing we'll simply insert these attributes as they
109
        // are. These attribute arrays must contain an "id" column previously placed
110
        // there by the developer as the manually determined key for these models.
111
        else {
112
            if (empty($masterAttributes)) {
113
                return true;
114
            }
115
116
            $query->insert($masterAttributes);
117
        }
118
119
        // insert the initial version into the version table
120
        $this->insertVersion($versionAttributes);
121
122
        // We will go ahead and set the exists property to true, so that it is set when
123
        // the created event is fired, just in case the developer tries to update it
124
        // during the event. This will allow them to do so and run an update here.
125
        $this->exists = true;
0 ignored issues
show
Bug Best Practice introduced by
The property exists does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
126
127
        $this->wasRecentlyCreated = true;
0 ignored issues
show
Bug Best Practice introduced by
The property wasRecentlyCreated does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
128
129
        $this->fireModelEvent('created', false);
130
131
        return true;
132
    }
133
134
    /**
135
     * Perform a model update operation.
136
     *
137
     * @param \Illuminate\Database\Eloquent\Builder $query
138
     *
139
     * @return bool
140
     */
141
    protected function performUpdate(Builder $query)
142
    {
143
        // If the updating event returns false, we will cancel the update operation so
144
        // developers can hook Validation systems into their models and cancel this
145
        // operation if the model does not pass validation. Otherwise, we update.
146
        if ($this->fireModelEvent('updating') === false) {
147
            return false;
148
        }
149
150
        // First we need to create a fresh query instance and touch the creation and
151
        // update timestamp on the model which are maintained by us for developer
152
        // convenience. Then we will just continue saving the model instances.
153
        if ($this->usesTimestamps()) {
154
            $this->updateTimestamps();
155
        }
156
157
        // Once we have run the update operation, we will fire the "updated" event for
158
        // this model instance. This will allow developers to hook into these after
159
        // models are updated, giving them a chance to do any special processing.
160
        $dirty = $this->getDirty();
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

160
        /** @scrutinizer ignore-call */ 
161
        $dirty = $this->getDirty();
Loading history...
161
162
        if (count($dirty) === 0) {
163
            return true;
164
        }
165
166
        // get version values & master attributes
167
        $versionAttributes = $this->getVersionAttributes($dirty);
168
        $masterAttributes = array_diff_key($dirty, $versionAttributes);
169
170
        $shouldCreateNewVersion = $this->shouldCreateNewVersion($versionAttributes);
171
172
        $this->setKeysForSaveQuery($query)
0 ignored issues
show
Bug introduced by
It seems like setKeysForSaveQuery() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

172
        $this->/** @scrutinizer ignore-call */ 
173
               setKeysForSaveQuery($query)
Loading history...
173
            ->increment(
174
                $this->getVersionKeyName(),
175
                $shouldCreateNewVersion ? 1 : 0,
176
                $masterAttributes
177
            );
178
179
        // If we need to create a new version, we first of all must manually increment
180
        // the version counter. This ensures the value is synced with the database.
181
        // We also use this value to insert a new version to the versions table.
182
        if ($shouldCreateNewVersion) {
183
            $this->setVersionKey($this->getVersionKey() + 1);
184
185
            $this->insertVersion(
186
                array_merge(
187
                    $this->getVersionAttributes($this->getAttributes()),
188
                    $versionAttributes
189
                )
190
            );
191
        }
192
193
        $this->syncChanges();
0 ignored issues
show
Bug introduced by
It seems like syncChanges() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

193
        $this->/** @scrutinizer ignore-call */ 
194
               syncChanges();
Loading history...
194
195
        $this->fireModelEvent('updated', false);
196
197
        return true;
198
    }
199
200
    /**
201
     * Delete the model from the database.
202
     *
203
     * @throws \Exception
204
     *
205
     * @return bool|null
206
     */
207
    public function delete()
208
    {
209
        if (is_null($this->getKeyName())) {
0 ignored issues
show
Bug introduced by
It seems like getKeyName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

209
        if (is_null($this->/** @scrutinizer ignore-call */ getKeyName())) {
Loading history...
210
            throw new Exception('No primary key defined on model.');
211
        }
212
213
        // If the model doesn't exist, there is nothing to delete so we'll just return
214
        // immediately and not do anything else. Otherwise, we will continue with a
215
        // deletion process on the model, firing the proper events, and so forth.
216
        if (!$this->exists) {
217
            return null;
218
        }
219
220
        if ($this->fireModelEvent('deleting') === false) {
221
            return false;
222
        }
223
224
        // Here, we'll touch the owning models, verifying these timestamps get updated
225
        // for the models. This will allow any caching to get broken on the parents
226
        // by the timestamp. Then we will go ahead and delete the model instance.
227
        $this->touchOwners();
0 ignored issues
show
Bug introduced by
It seems like touchOwners() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

227
        $this->/** @scrutinizer ignore-call */ 
228
               touchOwners();
Loading history...
228
229
        $this->performDeleteOnModel();
0 ignored issues
show
Bug introduced by
It seems like performDeleteOnModel() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

229
        $this->/** @scrutinizer ignore-call */ 
230
               performDeleteOnModel();
Loading history...
230
231
        // We'd like to remove all the versions associated with this model, but we need
232
        // to make sure the model wasn't soft deleted first.
233
        if (!$this->exists) {
234
            $this->versions()->delete();
235
        }
236
237
        // Once the model has been deleted, we will fire off the deleted event so that
238
        // the developers may hook into post-delete operations. We will then return
239
        // a boolean true as the delete is presumably successful on the database.
240
        $this->fireModelEvent('deleted', false);
241
242
        return true;
243
    }
244
245
    /**
246
     * @param array $attributes
247
     *
248
     * @return bool
249
     */
250
    protected function shouldCreateNewVersion(array $attributes): bool
251
    {
252
        return !empty($attributes);
253
    }
254
255
    /**
256
     * @param array $attributes
257
     *
258
     * @return mixed
259
     */
260
    protected function insertVersion(array $attributes)
261
    {
262
        return $this->versions()
263
            ->newModelInstance()
264
            ->forceFill(
265
                array_merge(
266
                    $attributes,
267
                    [
268
                        $this->getVersionTableForeignKeyName() => $this->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

268
                        $this->getVersionTableForeignKeyName() => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
269
                        $this->getVersionTableKeyName()        => $this->getVersionKey(),
270
                        $this->getVersionTableCreatedAtName()  => $this->freshTimestampString(),
0 ignored issues
show
Bug introduced by
It seems like freshTimestampString() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

270
                        $this->getVersionTableCreatedAtName()  => $this->/** @scrutinizer ignore-call */ freshTimestampString(),
Loading history...
271
                    ]
272
                )
273
            )
274
            ->save();
275
    }
276
277
    /**
278
     * @param array $attributes
279
     *
280
     * @return array
281
     */
282
    protected function getVersionAttributes(array $attributes)
283
    {
284
        $array = [];
285
286
        $versionableAttributes = $this->getVersionableAttributes();
287
288
        foreach ($attributes as $key => $value) {
289
            if ($newKey = $this->isVersionedKey($key, $versionableAttributes)) {
290
                $array[$newKey] = $value;
291
            }
292
        }
293
294
        return $array;
295
    }
296
297
    /**
298
     * Check if key is in versioned keys.
299
     *
300
     * @param string $key
301
     * @param array  $versionedKeys
302
     *
303
     * @return string|null
304
     */
305
    protected function isVersionedKey($key, array $versionedKeys)
306
    {
307
        return in_array($key, $versionedKeys)
308
            ? $key
309
            : null;
310
    }
311
312
    /**
313
     * @return VersionOptions
314
     */
315
    public function getVersionableOptions(): VersionOptions
316
    {
317
        return VersionOptions::create();
318
    }
319
320
    /**
321
     * @return string
322
     */
323
    public function getVersionKeyName(): string
324
    {
325
        return $this->getVersionableOptions()->versionKeyName;
326
    }
327
328
    /**
329
     * @return string
330
     */
331
    public function getQualifiedVersionKeyName(): string
332
    {
333
        return $this->getTable().'.'.$this->getVersionKeyName();
334
    }
335
336
    /**
337
     * @return int
338
     */
339
    public function getVersionKey(): int
340
    {
341
        return $this->getAttributeValue($this->getVersionKeyName());
0 ignored issues
show
Bug introduced by
It seems like getAttributeValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

341
        return $this->/** @scrutinizer ignore-call */ getAttributeValue($this->getVersionKeyName());
Loading history...
342
    }
343
344
    /**
345
     * @param int $version
346
     *
347
     * @return mixed
348
     */
349
    public function setVersionKey(int $version)
350
    {
351
        return $this->setAttribute($this->getVersionKeyName(), $version);
0 ignored issues
show
Bug introduced by
It seems like setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

351
        return $this->/** @scrutinizer ignore-call */ setAttribute($this->getVersionKeyName(), $version);
Loading history...
352
    }
353
354
    /**
355
     * @return string
356
     */
357
    public function getVersionTable(): string
358
    {
359
        return $this->getVersionableOptions()->versionTableName ?? Str::singular($this->getTable()).'_versions';
360
    }
361
362
    /**
363
     * @return string
364
     */
365
    public function getVersionTableKeyName(): string
366
    {
367
        return $this->getVersionableOptions()->versionTableKeyName;
368
    }
369
370
    /**
371
     * @return string
372
     */
373
    public function getQualifiedVersionTableKeyName(): string
374
    {
375
        return $this->getVersionTable().'.'.$this->getVersionTableKeyName();
376
    }
377
378
    /**
379
     * @return string
380
     */
381
    public function getVersionTableForeignKeyName(): string
382
    {
383
        return $this->getVersionableOptions()->versionTableForeignKeyName;
384
    }
385
386
    /**
387
     * @return string
388
     */
389
    public function getQualifiedVersionTableForeignKeyName(): string
390
    {
391
        return $this->getVersionTable().'.'.$this->getVersionTableForeignKeyName();
392
    }
393
394
    /**
395
     * @return string|null
396
     */
397
    public function getVersionTableCreatedAtName(): string
398
    {
399
        return $this->getVersionableOptions()->versionTableCreatedAtName;
400
    }
401
402
    /**
403
     * @return string
404
     */
405
    public function getQualifiedVersionTableCreatedAtName(): string
406
    {
407
        return $this->getVersionTable().'.'.$this->getVersionTableCreatedAtName();
408
    }
409
410
    /**
411
     * @return array
412
     */
413
    public function getVersionableAttributes(): array
414
    {
415
        return $this->getVersionableOptions()->versionableAttributes;
416
    }
417
418
    /**
419
     * @return array
420
     */
421
    public function getQualifiedVersionableAttributes(): array
422
    {
423
        return array_map(
424
            function (string $attribute) {
425
                return $this->getVersionTable().'.'.$attribute;
426
            }, $this->getVersionableOptions()->versionableAttributes
427
        );
428
    }
429
430
    /**
431
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
432
     */
433
    public function versions(): HasMany
434
    {
435
        return $this->newHasMany(
0 ignored issues
show
Bug introduced by
It seems like newHasMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

435
        return $this->/** @scrutinizer ignore-call */ newHasMany(
Loading history...
436
            $this->newRelatedInstance(Version::class)
0 ignored issues
show
Bug introduced by
It seems like newRelatedInstance() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

436
            $this->/** @scrutinizer ignore-call */ 
437
                   newRelatedInstance(Version::class)
Loading history...
437
                ->setTable($this->getVersionTable())
438
                ->newQuery(),
439
            $this,
440
            $this->getQualifiedVersionTableForeignKeyName(),
441
            $this->getKeyName()
442
        );
443
    }
444
445
    /**
446
     * @param mixed $version
447
     *
448
     * @return \Illuminate\Database\Eloquent\Model
449
     */
450
    public function version($version)
451
    {
452
        if ($version instanceof Version) {
453
            return $version;
454
        }
455
456
        return $this->versions()
457
            ->whereKey($version)
458
            ->first();
459
    }
460
461
    /**
462
     * @param  $version
463
     *
464
     * @return mixed
465
     */
466
    public function changeVersion($version)
467
    {
468
        if ($this->wasRecentlyCreated) {
469
            $this->refresh();
0 ignored issues
show
Bug introduced by
It seems like refresh() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

469
            $this->/** @scrutinizer ignore-call */ 
470
                   refresh();
Loading history...
470
        }
471
472
        $this->fill(
0 ignored issues
show
Bug introduced by
It seems like fill() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

472
        $this->/** @scrutinizer ignore-call */ 
473
               fill(
Loading history...
473
            $this->getVersionAttributes(
474
                $this->version($version)->getAttributes()
475
            )
476
        );
477
478
        $this->save();
0 ignored issues
show
Bug introduced by
It seems like save() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

478
        $this->/** @scrutinizer ignore-call */ 
479
               save();
Loading history...
479
480
        return $this;
481
    }
482
}
483