LineItem   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 605
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 174
dl 0
loc 605
rs 2
c 0
b 0
f 0
wmc 90

62 Methods

Rating   Name   Duplication   Size   Complexity  
A setReferencedId() 0 5 1
A setCover() 0 5 1
A getUniqueIdentifier() 0 3 1
A getChildren() 0 3 1
A getDataTimestamp() 0 3 1
A setGood() 0 5 1
A setPayload() 0 18 4
A setDataTimestamp() 0 3 1
A getRequirement() 0 3 1
A getDescription() 0 3 1
A isRemovable() 0 3 1
A markModified() 0 3 1
A setId() 0 5 1
A setQuantityInformation() 0 5 1
A createFromLineItem() 0 9 2
A getApiAlias() 0 3 1
A jsonSerialize() 0 15 4
A setLabel() 0 5 1
A getPrice() 0 3 1
A setPrice() 0 5 1
A getType() 0 3 1
A getQuantity() 0 3 1
A __construct() 0 13 2
A hasState() 0 3 1
A setRemovable() 0 5 1
A getDeliveryInformation() 0 3 1
A getStates() 0 3 1
A markModifiedByApp() 0 3 1
A getId() 0 3 1
A setDataContextHash() 0 3 1
A getCover() 0 3 1
A markUnModifiedByApp() 0 3 1
A setDeliveryInformation() 0 5 1
A getDataContextHash() 0 3 1
A getLabel() 0 3 1
A refreshChildQuantity() 0 16 5
A addChild() 0 6 1
A getQuantityInformation() 0 3 1
A validateChildQuantity() 0 15 4
A getPayload() 0 3 1
A isGood() 0 3 1
A getReferencedId() 0 3 1
A markUnmodified() 0 3 1
A isStackable() 0 3 1
A replacePayload() 0 10 2
A setChildren() 0 8 2
A setQuantity() 0 21 5
A hasPayloadValue() 0 3 1
A addPayloadProtection() 0 5 1
A setPayloadValue() 0 18 6
A setType() 0 5 1
A getPayloadValue() 0 7 2
A hasChildren() 0 3 1
A setPriceDefinition() 0 5 1
A getPriceDefinition() 0 3 1
A removePayloadValue() 0 7 2
A setDescription() 0 5 1
A setStackable() 0 5 1
A setRequirement() 0 5 1
A setStates() 0 5 1
A isModifiedByApp() 0 3 1
A isModified() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like LineItem 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 LineItem, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Checkout\Cart\LineItem;
4
5
use Shopware\Core\Checkout\Cart\CartException;
6
use Shopware\Core\Checkout\Cart\Delivery\Struct\DeliveryInformation;
7
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
8
use Shopware\Core\Checkout\Cart\Price\Struct\PriceDefinitionInterface;
9
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
10
use Shopware\Core\Content\Media\MediaEntity;
11
use Shopware\Core\Framework\Log\Package;
12
use Shopware\Core\Framework\Rule\Rule;
13
use Shopware\Core\Framework\Struct\Struct;
14
use Shopware\Core\Framework\Uuid\Uuid;
15
16
/**
17
 * @final LineItem class should not be extended because it is serialized into the storage and should not depend on individual implementations.
18
 */
19
#[Package('checkout')]
20
class LineItem extends Struct
21
{
22
    final public const CREDIT_LINE_ITEM_TYPE = 'credit';
23
    final public const PRODUCT_LINE_ITEM_TYPE = 'product';
24
    final public const CUSTOM_LINE_ITEM_TYPE = 'custom';
25
    final public const PROMOTION_LINE_ITEM_TYPE = 'promotion';
26
    final public const DISCOUNT_LINE_ITEM = 'discount';
27
    final public const CONTAINER_LINE_ITEM = 'container';
28
29
    /**
30
     * @var array<mixed>
31
     */
32
    protected array $payload = [];
33
34
    protected ?string $label = null;
35
36
    protected int $quantity;
37
38
    protected ?PriceDefinitionInterface $priceDefinition = null;
39
40
    protected ?CalculatedPrice $price = null;
41
42
    protected bool $good = true;
43
44
    protected ?string $description = null;
45
46
    protected ?MediaEntity $cover = null;
47
48
    protected ?DeliveryInformation $deliveryInformation = null;
49
50
    protected LineItemCollection $children;
51
52
    protected ?Rule $requirement = null;
53
54
    protected bool $removable = false;
55
56
    protected bool $stackable = false;
57
58
    protected ?QuantityInformation $quantityInformation = null;
59
60
    protected bool $modified = false;
61
62
    /**
63
     * The data timestamp can be used to record when the line item was last updated with data from the database.
64
     * Updating the data timestamp must be done by the corresponding cart data collector.
65
     */
66
    protected ?\DateTimeInterface $dataTimestamp = null;
67
68
    /**
69
     * Data data context hash can be used, like the data timestamp, to check if the line item was calculated with the same
70
     * context hash or not
71
     */
72
    protected ?string $dataContextHash = null;
73
74
    /**
75
     * Used as a unique id to identify a line item over multiple nested levels
76
     */
77
    protected string $uniqueIdentifier;
78
79
    /**
80
     * @var array<int, string>
81
     */
82
    protected array $states = [];
83
84
    protected bool $modifiedByApp = false;
85
86
    /**
87
     * @var array<string, bool>
88
     */
89
    private array $payloadProtection = [];
90
91
    /**
92
     * @throws CartException
93
     */
94
    public function __construct(
95
        protected string $id,
96
        protected string $type,
97
        protected ?string $referencedId = null,
98
        int $quantity = 1
99
    ) {
100
        $this->uniqueIdentifier = Uuid::randomHex();
101
        $this->children = new LineItemCollection();
102
103
        if ($quantity < 1) {
104
            throw CartException::invalidQuantity($quantity);
105
        }
106
        $this->quantity = $quantity;
107
    }
108
109
    /**
110
     * @throws CartException
111
     */
112
    public static function createFromLineItem(LineItem $lineItem): self
113
    {
114
        $self = new self($lineItem->id, $lineItem->type, $lineItem->getReferencedId(), $lineItem->quantity);
115
116
        foreach (get_object_vars($lineItem) as $property => $value) {
117
            $self->$property = $value; /* @phpstan-ignore-line */
118
        }
119
120
        return $self;
121
    }
122
123
    public function getId(): string
124
    {
125
        return $this->id;
126
    }
127
128
    public function setId(string $id): self
129
    {
130
        $this->id = $id;
131
132
        return $this;
133
    }
134
135
    public function getReferencedId(): ?string
136
    {
137
        return $this->referencedId;
138
    }
139
140
    public function setReferencedId(?string $referencedId): self
141
    {
142
        $this->referencedId = $referencedId;
143
144
        return $this;
145
    }
146
147
    public function getLabel(): ?string
148
    {
149
        return $this->label;
150
    }
151
152
    public function setLabel(?string $label): self
153
    {
154
        $this->label = $label;
155
156
        return $this;
157
    }
158
159
    public function getQuantity(): int
160
    {
161
        return $this->quantity;
162
    }
163
164
    /**
165
     * @throws CartException
166
     */
167
    public function setQuantity(int $quantity): self
168
    {
169
        if ($quantity < 1) {
170
            throw CartException::invalidQuantity($quantity);
171
        }
172
173
        if (!$this->isStackable()) {
174
            throw CartException::lineItemNotStackable($this->id);
175
        }
176
177
        if ($this->hasChildren()) {
178
            $this->refreshChildQuantity($this->children, $this->quantity, $quantity);
179
        }
180
181
        if ($this->priceDefinition instanceof QuantityPriceDefinition) {
182
            $this->price = null;
183
        }
184
185
        $this->quantity = $quantity;
186
187
        return $this;
188
    }
189
190
    public function getType(): string
191
    {
192
        return $this->type;
193
    }
194
195
    public function setType(string $type): self
196
    {
197
        $this->type = $type;
198
199
        return $this;
200
    }
201
202
    /**
203
     * @return array<string, mixed>
204
     */
205
    public function getPayload(): array
206
    {
207
        return $this->payload;
208
    }
209
210
    /**
211
     * @return mixed|null
212
     */
213
    public function getPayloadValue(string $key)
214
    {
215
        if (!$this->hasPayloadValue($key)) {
216
            return null;
217
        }
218
219
        return $this->payload[$key];
220
    }
221
222
    public function hasPayloadValue(string $key): bool
223
    {
224
        return isset($this->payload[$key]);
225
    }
226
227
    /**
228
     * @throws CartException
229
     */
230
    public function removePayloadValue(string $key): void
231
    {
232
        if (!$this->hasPayloadValue($key)) {
233
            throw CartException::payloadKeyNotFound($key, $this->getId());
234
        }
235
        unset($this->payload[$key]);
236
        unset($this->payloadProtection[$key]);
237
    }
238
239
    /**
240
     * @deprecated tag:v6.6.0 - reason:new-optional-parameter - Parameter $protected will be added in v6.6.0
241
     *
242
     * @param mixed|null $value
243
     *
244
     * @throws CartException
245
     */
246
    public function setPayloadValue(string $key, $value/* , ?bool $protected = null */): self
247
    {
248
        $protected = false;
249
        if (\func_num_args() === 3) {
250
            $protected = func_get_arg(2);
251
        }
252
253
        if ($value !== null && !\is_scalar($value) && !\is_array($value)) {
254
            throw CartException::invalidPayload($key, $this->getId());
255
        }
256
257
        $this->payload[$key] = $value;
258
259
        if ($protected !== null) {
260
            $this->addPayloadProtection([$key => $protected]);
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * @deprecated tag:v6.6.0 - reason:new-optional-parameter - Parameter $protection will be added in v6.6.0
268
     *
269
     * @param array<string, mixed> $payload
270
     *
271
     * @throws CartException
272
     */
273
    public function setPayload(array $payload/* , array $protection = [] */): self
274
    {
275
        $protection = [];
276
        if (\func_num_args() === 2) {
277
            $protection = func_get_arg(1);
278
        }
279
280
        foreach ($payload as $key => $value) {
281
            if (\is_string($key)) {
282
                $this->setPayloadValue($key, $value);
283
284
                continue;
285
            }
286
287
            throw CartException::invalidPayload((string) $key, $this->getId());
288
        }
289
290
        return $this->addPayloadProtection($protection);
291
    }
292
293
    /**
294
     * @param array<string, bool> $protection
295
     */
296
    public function addPayloadProtection(array $protection): self
297
    {
298
        $this->payloadProtection = \array_replace($this->payloadProtection, $protection);
299
300
        return $this;
301
    }
302
303
    /**
304
     * @deprecated tag:v6.6.0 - reason:new-optional-parameter - Parameter $protection will be added in v6.6.0
305
     *
306
     * @param array<string, mixed> $payload
307
     */
308
    public function replacePayload(array $payload/* , array $protection = [] */): self
309
    {
310
        $protection = [];
311
        if (\func_num_args() === 2) {
312
            $protection = func_get_arg(1);
313
        }
314
315
        $this->payload = \array_replace_recursive($this->payload, $payload);
316
317
        return $this->addPayloadProtection($protection);
318
    }
319
320
    public function getPriceDefinition(): ?PriceDefinitionInterface
321
    {
322
        return $this->priceDefinition;
323
    }
324
325
    public function setPriceDefinition(?PriceDefinitionInterface $priceDefinition): self
326
    {
327
        $this->priceDefinition = $priceDefinition;
328
329
        return $this;
330
    }
331
332
    public function getPrice(): ?CalculatedPrice
333
    {
334
        return $this->price;
335
    }
336
337
    public function setPrice(?CalculatedPrice $price): self
338
    {
339
        $this->price = $price;
340
341
        return $this;
342
    }
343
344
    public function isGood(): bool
345
    {
346
        return $this->good;
347
    }
348
349
    public function setGood(bool $good): self
350
    {
351
        $this->good = $good;
352
353
        return $this;
354
    }
355
356
    public function getDescription(): ?string
357
    {
358
        return $this->description;
359
    }
360
361
    public function setDescription(?string $description): self
362
    {
363
        $this->description = $description;
364
365
        return $this;
366
    }
367
368
    public function getCover(): ?MediaEntity
369
    {
370
        return $this->cover;
371
    }
372
373
    public function setCover(?MediaEntity $cover): self
374
    {
375
        $this->cover = $cover;
376
377
        return $this;
378
    }
379
380
    public function getDeliveryInformation(): ?DeliveryInformation
381
    {
382
        return $this->deliveryInformation;
383
    }
384
385
    public function setDeliveryInformation(?DeliveryInformation $deliveryInformation): self
386
    {
387
        $this->deliveryInformation = $deliveryInformation;
388
389
        return $this;
390
    }
391
392
    public function getChildren(): LineItemCollection
393
    {
394
        return $this->children;
395
    }
396
397
    public function setChildren(LineItemCollection $children): self
398
    {
399
        foreach ($children as $child) {
400
            $this->validateChildQuantity($child);
401
        }
402
        $this->children = $children;
403
404
        return $this;
405
    }
406
407
    public function hasChildren(): bool
408
    {
409
        return $this->children->count() > 0;
410
    }
411
412
    /**
413
     * @throws CartException
414
     */
415
    public function addChild(LineItem $child): self
416
    {
417
        $this->validateChildQuantity($child);
418
        $this->children->add($child);
419
420
        return $this;
421
    }
422
423
    public function setRequirement(?Rule $requirement): LineItem
424
    {
425
        $this->requirement = $requirement;
426
427
        return $this;
428
    }
429
430
    public function getRequirement(): ?Rule
431
    {
432
        return $this->requirement;
433
    }
434
435
    public function isRemovable(): bool
436
    {
437
        return $this->removable;
438
    }
439
440
    public function setRemovable(bool $removable): LineItem
441
    {
442
        $this->removable = $removable;
443
444
        return $this;
445
    }
446
447
    public function isStackable(): bool
448
    {
449
        return $this->stackable;
450
    }
451
452
    public function setStackable(bool $stackable): LineItem
453
    {
454
        $this->stackable = $stackable;
455
456
        return $this;
457
    }
458
459
    public function getQuantityInformation(): ?QuantityInformation
460
    {
461
        return $this->quantityInformation;
462
    }
463
464
    public function setQuantityInformation(?QuantityInformation $quantityInformation): LineItem
465
    {
466
        $this->quantityInformation = $quantityInformation;
467
468
        return $this;
469
    }
470
471
    public function isModified(): bool
472
    {
473
        return $this->modified;
474
    }
475
476
    public function markModified(): void
477
    {
478
        $this->modified = true;
479
    }
480
481
    public function markUnmodified(): void
482
    {
483
        $this->modified = false;
484
    }
485
486
    public function getApiAlias(): string
487
    {
488
        return 'cart_line_item';
489
    }
490
491
    /**
492
     * @see LineItem::$dataTimestamp
493
     */
494
    public function getDataTimestamp(): ?\DateTimeInterface
495
    {
496
        return $this->dataTimestamp;
497
    }
498
499
    /**
500
     * @see LineItem::$dataTimestamp
501
     */
502
    public function setDataTimestamp(?\DateTimeInterface $dataTimestamp): void
503
    {
504
        $this->dataTimestamp = $dataTimestamp;
505
    }
506
507
    /**
508
     * @see LineItem::$dataContextHash
509
     */
510
    public function getDataContextHash(): ?string
511
    {
512
        return $this->dataContextHash;
513
    }
514
515
    /**
516
     * @see LineItem::$dataContextHash
517
     */
518
    public function setDataContextHash(?string $dataContextHash): void
519
    {
520
        $this->dataContextHash = $dataContextHash;
521
    }
522
523
    public function getUniqueIdentifier(): string
524
    {
525
        return $this->uniqueIdentifier;
526
    }
527
528
    /**
529
     * @return array<int, string>
530
     */
531
    public function getStates(): array
532
    {
533
        return $this->states;
534
    }
535
536
    /**
537
     * @param array<int, string> $states
538
     */
539
    public function setStates(array $states): LineItem
540
    {
541
        $this->states = $states;
542
543
        return $this;
544
    }
545
546
    public function hasState(string $state): bool
547
    {
548
        return \in_array($state, $this->states, true);
549
    }
550
551
    public function markUnModifiedByApp(): void
552
    {
553
        $this->modifiedByApp = false;
554
    }
555
556
    public function markModifiedByApp(): void
557
    {
558
        $this->modifiedByApp = true;
559
    }
560
561
    public function isModifiedByApp(): bool
562
    {
563
        return $this->modifiedByApp;
564
    }
565
566
    public function jsonSerialize(): array
567
    {
568
        $data = parent::jsonSerialize();
569
570
        $payload = [];
571
572
        foreach ($data['payload'] as $key => $value) {
573
            if (isset($this->payloadProtection[$key]) && $this->payloadProtection[$key] === true) {
574
                continue;
575
            }
576
            $payload[$key] = $value;
577
        }
578
        $data['payload'] = $payload;
579
580
        return $data;
581
    }
582
583
    /**
584
     * @throws CartException
585
     */
586
    private function refreshChildQuantity(
587
        LineItemCollection $lineItems,
588
        int $oldParentQuantity,
589
        int $newParentQuantity
590
    ): void {
591
        foreach ($lineItems as $lineItem) {
592
            $newQuantity = intdiv($lineItem->getQuantity(), $oldParentQuantity) * $newParentQuantity;
593
594
            if ($lineItem->hasChildren()) {
595
                $this->refreshChildQuantity($lineItem->getChildren(), $lineItem->getQuantity(), $newQuantity);
596
            }
597
598
            $lineItem->quantity = $newQuantity;
599
            $priceDefinition = $lineItem->getPriceDefinition();
600
            if ($priceDefinition && $priceDefinition instanceof QuantityPriceDefinition) {
601
                $priceDefinition->setQuantity($lineItem->getQuantity());
602
            }
603
        }
604
    }
605
606
    /**
607
     * @throws CartException
608
     */
609
    private function validateChildQuantity(LineItem $child): void
610
    {
611
        $childQuantity = $child->getQuantity();
612
        $parentQuantity = $this->getQuantity();
613
        if ($childQuantity % $parentQuantity === 0) {
614
            return;
615
        }
616
617
        if ($childQuantity !== 1) {
618
            throw CartException::invalidChildQuantity($childQuantity, $parentQuantity);
619
        }
620
621
        // A quantity of 1 for a child line item is allowed, if the parent line item is not stackable
622
        if ($this->isStackable()) {
623
            throw CartException::invalidChildQuantity($childQuantity, $parentQuantity);
624
        }
625
    }
626
}
627