Completed
Push — master ( fb652a...5a44ae )
by Stéphane
13:32
created

src/Entities/Entity.php (1 issue)

Check for PhpDoc comments which do parse

Documentation Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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