Model   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 734
Duplicated Lines 0 %

Test Coverage

Coverage 98.89%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 69
eloc 152
c 6
b 0
f 0
dl 0
loc 734
ccs 178
cts 180
cp 0.9889
rs 2.88

45 Methods

Rating   Name   Duplication   Size   Complexity  
A getKey() 0 3 1
A belongsTo() 0 8 1
A __isset() 0 3 1
A __toString() 0 3 1
A getOrderByParameter() 0 3 1
A getIncrementing() 0 3 1
A getKeyName() 0 3 1
A childOf() 0 9 1
A __get() 0 3 1
A __unset() 0 3 1
A getDefaultWheres() 0 5 1
A delete() 0 16 3
A __construct() 0 11 1
A __set() 0 7 2
A getOrderByDirectionParameter() 0 3 1
A assumeForeignKey() 0 3 1
A fill() 0 7 2
A asDateTime() 0 7 3
A getKeyType() 0 3 1
A convertBoolToString() 0 6 2
A getResponseKey() 0 3 1
A getResponseCollectionKey() 0 3 1
A keyMap() 0 6 1
A isNested() 0 3 1
A givenOne() 0 4 2
A getRelationshipFromMethod() 0 18 3
A givenMany() 0 8 2
B getPath() 0 26 8
A hasMany() 0 7 1
A jsonSerialize() 0 3 1
A offsetSet() 0 7 2
A preventsAccessingMissingAttributes() 0 3 1
A setReadonly() 0 5 1
A save() 0 27 4
A relationLoaded() 0 3 1
A toJson() 0 11 2
A offsetGet() 0 3 1
A newFromBuilder() 0 10 1
A newInstance() 0 7 1
A relationResolver() 0 3 1
A offsetExists() 0 3 1
A saveOrFail() 0 7 2
A setRelation() 0 5 1
A toArray() 0 3 1
A offsetUnset() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spinen\Halo\Support;
4
5
use ArrayAccess;
6
use GuzzleHttp\Exception\GuzzleException;
7
use Illuminate\Contracts\Support\Arrayable;
8
use Illuminate\Contracts\Support\Jsonable;
9
use Illuminate\Database\Eloquent\Concerns\HasAttributes;
10
use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
11
use Illuminate\Database\Eloquent\Concerns\HidesAttributes;
12
use Illuminate\Database\Eloquent\JsonEncodingException;
13
use Illuminate\Support\Carbon;
14
use Illuminate\Support\Facades\Date;
15
use Illuminate\Support\Str;
16
use Illuminate\Support\Traits\Conditionable;
17
use JsonSerializable;
18
use LogicException;
19
use Spinen\Halo\Concerns\HasClient;
20
use Spinen\Halo\Exceptions\InvalidRelationshipException;
21
use Spinen\Halo\Exceptions\ModelNotFoundException;
22
use Spinen\Halo\Exceptions\ModelReadonlyException;
23
use Spinen\Halo\Exceptions\NoClientException;
24
use Spinen\Halo\Exceptions\TokenException;
25
use Spinen\Halo\Exceptions\UnableToSaveException;
26
use Spinen\Halo\Support\Relations\BelongsTo;
27
use Spinen\Halo\Support\Relations\ChildOf;
28
use Spinen\Halo\Support\Relations\HasMany;
29
use Spinen\Halo\Support\Relations\Relation;
30
31
/**
32
 * Class Model
33
 *
34
 * NOTE: Since we are trying to give a Laravel like feel when interacting
35
 * with the API, there are sections of this code that is very heavily
36
 * patterned/inspired directly from Laravel's Model class.
37
 */
38
abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable
39
{
40
    use Conditionable;
41
    use HasAttributes {
0 ignored issues
show
introduced by
The trait Illuminate\Database\Eloq...\Concerns\HasAttributes requires some properties which are not provided by Spinen\Halo\Support\Model: $preventsLazyLoading, $name, $map, $set, $withoutObjectCaching, $withCaching, $withObjectCaching, $get, $value
Loading history...
42
        asDateTime as originalAsDateTime;
43
    }
44
    use HasClient;
45
    use HasTimestamps;
46
    use HidesAttributes;
47
48
    /**
49
     * Default wheres to send.  They are overwrote by any matching where calls.
50
     */
51
    public array $defaultWheres = [];
52
53
    /**
54
     * Indicates if the model exists.
55
     */
56
    public bool $exists = false;
57
58
    /**
59
     * Indicates if the IDs are auto-incrementing.
60
     */
61
    public bool $incrementing = false;
62
63
    /**
64
     * The "type" of the primary key ID.
65
     */
66
    protected string $keyType = 'int';
67
68
    /**
69
     * Is resource nested behind parentModel
70
     *
71
     * Several of the endpoints are nested behind another model for relationship, but then to
72
     * interact with the specific model, then are not nested.  This property will know when to
73
     * keep the specific model nested.
74
     */
75
    protected bool $nested = false;
76
77
    /**
78
     * Parameter for order by direction
79
     *
80
     * Default is "$orderByParameter . 'desc'"
81
     */
82
    protected ?string $orderByDirectionParameter = null;
83
84
    /**
85
     * Parameter for order by column
86
     */
87
    protected string $orderByParameter = 'order';
88
89
    /**
90
     * Optional parentModel instance
91
     */
92
    public ?Model $parentModel;
93
94
    /**
95
     * Path to API endpoint.
96
     */
97
    protected string $path;
98
99
    /**
100
     * The primary key for the model.
101
     */
102
    protected string $primaryKey = 'id';
103
104
    /**
105
     * Is the model readonly?
106
     */
107
    protected bool $readonlyModel = false;
108
109
    /**
110
     * The loaded relationships for the model.
111
     */
112
    protected array $relations = [];
113
114
    /**
115
     * Some of the responses have the collections under a property
116
     */
117
    protected ?string $responseCollectionKey = null;
118
119
    /**
120
     * Some of the responses have the data under a property
121
     */
122
    protected ?string $responseKey = null;
123
124
    /**
125
     * Are timestamps in milliseconds?
126
     */
127
    protected bool $timestampsInMilliseconds = true;
128
129
    /**
130
     * Indicates if the model was inserted during the current request lifecycle.
131
     *
132
     * @var bool
133
     */
134
    public $wasRecentlyCreated = false;
135
136
    /**
137
     * The name of the "created at" column.
138
     *
139
     * @var string
140
     */
141
    const CREATED_AT = null;
142
143
    /**
144
     * The name of the "updated at" column.
145
     *
146
     * @var string
147
     */
148
    const UPDATED_AT = null;
149
150
    /**
151
     * Model constructor.
152
     */
153 108
    public function __construct(?array $attributes = [], Model $parentModel = null)
154
    {
155
        // All dates from API comes as epoch with milliseconds
156 108
        $this->dateFormat = 'Uv';
157
        // None of these models will use timestamps, but need the date casting
158 108
        $this->timestamps = false;
159
160 108
        $this->syncOriginal();
161
162 108
        $this->fill($attributes);
163 108
        $this->parentModel = $parentModel;
164
    }
165
166
    /**
167
     * Dynamically retrieve attributes on the model.
168
     */
169 25
    public function __get(string $key)
170
    {
171 25
        return $this->getAttribute($this->keyMap($key));
172
    }
173
174
    /**
175
     * Determine if an attribute or relation exists on the model.
176
     */
177 2
    public function __isset(string $key): bool
178
    {
179 2
        return $this->offsetExists($this->keyMap($key));
180
    }
181
182
    /**
183
     * Dynamically set attributes on the model.
184
     *
185
     * @param  string  $key
186
     * @return void
187
     *
188
     * @throws ModelReadonlyException
189
     */
190 13
    public function __set($key, $value)
191
    {
192 13
        if ($this->readonlyModel) {
193 1
            throw new ModelReadonlyException();
194
        }
195
196 12
        $this->setAttribute($this->keyMap($key), $value);
197
    }
198
199
    /**
200
     * Convert the model to its string representation.
201
     *
202
     * @return string
203
     */
204 1
    public function __toString()
205
    {
206 1
        return $this->toJson();
207
    }
208
209
    /**
210
     * Unset an attribute on the model.
211
     *
212
     * @param  string  $key
213
     * @return void
214
     */
215 2
    public function __unset($key)
216
    {
217 2
        $this->offsetUnset($this->keyMap($key));
218
    }
219
220
    /**
221
     * Return a timestamp as DateTime object.
222
     *
223
     * @return Carbon
224
     */
225 8
    protected function asDateTime($value)
226
    {
227 8
        if (is_numeric($value) && $this->timestampsInMilliseconds) {
228 1
            return Date::createFromTimestampMs($value);
229
        }
230
231 7
        return $this->originalAsDateTime($value);
232
    }
233
234
    /**
235
     * Assume foreign key
236
     *
237
     * @param  string  $related
238
     */
239 2
    protected function assumeForeignKey($related): string
240
    {
241 2
        return Str::snake((new $related())->getResponseKey()).'_id';
242
    }
243
244
    /**
245
     * Relationship that makes the model belongs to another model
246
     *
247
     * @param  string  $related
248
     * @param  string|null  $foreignKey
249
     *
250
     * @throws InvalidRelationshipException
251
     * @throws ModelNotFoundException
252
     * @throws NoClientException
253
     */
254 2
    public function belongsTo($related, $foreignKey = null): BelongsTo
255
    {
256 2
        $foreignKey = $foreignKey ?? $this->assumeForeignKey($related);
257
258 2
        $builder = (new Builder())->setClass($related)
259 2
                                  ->setClient($this->getClient());
260
261 2
        return new BelongsTo($builder, $this, $foreignKey);
262
    }
263
264
    /**
265
     * Relationship that makes the model child to another model
266
     *
267
     * @param  string  $related
268
     * @param  string|null  $foreignKey
269
     *
270
     * @throws InvalidRelationshipException
271
     * @throws ModelNotFoundException
272
     * @throws NoClientException
273
     */
274 2
    public function childOf($related, $foreignKey = null): ChildOf
275
    {
276 2
        $foreignKey = $foreignKey ?? $this->assumeForeignKey($related);
277
278 2
        $builder = (new Builder())->setClass($related)
279 2
                                  ->setClient($this->getClient())
280 2
                                  ->setParent($this);
281
282 2
        return new ChildOf($builder, $this, $foreignKey);
283
    }
284
285
    /**
286
     * Convert boolean to a string as their API expects "true"/"false
287
     */
288 18
    protected function convertBoolToString(mixed $value): mixed
289
    {
290 18
        return match (true) {
291 18
            is_array($value) => array_map([$this, 'convertBoolToString'], $value),
292 18
            is_bool($value) => $value ? 'true' : 'false',
293 18
            default => $value,
294 18
        };
295
    }
296
297
    /**
298
     * Delete the model from Halo
299
     *
300
     * @throws NoClientException
301
     * @throws TokenException
302
     */
303 3
    public function delete(): bool
304
    {
305
        // TODO: Make sure that the model supports being deleted
306 3
        if ($this->readonlyModel) {
307 1
            return false;
308
        }
309
310
        try {
311 2
            $this->getClient()
312 2
                 ->delete($this->getPath($this->getKey()));
313
314 1
            return true;
315 1
        } catch (GuzzleException $e) {
316
            // TODO: Do something with the error
317
318 1
            return false;
319
        }
320
    }
321
322
    /**
323
     * Fill the model with the supplied properties
324
     */
325 108
    public function fill(?array $attributes = []): self
326
    {
327 108
        foreach ((array) $attributes as $attribute => $value) {
328 59
            $this->setAttribute($attribute, $value);
329
        }
330
331 108
        return $this;
332
    }
333
334
    /**
335
     * Merge any where in the defaultWheres property with any passed in.
336
     */
337 57
    public function getDefaultWheres(array $query = []): array
338
    {
339 57
        return [
340 57
            ...$this->defaultWheres,
341 57
            ...$query,
342 57
        ];
343
    }
344
345
    /**
346
     * Get the value indicating whether the IDs are incrementing.
347
     */
348 105
    public function getIncrementing(): bool
349
    {
350 105
        return $this->incrementing;
351
    }
352
353
    /**
354
     * Get the value of the model's primary key.
355
     */
356 58
    public function getKey()
357
    {
358 58
        return $this->getAttribute($this->getKeyName());
359
    }
360
361
    /**
362
     * Get the primary key for the model.
363
     */
364 63
    public function getKeyName(): string
365
    {
366 63
        return $this->primaryKey;
367
    }
368
369
    /**
370
     * Get the auto-incrementing key type.
371
     */
372 1
    public function getKeyType(): string
373
    {
374 1
        return $this->keyType;
375
    }
376
377
    /**
378
     * Get the parameter the endpoint uses to sort.
379
     */
380 2
    public function getOrderByDirectionParameter(): string
381
    {
382 2
        return $this->orderByDirectionParameter ?? $this->getOrderByParameter().'desc';
383
    }
384
385
    /**
386
     * Get the parameter the endpoint uses to sort.
387
     */
388 2
    public function getOrderByParameter(): string
389
    {
390 2
        return $this->orderByParameter;
391
    }
392
393
    /**
394
     * Build API path
395
     *
396
     * Put anything on the end of the URI that is passed in
397
     *
398
     * @param  string|null  $extra
399
     * @param  array|null  $query
400
     * @return string
401
     */
402 57
    public function getPath($extra = null, array $query = []): ?string
403
    {
404
        // Start with path to resource without "/" on end
405 57
        $path = rtrim($this->path, '/');
406
407
        // If have an id, then put it on the end
408 57
        // NOTE: Halo treats creates & updates the same, so only on existing
409 5
        if ($this->exist && $this->getKey()) {
0 ignored issues
show
Bug Best Practice introduced by
The property exist does not exist on Spinen\Halo\Support\Model. Since you implemented __get, consider adding a @property annotation.
Loading history...
410
            $path .= '/'.$this->getKey();
411
        }
412
413 57
        // Stick any extra things on the end
414 6
        if (! is_null($extra)) {
415
            $path .= '/'.ltrim($extra, '/');
416
        }
417 57
418 18
        if (! empty($query = $this->getDefaultWheres($query))) {
419
            $path .= '?'.http_build_query($this->convertBoolToString($query));
420
        }
421
422 57
        // If there is a parentModel & not have an id (unless for nested), then prepend parentModel
423 4
        if (! is_null($this->parentModel) && (! $this->getKey() || $this->isNested())) {
424
            return $this->parentModel->getPath($path);
425
        }
426 57
427
        return $path;
428
    }
429
430
    /**
431
     * Get a relationship value from a method.
432
     *
433
     * @param  string  $method
434
     *
435
     * @throws LogicException
436 5
     */
437
    public function getRelationshipFromMethod($method)
438 5
    {
439
        $relation = $this->{$method}();
440 5
441 2
        if (! $relation instanceof Relation) {
442 1
            $exception_message = is_null($relation)
443 1
                ? '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?'
444
                : '%s::%s must return a relationship instance.';
445 2
446 2
            throw new LogicException(
447 2
                sprintf($exception_message, static::class, $method)
448
            );
449
        }
450 3
451 3
        return tap(
452 3
            $relation->getResults(),
453 3
            function ($results) use ($method) {
454 3
                $this->setRelation($method, $results);
455 3
            }
456
        );
457
    }
458
459
    /**
460
     * Name of the wrapping key when response is a collection
461
     *
462
     * If none provided, assume plural version responseKey
463 32
     */
464
    public function getResponseCollectionKey(): ?string
465 32
    {
466
        return $this->responseCollectionKey ?? Str::plural($this->getResponseKey());
467
    }
468
469
    /**
470
     * Name of the wrapping key of response
471
     *
472
     * If none provided, assume camelCase of class name
473 36
     */
474
    public function getResponseKey(): ?string
475 36
    {
476
        return $this->responseKey ?? Str::camel(class_basename(static::class));
477
    }
478
479
    /**
480
     * Many of the results include collection of related data, so cast it
481
     *
482
     * @param  string  $related
483
     * @param  array  $given
484
     * @param  bool  $reset Some of the values are nested under a property, so peel it off
485
     *
486
     * @throws NoClientException
487 1
     */
488
    public function givenMany($related, $given, $reset = false): Collection
489
    {
490 1
        /** @var Model $model */
491
        $model = (new $related([], $this->parentModel))->setClient($this->getClient());
492 1
493 1
        return (new Collection($given))->map(
0 ignored issues
show
Bug introduced by
$given of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Spinen\Halo\Support\Collection::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

493
        return (new Collection(/** @scrutinizer ignore-type */ $given))->map(
Loading history...
494 1
            function ($attributes) use ($model, $reset) {
495 1
                return $model->newFromBuilder($reset ? reset($attributes) : $attributes);
496 1
            }
497
        );
498
    }
499
500
    /**
501
     * Many of the results include related data, so cast it to object
502
     *
503
     * @param  string  $related
504
     * @param  array  $attributes
505
     * @param  bool  $reset Some of the values are nested under a property, so peel it off
506
     *
507
     * @throws NoClientException
508 1
     */
509
    public function givenOne($related, $attributes, $reset = false): Model
510 1
    {
511 1
        return (new $related([], $this->parentModel))->setClient($this->getClient())
512
                                                     ->newFromBuilder($reset ? reset($attributes) : $attributes);
513
    }
514
515
    /**
516
     * Relationship that makes the model have a collection of another model
517
     *
518
     * @param  string  $related
519
     *
520
     * @throws InvalidRelationshipException
521
     * @throws ModelNotFoundException
522
     * @throws NoClientException
523 4
     */
524
    public function hasMany($related): HasMany
525 4
    {
526 4
        $builder = (new Builder())->setClass($related)
527 4
                                  ->setClient($this->getClient())
528
                                  ->setParent($this);
529 4
530
        return new HasMany($builder, $this);
531
    }
532
533
    /**
534
     * Is endpoint nested behind another endpoint
535 2
     */
536
    public function isNested(): bool
537 2
    {
538
        return $this->nested ?? false;
539
    }
540
541
    /**
542
     * Convert the object into something JSON serializable.
543 1
     */
544
    public function jsonSerialize(): array
545 1
    {
546
        return $this->toArray();
547
    }
548
549
    /**
550
     * Map keys to names that are more standard to our use
551 32
     */
552
    protected function keyMap(string $key): string
553
    {
554 32
        // TODO: Is this a good idea?
555 32
        return match ($key) {
556 32
            'color' => 'colour',
557 32
            default => $key,
558
        };
559
    }
560
561
    /**
562
     * Create a new model instance that is existing.
563
     *
564
     * @param  array  $attributes
565
     * @return static
566 8
     */
567
    public function newFromBuilder($attributes = []): self
568 8
    {
569
        $model = $this->newInstance([], true);
570 8
571
        // TODO: Should we add a way to transform values?
572 8
        //       (i.e. SoftwareLicence gives 1899-12-30 00:00:00 for null date)
573
574
        $model->setRawAttributes((array) $attributes, true);
575
576
        return $model;
577
    }
578
579
    /**
580
     * Create a new instance of the given model.
581
     *
582
     * Provides a convenient way for us to generate fresh model instances of this current model.
583
     * It is particularly useful during the hydration of new objects via the builder.
584 13
     *
585
     * @param  bool  $exists
586 13
     * @return static
587
     */
588 13
    public function newInstance(array $attributes = [], $exists = false): self
589
    {
590 13
        $model = (new static($attributes, $this->parentModel))->setClient($this->client);
591
592
        $model->exists = $exists;
593
594
        return $model;
595
    }
596 5
597
    /**
598 5
     * Determine if the given attribute exists.
599
     */
600
    public function offsetExists($offset): bool
601
    {
602
        return ! is_null($this->getAttribute($offset));
603
    }
604 2
605
    /**
606 2
     * Get the value for a given offset.
607
     */
608
    public function offsetGet($offset): mixed
609
    {
610
        return $this->getAttribute($offset);
611
    }
612
613
    /**
614
     * Set the value for a given offset.
615 2
     *
616
     *
617 2
     * @throws ModelReadonlyException
618 1
     */
619
    public function offsetSet($offset, $value): void
620
    {
621 1
        if ($this->readonlyModel) {
622
            throw new ModelReadonlyException();
623
        }
624
625
        $this->setAttribute($offset, $value);
626
    }
627 3
628
    /**
629 3
     * Unset the value for a given offset.
630
     */
631
    public function offsetUnset($offset): void
632
    {
633
        unset($this->attributes[$offset], $this->relations[$offset]);
634
    }
635
636
    /**
637
     * Laravel allows control of accessing missing attributes, so we just return false
638
     *
639
     * @return bool
640
     */
641
    public static function preventsAccessingMissingAttributes()
642
    {
643
        return false;
644
    }
645
646
    /**
647 66
     * Determine if the given relation is loaded.
648
     *
649 66
     * @param  string  $key
650
     */
651
    public function relationLoaded($key): bool
652
    {
653
        return array_key_exists($key, $this->relations);
654
    }
655
656
    /**
657
     * Laravel allows the resolver to be set at runtime, so we just return null
658
     *
659 66
     * @param  string  $class
660
     * @param  string  $key
661 66
     * @return null
662
     */
663
    public function relationResolver($class, $key)
664
    {
665
        return null;
666
    }
667
668
    /**
669
     * Save the model in Halo
670 7
     *
671
     * @throws NoClientException
672
     * @throws TokenException
673 7
     */
674 1
    public function save(): bool
675
    {
676
        // TODO: Make sure that the model supports being saved
677
        if ($this->readonlyModel) {
678 6
            return false;
679 1
        }
680
681
        try {
682 5
            if (! $this->isDirty()) {
683
                return true;
684 1
            }
685 1
686
            $response = $this->getClient()
687
                             ->post($this->getPath(), [$this->toArray()]);
688 1
689
            $this->exists = true;
690
691 1
            $this->wasRecentlyCreated = true;
692
693 1
            // Reset the model with the results as we get back the full model
694
            $this->setRawAttributes($response, true);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $attributes of Spinen\Halo\Support\Model::setRawAttributes() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

694
            $this->setRawAttributes(/** @scrutinizer ignore-type */ $response, true);
Loading history...
695
696 5
            return true;
697 5
        } catch (GuzzleException $e) {
698
            // TODO: Do something with the error
699 3
700
            return false;
701 3
        }
702
    }
703
704 3
    /**
705
     * Save the model in Halo, but raise error if fail
706 3
     *
707 2
     * @throws NoClientException
708
     * @throws TokenException
709
     * @throws UnableToSaveException
710 2
     */
711
    public function saveOrFail(): bool
712
    {
713
        if (! $this->save()) {
714
            throw new UnableToSaveException();
715
        }
716
717
        return true;
718
    }
719
720
    /**
721 2
     * Set the readonly
722
     *
723 2
     * @param  bool  $readonly
724 1
     * @return $this
725
     */
726
    public function setReadonly($readonly = true): self
727 1
    {
728
        $this->readonlyModel = $readonly;
729
730
        return $this;
731
    }
732
733
    /**
734
     * Set the given relationship on the model.
735
     *
736 4
     * @param  string  $relation
737
     * @return $this
738 4
     */
739
    public function setRelation($relation, $value): self
740 4
    {
741
        $this->relations[$relation] = $value;
742
743
        return $this;
744
    }
745
746
    /**
747
     * Convert the model instance to an array.
748
     */
749 3
    public function toArray(): array
750
    {
751 3
        return array_merge($this->attributesToArray(), $this->relationsToArray());
752
    }
753 3
754
    /**
755
     * Convert the model instance to JSON.
756
     *
757
     * @param  int  $options
758
     *
759 12
     * @throws JsonEncodingException
760
     */
761 12
    public function toJson($options = 0): string
762
    {
763
        $json = json_encode($this->jsonSerialize(), $options);
764
765
        // @codeCoverageIgnoreStart
766
        if (JSON_ERROR_NONE !== json_last_error()) {
767
            throw JsonEncodingException::forModel($this, json_last_error_msg());
768
        }
769
        // @codeCoverageIgnoreEnd
770
771 1
        return $json;
772
    }
773
}
774