Completed
Push — master ( 0c75f6...35d3bb )
by Stéphane
14:38
created

Entity::setOnField()   B

↳ Parent: Entity

Complexity

Conditions 5
Paths 5

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 22
Code Lines 10

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 5
eloc 10
c 3
b 0
f 1
nc 5
nop 2
dl 0
loc 22
ccs 13
cts 13
cp 1
crap 5
rs 8.6737
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\MultipleFieldAssignmentException;
10
use Rocket\Entities\Exceptions\NonExistentFieldException;
11
use Rocket\Entities\Exceptions\NoPublishedRevisionForLanguageException;
12
use Rocket\Entities\Exceptions\NoRevisionForLanguageException;
13
use Rocket\Entities\Exceptions\ReservedFieldNameException;
14
use Rocket\Entities\Exceptions\RevisionEntityMismatchException;
15
use Rocket\Entities\Exceptions\RevisionNotFoundException;
16
17
/**
18
 * Entity manager
19
 *
20
 * @property int $id The content ID
21
 * @property int $language_id The language in which this entity is
22
 * @property int $revision_id The current revision id
23
 * @property string $type The type of the Entity
24
 * @property bool $published Is this content published
25
 * @property-read bool $publishedRevision Is the revision published
26
 * @property-read \Rocket\Entities\Revision[] $revisions all revisions in this content
27
 * @property-read \DateTime $created_at
28
 * @property-read \DateTime $updated_at
29
 */
30
abstract class Entity
31
{
32
    /**
33
     * @var int This key identifies where the only item is stored in single item fields.
34
     */
35
    public static $SINGLE_ITEM_KEY = 0;
36
37
    /**
38
     * @var array<class> The list of field types, filled by the ServiceProvider from a configuration file.
39
     */
40
    public static $types;
41
42
    /**
43
     * @var Content The content represented by this entity
44
     */
45
    protected $content; //id, created_at, type, published
46
47
    /**
48
     * @var Revision The revision represented by this entity
49
     */
50
    protected $revision; //language_id, updated_at, type, published
51
52
    /**
53
     * @var FieldCollection[] The fields in this entity
54
     */
55
    protected $data;
56
57
    /**
58
     * Entity constructor.
59
     *
60
     * @param int $language_id The language this specific entity is in
61
     */
62 111
    public function __construct($language_id)
63
    {
64 111
        if (!is_numeric($language_id) || $language_id == 0) {
65 3
            throw new InvalidArgumentException("You must set a valid 'language_id'.");
66
        }
67
68 108
        $fields = $this->getFields();
69
70 108
        $this->initialize($fields);
71
72 102
        $this->type = $this->getContentType();
73 102
        $this->language_id = $language_id;
74 102
    }
75
76
    /**
77
     * Creates the Content, Revision and FieldCollections
78
     *
79
     * @param array $fields The fields and their configurations
80
     * @throws InvalidFieldTypeException
81
     * @throws ReservedFieldNameException
82
     */
83 108
    protected function initialize(array $fields)
84
    {
85 108
        $this->content = new Content;
86 108
        $this->revision = new Revision;
87
88 108
        foreach ($fields as $field => $settings) {
89 108
            $this->data[$field] = $this->initializeField($field, $settings);
90 102
        }
91 102
    }
92
93
    /**
94
     * Validate configuration and prepare a FieldCollection
95
     *
96
     * @param string $field
97
     * @param array $settings
98
     * @throws InvalidFieldTypeException
99
     * @throws ReservedFieldNameException
100
     * @return FieldCollection
101
     */
102 108
    protected function initializeField($field, $settings)
103
    {
104 108
        if ($this->isContentField($field) || $this->isRevisionField($field)) {
105 3
            throw new ReservedFieldNameException(
106 3
                "The field '$field' cannot be used in '" . get_class($this) . "' as it is a reserved name"
107 3
            );
108
        }
109
110 105
        $type = $settings['type'];
111
112 105
        if (!array_key_exists($type, self::$types)) {
113 3
            throw new InvalidFieldTypeException("Unkown type '$type' in '" . get_class($this) . "'");
114
        }
115
116 102
        $settings['type'] = self::$types[$settings['type']];
117
118 102
        return FieldCollection::initField($settings);
119
    }
120
121
    /**
122
     * Return the fields in this entity
123
     *
124
     * @return array
125
     */
126
    abstract public function getFields();
127
128
    /**
129
     * Get the database friendly content type
130
     *
131
     * @return string
132
     */
133 108
    public static function getContentType()
134
    {
135 108
        return str_replace('\\', '', snake_case((new \ReflectionClass(get_called_class()))->getShortName()));
136
    }
137
138
    /**
139
     * Create a new revision based on the same content ID but without the content.
140
     * Very useful if you want to add a new language
141
     *
142
     * @param int $language_id
143
     * @return static
144
     */
145 3
    public function newRevision($language_id = null)
146
    {
147 3
        $created = new static($language_id ?: $this->language_id);
148 3
        $created->content = $this->content;
149
150 3
        return $created;
151
    }
152
153
    /**
154
     * Check if the field is related to the content
155
     *
156
     * @param string $field
157
     * @return bool
158
     */
159 108
    protected function isContentField($field)
160
    {
161 108
        return in_array($field, ['id', 'created_at', 'type', 'published']);
162
    }
163
164
    /**
165
     * Check if the field exists on the entity
166
     *
167
     * @param string $field
168
     * @return bool
169
     */
170 90
    public function hasField($field)
171
    {
172 90
        return array_key_exists($field, $this->data);
173
    }
174
175
    /**
176
     * Get a field's FieldCollection.
177
     *
178
     * Be careful as this gives you the real field instances.
179
     *
180
     * @param string $field
181
     * @return FieldCollection
182
     */
183 84
    public function getField($field)
184
    {
185 84
        return $this->data[$field];
186
    }
187
188
    /**
189
     * Check if the field is related to the revision
190
     *
191
     * @param string $field
192
     * @return bool
193
     */
194 105
    protected function isRevisionField($field)
195
    {
196 105
        return in_array($field, ['language_id', 'updated_at', 'published']);
197
    }
198
199
    /**
200
     * Dynamically retrieve attributes on the model.
201
     *
202
     * @param string $key
203
     * @throws NonExistentFieldException
204
     * @return $this|bool|\Carbon\Carbon|\DateTime|mixed|static
205
     */
206 90
    public function __get($key)
207
    {
208 90
        if ($this->isContentField($key)) {
209 60
            return $this->content->getAttribute($key);
210
        }
211
212 87
        if ($this->isRevisionField($key)) {
213 9
            return $this->revision->getAttribute($key);
214
        }
215
216 81
        if ($this->hasField($key)) {
217 78
            $field = $this->getField($key);
218
219 78
            if ($field->getMaxItems() != 1) {
220 75
                return $field;
221
            }
222
223 9
            if ($field->has(static::$SINGLE_ITEM_KEY)) {
224 9
                return $field->get(static::$SINGLE_ITEM_KEY)->value;
225
            }
226
227 3
            return null;
228
        }
229
230 25
        if ($key == 'revision_id') {
231 16
            return $this->revision->id;
232
        }
233
234 20
        if ($key == 'revisions') {
235 9
            return $this->content->revisions;
236
        }
237
238 11
        if ($key == 'publishedRevision') {
239 8
            return $this->revision->published;
240
        }
241
242 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
243
    }
244
245
    /**
246
     * Dynamically set attributes on the model.
247
     *
248
     * @param string $key
249
     * @param mixed $value
250
     * @throws NonExistentFieldException
251
     */
252 102
    public function __set($key, $value)
253
    {
254 102
        if ($this->isContentField($key)) {
255 102
            $this->content->setAttribute($key, $value);
256
257 102
            return;
258
        }
259
260 102
        if ($this->isRevisionField($key)) {
261 102
            $this->revision->setAttribute($key, $value);
262
263 102
            return;
264
        }
265
266 30
        if ($this->hasField($key)) {
267 27
            $this->setOnField($this->getField($key), $value);
268
269 24
            return;
270
        }
271
272 3
        throw new NonExistentFieldException("Field '$key' doesn't exist in '" . get_class($this) . "'");
273
    }
274
275
    /**
276
     * Set values on a field
277
     *
278
     * @param FieldCollection $field
279
     * @param $value
280
     * @throws MultipleFieldAssignmentException
281
     */
282 27
    protected function setOnField(FieldCollection $field, $value)
283
    {
284 27
        if (!is_array($value)) {
285 15
            if ($field->getMaxItems() != 1) {
286 3
                throw new MultipleFieldAssignmentException("You cannot assign a value to replace a multiple field");
287
            }
288
289 12
            $field->offsetSet(static::$SINGLE_ITEM_KEY, $value);
290
291 12
            return;
292
        }
293
294 18
        $field->clear();
295
296
        // This happens when the array is
297
        // replaced completely by another array
298 18
        if (count($value)) {
299 9
            foreach ($value as $k => $v) {
300 9
                $field->offsetSet($k, $v);
301 9
            }
302 9
        }
303 18
    }
304
305
    /**
306
     * Get all field types in this Entity.
307
     *
308
     * @return Collection
309
     */
310 43
    protected function getFieldTypes()
311
    {
312 43
        return (new Collection($this->getFields()))
313
            ->map(function ($options) {
314 43
                return $options['type'];
315 43
            })
316 43
            ->values()
317 43
            ->unique()
318
            ->map(function ($type) {
319 43
                return self::$types[$type];
320 43
            });
321
    }
322
323
    /**
324
     * @param int $id The content ID
325
     * @param int $language_id The language ID
326
     * @param int $revision_id The revision ID which you want to load, this is optional
327
     * @throws NoPublishedRevisionForLanguageException
328
     * @throws NoRevisionForLanguageException
329
     * @throws RevisionEntityMismatchException
330
     * @throws RevisionNotFoundException
331
     * @return Revision
332
     */
333 50
    protected static function findRevision($id, $language_id, $revision_id = null)
334
    {
335
        try {
336 50
            if (is_numeric($revision_id) && $revision_id != 0) {
337 10
                $revision = Revision::findOrFail($revision_id);
338
339 7
                if ($revision->content_id != $id) {
340 3
                    throw new RevisionEntityMismatchException("This revision doesn't belong to this entity");
341
                }
342
343 4
                return $revision;
344
            }
345
346 44
            return Revision::where('content_id', $id)
347 44
                ->where('language_id', $language_id)
348 44
                ->where('published', true)
349 44
                ->firstOrFail();
350 17
        } catch (ModelNotFoundException $e) {
351 14
            if (is_numeric($revision_id) && $revision_id != 0) {
352 3
                throw new RevisionNotFoundException("This revision doesn't exist", 0, $e);
353
            }
354
355 11
            $count = Revision::where('content_id', $id)->where('language_id', $language_id)->count();
356
357 11
            if ($count) {
358 2
                $message = "There are revisions in language_id='$language_id' for Entity '$id' but none is published";
359 2
                throw new NoPublishedRevisionForLanguageException($message, 0, $e);
360
            } else {
361 9
                $message = "There no revisions in language_id='$language_id' for Entity '$id' but none is published";
362 9
                throw new NoRevisionForLanguageException($message, 0, $e);
363
            }
364
        }
365
    }
366
367
    /**
368
     * Find the latest valid revision for this entity
369
     *
370
     * @param int $id The content ID
371
     * @param int $language_id The language ID
372
     * @param int $revision_id The revision ID which you want to load, this is optional
373
     * @throws EntityNotFoundException
374
     * @throws NoPublishedRevisionForLanguageException
375
     * @throws NoRevisionForLanguageException
376
     * @throws RevisionEntityMismatchException
377
     * @throws RevisionNotFoundException
378
     * @return static
379
     */
380 57
    public static function find($id, $language_id, $revision_id = null)
381
    {
382 57
        $instance = new static($language_id);
383
384
        try {
385 57
            $instance->content = Content::findOrFail($id);
386 57
        } catch (ModelNotFoundException $e) {
387 7
            throw new EntityNotFoundException("The entity with id '$id' doesn't exist", 0, $e);
388
        }
389
390 50
        $instance->revision = static::findRevision($id, $language_id, $revision_id);
391
392 33
        $instance->getFieldTypes()
393
            ->each(function ($type) use ($instance) {
394 33
                $type::where('revision_id', $instance->revision->id)
395 33
                    ->get()
396
                    ->each(function (Field $value) use ($instance) {
397 33
                        $instance->data[$value->name][$value->weight] = $value;
398 33
                    });
399 33
            });
400
401 33
        return $instance;
402
    }
403
404
    /**
405
     * Save a revision
406
     *
407
     * @param bool $newRevision Should we create a new revision, false by default
408
     * @param bool $publishRevision Should we immediately publish this revision, true by default
409
     */
410 60
    public function save($newRevision = false, $publishRevision = true)
411
    {
412 60
        if ($newRevision) {
413 24
            $revision = new Revision;
414 24
            $revision->language_id = $this->revision->language_id;
415
416 24
            $this->revision = $revision;
417 24
        }
418
419 60
        DB::transaction(
420
            function () use ($newRevision, $publishRevision) {
421 60
                $this->saveContent();
422
423 60
                $this->saveRevision($publishRevision);
424
425
                // Prepare and save fields
426 60
                foreach (array_keys($this->data) as $fieldName) {
427
                    /** @var FieldCollection $field */
428 60
                    $field = $this->data[$fieldName];
429
430 60
                    if (!$newRevision) {
431
                        $field->deleted()->each(function (Field $value) {
432 3
                            $value->delete();
433 51
                        });
434 51
                    }
435
436
                    $field->each(function (Field $value, $key) use ($newRevision, $fieldName) {
437 60
                        $value->weight = $key;
438 60
                        $value->name = $fieldName;
439 60
                        $this->saveField($value, $newRevision);
440 60
                    });
441
442 60
                    $field->syncOriginal();
443 60
                }
444 57
            }
445 60
        );
446 57
    }
447
448
    /**
449
     * Save the content
450
     */
451 60
    protected function saveContent()
452
    {
453 60
        $this->content->save();
454 60
    }
455
456
    /**
457
     * Save the revision
458
     *
459
     * @param bool $publishRevision Should we immediately publish this revision, true by default
460
     */
461 60
    protected function saveRevision($publishRevision)
462
    {
463 60
        if (!$this->revision->exists && !$publishRevision) {
464 12
            $this->revision->published = $publishRevision;
465 12
        }
466
467 60
        $this->revision->content_id = $this->content->id;
468 60
        $this->revision->save();
469
470 60
        if ($publishRevision) {
471 51
            $this->unpublishOtherRevisions();
472 51
        }
473 60
    }
474
475
    /**
476
     * Unpublish the revisions other than this one.
477
     * Only for the same content_id and language_id
478
     */
479 51
    protected function unpublishOtherRevisions()
480
    {
481 51
        if ($this->content->wasRecentlyCreated) {
482 51
            return;
483
        }
484
485
        // Unpublish all other revisions
486 18
        Revision::where('content_id', $this->content->id)
487 18
            ->where('language_id', $this->revision->language_id)
488 18
            ->where('id', '!=', $this->revision->id)
489 18
            ->update(['published' => false]);
490 18
    }
491
492
    /**
493
     * Save a single field instance
494
     *
495
     * @param Field $field The field instance to save
496
     * @param bool $newRevision Should we create a new revision?
497
     */
498 60
    protected function saveField(Field $field, $newRevision)
499
    {
500
        // If we create a new revision, this will
501
        // reinit the field to a non-saved field
502
        // and create a new row in the database
503 60
        if ($newRevision) {
504 24
            $field->id = null;
505 24
            $field->exists = false;
506 24
        }
507
508 60
        $field->revision_id = $this->revision->id;
509
510 60
        $field->save();
511 57
    }
512
513
    /**
514
     * Convert the Entity to an array.
515
     *
516
     * @return array
517
     */
518 27
    public function toArray()
519
    {
520
        $content = [
521 27
            'id' => $this->content->id,
522 27
            '_content' => $this->content->toArray(),
523 27
            '_revision' => $this->revision->toArray(),
524 27
        ];
525
526 27
        foreach ($this->data as $field => $data) {
527 27
            $content[$field] = $data->toArray();
528 27
        }
529
530 27
        return $content;
531
    }
532
533 4
    public function delete($clear = true)
534
    {
535 4
        $revisions = Revision::where('content_id', $this->content->id)->get();
536
537 4
        $ids = $revisions->pluck('id');
538
539
        $this->getFieldTypes()->each(function ($type) use ($ids) {
540 4
            $type::whereIn('revision_id', $ids)->delete();
541 4
        });
542
543 4
        Revision::whereIn('id', $ids)->delete();
544 4
        $this->revision->exists = false;
545
546
        // TODO :: add an event system to be able to remove this content from entity fields
547
548 4
        $this->content->delete();
549
550 4
        if ($clear) {
551 2
            $this->revision->id = null;
552 2
            $this->content->id = null;
553 2
            $this->clearFields();
554 2
        }
555 4
    }
556
557
    public function deleteRevision($clear = true)
558
    {
559 8
        $this->getFieldTypes()->each(function ($type) {
560 8
            $type::where('revision_id', $this->revision->id)->delete();
561 8
        });
562
563
        // If this revision is currently
564
        // published, we need to publish
565
        // another revision in place.
566 8
        if ($this->revision->published && $this->revision->exists) {
567
            //TODO :: improve this logic
568 8
            Revision::where('content_id', $this->content->id)
569 8
                ->where('id', '!=', $this->revision->id)
570 8
                ->take(1)
571 8
                ->update(['published' => true]);
572 8
        }
573
574 8
        $this->revision->delete();
575
576 8
        if ($clear) {
577 5
            $this->clearFields();
578 5
        }
579 8
    }
580
581 7
    protected function clearFields()
582
    {
583
        // Void all the fields
584 7
        foreach (array_keys($this->data) as $fieldName) {
585
            /** @var FieldCollection $field */
586 7
            $field = $this->data[$fieldName];
587
588 7
            $field->clear();
589 7
            $field->syncOriginal();
590 7
        }
591 7
    }
592
593 2
    public function publishRevision()
594
    {
595 2
        $this->revision->published = true;
596 2
        $this->revision->save();
597
598 2
        $this->unpublishOtherRevisions();
599 2
    }
600
}
601