Passed
Push — master ( b5b91f...783ab4 )
by Aimeos
01:53
created

Standard   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 108
dl 0
loc 370
rs 10
c 0
b 0
f 0
wmc 27

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
B run() 0 71 8
A getStockItems() 0 16 2
B importStocks() 0 59 8
A getDescription() 0 3 1
A getContainer() 0 43 1
B import() 0 108 6
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
	private $types = [];
0 ignored issues
show
introduced by
The private property $types is not used, and could be removed.
Loading history...
25
26
27
	/**
28
	 * Returns the localized name of the job.
29
	 *
30
	 * @return string Name of the job
31
	 */
32
	public function getName()
33
	{
34
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Stock import CSV' );
35
	}
36
37
38
	/**
39
	 * Returns the localized description of the job.
40
	 *
41
	 * @return string Description of the job
42
	 */
43
	public function getDescription()
44
	{
45
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Imports new and updates existing stocks from CSV files' );
46
	}
47
48
49
	/**
50
	 * Executes the job
51
	 *
52
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
53
	 */
54
	public function run()
55
	{
56
		$context = $this->getContext();
57
		$config = $context->getConfig();
58
		$logger = $context->getLogger();
59
60
		/** controller/jobs/stock/import/csv/location
61
		 * File or directory where the content is stored which should be imported
62
		 *
63
		 * You need to configure the CSV file or directory with the CSV files that
64
		 * should be imported. It should be an absolute path to be sure but can be
65
		 * relative path if you absolutely know from where the job will be executed
66
		 * from.
67
		 *
68
		 * @param string Absolute file or directory path
69
		 * @since 2019.04
70
		 * @category Developer
71
		 * @category User
72
		 * @see controller/jobs/stock/import/csv/container/type
73
		 * @see controller/jobs/stock/import/csv/container/content
74
		 * @see controller/jobs/stock/import/csv/container/options
75
		 */
76
		$location = $config->get( 'controller/jobs/stock/import/csv/location' );
77
78
		try
79
		{
80
			$msg = sprintf( 'Started stock import from "%1$s" (%2$s)', $location, __CLASS__ );
81
			$logger->log( $msg, \Aimeos\MW\Logger\Base::INFO );
82
83
			if( !file_exists( $location ) )
84
			{
85
				$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
86
				throw new \Aimeos\Controller\Jobs\Exception( $msg );
87
			}
88
89
			$files = [];
90
91
			if( is_dir( $location ) )
92
			{
93
				foreach( new \DirectoryIterator( $location ) as $entry )
94
				{
95
					if( strncmp( $entry->getFilename(), 'stock', 5 ) === 0 && $entry->getExtension() === 'csv' ) {
96
						$files[] = $entry->getPathname();
97
					}
98
				}
99
			}
100
			else
101
			{
102
				$files[] = $location;
103
			}
104
105
			sort( $files );
106
			$total = 0;
107
108
			foreach( $files as $filepath )
109
			{
110
				$num = $this->import( $filepath );
111
				$total += $num;
112
113
				$msg = sprintf( 'Stock import from "%1$s": %2$d items', $filepath, $num );
114
				$logger->log( $msg, \Aimeos\MW\Logger\Base::NOTICE );
115
			}
116
117
			$mem = memory_get_peak_usage() / 1024 / 1024;
118
			$msg = 'Finished stock import from "%1$s", %2$d items, %3$01.2F MB (%4$s)';
119
			$logger->log( sprintf( $msg, $location, $total, $mem, __CLASS__ ), \Aimeos\MW\Logger\Base::INFO );
120
		}
121
		catch( \Exception $e )
122
		{
123
			$logger->log( 'Product import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString() );
124
			throw $e;
125
		}
126
	}
127
128
129
	/**
130
	 * Executes the job.
131
	 *
132
	 * @param string $filepath Absolute path to the file that whould be imported
133
	 * @return integer Number of imported stocks
134
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
135
	 */
136
	public function import( $filepath )
137
	{
138
		$context = $this->getContext();
139
		$config = $context->getConfig();
140
		$logger = $context->getLogger();
141
142
		/** controller/common/stock/import/csv/max-size
143
		 * Maximum number of CSV rows to import at once
144
		 *
145
		 * It's more efficient to read and import more than one row at a time
146
		 * to speed up the import. Usually, the bigger the chunk that is imported
147
		 * at once, the less time the importer will need. The downside is that
148
		 * the amount of memory required by the import process will increase as
149
		 * well. Therefore, it's a trade-off between memory consumption and
150
		 * import speed.
151
		 *
152
		 * '''Note:''' The maximum size is 10000 records
153
		 *
154
		 * @param integer Number of rows
155
		 * @since 2019.04
156
		 * @category Developer
157
		 * @see controller/jobs/stock/import/csv/backup
158
		 * @see controller/jobs/stock/import/csv/skip-lines
159
		 */
160
		$maxcnt = (int) $config->get( 'controller/common/stock/import/csv/max-size', 1000 );
161
162
		/** controller/jobs/stock/import/csv/skip-lines
163
		 * Number of rows skipped in front of each CSV files
164
		 *
165
		 * Some CSV files contain header information describing the content of
166
		 * the column values. These data is for informational purpose only and
167
		 * can't be imported into the database. Using this option, you can
168
		 * define the number of lines that should be left out before the import
169
		 * begins.
170
		 *
171
		 * @param integer Number of rows
172
		 * @since 2019.04
173
		 * @category Developer
174
		 * @see controller/jobs/stock/import/csv/backup
175
		 * @see controller/common/stock/import/csv/max-size
176
		 */
177
		$skiplines = (int) $config->get( 'controller/jobs/stock/import/csv/skip-lines', 0 );
178
179
		/** controller/jobs/stock/import/csv/backup
180
		 * Name of the backup for sucessfully imported files
181
		 *
182
		 * After a CSV file was imported successfully, you can move it to another
183
		 * location, so it won't be imported again and isn't overwritten by the
184
		 * next file that is stored at the same location in the file system.
185
		 *
186
		 * You should use an absolute path to be sure but can be relative path
187
		 * if you absolutely know from where the job will be executed from. The
188
		 * name of the new backup location can contain placeholders understood
189
		 * by the PHP strftime() function to create dynamic paths, e.g. "backup/%Y-%m-%d"
190
		 * which would create "backup/2000-01-01". For more information about the
191
		 * strftime() placeholders, please have a look into the PHP documentation of
192
		 * the {@link http://php.net/manual/en/function.strftime.php strftime() function}.
193
		 *
194
		 * '''Note:''' If no backup name is configured, the file or directory
195
		 * won't be moved away. Please make also sure that the parent directory
196
		 * and the new directory are writable so the file or directory could be
197
		 * moved.
198
		 *
199
		 * @param integer Name of the backup file, optionally with date/time placeholders
200
		 * @since 2019.04
201
		 * @category Developer
202
		 * @see controller/common/stock/import/csv/max-size
203
		 * @see controller/jobs/stock/import/csv/skip-lines
204
		 */
205
		$backup = $config->get( 'controller/jobs/stock/import/csv/backup' );
206
207
208
		$total = 0;
209
		$container = $this->getContainer( $filepath );
210
211
		try
212
		{
213
			$msg = sprintf( 'Started stock import from "%1$s" (%2$s)', $container->getName(), __CLASS__ );
214
			$logger->log( $msg, \Aimeos\MW\Logger\Base::NOTICE );
215
216
			foreach( $container as $content )
217
			{
218
				$name = $content->getName();
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
219
220
				for( $i = 0; $i < $skiplines; $i++ ) {
221
					$content->next();
222
				}
223
224
				$total += $this->importStocks( $content, $maxcnt );
225
			}
226
227
			$container->close();
228
		}
229
		catch( \Exception $e )
230
		{
231
			$container->close();
232
233
			$logger->log( 'Stock import error: ' . $e->getMessage() );
234
			$logger->log( $e->getTraceAsString() );
235
236
			throw new \Aimeos\Controller\Jobs\Exception( $e->getMessage() );
237
		}
238
239
		if( !empty( $backup ) && @rename( $path, strftime( $backup ) ) === false ) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $path seems to be never defined.
Loading history...
240
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'Unable to move imported file' ) );
241
		}
242
243
		return $total;
244
	}
245
246
247
	/**
248
	 * Opens and returns the container which includes the stock data
249
	 *
250
	 * @param $location Absolute path to the file
0 ignored issues
show
Bug introduced by
The type Aimeos\Controller\Jobs\Stock\Import\Csv\Absolute was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
251
	 * @return \Aimeos\MW\Container\Iface Container object
252
	 */
253
	protected function getContainer( $location )
254
	{
255
		$config = $this->getContext()->getConfig();
256
257
		/** controller/jobs/stock/import/csv/container/type
258
		 * Nave of the container type to read the data from
259
		 *
260
		 * The container type tells the importer how it should retrieve the data.
261
		 * There are currently two container types that support the necessary
262
		 * CSV content:
263
		 * * File
264
		 * * Zip
265
		 *
266
		 * '''Note:''' For the PHPExcel container, you need to install the
267
		 * "ai-container" extension.
268
		 *
269
		 * @param string Container type name
270
		 * @since 2019.04
271
		 * @category Developer
272
		 * @category User
273
		 * @see controller/jobs/stock/import/csv/location
274
		 * @see controller/jobs/stock/import/csv/container/options
275
		 */
276
		$container = $config->get( 'controller/jobs/stock/import/csv/container/type', 'File' );
277
278
		/** controller/jobs/stock/import/csv/container/options
279
		 * List of file container options for the stock import files
280
		 *
281
		 * Some container/content type allow you to hand over additional settings
282
		 * for configuration. Please have a look at the article about
283
		 * {@link http://aimeos.org/docs/Developers/Utility/Create_and_read_files container/content files}
284
		 * for more information.
285
		 *
286
		 * @param array Associative list of option name/value pairs
287
		 * @since 2019.04
288
		 * @category Developer
289
		 * @category User
290
		 * @see controller/jobs/stock/import/csv/location
291
		 * @see controller/jobs/stock/import/csv/container/type
292
		 */
293
		$options = $config->get( 'controller/jobs/stock/import/csv/container/options', [] );
294
295
		return \Aimeos\MW\Container\Factory::getContainer( $location, $container, 'CSV', $options );
296
	}
297
298
299
	/**
300
	 * Returns the stock items for the given codes and types
301
	 *
302
	 * @param array $codes List of stock codes
303
	 * @param array $types List of stock types
304
	 * @return array Multi-dimensional array of code/type/item map
305
	 */
306
	protected function getStockItems( array $codes, array $types )
307
	{
308
		$map = [];
309
		$manager = \Aimeos\MShop::create( $this->getContext(), 'stock' );
310
311
		$search = $manager->createSearch()->setSlice( 0, 10000 );
312
		$search->setConditions( $search->combine( '&&', [
313
			$search->compare( '==', 'stock.productcode', $codes ),
314
			$search->compare( '==', 'stock.type', $types )
315
		] ) );
316
317
		foreach( $manager->searchItems( $search ) as $item ) {
318
			$map[$item->getProductCode()][$item->getType()] = $item;
319
		}
320
321
		return $map;
322
	}
323
324
325
	/**
326
	 * Imports the CSV data and creates new stocks or updates existing ones
327
	 *
328
	 * @param \Aimeos\MW\Container\Content\Iface $content Content object
329
	 * @return integer Number of imported stocks
330
	 */
331
	protected function importStocks( \Aimeos\MW\Container\Content\Iface $content, $maxcnt )
332
	{
333
		$total = 0;
334
		$manager = \Aimeos\MShop::create( $this->getContext(), 'stock' );
335
336
		do
337
		{
338
			$count = 0;
339
			$codes = $data = $items = $types = [];
340
341
			while( $content->valid() && $count < $maxcnt )
342
			{
343
				$row = $content->current();
344
				$content->next();
345
346
				if( $row[0] == '' ) {
347
					continue;
348
				}
349
350
				$type = $this->getValue( $row, 2, 'default' );
351
				$types[$type] = null;
352
				$codes[] = $row[0];
353
				$row[2] = $type;
354
				$data[] = $row;
355
356
				$count++;
357
			}
358
359
			if( $count === 0 ) {
360
				break;
361
			}
362
363
			$map = $this->getStockItems( $codes, $types );
0 ignored issues
show
Unused Code introduced by
The assignment to $map is dead and can be removed.
Loading history...
364
365
			foreach( $data as $entry )
366
			{
367
				$code = $entry[0];
368
				$type = $entry[2];
369
370
				if( isset( $items[$code][$type] ) ) {
371
					$item = $items[$code][$type];
372
				} else {
373
					$item = $manager->createItem();
374
				}
375
376
				$items[] = $item->setProductCode( $code )->setType( $type )
377
					->setStocklevel( $this->getValue( $entry, 1 ) )
378
					->setDateBack( $this->getValue( $entry, 3 ) );
379
380
				unset( $items[$code][$type] );
381
			}
382
383
			$manager->saveItems( $items );
384
385
			$total += $count;
386
		}
387
		while( $count > 0 );
388
389
		return $total;
390
	}
391
}
392