Passed
Push — master ( 153942...a6851b )
by Aimeos
08:33
created

Standard::getSuggestions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 3
nop 8
dl 0
loc 28
rs 9.7666
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Standard::size() 0 17 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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-2022
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
	/** controller/jobs/product/bought/name
26
	 * Class name of the used product suggestions scheduler controller implementation
27
	 *
28
	 * Each default job controller can be replace by an alternative imlementation.
29
	 * To use this implementation, you have to set the last part of the class
30
	 * name as configuration value so the controller factory knows which class it
31
	 * has to instantiate.
32
	 *
33
	 * For example, if the name of the default class is
34
	 *
35
	 *  \Aimeos\Controller\Jobs\Product\Bought\Standard
36
	 *
37
	 * and you want to replace it with your own version named
38
	 *
39
	 *  \Aimeos\Controller\Jobs\Product\Bought\Myalgorithm
40
	 *
41
	 * then you have to set the this configuration option:
42
	 *
43
	 *  controller/jobs/product/bought/name = Myalgorithm
44
	 *
45
	 * The value is the last part of your own class name and it's case sensitive,
46
	 * so take care that the configuration value is exactly named like the last
47
	 * part of the class name.
48
	 *
49
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
50
	 * characters are possible! You should always start the last part of the class
51
	 * name with an upper case character and continue only with lower case characters
52
	 * or numbers. Avoid chamel case names like "MyOptimizer"!
53
	 *
54
	 * @param string Last part of the class name
55
	 * @since 2014.03
56
	 * @category Developer
57
	 */
58
59
	/** controller/jobs/product/bought/decorators/excludes
60
	 * Excludes decorators added by the "common" option from the product bought job controller
61
	 *
62
	 * Decorators extend the functionality of a class by adding new aspects
63
	 * (e.g. log what is currently done), executing the methods of the underlying
64
	 * class only in certain conditions (e.g. only for logged in users) or
65
	 * modify what is returned to the caller.
66
	 *
67
	 * This option allows you to remove a decorator added via
68
	 * "controller/jobs/common/decorators/default" before they are wrapped
69
	 * around the job controller.
70
	 *
71
	 *  controller/jobs/product/bought/decorators/excludes = array( 'decorator1' )
72
	 *
73
	 * This would remove the decorator named "decorator1" from the list of
74
	 * common decorators ("\Aimeos\Controller\Jobs\Common\Decorator\*") added via
75
	 * "controller/jobs/common/decorators/default" to the job controller.
76
	 *
77
	 * @param array List of decorator names
78
	 * @since 2014.03
79
	 * @category Developer
80
	 * @see controller/jobs/common/decorators/default
81
	 * @see controller/jobs/product/bought/decorators/global
82
	 * @see controller/jobs/product/bought/decorators/local
83
	 */
84
85
	/** controller/jobs/product/bought/decorators/global
86
	 * Adds a list of globally available decorators only to the product bought job controller
87
	 *
88
	 * Decorators extend the functionality of a class by adding new aspects
89
	 * (e.g. log what is currently done), executing the methods of the underlying
90
	 * class only in certain conditions (e.g. only for logged in users) or
91
	 * modify what is returned to the caller.
92
	 *
93
	 * This option allows you to wrap global decorators
94
	 * ("\Aimeos\Controller\Jobs\Common\Decorator\*") around the job controller.
95
	 *
96
	 *  controller/jobs/product/bought/decorators/global = array( 'decorator1' )
97
	 *
98
	 * This would add the decorator named "decorator1" defined by
99
	 * "\Aimeos\Controller\Jobs\Common\Decorator\Decorator1" only to the job controller.
100
	 *
101
	 * @param array List of decorator names
102
	 * @since 2014.03
103
	 * @category Developer
104
	 * @see controller/jobs/common/decorators/default
105
	 * @see controller/jobs/product/bought/decorators/excludes
106
	 * @see controller/jobs/product/bought/decorators/local
107
	 */
108
109
	/** controller/jobs/product/bought/decorators/local
110
	 * Adds a list of local decorators only to the product bought job controller
111
	 *
112
	 * Decorators extend the functionality of a class by adding new aspects
113
	 * (e.g. log what is currently done), executing the methods of the underlying
114
	 * class only in certain conditions (e.g. only for logged in users) or
115
	 * modify what is returned to the caller.
116
	 *
117
	 * This option allows you to wrap local decorators
118
	 * ("\Aimeos\Controller\Jobs\Product\Bought\Decorator\*") around the job
119
	 * controller.
120
	 *
121
	 *  controller/jobs/product/bought/decorators/local = array( 'decorator2' )
122
	 *
123
	 * This would add the decorator named "decorator2" defined by
124
	 * "\Aimeos\Controller\Jobs\Product\Bought\Decorator\Decorator2"
125
	 * only to the job controller.
126
	 *
127
	 * @param array List of decorator names
128
	 * @since 2014.03
129
	 * @category Developer
130
	 * @see controller/jobs/common/decorators/default
131
	 * @see controller/jobs/product/bought/decorators/excludes
132
	 * @see controller/jobs/product/bought/decorators/global
133
	 */
134
135
136
	/**
137
	 * Returns the localized name of the job.
138
	 *
139
	 * @return string Name of the job
140
	 */
141
	public function getName() : string
142
	{
143
		return $this->context()->translate( 'controller/jobs', 'Products bought together' );
144
	}
145
146
147
	/**
148
	 * Returns the localized description of the job.
149
	 *
150
	 * @return string Description of the job
151
	 */
152
	public function getDescription() : string
153
	{
154
		return $this->context()->translate( 'controller/jobs', 'Creates bought together product suggestions' );
155
	}
156
157
158
	/**
159
	 * Executes the job.
160
	 *
161
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
162
	 */
163
	public function run()
164
	{
165
		if( empty( $total = $this->total() ) ) {
166
			return;
167
		}
168
169
		$manager = \Aimeos\MShop::create( $this->context(), 'product' );
170
		$domains = $this->domains();
171
		$size = $this->size();
172
173
		$counts = $this->counts();
174
		$ids = $counts->keys();
175
		$start = 0;
176
177
		while( !( $prodIds = $ids->slice( $start, $size ) )->isEmpty() )
178
		{
179
			$filter = $manager->filter()->add( 'product.id', '==', $prodIds )->slice( 0, 0x7fffffff );
180
			$products = $manager->search( $filter, $domains );
181
182
			foreach( $counts as $id => $count )
183
			{
184
				if( $item = $products->get( $id ) ) {
185
					$this->update( $item, $prodIds, $total, $count );
186
				}
187
			}
188
189
			$manager->save( $products );
190
			$start += $size;
191
		}
192
	}
193
194
195
	/**
196
	 * Returns the minimum confidence value for high quality suggestions
197
	 *
198
	 * @return float Minimum confidence value from 0 to 1
199
	 */
200
	protected function confidence() : float
201
	{
202
		/** controller/jobs/product/bought/min-confidence
203
		 * Minimum confidence value for high quality suggestions
204
		 *
205
		 * The confidence value is used to remove low quality suggestions. Using
206
		 * a confidence value of 0.95 would only suggest product combinations
207
		 * that are almost always bought together. Contrary, a value of 0.1 would
208
		 * yield a lot of combinations that are bought together only in very rare
209
		 * cases.
210
		 *
211
		 * To get good product suggestions, the value should be at least above
212
		 * 0.5 and the higher the value, the better the suggestions. You can
213
		 * either increase the default value to get better suggestions or lower
214
		 * the value to get more suggestions per product if you have only a few
215
		 * ones in total.
216
		 *
217
		 * @param float Minimum confidence value from 0 to 1
218
		 * @since 2014.09
219
		 * @category Developer
220
		 * @category User
221
		 * @see controller/jobs/product/bought/max-items
222
		 * @see controller/jobs/product/bought/min-support
223
		 * @see controller/jobs/product/bought/limit-days
224
		 * @see controller/jobs/product/bought/size
225
		 */
226
		return $this->context()->config()->get( 'controller/jobs/product/bought/min-confidence', 0.66 );
227
	}
228
229
230
	/**
231
	 * Returns how often the product has been bought
232
	 *
233
	 * @return \Aimeos\Map Map of product IDs as keys and count as values
234
	 */
235
	protected function counts() : \Aimeos\Map
236
	{
237
		$manager = \Aimeos\MShop::create( $this->context(), 'order/base/product' );
238
		$filter = $manager->filter()->add( 'order.base.product.ctime', '>', $this->ctime() )->slice( 0, 0x7fffffff );
239
240
		return $manager->aggregate( $filter, 'order.base.product.productid' );
241
	}
242
243
244
	/**
245
	 * Returns the date of the oldest ordered product to use
246
	 *
247
	 * @return string Date of the oldest ordered product to use
248
	 */
249
	protected function ctime() : string
250
	{
251
		/** controller/jobs/product/bought/limit-days
252
		 * Only use orders placed in the past within the configured number of days for calculating bought together products
253
		 *
254
		 * This option limits the orders that are evaluated for calculating the
255
		 * bought together products. Only ordered products that were bought by
256
		 * customers within the configured number of days are used.
257
		 *
258
		 * Limiting the orders taken into account to the last ones increases the
259
		 * quality of suggestions if customer interests shifts to new products.
260
		 * If you only have a few orders per month, you can also increase this
261
		 * value to several years to get enough suggestions. Please keep in mind
262
		 * that the more orders are evaluated, the longer the it takes to
263
		 * calculate the product combinations.
264
		 *
265
		 * @param integer Number of days
266
		 * @since 2014.09
267
		 * @category User
268
		 * @category Developer
269
		 * @see controller/jobs/product/bought/max-items
270
		 * @see controller/jobs/product/bought/min-support
271
		 * @see controller/jobs/product/bought/min-confidence
272
		 * @see controller/jobs/product/bought/size
273
		 */
274
		$days = $this->context()->config()->get( 'controller/jobs/product/bought/limit-days', 360 );
275
		return date( 'Y-m-d H:i:s', time() - $days * 86400 );
276
	}
277
278
279
	/**
280
	 * Returns the domain names to fetch for each product
281
	 *
282
	 * @return array Domain names to fetch for each product
283
	 */
284
	protected function domains() : array
285
	{
286
		return $this->context()->config()->get( 'mshop/product/manager/domains', [] ) + ['product'];
287
	}
288
289
290
	/**
291
	 * Returns the maximum number of suggested items per product
292
	 *
293
	 * @return int Number of suggested products
294
	 */
295
	protected function max() : int
296
	{
297
		/** controller/jobs/product/bought/max-items
298
		 * Maximum number of suggested items per product
299
		 *
300
		 * Each product can contain zero or more suggested products based on
301
		 * the used algorithm. The maximum number of items limits the quantity
302
		 * of products that are associated as suggestions to one product.
303
		 * Usually, you don't need more products than shown in the product
304
		 * detail view as suggested products.
305
		 *
306
		 * @param integer Number of suggested products
307
		 * @since 2014.09
308
		 * @category Developer
309
		 * @category User
310
		 * @see controller/jobs/product/bought/min-support
311
		 * @see controller/jobs/product/bought/min-confidence
312
		 * @see controller/jobs/product/bought/limit-days
313
		 * @see controller/jobs/product/bought/size
314
		 */
315
		return $this->context()->config()->get( 'controller/jobs/product/bought/max-items', 5 );
316
	}
317
318
319
	/**
320
	 * Returns the relative counts for the given product IDs
321
	 *
322
	 * @param string $id Product ID to calculate the suggestions for
323
	 * @param iterable $prodIds List of product IDs to create suggestions for
324
	 * @return \Aimeos\Map Map with the product IDs as keys and the relative counts as values
325
	 */
326
	protected function relative( string $id, iterable $prodIds ) : \Aimeos\Map
327
	{
328
		$manager = \Aimeos\MShop::create( $this->context(), 'order/base/product' );
329
330
		$search = $manager->filter();
331
		$search->add( $search->and( [
332
			$search->is( 'order.base.product.productid', '==', $prodIds ),
333
			$search->is( 'order.base.product.ctime', '>', $this->ctime() ),
334
			$search->is( $search->make( 'order.base.product.count', [$id] ), '==', 1 ),
335
		] ) );
336
337
		return $manager->aggregate( $search, 'order.base.product.productid' )->remove( $id );
338
	}
339
340
341
	/**
342
	 * Returns the number of items processed at once
343
	 *
344
	 * @return int Number of items processed at once
345
	 */
346
	protected function size() : int
347
	{
348
		/** controller/jobs/product/bought/size
349
		 * Number of items processed at once
350
		 *
351
		 * The items which are bought together are processed in batches to reduce
352
		 * the time needed for associating all items. Higher numbers can improve
353
		 * the speed while requiring more memory.
354
		 *
355
		 * @param integer Number of items processed at once
356
		 * @since 2023.01
357
		 * @see controller/jobs/product/bought/max-items
358
		 * @see controller/jobs/product/bought/min-support
359
		 * @see controller/jobs/product/bought/min-confidence
360
		 * @see controller/jobs/product/bought/limit-days
361
		 */
362
		return $this->context()->config()->get( 'controller/jobs/product/bought/size', 100 );
363
	}
364
365
366
	/**
367
	 * Returns the IDs of the suggested products.
368
	 *
369
	 * @param string $id Product ID to calculate the suggestions for
370
	 * @param iterable $prodIds List of product IDs to create suggestions for
371
	 * @param int $count Number of ordered products
372
	 * @param int $total Total number of orders
373
	 * @return \Aimeos\Map List of suggested product IDs
374
	 */
375
	protected function suggest( string $id, iterable $prodIds, int $count, int $total ) : \Aimeos\Map
376
	{
377
		$products = [];
378
		$supportA = $count / $total;
379
380
		$minSupport = $this->support();
381
		$minConfidence = $this->confidence();
382
383
		foreach( $this->relative( $id, $prodIds ) as $prodId => $relCnt )
384
		{
385
			$supportAB = $relCnt / $total;
386
387
			if( $supportAB > $minSupport && ( $conf = ( $supportAB / $supportA ) ) > $minConfidence ) {
388
				$products[$prodId] = $conf;
389
			}
390
		}
391
392
		return map( $products )->arsort()->keys();
393
	}
394
395
396
	/**
397
	 * Returns the minimum support value to sort out irrelevant combinations
398
	 *
399
	 * @return float Minimum support value from 0 to 1
400
	 */
401
	protected function support() : float
402
	{
403
		/** controller/jobs/product/bought/min-support
404
		 * Minimum support value to sort out all irrelevant combinations
405
		 *
406
		 * A minimum support value of 0.02 requires the combination of two
407
		 * products to be in at least 2% of all orders to be considered relevant
408
		 * enough as product suggestion.
409
		 *
410
		 * You can tune this value for your needs, e.g. if you sell several
411
		 * thousands different products and you have only a few suggestions for
412
		 * all products, a lower value might work better for you. The other way
413
		 * round, if you sell less than thousand different products, you may
414
		 * have a lot of product suggestions of low quality. In this case it's
415
		 * better to increase this value, e.g. to 0.05 or higher.
416
		 *
417
		 * Caution: Decreasing the support to lower values than 0.01 exponentially
418
		 * increases the time for generating the suggestions. If your database
419
		 * contains a lot of orders, the time to complete the job may rise from
420
		 * hours to days!
421
		 *
422
		 * @param float Minimum support value from 0 to 1
423
		 * @since 2014.09
424
		 * @category Developer
425
		 * @category User
426
		 * @see controller/jobs/product/bought/max-items
427
		 * @see controller/jobs/product/bought/min-confidence
428
		 * @see controller/jobs/product/bought/limit-days
429
		 * @see controller/jobs/product/bought/size
430
		 */
431
		return $this->context()->config()->get( 'controller/jobs/product/bought/min-support', 0.02 );
432
	}
433
434
435
	/**
436
	 * Returns the total number of orders available
437
	 *
438
	 * @return int Total number of orders
439
	 */
440
	protected function total() : int
441
	{
442
		$total = 0;
443
444
		$manager = \Aimeos\MShop::create( $this->context(), 'order/base' );
445
		$filter = $manager->filter()->add( 'order.base.ctime', '>', $this->ctime() )->slice( 0, 0 );
446
		$manager->search( $filter, [], $total )->all();
447
448
		return $total;
449
	}
450
451
452
	/**
453
	 * Updates the products bought together for the given item
454
	 *
455
	 * @param \Aimeos\MShop\Product\Item\Iface $item Product item to update
456
	 * @param iterable $prodIds List of product IDs to create suggestions for
457
	 * @param int $count Number of ordered products
458
	 * @param int $total Total number of orders
459
	 * return \Aimeos\MShop\Product\Item\Iface Updated product item
460
	 */
461
	protected function update( \Aimeos\MShop\Product\Item\Iface $item, iterable $prodIds, int $count, int $total ) : \Aimeos\MShop\Product\Item\Iface
462
	{
463
		$listItems = $item->getListItems( 'product', 'bought-together' );
464
465
		if( $count / $total > $this->support() )
466
		{
467
			$productIds = $this->suggest( $item->getId(), $prodIds, $count, $total )->slice( 0, $this->max() );
0 ignored issues
show
Bug introduced by
It seems like $item->getId() can also be of type null; however, parameter $id of Aimeos\Controller\Jobs\P...ght\Standard::suggest() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

467
			$productIds = $this->suggest( /** @scrutinizer ignore-type */ $item->getId(), $prodIds, $count, $total )->slice( 0, $this->max() );
Loading history...
468
469
			foreach( $productIds as $pid )
470
			{
471
				$litem = $item->getListItem( 'product', 'bought-together', $pid, false ) ?: $manager->createListItem();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $manager seems to be never defined.
Loading history...
472
				$item->addListItem( 'product', $litem->setType( 'bought-together' )->setRefId( $pid ) );
473
				$listItems->remove( $litem->getId() );
0 ignored issues
show
Bug introduced by
It seems like $litem->getId() can also be of type string; however, parameter $keys of Aimeos\Map::remove() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

473
				$listItems->remove( /** @scrutinizer ignore-type */ $litem->getId() );
Loading history...
474
			}
475
		}
476
477
		return $item->deleteListItems( $listItems );
478
	}
479
}
480