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