Completed
Push — master ( 5a44ae...0c75f6 )
by Stéphane
14:22
created

Entity::deleteRevision()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

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