• 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

97.83
/src/MShop/Plugin/Provider/Order/ProductStock.php
1
<?php
2

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

11

12
namespace Aimeos\MShop\Plugin\Provider\Order;
13

14

15
/**
16
 * Checks the products in a basket for sufficient stocklevel
17
 *
18
 * Notifies the customers if one or more products have gone out of stock in the
19
 * meantime. They have to remove this products before they can continue in the
20
 * checkout process.
21
 *
22
 * Also, the plugin reduces the product quantity automatically if there are not
23
 * enough products in stock.
24
 *
25
 * The checks are executed for the basket and checkout summary view.
26
 *
27
 * To trace the execution and interaction of the plugins, set the log level to DEBUG:
28
 *        madmin/log/manager/loglevel = 7
29
 *
30
 * @package MShop
31
 * @subpackage Plugin
32
 */
33
class ProductStock
34
        extends \Aimeos\MShop\Plugin\Provider\Factory\Base
35
        implements \Aimeos\MShop\Plugin\Provider\Iface, \Aimeos\MShop\Plugin\Provider\Factory\Iface
36
{
37
        /**
38
         * Subscribes itself to a publisher
39
         *
40
         * @param \Aimeos\MShop\Order\Item\Iface $p Object implementing publisher interface
41
         * @return static Plugin object for method chaining
42
         */
43
        public function register( \Aimeos\MShop\Order\Item\Iface $p ) : static
44
        {
45
                $p->attach( $this->object(), 'addProduct.after' );
466✔
46
                $p->attach( $this->object(), 'check.after' );
466✔
47
                return $this;
466✔
48
        }
49

50

51
        /**
52
         * Receives a notification from a publisher object
53
         *
54
         * @param \Aimeos\MShop\Order\Item\Iface $order Shop basket instance implementing publisher interface
55
         * @param string $action Name of the action to listen for
56
         * @param mixed $value Object or value changed in publisher
57
         * @return mixed Modified value parameter
58
         * @throws \Aimeos\MShop\Plugin\Provider\Exception if checks fail
59
         */
60
        public function update( \Aimeos\MShop\Order\Item\Iface $order, string $action, $value = null )
61
        {
62
                if( !in_array( 'order/product', (array) $value ) ) {
16✔
63
                        return $value;
1✔
64
                }
65

66
                if( !$order->getProducts()->isEmpty() && ( $outOfStock = $this->checkStock( $order ) ) !== [] )
15✔
67
                {
68
                        $msg = $this->context()->translate( 'mshop', 'Products out of stock' );
2✔
69
                        throw new \Aimeos\MShop\Plugin\Provider\Exception( $msg, 409, null, array( 'product' => $outOfStock ) );
2✔
70
                }
71

72
                return $value;
13✔
73
        }
74

75

76
        /**
77
         * Checks if all products in the basket have enough stock
78
         *
79
         * @param \Aimeos\MShop\Order\Item\Iface $order Shop basket object
80
         * @return array Associative list of basket product positions as keys and the error codes as values
81
         */
82
        protected function checkStock( \Aimeos\MShop\Order\Item\Iface $order ) : array
83
        {
84
                $context = $this->context();
14✔
85
                $siteIds = $context->locale()->getSitePath();
14✔
86

87
                $manager = \Aimeos\MShop::create( $context, 'stock' );
14✔
88
                $filter = $manager->filter();
14✔
89
                $expr = $stockMap = [];
14✔
90

91
                foreach( $order->getProducts() as $orderProduct )
14✔
92
                {
93
                        $expr[] = $filter->and( [
14✔
94
                                // use stocks from parent sites if none for the site the product is from is available
95
                                $filter->is( 'stock.siteid', '==', array_merge( $siteIds, [$orderProduct->getSiteId()] ) ),
14✔
96
                                $filter->is( 'stock.productid', '==', $orderProduct->getProductId() ),
14✔
97
                                $filter->is( 'stock.type', '==', $orderProduct->getStockType() )
14✔
98
                        ] );
14✔
99
                }
100

101
                // @phpstan-ignore argument.type
102
                $filter->add( $filter->or( $expr ) )->slice( 0, 0x7fffffff );
14✔
103

104
                foreach( $manager->search( $filter ) as $item ) {
14✔
105
                        $stockMap[$item->getSiteId()][$item->getProductId()][$item->getType()] = $item;
12✔
106
                }
107

108
                return $this->checkStockLevels( $order, $stockMap );
14✔
109
        }
110

111

112
        /**
113
         * Checks if the products in the basket have enough stock
114
         *
115
         * Removes products from the basket which are out of stock and decreases the
116
         * quantities of orders products if there's not enough stock.
117
         *
118
         * @param \Aimeos\MShop\Order\Item\Iface $order Shop basket object
119
         * @param array $stockMap Multi-dimensional associative list of product ID / stock type as keys and stock level as values
120
         * @return array Associative list of basket positions as keys and error codes as values
121
         */
122
        protected function checkStockLevels( \Aimeos\MShop\Order\Item\Iface $order, array $stockMap ) : array
123
        {
124
                $outOfStock = [];
14✔
125
                $products = $order->getProducts();
14✔
126
                $siteIds = $this->context()->locale()->getSitePath();
14✔
127

128
                foreach( $products as $pos => $orderProduct )
14✔
129
                {
130
                        $stocklevel = 0;
14✔
131
                        $type = $orderProduct->getStockType();
14✔
132
                        $prodid = $orderProduct->getProductId();
14✔
133

134
                        foreach( array_merge( $siteIds, [$orderProduct->getSiteId()] ) as $siteid )
14✔
135
                        {
136
                                if( isset( $stockMap[$siteid][$prodid][$type] ) )
14✔
137
                                {
138
                                        $stockItem = $stockMap[$siteid][$prodid][$type];
12✔
139
                                        $orderProduct->setTimeFrame( $stockItem->getTimeFrame() );
12✔
140

141
                                        if( ( $stocklevel = $stockItem->getStockLevel() ) === null ) {
12✔
142
                                                continue 2;
1✔
143
                                        }
144

145
                                        if( $stocklevel >= $orderProduct->getQuantity() )
11✔
146
                                        {
147
                                                $stock = $stockItem->getStockLevel() - $orderProduct->getQuantity();
11✔
148
                                                $stockItem->setStockLevel( $stock );
11✔
149
                                                continue 2;
11✔
150
                                        }
151
                                }
152
                        }
153

154
                        if( $stocklevel > 0 ) { // update quantity to actual stock level
2✔
155
                                // @phpstan-ignore argument.type, argument.type
NEW
156
                                $order->addProduct( $orderProduct->setQuantity( $stocklevel ), (int) $pos );
×
157
                        } else {
158
                                // @phpstan-ignore argument.type
159
                                $order->deleteProduct( (int) $pos );
2✔
160
                        }
161

162
                        $outOfStock[$pos] = 'stock.notenough';
2✔
163
                }
164

165
                return $outOfStock;
14✔
166
        }
167
}
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