Passed
Push — master ( 417598...532336 )
by Aimeos
03:26
created

Standard::addListItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 20
rs 9.8333
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2014
6
 * @copyright Aimeos (aimeos.org), 2015-2021
7
 * @package Controller
8
 * @subpackage Jobs
9
 */
10
11
12
namespace Aimeos\Controller\Jobs\Product\Bought;
13
14
15
/**
16
 * Job controller for bought together products.
17
 *
18
 * @package Controller
19
 * @subpackage Jobs
20
 */
21
class Standard
22
	extends \Aimeos\Controller\Jobs\Base
23
	implements \Aimeos\Controller\Jobs\Iface
24
{
25
	/**
26
	 * Returns the localized name of the job.
27
	 *
28
	 * @return string Name of the job
29
	 */
30
	public function getName() : string
31
	{
32
		return $this->context()->translate( 'controller/jobs', 'Products bought together' );
33
	}
34
35
36
	/**
37
	 * Returns the localized description of the job.
38
	 *
39
	 * @return string Description of the job
40
	 */
41
	public function getDescription() : string
42
	{
43
		return $this->context()->translate( 'controller/jobs', 'Creates bought together product suggestions' );
44
	}
45
46
47
	/**
48
	 * Executes the job.
49
	 *
50
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
51
	 */
52
	public function run()
53
	{
54
		$context = $this->context();
55
		$config = $context->config();
56
57
58
		/** controller/jobs/product/bought/max-items
59
		 * Maximum number of suggested items per product
60
		 *
61
		 * Each product can contain zero or more suggested products based on
62
		 * the used algorithm. The maximum number of items limits the quantity
63
		 * of products that are associated as suggestions to one product.
64
		 * Usually, you don't need more products than shown in the product
65
		 * detail view as suggested products.
66
		 *
67
		 * @param integer Number of suggested products
68
		 * @since 2014.09
69
		 * @category Developer
70
		 * @category User
71
		 * @see controller/jobs/product/bought/min-support
72
		 * @see controller/jobs/product/bought/min-confidence
73
		 * @see controller/jobs/product/bought/limit-days
74
		 */
75
		$maxItems = $config->get( 'controller/jobs/product/bought/max-items', 5 );
76
77
		/** controller/jobs/product/bought/min-support
78
		 * Minimum support value to sort out all irrelevant combinations
79
		 *
80
		 * A minimum support value of 0.02 requires the combination of two
81
		 * products to be in at least 2% of all orders to be considered relevant
82
		 * enough as product suggestion.
83
		 *
84
		 * You can tune this value for your needs, e.g. if you sell several
85
		 * thousands different products and you have only a few suggestions for
86
		 * all products, a lower value might work better for you. The other way
87
		 * round, if you sell less than thousand different products, you may
88
		 * have a lot of product suggestions of low quality. In this case it's
89
		 * better to increase this value, e.g. to 0.05 or higher.
90
		 *
91
		 * Caution: Decreasing the support to lower values than 0.01 exponentially
92
		 * increases the time for generating the suggestions. If your database
93
		 * contains a lot of orders, the time to complete the job may rise from
94
		 * hours to days!
95
		 *
96
		 * @param float Minimum support value from 0 to 1
97
		 * @since 2014.09
98
		 * @category Developer
99
		 * @category User
100
		 * @see controller/jobs/product/bought/max-items
101
		 * @see controller/jobs/product/bought/min-confidence
102
		 * @see controller/jobs/product/bought/limit-days
103
		 */
104
		$minSupport = $config->get( 'controller/jobs/product/bought/min-support', 0.02 );
105
106
		/** controller/jobs/product/bought/min-confidence
107
		 * Minimum confidence value for high quality suggestions
108
		 *
109
		 * The confidence value is used to remove low quality suggestions. Using
110
		 * a confidence value of 0.95 would only suggest product combinations
111
		 * that are almost always bought together. Contrary, a value of 0.1 would
112
		 * yield a lot of combinations that are bought together only in very rare
113
		 * cases.
114
		 *
115
		 * To get good product suggestions, the value should be at least above
116
		 * 0.5 and the higher the value, the better the suggestions. You can
117
		 * either increase the default value to get better suggestions or lower
118
		 * the value to get more suggestions per product if you have only a few
119
		 * ones in total.
120
		 *
121
		 * @param float Minimum confidence value from 0 to 1
122
		 * @since 2014.09
123
		 * @category Developer
124
		 * @category User
125
		 * @see controller/jobs/product/bought/max-items
126
		 * @see controller/jobs/product/bought/min-support
127
		 * @see controller/jobs/product/bought/limit-days
128
		 */
129
		$minConfidence = $config->get( 'controller/jobs/product/bought/min-confidence', 0.66 );
130
131
		/** controller/jobs/product/bought/limit-days
132
		 * Only use orders placed in the past within the configured number of days for calculating bought together products
133
		 *
134
		 * This option limits the orders that are evaluated for calculating the
135
		 * bought together products. Only ordered products that were bought by
136
		 * customers within the configured number of days are used.
137
		 *
138
		 * Limiting the orders taken into account to the last ones increases the
139
		 * quality of suggestions if customer interests shifts to new products.
140
		 * If you only have a few orders per month, you can also increase this
141
		 * value to several years to get enough suggestions. Please keep in mind
142
		 * that the more orders are evaluated, the longer the it takes to
143
		 * calculate the product combinations.
144
		 *
145
		 * @param integer Number of days
146
		 * @since 2014.09
147
		 * @category User
148
		 * @category Developer
149
		 * @see controller/jobs/product/bought/max-items
150
		 * @see controller/jobs/product/bought/min-support
151
		 * @see controller/jobs/product/bought/min-confidence
152
		 */
153
		$days = $config->get( 'controller/jobs/product/bought/limit-days', 180 );
154
		$date = date( 'Y-m-d H:i:s', time() - $days * 86400 );
155
156
		$domains = [
157
			'attribute', 'catalog', 'media', 'media/property', 'price',
158
			'product', 'product/property', 'supplier', 'text'
159
		];
160
161
162
		$manager = \Aimeos\MShop::create( $context, 'product' );
163
		$baseManager = \Aimeos\MShop::create( $context, 'order/base' );
164
		$baseProductManager = \Aimeos\MShop::create( $context, 'order/base/product' );
165
166
		$search = $baseProductManager->filter()->add( 'order.base.product.ctime', '>', $date );
167
		$filter = $baseManager->filter()->add( 'order.base.ctime', '>', $date )->slice( 0, 0 );
168
169
		$start = $total = 0;
170
		$baseManager->search( $filter, [], $total );
171
172
		do
173
		{
174
			$counts = $baseProductManager->aggregate( $search, 'order.base.product.productid' );
175
			$prodIds = $counts->keys()->all();
176
			$products = $manager->search( $manager->filter()->add( 'product.id', '==', $prodIds ), $domains );
177
178
			foreach( $counts as $id => $count )
179
			{
180
				if( ( $item = $products->get( $id ) ) === null ) {
181
					continue;
182
				}
183
184
				$listItems = $item->getListItems( 'product', 'bought-together' );
185
186
				if( $count / $total > $minSupport )
187
				{
188
					$productIds = $this->getSuggestions( $id, $prodIds, $count, $total, $maxItems,
189
						$minSupport, $minConfidence, $date );
190
191
					foreach( $productIds as $pid )
192
					{
193
						$litem = $item->getListItem( 'product', 'bought-together', $pid ) ?: $manager->createListItem();
0 ignored issues
show
Bug introduced by
The method createListItem() does not exist on Aimeos\MShop\Common\Manager\Iface. Did you maybe mean create()? ( Ignorable by Annotation )

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

193
						$litem = $item->getListItem( 'product', 'bought-together', $pid ) ?: $manager->/** @scrutinizer ignore-call */ createListItem();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
194
						$item->addListItem( 'product', $litem->setRefId( $pid ) );
195
						$listItems->remove( $litem->getId() );
196
					}
197
				}
198
199
				$item->deleteListItems( $listItems );
200
			}
201
202
			$manager->save( $products );
203
204
			$count = count( $counts );
205
			$start += $count;
206
			$search->slice( $start );
207
		}
208
		while( $count >= $search->getLimit() );
209
	}
210
211
212
	/**
213
	 * Returns the IDs of the suggested products.
214
	 *
215
	 * @param string $id Product ID to calculate the suggestions for
216
	 * @param string[] $prodIds List of product IDs to create suggestions for
217
	 * @param int $count Number of ordered products
218
	 * @param int $total Total number of orders
219
	 * @param int $maxItems Maximum number of suggestions
220
	 * @param float $minSupport Minium support value for calculating the suggested products
221
	 * @param float $minConfidence Minium confidence value for calculating the suggested products
222
	 * @param string $date Date in YYYY-MM-DD HH:mm:ss format after which orders should be used for calculations
223
	 * @return array List of suggested product IDs as key and their confidence as value
224
	 */
225
	protected function getSuggestions( string $id, array $prodIds, int $count, int $total, int $maxItems,
226
		float $minSupport, float $minConfidence, string $date ) : array
227
	{
228
		$baseProductManager = \Aimeos\MShop::create( $this->context(), 'order/base/product' );
229
230
		$search = $baseProductManager->filter();
231
		$search->add( $search->and( [
232
			$search->is( 'order.base.product.productid', '==', $prodIds ),
233
			$search->is( 'order.base.product.ctime', '>', $date ),
234
			$search->is( $search->make( 'order.base.product.count', [(string) $id] ), '==', 1 ),
235
		] ) );
236
		$relativeCounts = $baseProductManager->aggregate( $search, 'order.base.product.productid' );
237
238
239
		unset( $relativeCounts[$id] );
240
		$supportA = $count / $total;
241
		$products = [];
242
243
		foreach( $relativeCounts as $prodId => $relCnt )
244
		{
245
			$supportAB = $relCnt / $total;
246
247
			if( $supportAB > $minSupport && ( $conf = ( $supportAB / $supportA ) ) > $minConfidence ) {
248
				$products[$prodId] = $conf;
249
			}
250
		}
251
252
		arsort( $products );
253
254
		return array_keys( array_slice( $products, 0, $maxItems, true ) );
255
	}
256
}
257