• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aimeos / aimeos-core / 4f8b7e8d-5595-43b2-ba5c-df9921830178

17 May 2026 07:32AM UTC coverage: 92.581%. Remained the same
4f8b7e8d-5595-43b2-ba5c-df9921830178

push

circleci

aimeos
Fixed PHPStan issues

896 of 980 new or added lines in 165 files covered. (91.43%)

35 existing lines in 29 files now uncovered.

9734 of 10514 relevant lines covered (92.58%)

80.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.72
/src/MShop/Order/Item/Base.php
1
<?php
2

3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2015-2026
6
 * @package MShop
7
 * @subpackage Order
8
 */
9

10

11
namespace Aimeos\MShop\Order\Item;
12

13

14
/**
15
 * Base order item class with common constants and methods.
16
 *
17
 * @package MShop
18
 * @subpackage Order
19
 * @implements \ArrayAccess<string, mixed>
20
 */
21
abstract class Base
22
        extends \Aimeos\MShop\Common\Item\Base
23
        implements \Aimeos\MShop\Order\Item\Iface, \Aimeos\Macro\Iface, \ArrayAccess, \JsonSerializable
24
{
25
        use \Aimeos\Macro\Macroable;
26
        use Publisher;
27

28

29
        /**
30
         * Unfinished delivery.
31
         * This is the default status after creating an order and this status
32
         * should be also used as long as technical errors occurs.
33
         */
34
        const STAT_UNFINISHED = -1;
35

36
        /**
37
         * Delivery was deleted.
38
         * The delivery of the order was deleted manually.
39
         */
40
        const STAT_DELETED = 0;
41

42
        /**
43
         * Delivery is pending.
44
         * The order is not yet in the fulfillment process until further actions
45
         * are taken.
46
         */
47
        const STAT_PENDING = 1;
48

49
        /**
50
         * Fulfillment in progress.
51
         * The delivery of the order is in the (internal) fulfillment process and
52
         * will be ready soon.
53
         */
54
        const STAT_PROGRESS = 2;
55

56
        /**
57
         * Parcel is dispatched.
58
         * The parcel was given to the logistic partner for delivery to the
59
         * customer.
60
         */
61
        const STAT_DISPATCHED = 3;
62

63
        /**
64
         * Parcel was delivered.
65
         * The logistic partner delivered the parcel and the customer received it.
66
         */
67
        const STAT_DELIVERED = 4;
68

69
        /**
70
         * Parcel is lost.
71
         * The parcel is lost during delivery by the logistic partner and haven't
72
         * reached the customer nor it's returned to the merchant.
73
         */
74
        const STAT_LOST = 5;
75

76
        /**
77
         * Parcel was refused.
78
         * The delivery of the parcel failed because the customer has refused to
79
         * accept it or the address was invalid.
80
         */
81
        const STAT_REFUSED = 6;
82

83
        /**
84
         * Parcel was returned.
85
         * The parcel was sent back by the customer.
86
         */
87
        const STAT_RETURNED = 7;
88

89

90
        /**
91
         * Unfinished payment.
92
         * This is the default status after creating an order and this status
93
         * should be also used as long as technical errors occurs.
94
         */
95
        const PAY_UNFINISHED = -1;
96

97
        /**
98
         * Payment was deleted.
99
         * The payment for the order was deleted manually.
100
         */
101
        const PAY_DELETED = 0;
102

103
        /**
104
         * Payment was canceled.
105
         * The customer canceled the payment process.
106
         */
107
        const PAY_CANCELED = 1;
108

109
        /**
110
         * Payment was refused.
111
         * The customer didn't enter valid payment details.
112
         */
113
        const PAY_REFUSED = 2;
114

115
        /**
116
         * Payment was refund.
117
         * The payment was OK but refund and the customer got his money back.
118
         */
119
        const PAY_REFUND = 3;
120

121
        /**
122
         * Payment is pending.
123
         * The payment is not yet done until further actions are taken.
124
         */
125
        const PAY_PENDING = 4;
126

127
        /**
128
         * Payment is authorized.
129
         * The customer authorized the merchant to invoice the amount but the money
130
         * is not yet received. This is used for all post-paid orders.
131
         */
132
        const PAY_AUTHORIZED = 5;
133

134
        /**
135
         * Payment is received.
136
         * The merchant received the money from the customer.
137
         */
138
        const PAY_RECEIVED = 6;
139

140
        /**
141
         * Payment is transferred.
142
         * The vendor received the money from the platform.
143
         */
144
        const PAY_TRANSFERRED = 7;
145

146

147
        // protected is a workaround for serialize problem
148
        protected ?\Aimeos\MShop\Customer\Item\Iface $customer;
149
        protected \Aimeos\MShop\Locale\Item\Iface $locale;
150
        protected \Aimeos\MShop\Price\Item\Iface $price;
151
        protected array $coupons = [];
152
        protected array $products = [];
153
        protected array $services = [];
154
        protected array $statuses = [];
155
        protected array $addresses = [];
156

157

158
        /**
159
         * Initializes the order object
160
         *
161
         * @param string $prefix Prefix for the keys in the associative array
162
         * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
163
         */
164
        public function __construct( string $prefix, array $values = [] )
165
        {
166
                $this->customer = $values['.customer'] ?? null;
548✔
167
                $this->locale = $values['.locale'];
548✔
168
                $this->price = $values['.price'];
548✔
169

170
                $products = $values['.products'] ?? [];
548✔
171

172
                foreach( $values['.coupons'] ?? [] as $coupon )
548✔
173
                {
174
                        if( !isset( $this->coupons[$coupon->getCode()] ) ) {
6✔
175
                                $this->coupons[$coupon->getCode()] = [];
6✔
176
                        }
177

178
                        if( isset( $products[$coupon->getProductId()] ) ) {
6✔
179
                                $this->coupons[$coupon->getCode()][] = $products[$coupon->getProductId()];
6✔
180
                        }
181
                }
182

183
                foreach( $values['.products'] ?? [] as $product ) {
548✔
184
                        $this->products[$product->getPosition()] = $product;
38✔
185
                }
186

187
                foreach( $values['.addresses'] ?? [] as $address ) {
548✔
188
                        $this->addresses[$address->getType()][] = $address;
32✔
189
                }
190

191
                foreach( $values['.services'] ?? [] as $service ) {
548✔
192
                        $this->services[$service->getType()][] = $service;
33✔
193
                }
194

195
                foreach( $values['.statuses'] ?? [] as $status ) {
548✔
196
                        $this->statuses[$status->getType()][$status->getValue()] = $status;
1✔
197
                }
198

199
                unset( $values['.customer'], $values['.locale'], $values['.price'], $values['.statuses'] );
548✔
200
                unset( $values['.products'], $values['.coupons'], $values['.addresses'], $values['.services'] );
548✔
201

202
                parent::__construct( $prefix, $values );
548✔
203
        }
204

205

206
        /**
207
         * Clones internal objects of the order item.
208
         */
209
        public function __clone()
210
        {
211
                parent::__clone();
4✔
212

213
                $this->price = clone $this->price;
4✔
214
                $this->locale = clone $this->locale;
4✔
215
        }
216

217

218
        /**
219
         * Specifies the data which should be serialized to JSON by json_encode().
220
         *
221
         * @return array Data to serialize to JSON
222
         */
223
        #[\ReturnTypeWillChange]
224
        public function jsonSerialize()
225
        {
226
                return parent::jsonSerialize() + [
1✔
227
                        'addresses' => $this->addresses,
1✔
228
                        'products' => $this->products,
1✔
229
                        'services' => $this->services,
1✔
230
                        'coupons' => $this->coupons,
1✔
231
                        'customer' => $this->customer,
1✔
232
                        'locale' => $this->locale,
1✔
233
                        'price' => $this->price,
1✔
234
                ];
1✔
235
        }
236

237

238
        /**
239
         * Prepares the object for serialization.
240
         *
241
         * @return array List of properties that should be serialized
242
         */
243
        public function __sleep() : array
244
        {
245
                /*
246
                 * Workaround because database connections can't be serialized
247
                 * Listeners will be reattached on wakeup by the order base manager
248
                 */
249
                $this->off();
3✔
250

251
                return array_keys( get_object_vars( $this ) );
3✔
252
        }
253

254

255
        /**
256
         * Returns the ID of the items
257
         *
258
         * @return string ID of the item or null
259
         */
260
        public function __toString() : string
261
        {
262
                return (string) $this->getId();
×
263
        }
264

265

266
        /**
267
         * Tests if all necessary items are available to create the order.
268
         *
269
         * @param array $what Type of data
270
         * @return static Order base item for method chaining
271
         * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
272
         */
273
        public function check( array $what = ['order/address', 'order/coupon', 'order/product', 'order/service'] ) : static
274
        {
275
                $this->notify( 'check.before', $what );
3✔
276

277
                if( in_array( 'order/product', $what ) && ( count( $this->getProducts() ) < 1 ) ) {
3✔
278
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ), 0 );
2✔
279
                }
280

281
                $this->notify( 'check.after', $what );
1✔
282

283
                return $this;
1✔
284
        }
285

286

287
        /**
288
         * Notifies listeners before the basket becomes an order.
289
         *
290
         * @return static Order base item for chaining method calls
291
         */
292
        public function finish() : static
293
        {
294
                $this->notify( 'setOrder.before' );
×
295
                return $this;
×
296
        }
297

298

299
        /**
300
         * Adds the address of the given type to the basket
301
         *
302
         * @param \Aimeos\MShop\Order\Item\Address\Iface $address Order address item for the given type
303
         * @param string $type Address type, usually "billing" or "delivery"
304
         * @param int|null $position Position of the address in the list
305
         * @return static Order base item for method chaining
306
         */
307
        public function addAddress( \Aimeos\MShop\Order\Item\Address\Iface $address, string $type, ?int $position = null ) : static
308
        {
309
                $address = $this->notify( 'addAddress.before', $address );
52✔
310

311
                $address = clone $address; // @phpstan-ignore clone.nonObject
52✔
312
                $address = $address->setType( $type ); // @phpstan-ignore method.notFound
52✔
313

314
                if( $position !== null ) {
52✔
315
                        $this->addresses[$type][$position] = $address;
1✔
316
                } else {
317
                        $this->addresses[$type][] = $address;
52✔
318
                }
319

320
                $this->price->setModified();
52✔
321
                $this->setModified();
52✔
322

323
                $this->notify( 'addAddress.after', $address );
52✔
324

325
                return $this;
52✔
326
        }
327

328

329
        /**
330
         * Deletes an order address from the basket
331
         *
332
         * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Address\Base
333
         * @param int|null $position Position of the address in the list
334
         * @return static Order base item for method chaining
335
         */
336
        public function deleteAddress( string $type, ?int $position = null ) : static
337
        {
338
                if( ( $position === null && isset( $this->addresses[$type] ) ) || ( $position !== null && isset( $this->addresses[$type][$position] ) ) )
4✔
339
                {
340
                        $old = ( $position !== null && isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
3✔
341
                        $old = $this->notify( 'deleteAddress.before', $old );
3✔
342

343
                        if( $position !== null ) {
3✔
344
                                unset( $this->addresses[$type][$position] );
1✔
345
                        } else {
346
                                unset( $this->addresses[$type] );
2✔
347
                        }
348

349
                        $this->price->setModified();
3✔
350
                        $this->setModified();
3✔
351

352
                        $this->notify( 'deleteAddress.after', $old );
3✔
353
                }
354

355
                return $this;
4✔
356
        }
357

358

359
        /**
360
         * Returns the order address depending on the given type
361
         *
362
         * @param string $type Address type, usually "billing" or "delivery"
363
         * @param int|null $position Address position in list of addresses
364
         * @return \Aimeos\MShop\Order\Item\Address\Iface[]|\Aimeos\MShop\Order\Item\Address\Iface Order address item or list of
365
         */
366
        public function getAddress( string $type, ?int $position = null )
367
        {
368
                if( $position !== null )
59✔
369
                {
370
                        if( isset( $this->addresses[$type][$position] ) ) {
4✔
371
                                // @phpstan-ignore return.type
372
                                return $this->addresses[$type][$position];
3✔
373
                        }
374

375
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
1✔
376
                }
377

378
                // @phpstan-ignore return.type
379
                return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
55✔
380
        }
381

382

383
        /**
384
         * Returns all addresses that are part of the basket
385
         *
386
         * @return \Aimeos\Map Associative list of address items implementing
387
         *  \Aimeos\MShop\Order\Item\Address\Iface with "billing" or "delivery" as key
388
         */
389
        public function getAddresses() : \Aimeos\Map
390
        {
391
                return map( $this->addresses );
30✔
392
        }
393

394

395
        /**
396
         * Replaces all addresses in the current basket with the new ones
397
         *
398
         * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
399
         * @return static Order base item for method chaining
400
         */
401
        public function setAddresses( iterable $map ) : static
402
        {
403
                $map = $this->notify( 'setAddresses.before', $map );
7✔
404

405
                foreach( $map as $type => $items ) {
7✔
406
                        // @phpstan-ignore argument.type
407
                        $this->checkAddresses( $items, (string) $type );
7✔
408
                }
409

410
                $old = $this->addresses;
7✔
411
                $this->addresses = is_map( $map ) ? $map->toArray() : $map;
7✔
412

413
                $this->price->setModified();
7✔
414
                $this->setModified();
7✔
415

416
                $this->notify( 'setAddresses.after', $old );
7✔
417

418
                return $this;
7✔
419
        }
420

421

422
        /**
423
         * Adds a coupon code and the given product item to the basket
424
         *
425
         * @param string $code Coupon code
426
         * @return static Order base item for method chaining
427
         */
428
        public function addCoupon( string $code ) : static
429
        {
430
                if( !isset( $this->coupons[$code] ) )
3✔
431
                {
432
                        $code = $this->notify( 'addCoupon.before', $code );
3✔
433
                        $this->coupons[$code] = [];
3✔
434

435
                        $this->price->setModified();
3✔
436
                        $this->setModified();
3✔
437

438
                        $this->notify( 'addCoupon.after', $code );
3✔
439
                }
440

441
                return $this;
3✔
442
        }
443

444

445
        /**
446
         * Removes a coupon and the related product items from the basket
447
         *
448
         * @param string $code Coupon code
449
         * @return static Order base item for method chaining
450
         */
451
        public function deleteCoupon( string $code ) : static
452
        {
453
                if( isset( $this->coupons[$code] ) )
1✔
454
                {
455
                        $old = [$code => $this->coupons[$code]];
1✔
456
                        $old = $this->notify( 'deleteCoupon.before', $old );
1✔
457

458
                        foreach( $this->coupons[$code] as $product )
1✔
459
                        {
460
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
1✔
461
                                        unset( $this->products[$key] );
1✔
462
                                }
463
                        }
464

465
                        unset( $this->coupons[$code] );
1✔
466

467
                        $this->price->setModified();
1✔
468
                        $this->setModified();
1✔
469

470
                        $this->notify( 'deleteCoupon.after', $old );
1✔
471
                }
472

473
                return $this;
1✔
474
        }
475

476

477
        /**
478
         * Returns the available coupon codes and the lists of affected product items
479
         *
480
         * @return \Aimeos\Map Associative array of codes and lists of product items
481
         *  implementing \Aimeos\MShop\Order\Product\Iface
482
         */
483
        public function getCoupons() : \Aimeos\Map
484
        {
485
                return map( $this->coupons );
76✔
486
        }
487

488

489
        /**
490
         * Sets a coupon code and the given product items in the basket.
491
         *
492
         * @param string $code Coupon code
493
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of coupon products
494
         * @return static Order base item for method chaining
495
         */
496
        public function setCoupon( string $code, iterable $products = [] ) : static
497
        {
498
                $new = $this->notify( 'setCoupon.before', [$code => $products] );
17✔
499

500
                // @phpstan-ignore argument.type
501
                $products = $this->checkProducts( map( $new )->first( [] ) );
17✔
502

503
                if( isset( $this->coupons[$code] ) )
17✔
504
                {
505
                        foreach( $this->coupons[$code] as $product )
9✔
506
                        {
507
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
1✔
508
                                        unset( $this->products[$key] );
1✔
509
                                }
510
                        }
511
                }
512

513
                foreach( $products as $product ) {
17✔
514
                        $this->products[] = $product;
15✔
515
                }
516

517
                $old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
17✔
518
                $this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
17✔
519

520
                $this->price->setModified();
17✔
521
                $this->setModified();
17✔
522

523
                $this->notify( 'setCoupon.after', $old );
17✔
524

525
                return $this;
17✔
526
        }
527

528

529
        /**
530
         * Replaces all coupons in the current basket with the new ones
531
         *
532
         * @param iterable $map Associative list of order coupons as returned by getCoupons()
533
         * @return static Order base item for method chaining
534
         */
535
        public function setCoupons( iterable $map ) : static
536
        {
537
                $map = $this->notify( 'setCoupons.before', $map );
3✔
538

539
                foreach( $map as $code => $products ) {
3✔
540
                        // @phpstan-ignore argument.type
541
                        $map[$code] = $this->checkProducts( $products );
3✔
542
                }
543

544
                foreach( $this->coupons as $code => $products )
3✔
545
                {
546
                        foreach( $products as $product )
×
547
                        {
548
                                if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
×
549
                                        unset( $this->products[$key] );
×
550
                                }
551
                        }
552
                }
553

554
                foreach( $map as $code => $products )
3✔
555
                {
556
                        foreach( $products as $product ) {
3✔
557
                                $this->products[] = $product;
3✔
558
                        }
559
                }
560

561
                $old = $this->coupons;
3✔
562
                $this->coupons = is_map( $map ) ? $map->toArray() : $map;
3✔
563

564
                $this->price->setModified();
3✔
565
                $this->setModified();
3✔
566

567
                $this->notify( 'setCoupons.after', $old );
3✔
568

569
                return $this;
3✔
570
        }
571

572

573
        /**
574
         * Adds an order product item to the basket
575
         * If a similar item is found, only the quantity is increased.
576
         *
577
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item to be added
578
         * @param int|null $position position of the new order product item
579
         * @return static Order base item for method chaining
580
         */
581
        public function addProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, ?int $position = null ) : static
582
        {
583
                $item = $this->notify( 'addProduct.before', $item );
111✔
584

585
                // @phpstan-ignore argument.type
586
                $this->checkProducts( [$item] );
111✔
587

588
                if( $position !== null ) {
111✔
589
                        $this->products[$position] = $item;
8✔
590
                // @phpstan-ignore argument.type, argument.type
591
                } elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
109✔
592
                        $item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
2✔
593
                } else {
594
                        $this->products[] = $item;
109✔
595
                }
596

597
                ksort( $this->products );
111✔
598

599
                $this->price->setModified();
111✔
600
                $this->setModified();
111✔
601

602
                $this->notify( 'addProduct.after', $item );
111✔
603

604
                return $this;
111✔
605
        }
606

607

608
        /**
609
         * Deletes an order product item from the basket
610
         *
611
         * @param int $position Position of the order product item
612
         * @return static Order base item for method chaining
613
         */
614
        public function deleteProduct( int $position ) : static
615
        {
616
                if( isset( $this->products[$position] ) )
4✔
617
                {
618
                        $old = $this->products[$position];
4✔
619
                        $old = $this->notify( 'deleteProduct.before', $old );
4✔
620

621
                        unset( $this->products[$position] );
4✔
622

623
                        $this->price->setModified();
4✔
624
                        $this->setModified();
4✔
625

626
                        $this->notify( 'deleteProduct.after', $old );
4✔
627
                }
628

629
                return $this;
4✔
630
        }
631

632

633
        /**
634
         * Returns the product item of an basket specified by its key
635
         *
636
         * @param int $key Key returned by getProducts() identifying the requested product
637
         * @return \Aimeos\MShop\Order\Item\Product\Iface Product item of an order
638
         */
639
        public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Product\Iface
640
        {
641
                if( !isset( $this->products[$key] ) ) {
13✔
642
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
×
643
                }
644

645
                // @phpstan-ignore return.type
646
                return $this->products[$key];
13✔
647
        }
648

649

650
        /**
651
         * Returns the product items that are or should be part of a basket
652
         *
653
         * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Product\Iface
654
         */
655
        public function getProducts() : \Aimeos\Map
656
        {
657
                return map( $this->products );
149✔
658
        }
659

660

661
        /**
662
         * Replaces all products in the current basket with the new ones
663
         *
664
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
665
         * @return static Order base item for method chaining
666
         */
667
        public function setProducts( iterable $map ) : static
668
        {
669
                $map = $this->notify( 'setProducts.before', $map );
7✔
670

671
                // @phpstan-ignore argument.type
672
                $this->checkProducts( $map );
7✔
673

674
                $old = $this->products;
7✔
675
                $this->products = is_map( $map ) ? $map->toArray() : $map;
7✔
676

677
                $this->price->setModified();
7✔
678
                $this->setModified();
7✔
679

680
                $this->notify( 'setProducts.after', $old );
7✔
681

682
                return $this;
7✔
683
        }
684

685

686
        /**
687
         * Adds an order service to the basket
688
         *
689
         * @param \Aimeos\MShop\Order\Item\Service\Iface $service Order service item for the given domain
690
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
691
         * @param int|null $position Position of the service in the list to overwrite
692
         * @return static Order base item for method chaining
693
         */
694
        public function addService( \Aimeos\MShop\Order\Item\Service\Iface $service, string $type, ?int $position = null ) : static
695
        {
696
                $service = $this->notify( 'addService.before', $service );
23✔
697

698
                // @phpstan-ignore argument.type
699
                $this->checkPrice( $service->getPrice() );
23✔
700

701
                $service = clone $service; // @phpstan-ignore clone.nonObject
23✔
702
                $service = $service->setType( $type ); // @phpstan-ignore method.notFound
23✔
703

704
                if( $position !== null ) {
23✔
705
                        $this->services[$type][$position] = $service;
1✔
706
                } else {
707
                        $this->services[$type][] = $service;
23✔
708
                }
709

710
                $this->price->setModified();
23✔
711
                $this->setModified();
23✔
712

713
                $this->notify( 'addService.after', $service );
23✔
714

715
                return $this;
23✔
716
        }
717

718

719
        /**
720
         * Deletes an order service from the basket
721
         *
722
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
723
         * @param int|null $position Position of the service in the list to delete
724
         * @return static Order base item for method chaining
725
         */
726
        public function deleteService( string $type, ?int $position = null ) : static
727
        {
728
                if( ( $position === null && isset( $this->services[$type] ) ) || ( $position !== null && isset( $this->services[$type][$position] ) ) )
4✔
729
                {
730
                        $old = ( $position !== null && isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
3✔
731
                        $old = $this->notify( 'deleteService.before', $old );
3✔
732

733
                        if( $position !== null ) {
3✔
734
                                unset( $this->services[$type][$position] );
1✔
735
                        } else {
736
                                unset( $this->services[$type] );
2✔
737
                        }
738

739
                        $this->price->setModified();
3✔
740
                        $this->setModified();
3✔
741

742
                        $this->notify( 'deleteService.after', $old );
3✔
743
                }
744

745
                return $this;
4✔
746
        }
747

748

749
        /**
750
         * Returns the order services depending on the given type
751
         *
752
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
753
         * @param int|null $position Position of the service in the list to retrieve
754
         * @return \Aimeos\MShop\Order\Item\Service\Iface[]|\Aimeos\MShop\Order\Item\Service\Iface
755
         *         Order service item or list of items for the requested type
756
         * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
757
         */
758
        public function getService( string $type, ?int $position = null )
759
        {
760
                if( $position !== null )
48✔
761
                {
762
                        if( isset( $this->services[$type][$position] ) ) {
20✔
763
                                // @phpstan-ignore return.type
764
                                return $this->services[$type][$position];
19✔
765
                        }
766

767
                        throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
1✔
768
                }
769

770
                // @phpstan-ignore return.type
771
                return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
38✔
772
        }
773

774

775
        /**
776
         * Returns all services that are part of the basket
777
         *
778
         * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
779
         *        service items implementing \Aimeos\MShop\Order\Service\Iface as values
780
         */
781
        public function getServices() : \Aimeos\Map
782
        {
783
                return map( $this->services );
95✔
784
        }
785

786

787
        /**
788
         * Replaces all services in the current basket with the new ones
789
         *
790
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $map Associative list of order services as returned by getServices()
791
         * @return static Order base item for method chaining
792
         */
793
        public function setServices( iterable $map ) : static
794
        {
795
                $map = $this->notify( 'setServices.before', $map );
15✔
796

797
                foreach( $map as $type => $services ) {
15✔
798
                        // @phpstan-ignore argument.type, argument.type
799
                        $map[$type] = $this->checkServices( $services, (string) $type );
15✔
800
                }
801

802
                $old = $this->services;
15✔
803
                $this->services = is_map( $map ) ? $map->toArray() : $map;
15✔
804

805
                $this->price->setModified();
15✔
806
                $this->setModified();
15✔
807

808
                $this->notify( 'setServices.after', $old );
15✔
809

810
                return $this;
15✔
811
        }
812

813

814
        /**
815
         * Adds a status item to the order
816
         *
817
         * @param \Aimeos\MShop\Order\Item\Status\Iface $item Order status item
818
         * @return static Order item for method chaining
819
         */
820
        public function addStatus( \Aimeos\MShop\Order\Item\Status\Iface $item ) : static
821
        {
822
                $type = $item->getType();
5✔
823
                $value = $item->getValue();
5✔
824

825
                if( isset( $this->statuses[$type][$value] ) ) {
5✔
826
                        // @phpstan-ignore argument.type
UNCOV
827
                        $this->statuses[$type][$value] = $item->setId( $this->statuses[$type][$value]->getId() );
×
828
                } else {
829
                        $this->statuses[$type][$value] = $item;
5✔
830
                }
831

832
                return $this;
5✔
833
        }
834

835

836
        /**
837
         * Returns the latest status item specified by its type and value
838
         *
839
         * @param string $type Status type
840
         * @param string $value Status value
841
         * @return \Aimeos\MShop\Order\Item\Status\Iface|null Status item of an order or null if not available
842
         */
843
        public function getStatus( string $type, string $value ) : ?\Aimeos\MShop\Order\Item\Status\Iface
844
        {
845
                // @phpstan-ignore return.type
846
                return $this->statuses[$type][$value] ?? null;
1✔
847
        }
848

849

850
        /**
851
         * Returns the status items
852
         *
853
         * @return \Aimeos\Map Associative list of status types as keys and list of
854
         *        status value/item pairs implementing \Aimeos\MShop\Order\Status\Iface as values
855
         */
856
        public function getStatuses() : \Aimeos\Map
857
        {
858
                return map( $this->statuses );
12✔
859
        }
860

861

862
        /**
863
         * Returns the service costs
864
         *
865
         * @param string $type Service type like "delivery" or "payment"
866
         * @return float Service costs value
867
         */
868
        public function getCosts( string $type = 'delivery' ) : float
869
        {
870
                $costs = 0;
2✔
871

872
                foreach( $this->getService( $type ) as $service ) {
2✔
873
                        $costs += $service->getPrice()->getCosts(); // @phpstan-ignore assignOp.invalid
2✔
874
                }
875

876
                if( $type === 'delivery' )
2✔
877
                {
878
                        foreach( $this->getProducts() as $product ) {
2✔
879
                                $costs += $product->getPrice()->getCosts() * $product->getQuantity(); // @phpstan-ignore assignOp.invalid
1✔
880
                        }
881
                }
882

883
                // @phpstan-ignore return.type
884
                return $costs;
2✔
885
        }
886

887

888
        /**
889
         * Returns a price item with amounts calculated for the products, costs, etc.
890
         *
891
         * @return \Aimeos\MShop\Price\Item\Iface Price item with price, costs and rebate the customer has to pay
892
         */
893
        public function getPrice() : \Aimeos\MShop\Price\Item\Iface
894
        {
895
                if( $this->price->isModified() )
179✔
896
                {
897
                        $price = $this->price->clear();
65✔
898

899
                        foreach( $this->getServices() as $list )
65✔
900
                        {
901
                                foreach( $list as $service ) {
13✔
902
                                        // @phpstan-ignore argument.type
903
                                        $price = $price->addItem( $service->getPrice() );
13✔
904
                                }
905
                        }
906

907
                        foreach( $this->getProducts() as $product ) {
65✔
908
                                // @phpstan-ignore argument.type, argument.type
909
                                $price = $price->addItem( $product->getPrice(), $product->getQuantity() );
48✔
910
                        }
911

912
                        $this->price = $price->setId( '' ); // clear modified flag
65✔
913
                }
914

915
                return $this->price;
179✔
916
        }
917

918

919
        /**
920
         * Returns a list of tax names and values
921
         *
922
         * @return array Associative list of tax names as key and price items as value
923
         */
924
        public function getTaxes() : array
925
        {
926
                $taxes = [];
1✔
927

928
                foreach( $this->getProducts() as $product )
1✔
929
                {
930
                        $price = $product->getPrice();
1✔
931

932
                        foreach( $price->getTaxrates() as $name => $taxrate )
1✔
933
                        {
934
                                $price = ( clone $price )->setTaxRate( $taxrate ); // @phpstan-ignore clone.nonObject, method.notFound
1✔
935

936
                                if( isset( $taxes[$name][$taxrate] ) ) {
1✔
937
                                        $taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
×
938
                                } else {
939
                                        $taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
1✔
940
                                }
941
                        }
942
                }
943

944
                foreach( $this->getServices() as $services )
1✔
945
                {
946
                        foreach( $services as $service )
1✔
947
                        {
948
                                $price = $service->getPrice();
1✔
949

950
                                foreach( $price->getTaxrates() as $name => $taxrate )
1✔
951
                                {
952
                                        $price = ( clone $price )->setTaxRate( $taxrate ); // @phpstan-ignore clone.nonObject, method.notFound
1✔
953

954
                                        if( isset( $taxes[$name][$taxrate] ) ) {
1✔
955
                                                $taxes[$name][$taxrate]->addItem( $price );
1✔
956
                                        } else {
957
                                                $taxes[$name][$taxrate] = $price;
×
958
                                        }
959
                                }
960
                        }
961
                }
962

963
                return $taxes;
1✔
964
        }
965

966

967
        /**
968
         * Returns the locales for the basic order item.
969
         *
970
         * @return \Aimeos\MShop\Locale\Item\Iface Object containing information
971
         *  about site, language, country and currency
972
         */
973
        public function locale() : \Aimeos\MShop\Locale\Item\Iface
974
        {
975
                return $this->locale;
21✔
976
        }
977

978

979
        /**
980
         * Sets the locales for the basic order item.
981
         *
982
         * @param \Aimeos\MShop\Locale\Item\Iface $locale Object containing information
983
         *  about site, language, country and currency
984
         * @return static Order base item for chaining method calls
985
         */
986
        public function setLocale( \Aimeos\MShop\Locale\Item\Iface $locale ) : static
987
        {
988
                $this->notify( 'setLocale.before', $locale );
2✔
989
                $this->locale = clone $locale;
2✔
990
                $this->notify( 'setLocale.after', $locale );
2✔
991

992
                return $this->setModified();
2✔
993
        }
994

995

996
        /**
997
         * Returns the item values as array.
998
         *
999
         * @param bool $private True to return private properties, false for public only
1000
         * @return array Associative list of item properties and their values
1001
         */
1002
        public function toArray( bool $private = false ) : array
1003
        {
1004
                $price = $this->getPrice();
14✔
1005
                $list = parent::toArray( $private );
14✔
1006

1007
                $list['order.currencyid'] = $price->getCurrencyId();
14✔
1008
                $list['order.price'] = $price->getValue();
14✔
1009
                $list['order.costs'] = $price->getCosts();
14✔
1010
                $list['order.rebate'] = $price->getRebate();
14✔
1011
                $list['order.taxflag'] = $price->getTaxFlag();
14✔
1012
                $list['order.taxvalue'] = $price->getTaxValue();
14✔
1013

1014
                return $list;
14✔
1015
        }
1016

1017

1018
        /**
1019
         * Checks if the price uses the same currency as the price in the basket.
1020
         *
1021
         * @param \Aimeos\MShop\Price\Item\Iface $item Price item
1022
         * @return void
1023
         */
1024
        protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item ) : void
1025
        {
1026
                $price = clone $this->getPrice();
146✔
1027
                $price->addItem( $item );
146✔
1028
        }
1029

1030

1031
        /**
1032
         * Checks if all order addresses are valid
1033
         *
1034
         * @param \Aimeos\MShop\Order\Item\Address\Iface[] $items Order address items
1035
         * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Address\Base
1036
         * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of checked items
1037
         * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
1038
         */
1039
        protected function checkAddresses( iterable $items, string $type ) : iterable
1040
        {
1041
                map( $items )->implements( \Aimeos\MShop\Order\Item\Address\Iface::class, true );
7✔
1042

1043
                foreach( $items as $key => $item ) {
7✔
1044
                        $items[$key] = $item->setType( $type );
7✔
1045
                }
1046

1047
                return $items;
7✔
1048
        }
1049

1050

1051
        /**
1052
         * Checks if all order products are valid
1053
         *
1054
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $items Order product items
1055
         * @return \Aimeos\Map List of checked items
1056
         * @throws \Aimeos\MShop\Exception If one of the order products is invalid
1057
         */
1058
        protected function checkProducts( iterable $items ) : \Aimeos\Map
1059
        {
1060
                map( $items )->implements( \Aimeos\MShop\Order\Item\Product\Iface::class, true );
123✔
1061

1062
                foreach( $items as $key => $item )
123✔
1063
                {
1064
                        if( $item->getProductCode() === '' ) {
121✔
1065
                                throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
×
1066
                        }
1067

1068
                        $this->checkPrice( $item->getPrice() );
121✔
1069
                }
1070

1071
                return map( $items );
123✔
1072
        }
1073

1074

1075
        /**
1076
         * Checks if all order services are valid
1077
         *
1078
         * @param \Aimeos\MShop\Order\Item\Service\Iface[] $items Order service items
1079
         * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
1080
         * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of checked items
1081
         * @throws \Aimeos\MShop\Exception If one of the order services is invalid
1082
         */
1083
        protected function checkServices( iterable $items, string $type ) : iterable
1084
        {
1085
                map( $items )->implements( \Aimeos\MShop\Order\Item\Service\Iface::class, true );
15✔
1086

1087
                foreach( $items as $key => $item )
15✔
1088
                {
1089
                        $this->checkPrice( $item->getPrice() );
13✔
1090
                        $items[$key] = $item->setType( $type );
13✔
1091
                }
1092

1093
                return $items;
15✔
1094
        }
1095

1096

1097
        /**
1098
         * Tests if the given product is similar to an existing one.
1099
         * Similarity is described by the equality of properties so the quantity of
1100
         * the existing product can be updated.
1101
         *
1102
         * @param \Aimeos\MShop\Order\Item\Product\Iface $item Order product item
1103
         * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of order product items to check against
1104
         * @return int|null Positon of the same product in the product list of false if product is unique
1105
         * @throws \Aimeos\MShop\Order\Exception If no similar item was found
1106
         */
1107
        protected function getSameProduct( \Aimeos\MShop\Order\Item\Product\Iface $item, iterable $products ) : ?int
1108
        {
1109
                $map = [];
109✔
1110
                $count = 0;
109✔
1111

1112
                foreach( $item->getAttributeItems() as $attributeItem )
109✔
1113
                {
1114
                        $key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
11✔
1115
                        $map[$key] = $attributeItem;
11✔
1116
                        $count++;
11✔
1117
                }
1118

1119
                foreach( $products as $position => $product )
109✔
1120
                {
1121
                        if( $product->compare( $item ) === false ) {
20✔
1122
                                continue;
19✔
1123
                        }
1124

1125
                        $prodAttributes = $product->getAttributeItems();
2✔
1126

1127
                        if( count( $prodAttributes ) !== $count ) {
2✔
1128
                                continue;
×
1129
                        }
1130

1131
                        foreach( $prodAttributes as $attribute )
2✔
1132
                        {
1133
                                $key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
×
1134

1135
                                if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
×
1136
                                        continue 2; // jump to outer loop
×
1137
                                }
1138
                        }
1139

1140
                        // @phpstan-ignore return.type
1141
                        return $position;
2✔
1142
                }
1143

1144
                return null;
109✔
1145
        }
1146
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc