Passed
Push — master ( e09dab...d2fcbc )
by Aimeos
03:00
created

Standard::getCodePosition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2017-2022
6
 * @package Controller
7
 * @subpackage Jobs
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Coupon\Import\Csv\Code;
12
13
14
/**
15
 * Job controller for CSV coupon imports.
16
 *
17
 * @package Controller
18
 * @subpackage Jobs
19
 */
20
class Standard
21
	extends \Aimeos\Controller\Common\Coupon\Import\Csv\Base
22
	implements \Aimeos\Controller\Jobs\Iface
23
{
24
	/** controller/jobs/coupon/import/csv/code/name
25
	 * Class name of the used coupon code import job controller implementation
26
	 *
27
	 * Each default job controller can be replace by an alternative imlementation.
28
	 * To use this implementation, you have to set the last part of the class
29
	 * name as configuration value so the controller factory knows which class it
30
	 * has to instantiate.
31
	 *
32
	 * For example, if the name of the default class is
33
	 *
34
	 *  \Aimeos\Controller\Jobs\Coupon\Import\Csv\Code\Standard
35
	 *
36
	 * and you want to replace it with your own version named
37
	 *
38
	 *  \Aimeos\Controller\Jobs\Coupon\Import\Csv\Code\Mycsv
39
	 *
40
	 * then you have to set the this configuration option:
41
	 *
42
	 *  controller/jobs/coupon/import/csv/code/name = Mycsv
43
	 *
44
	 * The value is the last part of your own class name and it's case sensitive,
45
	 * so take care that the configuration value is exactly named like the last
46
	 * part of the class name.
47
	 *
48
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
49
	 * characters are possible! You should always start the last part of the class
50
	 * name with an upper case character and continue only with lower case characters
51
	 * or numbers. Avoid chamel case names like "MyCsv"!
52
	 *
53
	 * @param string Last part of the class name
54
	 * @since 2017.10
55
	 * @category Developer
56
	 */
57
58
	/** controller/jobs/coupon/import/csv/code/decorators/excludes
59
	 * Excludes decorators added by the "common" option from the coupon code import CSV job controller
60
	 *
61
	 * Decorators extend the functionality of a class by adding new aspects
62
	 * (e.g. log what is currently done), executing the methods of the underlying
63
	 * class only in certain conditions (e.g. only for logged in users) or
64
	 * modify what is returned to the caller.
65
	 *
66
	 * This option allows you to remove a decorator added via
67
	 * "controller/jobs/common/decorators/default" before they are wrapped
68
	 * around the job controller.
69
	 *
70
	 *  controller/jobs/coupon/import/csv/code/decorators/excludes = array( 'decorator1' )
71
	 *
72
	 * This would remove the decorator named "decorator1" from the list of
73
	 * common decorators ("\Aimeos\Controller\Jobs\Common\Decorator\*") added via
74
	 * "controller/jobs/common/decorators/default" to the job controller.
75
	 *
76
	 * @param array List of decorator names
77
	 * @since 2017.10
78
	 * @category Developer
79
	 * @see controller/jobs/common/decorators/default
80
	 * @see controller/jobs/coupon/import/csv/code/decorators/global
81
	 * @see controller/jobs/coupon/import/csv/code/decorators/local
82
	 */
83
84
	/** controller/jobs/coupon/import/csv/code/decorators/global
85
	 * Adds a list of globally available decorators only to the coupon code import CSV job controller
86
	 *
87
	 * Decorators extend the functionality of a class by adding new aspects
88
	 * (e.g. log what is currently done), executing the methods of the underlying
89
	 * class only in certain conditions (e.g. only for logged in users) or
90
	 * modify what is returned to the caller.
91
	 *
92
	 * This option allows you to wrap global decorators
93
	 * ("\Aimeos\Controller\Jobs\Common\Decorator\*") around the job controller.
94
	 *
95
	 *  controller/jobs/coupon/import/csv/code/decorators/global = array( 'decorator1' )
96
	 *
97
	 * This would add the decorator named "decorator1" defined by
98
	 * "\Aimeos\Controller\Jobs\Common\Decorator\Decorator1" only to the job controller.
99
	 *
100
	 * @param array List of decorator names
101
	 * @since 2017.10
102
	 * @category Developer
103
	 * @see controller/jobs/common/decorators/default
104
	 * @see controller/jobs/coupon/import/csv/code/decorators/excludes
105
	 * @see controller/jobs/coupon/import/csv/code/decorators/local
106
	 */
107
108
	/** controller/jobs/coupon/import/csv/code/decorators/local
109
	 * Adds a list of local decorators only to the coupon code import CSV job controller
110
	 *
111
	 * Decorators extend the functionality of a class by adding new aspects
112
	 * (e.g. log what is currently done), executing the methods of the underlying
113
	 * class only in certain conditions (e.g. only for logged in users) or
114
	 * modify what is returned to the caller.
115
	 *
116
	 * This option allows you to wrap local decorators
117
	 * ("\Aimeos\Controller\Jobs\Coupon\Import\Csv\Code\Decorator\*") around the job
118
	 * controller.
119
	 *
120
	 *  controller/jobs/coupon/import/csv/code/decorators/local = array( 'decorator2' )
121
	 *
122
	 * This would add the decorator named "decorator2" defined by
123
	 * "\Aimeos\Controller\Jobs\Coupon\Import\Csv\Code\Decorator\Decorator2"
124
	 * only to the job controller.
125
	 *
126
	 * @param array List of decorator names
127
	 * @since 2017.10
128
	 * @category Developer
129
	 * @see controller/jobs/common/decorators/default
130
	 * @see controller/jobs/coupon/import/csv/code/decorators/excludes
131
	 * @see controller/jobs/coupon/import/csv/code/decorators/global
132
	 */
133
134
135
	/**
136
	 * Returns the localized name of the job.
137
	 *
138
	 * @return string Name of the job
139
	 */
140
	public function getName() : string
141
	{
142
		return $this->context()->translate( 'controller/jobs', 'Coupon code import CSV' );
143
	}
144
145
146
	/**
147
	 * Returns the localized description of the job.
148
	 *
149
	 * @return string Description of the job
150
	 */
151
	public function getDescription() : string
152
	{
153
		return $this->context()->translate( 'controller/jobs', 'Imports new and updates existing coupon code from CSV files' );
154
	}
155
156
157
	/**
158
	 * Executes the job.
159
	 *
160
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
161
	 */
162
	public function run()
163
	{
164
		$fcn = function( \Aimeos\MShop\ContextIface $context, \Aimeos\MW\Container\Iface $container, $couponId, $path ) {
165
			$this->process( $context, $container, $couponId, $path );
166
		};
167
168
		try
169
		{
170
			$context = $this->context();
171
			$process = $context->process();
172
			$fs = $context->fs( 'fs-import' );
173
			$dir = 'couponcode/' . $context->locale()->getSiteItem()->getCode();
174
175
			if( $fs->isDir( $dir ) === false ) {
176
				return;
177
			}
178
179
			foreach( $fs->scan( $dir ) as $filename )
180
			{
181
				if( $filename == '.' || $filename == '..' ) {
182
					continue;
183
				}
184
185
				$path = $dir . '/' . $filename;
186
				list( $couponId,) = explode( '.', $filename );
187
				$container = $this->getContainer( $fs->readf( $path ) );
188
189
				$process->start( $fcn, [$context, $container, $couponId, $path] );
190
			}
191
192
			$process->wait();
193
		}
194
		catch( \Exception $e )
195
		{
196
			$context->logger()->error( 'Coupon import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString(), 'import/csv/coupon/code' );
197
			$this->mail( 'Coupon CSV import error', $e->getMessage() . "\n" . $e->getTraceAsString() );
198
			throw $e;
199
		}
200
	}
201
202
203
	/**
204
	 * Returns the position of the "coupon.code" column from the coupon item mapping
205
	 *
206
	 * @param array $mapping Mapping of the "item" columns with position as key and code as value
207
	 * @return int Position of the "coupon.code" column
208
	 * @throws \Aimeos\Controller\Jobs\Exception If no mapping for "coupon.code.code" is found
209
	 */
210
	protected function getCodePosition( array $mapping ) : int
211
	{
212
		foreach( $mapping as $pos => $key )
213
		{
214
			if( $key === 'coupon.code.code' ) {
215
				return $pos;
216
			}
217
		}
218
219
		throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'No "coupon.code.code" column in CSV mapping found' ) );
220
	}
221
222
223
	/**
224
	 * Opens and returns the container which includes the coupon data
225
	 *
226
	 * @param string $filepath Path to the container file
227
	 * @return \Aimeos\MW\Container\Iface Container object
228
	 */
229
	protected function getContainer( string $filepath ) : \Aimeos\MW\Container\Iface
230
	{
231
		$config = $this->context()->config();
232
233
		/** controller/jobs/coupon/import/csv/code/container/type
234
		 * Name of the container type to read the data from
235
		 *
236
		 * The container type tells the importer how it should retrieve the data.
237
		 * There are currently three container types that support the necessary
238
		 * CSV content:
239
		 *
240
		 * * File (plain)
241
		 * * Zip
242
		 *
243
		 * @param string Container type name
244
		 * @since 2017.10
245
		 * @category Developer
246
		 * @category User
247
		 * @see controller/jobs/coupon/import/csv/code/container/content
248
		 * @see controller/jobs/coupon/import/csv/code/container/options
249
		 */
250
		$container = $config->get( 'controller/jobs/coupon/import/csv/code/container/type', 'File' );
251
252
		/** controller/jobs/coupon/import/csv/code/container/content
253
		 * Name of the content type inside the container to read the data from
254
		 *
255
		 * The content type must always be a CSV-like format and there are
256
		 * currently two format types that are supported:
257
		 *
258
		 * * CSV
259
		 *
260
		 * @param array Content type name
261
		 * @since 2017.10
262
		 * @category Developer
263
		 * @category User
264
		 * @see controller/jobs/coupon/import/csv/code/container/type
265
		 * @see controller/jobs/coupon/import/csv/code/container/options
266
		 */
267
		$content = $config->get( 'controller/jobs/coupon/import/csv/code/container/content', 'CSV' );
268
269
		/** controller/jobs/coupon/import/csv/code/container/options
270
		 * List of file container options for the coupon import files
271
		 *
272
		 * Some container/content type allow you to hand over additional settings
273
		 * for configuration. Please have a look at the article about
274
		 * {@link http://aimeos.org/docs/Developers/Utility/Create_and_read_files container/content files}
275
		 * for more information.
276
		 *
277
		 * @param array Associative list of option name/value pairs
278
		 * @since 2017.10
279
		 * @category Developer
280
		 * @category User
281
		 * @see controller/jobs/coupon/import/csv/code/container/content
282
		 * @see controller/jobs/coupon/import/csv/code/container/type
283
		 */
284
		$options = $config->get( 'controller/jobs/coupon/import/csv/code/container/options', [] );
285
286
		return \Aimeos\MW\Container\Factory::getContainer( $filepath, $container, $content, $options );
287
	}
288
289
290
	/**
291
	 * Imports the CSV data and creates new coupons or updates existing ones
292
	 *
293
	 * @param \Aimeos\MShop\Coupon\Item\Code\Iface[] $items List of coupons code items
294
	 * @param array $data Associative list of import data as index/value pairs
295
	 * @param string $couponId ID of the coupon item the coupon code should be added to
296
	 * @param \Aimeos\Controller\Common\Coupon\Import\Csv\Processor\Iface $processor Processor object
297
	 * @return int Number of coupons that couldn't be imported
298
	 * @throws \Aimeos\Controller\Jobs\Exception
299
	 */
300
	protected function import( array $items, array $data, string $couponId,
301
		\Aimeos\Controller\Common\Coupon\Import\Csv\Processor\Iface $processor ) : int
302
	{
303
		$errors = 0;
304
		$context = $this->context();
305
		$manager = \Aimeos\MShop::create( $context, 'coupon/code' );
306
307
		foreach( $data as $code => $list )
308
		{
309
			$manager->begin();
310
311
			try
312
			{
313
				if( isset( $items[$code] ) ) {
314
					$item = $items[$code];
315
				} else {
316
					$item = $manager->create();
317
				}
318
319
				$item->setParentId( $couponId );
320
				$list = $processor->process( $item, $list );
0 ignored issues
show
Unused Code introduced by
The assignment to $list is dead and can be removed.
Loading history...
321
322
				$manager->commit();
323
			}
324
			catch( \Exception $e )
325
			{
326
				$manager->rollback();
327
328
				$str = 'Unable to import coupon with code "%1$s": %2$s';
329
				$msg = sprintf( $str, $code, $e->getMessage() . "\n" . $e->getTraceAsString() );
330
				$context->logger()->error( $msg, 'import/csv/coupon/code' );
331
332
				$errors++;
333
			}
334
		}
335
336
		return $errors;
337
	}
338
339
340
	/**
341
	 * Imports content from the given container
342
	 *
343
	 * @param \Aimeos\MShop\ContextIface $context Context object
344
	 * @param \Aimeos\MW\Container\Iface $container File container object
345
	 * @param string $couponId Unique coupon ID the codes should be imported for
346
	 * @param string $path Path to the container file
347
	 */
348
	protected function process( \Aimeos\MShop\ContextIface $context, \Aimeos\MW\Container\Iface $container, string $couponId, string $path )
349
	{
350
		$total = $errors = 0;
351
		$logger = $context->logger();
352
353
		$maxcnt = $this->size();
354
		$skiplines = $this->skip();
355
		$mappings = $this->mapping();
356
357
358
		$msg = sprintf( 'Started coupon import from "%1$s" (%2$s)', $path, __CLASS__ );
359
		$logger->notice( $msg, 'import/csv/coupon/code' );
360
361
		$processor = $this->getProcessors( $mappings );
362
		$codePos = $this->getCodePosition( $mappings['code'] );
363
364
		foreach( $container as $content )
365
		{
366
			for( $i = 0; $i < $skiplines; $i++ ) {
367
				$content->next();
368
			}
369
370
			while( ( $data = $this->getData( $content, $maxcnt, $codePos ) ) !== [] )
371
			{
372
				$items = $this->getCouponCodeItems( array_keys( $data ) );
373
				$errcnt = $this->import( $items, $data, $couponId, $processor );
374
				$chunkcnt = count( $data );
375
376
				$str = 'Imported coupon lines from "%1$s": %2$d/%3$d (%4$s)';
377
				$msg = sprintf( $str, $path, $chunkcnt - $errcnt, $chunkcnt, __CLASS__ );
378
				$logger->notice( $msg, 'import/csv/coupon/code' );
379
380
				$errors += $errcnt;
381
				$total += $chunkcnt;
382
				unset( $items, $data );
383
			}
384
		}
385
386
		$str = 'Finished coupon import: %1$d successful, %2$s errors, %3$s total (%4$s)';
387
		$msg = sprintf( $str, $total - $errors, $errors, $total, __CLASS__ );
388
		$logger->info( $msg, 'import/csv/coupon/code' );
389
390
		$container->close();
391
		$context->fs( 'fs-import' )->rm( $path );
392
	}
393
394
395
	/**
396
	 * Returns the column mapping
397
	 *
398
	 * @return array Mapping of the columns
399
	 */
400
	protected function mapping() : array
401
	{
402
		/** controller/jobs/coupon/import/csv/code/mapping
403
		 * List of mappings between the position in the CSV file and item keys
404
		 *
405
		 * This configuration setting overwrites the shared option
406
		 * "controller/common/coupon/import/csv/mapping" if you need a
407
		 * specific setting for the job controller. Otherwise, you should
408
		 * use the shared option for consistency.
409
		 *
410
		 * @param array Associative list of processor names and lists of key/position pairs
411
		 * @since 2017.10
412
		 * @category Developer
413
		 * @see controller/jobs/coupon/import/csv/code/skip-lines
414
		 * @see controller/jobs/coupon/import/csv/code/max-size
415
		 */
416
		return $this->context()->config()->get( 'controller/jobs/coupon/import/csv/code/mapping', $this->getDefaultMapping() );
417
	}
418
419
420
	/**
421
	 * Returns the maximum number of items processed at once
422
	 *
423
	 * @return int Maximum number of items
424
	 */
425
	protected function size() : int
426
	{
427
		/** controller/jobs/coupon/import/csv/code/max-size
428
		 * Maximum number of CSV rows to import at once
429
		 *
430
		 * It's more efficient to read and import more than one row at a time
431
		 * to speed up the import. Usually, the bigger the chunk that is imported
432
		 * at once, the less time the importer will need. The downside is that
433
		 * the amount of memory required by the import process will increase as
434
		 * well. Therefore, it's a trade-off between memory consumption and
435
		 * import speed.
436
		 *
437
		 * @param integer Number of rows
438
		 * @since 2017.10
439
		 * @category Developer
440
		 * @see controller/jobs/coupon/import/csv/code/skip-lines
441
		 * @see controller/jobs/coupon/import/csv/code/mapping
442
		 */
443
		return (int) $this->context()->config()->get( 'controller/jobs/coupon/import/csv/code/max-size', 1000 );
444
	}
445
446
447
	/**
448
	 * Returns the number of lines to skip at the beginning of the file
449
	 *
450
	 * @return int Number of linees to skip
451
	 */
452
	protected function skip() : int
453
	{
454
		/** controller/jobs/coupon/import/csv/code/skip-lines
455
		 * Number of rows skipped in front of each CSV files
456
		 *
457
		 * Some CSV files contain header information describing the content of
458
		 * the column values. These data is for informational purpose only and
459
		 * can't be imported into the database. Using this option, you can
460
		 * define the number of lines that should be left out before the import
461
		 * begins.
462
		 *
463
		 * @param integer Number of rows
464
		 * @since 2015.08
465
		 * @category Developer
466
		 * @see controller/jobs/coupon/import/csv/code/mapping
467
		 * @see controller/jobs/coupon/import/csv/code/max-size
468
		 */
469
		return (int) $this->context()->config()->get( 'controller/jobs/coupon/import/csv/code/skip-lines', 0 );
470
	}
471
}
472