Completed
Push — master ( 7da6af...c0c3c8 )
by Aimeos
02:52
created

Standard   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 97
c 4
b 0
f 0
dl 0
loc 351
rs 10
wmc 26

7 Methods

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