Passed
Push — master ( bba07b...c2f305 )
by Aimeos
02:28
created

Standard   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 100
c 5
b 0
f 0
dl 0
loc 364
rs 10
wmc 27

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
B run() 0 70 8
A getStockItems() 0 16 2
B importStocks() 0 62 8
A getDescription() 0 3 1
A __destruct() 0 3 1
A getContainer() 0 43 1
A import() 0 92 5
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2019
6
 * @package Controller
7
 * @subpackage Jobs
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Stock\Import\Csv;
12
13
14
/**
15
 * Job controller for CSV stock imports
16
 *
17
 * @package Controller
18
 * @subpackage Jobs
19
 */
20
class Standard
21
	extends \Aimeos\Controller\Jobs\Base
22
	implements \Aimeos\Controller\Jobs\Iface
23
{
24
	use \Aimeos\Controller\Common\Common\Import\Traits;
25
26
27
	/**
28
	 * Cleanup before removing the object
29
	 */
30
	public function __destruct()
31
	{
32
		$this->saveTypes();
33
	}
34
35
36
	/**
37
	 * Returns the localized name of the job.
38
	 *
39
	 * @return string Name of the job
40
	 */
41
	public function getName()
42
	{
43
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Stock import CSV' );
44
	}
45
46
47
	/**
48
	 * Returns the localized description of the job.
49
	 *
50
	 * @return string Description of the job
51
	 */
52
	public function getDescription()
53
	{
54
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Imports new and updates existing stocks from CSV files' );
55
	}
56
57
58
	/**
59
	 * Executes the job
60
	 *
61
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
62
	 */
63
	public function run()
64
	{
65
		$context = $this->getContext();
66
		$config = $context->getConfig();
67
		$logger = $context->getLogger();
68
69
70
		/** controller/jobs/stock/import/csv/location
71
		 * File or directory where the content is stored which should be imported
72
		 *
73
		 * You need to configure the CSV file or directory with the CSV files that
74
		 * should be imported. It should be an absolute path to be sure but can be
75
		 * relative path if you absolutely know from where the job will be executed
76
		 * from.
77
		 *
78
		 * @param string Absolute file or directory path
79
		 * @since 2019.04
80
		 * @category Developer
81
		 * @category User
82
		 * @see controller/jobs/stock/import/csv/container/type
83
		 * @see controller/jobs/stock/import/csv/container/content
84
		 * @see controller/jobs/stock/import/csv/container/options
85
		 */
86
		$location = $config->get( 'controller/jobs/stock/import/csv/location' );
87
88
		try
89
		{
90
			$logger->log( sprintf( 'Started stock import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
91
92
			if( !file_exists( $location ) )
93
			{
94
				$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
95
				throw new \Aimeos\Controller\Jobs\Exception( $msg );
96
			}
97
98
			$files = [];
99
100
			if( is_dir( $location ) )
101
			{
102
				foreach( new \DirectoryIterator( $location ) as $entry )
103
				{
104
					if( strncmp( $entry->getFilename(), 'stock', 5 ) === 0 && $entry->getExtension() === 'csv' ) {
105
						$files[] = $entry->getPathname();
106
					}
107
				}
108
			}
109
			else
110
			{
111
				$files[] = $location;
112
			}
113
114
			sort( $files );
115
			$context->__sleep();
116
117
			$fcn = function( $filepath ) {
118
				$this->import( $filepath );
119
			};
120
121
			foreach( $files as $filepath ) {
122
				$context->getProcess()->start( $fcn, [$filepath] );
123
			}
124
125
			$context->getProcess()->wait();
126
127
			$logger->log( sprintf( 'Finished stock import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
128
		}
129
		catch( \Exception $e )
130
		{
131
			$logger->log( 'Stock import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString() );
132
			throw $e;
133
		}
134
	}
135
136
137
	/**
138
	 * Executes the job.
139
	 *
140
	 * @param string $filename Absolute path to the file that whould be imported
141
	 */
142
	public function import( $filename )
143
	{
144
		$context = $this->getContext();
145
		$config = $context->getConfig();
146
		$logger = $context->getLogger();
147
148
149
		/** controller/common/stock/import/csv/max-size
150
		 * Maximum number of CSV rows to import at once
151
		 *
152
		 * It's more efficient to read and import more than one row at a time
153
		 * to speed up the import. Usually, the bigger the chunk that is imported
154
		 * at once, the less time the importer will need. The downside is that
155
		 * the amount of memory required by the import process will increase as
156
		 * well. Therefore, it's a trade-off between memory consumption and
157
		 * import speed.
158
		 *
159
		 * '''Note:''' The maximum size is 10000 records
160
		 *
161
		 * @param integer Number of rows
162
		 * @since 2019.04
163
		 * @category Developer
164
		 * @see controller/jobs/stock/import/csv/backup
165
		 * @see controller/jobs/stock/import/csv/skip-lines
166
		 */
167
		$maxcnt = (int) $config->get( 'controller/common/stock/import/csv/max-size', 1000 );
168
169
		/** controller/jobs/stock/import/csv/skip-lines
170
		 * Number of rows skipped in front of each CSV files
171
		 *
172
		 * Some CSV files contain header information describing the content of
173
		 * the column values. These data is for informational purpose only and
174
		 * can't be imported into the database. Using this option, you can
175
		 * define the number of lines that should be left out before the import
176
		 * begins.
177
		 *
178
		 * @param integer Number of rows
179
		 * @since 2019.04
180
		 * @category Developer
181
		 * @see controller/jobs/stock/import/csv/backup
182
		 * @see controller/common/stock/import/csv/max-size
183
		 */
184
		$skiplines = (int) $config->get( 'controller/jobs/stock/import/csv/skip-lines', 0 );
185
186
		/** controller/jobs/stock/import/csv/backup
187
		 * Name of the backup for sucessfully imported files
188
		 *
189
		 * After a CSV file was imported successfully, you can move it to another
190
		 * location, so it won't be imported again and isn't overwritten by the
191
		 * next file that is stored at the same location in the file system.
192
		 *
193
		 * You should use an absolute path to be sure but can be relative path
194
		 * if you absolutely know from where the job will be executed from. The
195
		 * name of the new backup location can contain placeholders understood
196
		 * by the PHP strftime() function to create dynamic paths, e.g. "backup/%Y-%m-%d"
197
		 * which would create "backup/2000-01-01". For more information about the
198
		 * strftime() placeholders, please have a look into the PHP documentation of
199
		 * the {@link http://php.net/manual/en/function.strftime.php strftime() function}.
200
		 *
201
		 * '''Note:''' If no backup name is configured, the file or directory
202
		 * won't be moved away. Please make also sure that the parent directory
203
		 * and the new directory are writable so the file or directory could be
204
		 * moved.
205
		 *
206
		 * @param integer Name of the backup file, optionally with date/time placeholders
207
		 * @since 2019.04
208
		 * @category Developer
209
		 * @see controller/common/stock/import/csv/max-size
210
		 * @see controller/jobs/stock/import/csv/skip-lines
211
		 */
212
		$backup = $config->get( 'controller/jobs/stock/import/csv/backup' );
213
214
215
		$container = $this->getContainer( $filename );
216
217
		$logger->log( sprintf( 'Started stock import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
218
219
		foreach( $container as $content )
220
		{
221
			for( $i = 0; $i < $skiplines; $i++ ) {
222
				$content->next();
223
			}
224
225
			$this->importStocks( $content, $maxcnt );
226
		}
227
228
		$logger->log( sprintf( 'Finished stock import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
229
230
		$container->close();
231
232
		if( !empty( $backup ) && @rename( $filename, strftime( $backup ) ) === false ) {
233
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'Unable to move imported file' ) );
234
		}
235
	}
236
237
238
	/**
239
	 * Opens and returns the container which includes the stock data
240
	 *
241
	 * @param string $location Absolute path to the file
242
	 * @return \Aimeos\MW\Container\Iface Container object
243
	 */
244
	protected function getContainer( $location )
245
	{
246
		$config = $this->getContext()->getConfig();
247
248
		/** controller/jobs/stock/import/csv/container/type
249
		 * Nave of the container type to read the data from
250
		 *
251
		 * The container type tells the importer how it should retrieve the data.
252
		 * There are currently two container types that support the necessary
253
		 * CSV content:
254
		 * * File
255
		 * * Zip
256
		 *
257
		 * '''Note:''' For the PHPExcel container, you need to install the
258
		 * "ai-container" extension.
259
		 *
260
		 * @param string Container type name
261
		 * @since 2019.04
262
		 * @category Developer
263
		 * @category User
264
		 * @see controller/jobs/stock/import/csv/location
265
		 * @see controller/jobs/stock/import/csv/container/options
266
		 */
267
		$container = $config->get( 'controller/jobs/stock/import/csv/container/type', 'File' );
268
269
		/** controller/jobs/stock/import/csv/container/options
270
		 * List of file container options for the stock 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 2019.04
279
		 * @category Developer
280
		 * @category User
281
		 * @see controller/jobs/stock/import/csv/location
282
		 * @see controller/jobs/stock/import/csv/container/type
283
		 */
284
		$options = $config->get( 'controller/jobs/stock/import/csv/container/options', [] );
285
286
		return \Aimeos\MW\Container\Factory::getContainer( $location, $container, 'CSV', $options );
287
	}
288
289
290
	/**
291
	 * Returns the stock items for the given codes and types
292
	 *
293
	 * @param array $codes List of stock codes
294
	 * @param array $types List of stock types
295
	 * @return array Multi-dimensional array of code/type/item map
296
	 */
297
	protected function getStockItems( array $codes, array $types )
298
	{
299
		$map = [];
300
		$manager = \Aimeos\MShop::create( $this->getContext(), 'stock' );
301
302
		$search = $manager->createSearch()->setSlice( 0, 10000 );
303
		$search->setConditions( $search->combine( '&&', [
304
			$search->compare( '==', 'stock.productcode', $codes ),
305
			$search->compare( '==', 'stock.type', $types )
306
		] ) );
307
308
		foreach( $manager->searchItems( $search ) as $item ) {
309
			$map[$item->getProductCode()][$item->getType()] = $item;
310
		}
311
312
		return $map;
313
	}
314
315
316
	/**
317
	 * Imports the CSV data and creates new stocks or updates existing ones
318
	 *
319
	 * @param \Aimeos\MW\Container\Content\Iface $content Content object
320
	 * @return integer Number of imported stocks
321
	 */
322
	protected function importStocks( \Aimeos\MW\Container\Content\Iface $content, $maxcnt )
323
	{
324
		$total = 0;
325
		$manager = \Aimeos\MShop::create( $this->getContext(), 'stock' );
326
327
		do
328
		{
329
			$count = 0;
330
			$codes = $data = $types = [];
331
332
			while( $content->valid() && $count < $maxcnt )
333
			{
334
				$row = $content->current();
335
				$content->next();
336
337
				if( $row[0] == '' ) {
338
					continue;
339
				}
340
341
				$type = $this->getValue( $row, 2, 'default' );
342
				$types[$type] = null;
343
				$codes[] = $row[0];
344
				$row[2] = $type;
345
				$data[] = $row;
346
347
				$count++;
348
			}
349
350
			if( $count === 0 ) {
351
				break;
352
			}
353
354
			$items = [];
355
			$map = $this->getStockItems( $codes, array_keys( $types ) );
356
357
			foreach( $data as $entry )
358
			{
359
				$code = $entry[0];
360
				$type = $entry[2];
361
362
				if( isset( $map[$code][$type] ) ) {
363
					$item = $map[$code][$type];
364
				} else {
365
					$item = $manager->createItem();
366
				}
367
368
				$items[] = $item->setProductCode( $code )->setType( $type )
369
					->setStocklevel( $this->getValue( $entry, 1 ) )
370
					->setDateBack( $this->getValue( $entry, 3 ) );
371
372
				$this->addType( 'stock/type', 'product', $type );
373
				unset( $map[$code][$type] );
374
			}
375
376
			$manager->saveItems( $items );
377
			unset( $items );
378
379
			$total += $count;
380
		}
381
		while( $count > 0 );
382
383
		return $total;
384
	}
385
}
386