Completed
Push — master ( 24c826...e3a2d5 )
by Stéphane
13:49
created

Entity   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 415
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

Changes 17
Bugs 4 Features 3
Metric Value
c 17
b 4
f 3
dl 0
loc 415
ccs 153
cts 153
cp 1
rs 8.3673
wmc 45
lcom 1
cbo 8

20 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
B find() 0 30 1
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

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\Support\Collection;
4
use Illuminate\Support\Facades\DB;
5
use InvalidArgumentException;
6
use Rocket\Entities\Exceptions\InvalidFieldTypeException;
7
use Rocket\Entities\Exceptions\NonExistentFieldException;
8
use Rocket\Entities\Exceptions\ReservedFieldNameException;
9
10
/**
11
 * Entity manager
12
 *
13
 * @property int $id The content ID
14
 * @property int $language_id The language in which this entity is
15
 * @property string $type The type of the Entity
16
 * @property bool $published Is this content published
17
 * @property \Rocket\Entities\Revision[] $revisions all revisions in this content
18
 * @property-read \DateTime $created_at
19
 * @property-read \DateTime $updated_at
20
 */
21
abstract class Entity
22
{
23
    /**
24
     * @var array<class> The list of field types, filled with the configuration
25
     */
26
    public static $types;
27
28
    /**
29
     * The content represented by this entity
30
     *
31
     * @var Content
32
     */
33
    protected $content; //id, created_at, type, published
34
35
    /**
36
     * The revision represented by this entity
37
     *
38
     * @var Revision
39
     */
40
    protected $revision; //language_id, updated_at, type, published
41
42
    /**
43
     * The fields in this entity
44
     *
45
     * @var array<FieldCollection>
46
     */
47
    protected $data;
48
49
    /**
50
     * Entity constructor.
51
     *
52
     * @param int $language_id The language this specific entity is in
53
     */
54 57
    public function __construct($language_id)
55
    {
56 57
        if (!is_int($language_id) || $language_id == 0) {
57 3
            throw new InvalidArgumentException("You must set a valid 'language_id'.");
58
        }
59
60 54
        $fields = $this->getFields();
61
62 54
        $this->initialize($fields);
63
64 48
        $this->type = $this->getContentType();
65 48
        $this->language_id = $language_id;
66 48
    }
67
68
    /**
69
     * Creates the Content, Revision and FieldCollections
70
     *
71
     * @param array $fields The fields and their configurations
72
     * @throws InvalidFieldTypeException
73
     * @throws ReservedFieldNameException
74
     */
75 54
    protected function initialize(array $fields)
76
    {
77 54
        $this->content = new Content;
78 54
        $this->revision = new Revision;
79
80 54
        foreach ($fields as $field => $settings) {
81 54
            $this->data[$field] = $this->initializeField($field, $settings);
82 48
        }
83 48
    }
84
85
    /**
86
     * Validate configuration and prepare a FieldCollection
87
     *
88
     * @param string $field
89
     * @param array $settings
90
     * @throws InvalidFieldTypeException
91
     * @throws ReservedFieldNameException
92
     * @return FieldCollection
93
     */
94 54
    protected function initializeField($field, $settings)
95
    {
96 54
        if ($this->isContentField($field) || $this->isRevisionField($field)) {
97 3
            throw new ReservedFieldNameException(
98 3
                "The field '$field' cannot be used in '" . get_class($this) . "' as it is a reserved name"
99 3
            );
100
        }
101
102 51
        $type = $settings['type'];
103
104 51
        if (!array_key_exists($type, self::$types)) {
105 3
            throw new InvalidFieldTypeException("Unkown type '$type' in '" . get_class($this) . "'");
106
        }
107
108 48
        $settings['type'] = self::$types[$settings['type']];
109
110 48
        return FieldCollection::initField($settings);
111
    }
112
113
    /**
114
     * Return the fields in this entity
115
     *
116
     * @return array
117
     */
118
    abstract public function getFields();
119
120
    /**
121
     * Get the database friendly content type
122
     *
123
     * @return string
124
     */
125 54
    public static function getContentType()
126
    {
127 54
        return str_replace('\\', '', snake_case((new \ReflectionClass(get_called_class()))->getShortName()));
128
    }
129
130
    /**
131
     * Create a new revision based on the same content ID but without the content.
132
     * Very useful if you want to add a new language
133
     *
134
     * @param int $language_id
135
     * @return static
136
     */
137 3
    public function newRevision($language_id = null)
138
    {
139 3
        $created = new static($language_id ?: $this->language_id);
140 3
        $created->content = $this->content;
141
142 3
        return $created;
143
    }
144
145
    /**
146
     * Check if the field is related to the content
147
     *
148
     * @param string $field
149
     * @return bool
150
     */
151 54
    protected function isContentField($field)
152
    {
153 54
        return in_array($field, ['id', 'created_at', 'type', 'published']);
154
    }
155
156
    /**
157
     * Check if the field exists on the entity
158
     *
159
     * @param string $field
160
     * @return bool
161
     */
162 42
    public function hasField($field)
163
    {
164 42
        return array_key_exists($field, $this->data);
165
    }
166
167
    /**
168
     * @param string $field
169
     * @return FieldCollection
170
     */
171 36
    public function getField($field)
172
    {
173 36
        return $this->data[$field];
174
    }
175
176
    /**
177
     * Check if the field is related to the revision
178
     *
179
     * @param string $field
180
     * @return bool
181
     */
182 51
    protected function isRevisionField($field)
183
    {
184 51
        return in_array($field, ['language_id', 'updated_at', 'published']);
185
    }
186
187
    /**
188
     * Dynamically retrieve attributes on the model.
189
     *
190
     * @param string $key
191
     * @throws NonExistentFieldException
192
     * @return $this|bool|\Carbon\Carbon|\DateTime|mixed|static
193
     */
194 42
    public function __get($key)
195
    {
196 42
        if ($this->isContentField($key)) {
197 18
            return $this->content->getAttribute($key);
198
        }
199
200 39
        if ($this->isRevisionField($key)) {
201 6
            return $this->revision->getAttribute($key);
202
        }
203
204 36
        if ($this->hasField($key)) {
205 33
            return $this->getField($key);
206
        }
207
208 10
        if ($key == 'revisions') {
209 7
            return $this->content->revisions;
0 ignored issues
show
Documentation introduced by
The property revisions does not exist on object<Rocket\Entities\Content>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
210
        }
211
212 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
213
    }
214
215
    /**
216
     * Dynamically set attributes on the model.
217
     *
218
     * @param string $key
219
     * @param mixed $value
220
     * @throws NonExistentFieldException
221
     */
222 48
    public function __set($key, $value)
223
    {
224 48
        if ($this->isContentField($key)) {
225 48
            $this->content->setAttribute($key, $value);
226
227 48
            return;
228
        }
229
230 48
        if ($this->isRevisionField($key)) {
231 48
            $this->revision->setAttribute($key, $value);
232
233 48
            return;
234
        }
235
236 21
        if ($this->hasField($key)) {
237 18
            $this->setOnField($this->getField($key), $value);
238
239 18
            return;
240
        }
241
242 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
243
    }
244
245
    /**
246
     * Set values on a field
247
     *
248
     * @param FieldCollection $field
249
     * @param $value
250
     */
251 18
    protected function setOnField(FieldCollection $field, $value)
252
    {
253 18
        if (!is_array($value)) {
254 6
            $field->offsetSet(0, $value);
255
256 6
            return;
257
        }
258
259 12
        $field->clear();
260
261
        // This happens when the array is
262
        // replaced completely by another array
263 12
        if (count($value)) {
264 3
            foreach ($value as $k => $v) {
265 3
                $field->offsetSet($k, $v);
266 3
            }
267 3
        }
268 12
    }
269
270
    /**
271
     * Find the latest valid revision for this entity
272
     *
273
     * @param int $id
274
     * @param int $language_id
275
     * @return static
276
     */
277 12
    public static function find($id, $language_id)
278
    {
279 12
        $instance = new static($language_id);
280
281 12
        $instance->content = Content::findOrFail($id);
282
283 12
        $instance->revision = Revision::where('content_id', $id)
284 12
            ->where('language_id', $language_id)
285 12
            ->where('published', true)
286 12
            ->firstOrFail();
287
288 12
        (new Collection($instance->getFields()))
289
            ->map(function ($options) {
290 12
                return $options['type'];
291 12
            })
292 12
            ->values()
293 12
            ->unique()
294
            ->map(function ($type) {
295 12
                return self::$types[$type];
296 12
            })
297
            ->each(function ($type) use ($instance) {
298 12
                $type::where('revision_id', $instance->revision->id)
299 12
                    ->get()
300
                    ->each(function (Field $value) use ($instance) {
301 12
                        $instance->data[$value->name][$value->weight] = $value;
302 12
                    });
303 12
            });
304
305 12
        return $instance;
306
    }
307
308
    /**
309
     * Save a revision
310
     *
311
     * @param bool $newRevision Should we create a new revision, false by default
312
     * @param bool $publishRevision Should we immediately publish this revision, true by default
313
     */
314 15
    public function save($newRevision = false, $publishRevision = true)
315
    {
316 15
        if ($newRevision) {
317 6
            $revision = new Revision;
318 6
            $revision->language_id = $this->revision->language_id;
319
320 6
            $this->revision = $revision;
321 6
        }
322
323 15
        DB::transaction(
324
            function () use ($newRevision, $publishRevision) {
325 15
                $this->saveContent();
326
327 15
                $this->saveRevision($publishRevision);
328
329
                // Prepare and save fields
330 15
                foreach (array_keys($this->data) as $fieldName) {
331
                    /** @var FieldCollection $field */
332 15
                    $field = $this->data[$fieldName];
333
334 15
                    if (!$newRevision) {
335
                        $field->deleted()->each(function (Field $value) {
336 3
                            $value->delete();
337 15
                        });
338 15
                    }
339
340 15
                    $field->each(function (Field $value, $key) use ($newRevision, $fieldName) {
341 15
                        $value->weight = $key;
342 15
                        $value->name = $fieldName;
343 15
                        $this->saveField($value, $newRevision);
344 15
                    });
345
346 15
                    $field->syncOriginal();
347 15
                }
348 15
            }
349 15
        );
350 15
    }
351
352
    /**
353
     * Save the content
354
     */
355 15
    protected function saveContent()
356
    {
357 15
        $this->content->save();
358 15
    }
359
360
    /**
361
     * Save the revision
362
     *
363
     * @param bool $publishRevision Should we immediately publish this revision, true by default
364
     */
365 15
    protected function saveRevision($publishRevision)
366
    {
367 15
        if (!$this->revision->exists && !$publishRevision) {
368 3
            $this->revision->published = $publishRevision;
369 3
        }
370
371 15
        $this->revision->content_id = $this->content->id;
372 15
        $this->revision->save();
373
374 15
        if ($publishRevision) {
375 15
            $this->unpublishOtherRevisions();
376 15
        }
377 15
    }
378
379
    /**
380
     * Unpublish the revisions other than this one.
381
     * Only for the same content_id and language_id
382
     */
383 15
    protected function unpublishOtherRevisions()
384
    {
385 15
        if ($this->content->wasRecentlyCreated) {
386 15
            return;
387
        }
388
389
        // Unpublish all other revisions
390 6
        Revision::where('content_id', $this->content->id)
391 6
            ->where('language_id', $this->revision->language_id)
392 6
            ->where('id', '!=', $this->revision->id)
393 6
            ->update(['published' => false]);
394 6
    }
395
396
    /**
397
     * Save a single field instance
398
     *
399
     * @param Field $field The field instance to save
400
     * @param bool $newRevision Should we create a new revision?
401
     */
402 15
    protected function saveField(Field $field, $newRevision)
403
    {
404
        // If we create a new revision, this will
405
        // reinit the field to a non-saved field
406
        // and create a new row in the database
407 15
        if ($newRevision) {
408 6
            $field->id = null;
409 6
            $field->exists = false;
410 6
        }
411
412 15
        $field->revision_id = $this->revision->id;
413
414 15
        $field->save();
415 15
    }
416
417
    /**
418
     * Convert the Entity to an array.
419
     *
420
     * @return array
421
     */
422 16
    public function toArray()
423
    {
424
        $content = [
425 16
            '_content' => $this->content->toArray(),
426 16
            '_revision' => $this->revision->toArray(),
427 16
        ];
428
429 16
        foreach ($this->data as $field => $data) {
430 16
            $content[$field] = $data->toArray();
431 16
        }
432
433 16
        return $content;
434
    }
435
}
436