Completed
Pushe3a2d5...fb652a
passed — Build
created

Entity   C

↳ Parent: Project

Coupling/Cohesion

Components 1
Dependencies 11

Complexity

Total Complexity 57

Size/Duplication

Total Lines 509
Duplicated Lines 0 %

Test Coverage

Coverage 97.47%

Importance

Changes 18
Bugs 4 Features 4
Metric Value
c 18
b 4
f 4
dl 0
loc 509
ccs 193
cts 198
cp 0.9747
rs 6.433
wmc 57
lcom 1
cbo 11

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A initialize() 0 9 2
A initializeField() 0 18 4
getFields() 0 1 ?
A getContentType() 0 4 1
A newRevision() 0 7 2
A isContentField() 0 4 1
A hasField() 0 4 1
A getField() 0 4 1
A isRevisionField() 0 4 1
B __get() 0 20 5
B __set() 0 22 4
A setOnField() 0 18 4
A getFieldTypes() 0 12 1
B find() 0 39 4
B save() 0 37 4
A saveContent() 0 4 1
A saveRevision() 0 13 4
A unpublishOtherRevisions() 0 12 2
A saveField() 0 14 2
A toArray() 0 13 2
A delete() 0 23 2
B deleteRevision() 0 23 4
A clearFields() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Entity 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Entity, and based on these observations, apply Extract Interface, too.

1
<?php namespace Rocket\Entities;
2
3
use Illuminate\Database\Eloquent\ModelNotFoundException;
4
use Illuminate\Support\Collection;
5
use Illuminate\Support\Facades\DB;
6
use InvalidArgumentException;
7
use Rocket\Entities\Exceptions\EntityNotFoundException;
8
use Rocket\Entities\Exceptions\InvalidFieldTypeException;
9
use Rocket\Entities\Exceptions\NonExistentFieldException;
10
use Rocket\Entities\Exceptions\NoPublishedRevisionForLanguageException;
11
use Rocket\Entities\Exceptions\NoRevisionForLanguageException;
12
use Rocket\Entities\Exceptions\ReservedFieldNameException;
13
14
/**
15
 * Entity manager
16
 *
17
 * @property int $id The content ID
18
 * @property int $language_id The language in which this entity is
19
 * @property string $type The type of the Entity
20
 * @property bool $published Is this content published
21
 * @property \Rocket\Entities\Revision[] $revisions all revisions in this content
22
 * @property-read \DateTime $created_at
23
 * @property-read \DateTime $updated_at
24
 */
25
abstract class Entity
26
{
27
    /**
28
     * @var array<class> The list of field types, filled with the configuration
29
     */
30
    public static $types;
31
32
    /**
33
     * The content represented by this entity
34
     *
35
     * @var Content
36
     */
37
    protected $content; //id, created_at, type, published
38
39
    /**
40
     * The revision represented by this entity
41
     *
42
     * @var Revision
43
     */
44
    protected $revision; //language_id, updated_at, type, published
45
46
    /**
47
     * The fields in this entity
48
     *
49
     * @var array<FieldCollection>
50
     */
51
    protected $data;
52
53
    /**
54
     * Entity constructor.
55
     *
56
     * @param int $language_id The language this specific entity is in
57
     */
58 81
    public function __construct($language_id)
59
    {
60 81
        if (!is_numeric($language_id) || $language_id == 0) {
61 3
            throw new InvalidArgumentException("You must set a valid 'language_id'.");
62
        }
63
64 78
        $fields = $this->getFields();
65
66 78
        $this->initialize($fields);
67
68 72
        $this->type = $this->getContentType();
69 72
        $this->language_id = $language_id;
70 72
    }
71
72
    /**
73
     * Creates the Content, Revision and FieldCollections
74
     *
75
     * @param array $fields The fields and their configurations
76
     * @throws InvalidFieldTypeException
77
     * @throws ReservedFieldNameException
78
     */
79 78
    protected function initialize(array $fields)
80
    {
81 78
        $this->content = new Content;
82 78
        $this->revision = new Revision;
83
84 78
        foreach ($fields as $field => $settings) {
85 78
            $this->data[$field] = $this->initializeField($field, $settings);
86 72
        }
87 72
    }
88
89
    /**
90
     * Validate configuration and prepare a FieldCollection
91
     *
92
     * @param string $field
93
     * @param array $settings
94
     * @throws InvalidFieldTypeException
95
     * @throws ReservedFieldNameException
96
     * @return FieldCollection
97
     */
98 78
    protected function initializeField($field, $settings)
99
    {
100 78
        if ($this->isContentField($field) || $this->isRevisionField($field)) {
101 3
            throw new ReservedFieldNameException(
102 3
                "The field '$field' cannot be used in '" . get_class($this) . "' as it is a reserved name"
103 3
            );
104
        }
105
106 75
        $type = $settings['type'];
107
108 75
        if (!array_key_exists($type, self::$types)) {
109 3
            throw new InvalidFieldTypeException("Unkown type '$type' in '" . get_class($this) . "'");
110
        }
111
112 72
        $settings['type'] = self::$types[$settings['type']];
113
114 72
        return FieldCollection::initField($settings);
115
    }
116
117
    /**
118
     * Return the fields in this entity
119
     *
120
     * @return array
121
     */
122
    abstract public function getFields();
123
124
    /**
125
     * Get the database friendly content type
126
     *
127
     * @return string
128
     */
129 78
    public static function getContentType()
130
    {
131 78
        return str_replace('\\', '', snake_case((new \ReflectionClass(get_called_class()))->getShortName()));
132
    }
133
134
    /**
135
     * Create a new revision based on the same content ID but without the content.
136
     * Very useful if you want to add a new language
137
     *
138
     * @param int $language_id
139
     * @return static
140
     */
141 3
    public function newRevision($language_id = null)
142
    {
143 3
        $created = new static($language_id ?: $this->language_id);
144 3
        $created->content = $this->content;
145
146 3
        return $created;
147
    }
148
149
    /**
150
     * Check if the field is related to the content
151
     *
152
     * @param string $field
153
     * @return bool
154
     */
155 78
    protected function isContentField($field)
156
    {
157 78
        return in_array($field, ['id', 'created_at', 'type', 'published']);
158
    }
159
160
    /**
161
     * Check if the field exists on the entity
162
     *
163
     * @param string $field
164
     * @return bool
165
     */
166 60
    public function hasField($field)
167
    {
168 60
        return array_key_exists($field, $this->data);
169
    }
170
171
    /**
172
     * Get a field's FieldCollection.
173
     *
174
     * Be careful as this gives you the real field instances.
175
     *
176
     * @param string $field
177
     * @return FieldCollection
178
     */
179 54
    public function getField($field)
180
    {
181 54
        return $this->data[$field];
182
    }
183
184
    /**
185
     * Check if the field is related to the revision
186
     *
187
     * @param string $field
188
     * @return bool
189
     */
190 75
    protected function isRevisionField($field)
191
    {
192 75
        return in_array($field, ['language_id', 'updated_at', 'published']);
193
    }
194
195
    /**
196
     * Dynamically retrieve attributes on the model.
197
     *
198
     * @param string $key
199
     * @throws NonExistentFieldException
200
     * @return $this|bool|\Carbon\Carbon|\DateTime|mixed|static
201
     */
202 63
    public function __get($key)
203
    {
204 63
        if ($this->isContentField($key)) {
205 31
            return $this->content->getAttribute($key);
206
        }
207
208 60
        if ($this->isRevisionField($key)) {
209 9
            return $this->revision->getAttribute($key);
210
        }
211
212 54
        if ($this->hasField($key)) {
213 51
            return $this->getField($key);
214
        }
215
216 10
        if ($key == 'revisions') {
217 7
            return $this->content->revisions;
218
        }
219
220 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
221
    }
222
223
    /**
224
     * Dynamically set attributes on the model.
225
     *
226
     * @param string $key
227
     * @param mixed $value
228
     * @throws NonExistentFieldException
229
     */
230 72
    public function __set($key, $value)
231
    {
232 72
        if ($this->isContentField($key)) {
233 72
            $this->content->setAttribute($key, $value);
234
235 72
            return;
236
        }
237
238 72
        if ($this->isRevisionField($key)) {
239 72
            $this->revision->setAttribute($key, $value);
240
241 72
            return;
242
        }
243
244 21
        if ($this->hasField($key)) {
245 18
            $this->setOnField($this->getField($key), $value);
246
247 18
            return;
248
        }
249
250 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
251
    }
252
253
    /**
254
     * Set values on a field
255
     *
256
     * @param FieldCollection $field
257
     * @param $value
258
     */
259 18
    protected function setOnField(FieldCollection $field, $value)
260
    {
261 18
        if (!is_array($value)) {
262 6
            $field->offsetSet(0, $value);
263
264 6
            return;
265
        }
266
267 12
        $field->clear();
268
269
        // This happens when the array is
270
        // replaced completely by another array
271 12
        if (count($value)) {
272 3
            foreach ($value as $k => $v) {
273 3
                $field->offsetSet($k, $v);
274 3
            }
275 3
        }
276 12
    }
277
278
    /**
279
     * Get all field types in this Entity.
280
     *
281
     * @return Collection<string>
0 ignored issues
show
Documentation introduced by Stéphane Goetz
The doc-type Collection<string> could not be parsed: Expected "|" or "end of type", but got "<" at position 10. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
282
     */
283 20
    protected function getFieldTypes()
284
    {
285 20
        return (new Collection($this->getFields()))
286
            ->map(function ($options) {
287 20
                return $options['type'];
288 20
            })
289 20
            ->values()
290 20
            ->unique()
291
            ->map(function ($type) {
292 20
                return self::$types[$type];
293 20
            });
294
    }
295
296
    /**
297
     * Find the latest valid revision for this entity
298
     *
299
     * @param int $id
300
     * @param int $language_id
301
     * @return static
302
     * @throws EntityNotFoundException
303
     * @throws NoPublishedRevisionForLanguageException
304
     * @throws NoRevisionForLanguageException
305
     */
306 28
    public static function find($id, $language_id)
307
    {
308 28
        $instance = new static($language_id);
309
310
        try {
311 28
            $instance->content = Content::findOrFail($id);
312 28
        } catch (ModelNotFoundException $e) {
313 7
            throw new EntityNotFoundException("The entity with id '$id' doesn't exist", 0, $e);
314
        }
315
316
        try {
317 21
            $instance->revision = Revision::where('content_id', $id)
318 21
                ->where('language_id', $language_id)
319 21
                ->where('published', true)
320 21
                ->firstOrFail();
321 21
        } catch (ModelNotFoundException $e) {
322 9
            $count = Revision::where('content_id', $id)->where('language_id', $language_id)->count();
323
324 9
            if ($count) {
325 2
                $message = "There are revisions in language_id='$language_id' for Entity '$id' but none is published";
326 2
                throw new NoPublishedRevisionForLanguageException($message, 0, $e);
327
            } else {
328 7
                $message = "There no revisions in language_id='$language_id' for Entity '$id' but none is published";
329 7
                throw new NoRevisionForLanguageException($message, 0, $e);
330
            }
331
        }
332
333
334 12
        $instance->getFieldTypes()
335
            ->each(function ($type) use ($instance) {
336 12
                $type::where('revision_id', $instance->revision->id)
337 12
                    ->get()
338
                    ->each(function (Field $value) use ($instance) {
339 12
                        $instance->data[$value->name][$value->weight] = $value;
340 12
                    });
341 12
            });
342
343 12
        return $instance;
344
    }
345
346
    /**
347
     * Save a revision
348
     *
349
     * @param bool $newRevision Should we create a new revision, false by default
350
     * @param bool $publishRevision Should we immediately publish this revision, true by default
351
     */
352 33
    public function save($newRevision = false, $publishRevision = true)
353
    {
354 33
        if ($newRevision) {
355 21
            $revision = new Revision;
356 21
            $revision->language_id = $this->revision->language_id;
357
358 21
            $this->revision = $revision;
359 21
        }
360
361 33
        DB::transaction(
362
            function () use ($newRevision, $publishRevision) {
363 33
                $this->saveContent();
364
365 33
                $this->saveRevision($publishRevision);
366
367
                // Prepare and save fields
368 33
                foreach (array_keys($this->data) as $fieldName) {
369
                    /** @var FieldCollection $field */
370 33
                    $field = $this->data[$fieldName];
371
372 33
                    if (!$newRevision) {
373
                        $field->deleted()->each(function (Field $value) {
374 3
                            $value->delete();
375 18
                        });
376 18
                    }
377
378
                    $field->each(function (Field $value, $key) use ($newRevision, $fieldName) {
379 33
                        $value->weight = $key;
380 33
                        $value->name = $fieldName;
381 33
                        $this->saveField($value, $newRevision);
382 33
                    });
383
384 33
                    $field->syncOriginal();
385 33
                }
386 28
            }
387 33
        );
388 28
    }
389
390
    /**
391
     * Save the content
392
     */
393 33
    protected function saveContent()
394
    {
395 33
        $this->content->save();
396 33
    }
397
398
    /**
399
     * Save the revision
400
     *
401
     * @param bool $publishRevision Should we immediately publish this revision, true by default
402
     */
403 33
    protected function saveRevision($publishRevision)
404
    {
405 33
        if (!$this->revision->exists && !$publishRevision) {
406 18
            $this->revision->published = $publishRevision;
407 18
        }
408
409 33
        $this->revision->content_id = $this->content->id;
410 33
        $this->revision->save();
411
412 33
        if ($publishRevision) {
413 18
            $this->unpublishOtherRevisions();
414 18
        }
415 33
    }
416
417
    /**
418
     * Unpublish the revisions other than this one.
419
     * Only for the same content_id and language_id
420
     */
421 18
    protected function unpublishOtherRevisions()
422
    {
423 18
        if ($this->content->wasRecentlyCreated) {
424 18
            return;
425
        }
426
427
        // Unpublish all other revisions
428 6
        Revision::where('content_id', $this->content->id)
429 6
            ->where('language_id', $this->revision->language_id)
430 6
            ->where('id', '!=', $this->revision->id)
431 6
            ->update(['published' => false]);
432 6
    }
433
434
    /**
435
     * Save a single field instance
436
     *
437
     * @param Field $field The field instance to save
438
     * @param bool $newRevision Should we create a new revision?
439
     */
440 33
    protected function saveField(Field $field, $newRevision)
441
    {
442
        // If we create a new revision, this will
443
        // reinit the field to a non-saved field
444
        // and create a new row in the database
445 33
        if ($newRevision) {
446 21
            $field->id = null;
447 21
            $field->exists = false;
448 21
        }
449
450 33
        $field->revision_id = $this->revision->id;
451
452 33
        $field->save();
453 28
    }
454
455
    /**
456
     * Convert the Entity to an array.
457
     *
458
     * @return array
459
     */
460 16
    public function toArray()
461
    {
462
        $content = [
463 16
            '_content' => $this->content->toArray(),
464 16
            '_revision' => $this->revision->toArray(),
465 16
        ];
466
467 16
        foreach ($this->data as $field => $data) {
468 16
            $content[$field] = $data->toArray();
469 16
        }
470
471 16
        return $content;
472
    }
473
474 4
    public function delete($clear = true)
475
    {
476 4
        $revisions = Revision::where('content_id', $this->content->id)->get();
477
478 4
        $ids = $revisions->pluck('id');
479
480
        $this->getFieldTypes()->each(function ($type) use ($ids) {
481 4
            $type::whereIn('revision_id', $ids)->delete();
482 4
        });
483
484 4
        Revision::whereIn('id', $ids)->delete();
485 4
        $this->revision->exists = false;
486
487
        // TODO :: add an event system to be able to remove this content from entity fields
488
489 4
        $this->content->delete();
490
491 4
        if ($clear) {
492 2
            $this->revision->id = null;
493 2
            $this->content->id = null;
494 2
            $this->clearFields();
495 2
        }
496 4
    }
497
498
    public function deleteRevision($clear = true)
499
    {
500 4
        $this->getFieldTypes()->each(function ($type) {
501 4
            $type::where('revision_id', $this->revision->id)->delete();
502 4
        });
503
504
        // If this revision is currently
505
        // published, we need to publish
506
        // another revision in place.
507 4
        if ($this->revision->published && $this->revision->exists) {
508
            //TODO :: improve this logic
509
            Revision::where('content_id', $this->content->id)
510
                ->where('id', '!=', $this->revision->id)
511
                ->take(1)
512
                ->update(['published' => true]);
513
        }
514
515 4
        $this->revision->delete();
516
517 4
        if ($clear) {
518 2
            $this->clearFields();
519 2
        }
520 4
    }
521
522 4
    protected function clearFields()
523
    {
524
        // Void all the fields
525 4
        foreach (array_keys($this->data) as $fieldName) {
526
            /** @var FieldCollection $field */
527 4
            $field = $this->data[$fieldName];
528
529 4
            $field->clear();
530 4
            $field->syncOriginal();
531 4
        }
532 4
    }
533
}
534