Passed
Push — trunk ( 51f3c1...6fc738 )
by Christian
11:40 queued 13s
created

LineItem::addPayloadProtection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
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;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\Content\Media\MediaEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\Checkout\C...Item::setPayloadValue() has been deprecated: tag:v6.6.0 - reason:new-optional-parameter - Parameter $protected will be added in v6.6.0 ( Ignorable by Annotation )

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

282
                /** @scrutinizer ignore-deprecated */ $this->setPayloadValue($key, $value);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Bug introduced by
$child of type array is incompatible with the type Shopware\Core\Checkout\Cart\LineItem\LineItem expected by parameter $child of Shopware\Core\Checkout\C...validateChildQuantity(). ( Ignorable by Annotation )

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

400
            $this->validateChildQuantity(/** @scrutinizer ignore-type */ $child);
Loading history...
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