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