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

Standard   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 94
c 2
b 0
f 0
dl 0
loc 310
rs 9.68
wmc 34

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __destruct() 0 3 1
B run() 0 70 8
A getDescription() 0 3 1
B importNodes() 0 25 7
B import() 0 113 10
A process() 0 27 6
A getName() 0 3 1
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\Product\Import\Xml;
12
13
14
/**
15
 * Job controller for XML product 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
	use \Aimeos\Controller\Common\Common\Import\Xml\Traits;
26
27
28
	/**
29
	 * Cleanup before removing the object
30
	 */
31
	public function __destruct()
32
	{
33
		$this->saveTypes();
34
	}
35
36
37
	/**
38
	 * Returns the localized name of the job.
39
	 *
40
	 * @return string Name of the job
41
	 */
42
	public function getName()
43
	{
44
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Product import XML' );
45
	}
46
47
48
	/**
49
	 * Returns the localized description of the job.
50
	 *
51
	 * @return string Description of the job
52
	 */
53
	public function getDescription()
54
	{
55
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Imports new and updates existing products from XML files' );
56
	}
57
58
59
	/**
60
	 * Executes the job.
61
	 *
62
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
63
	 */
64
	public function run()
65
	{
66
		$context = $this->getContext();
67
		$config = $context->getConfig();
68
		$logger = $context->getLogger();
69
70
71
		/** controller/jobs/product/import/xml/location
72
		 * File or directory where the content is stored which should be imported
73
		 *
74
		 * You need to configure the XML file or directory with the XML files that
75
		 * should be imported. It should be an absolute path to be sure but can be
76
		 * relative path if you absolutely know from where the job will be executed
77
		 * from.
78
		 *
79
		 * @param string Absolute file or directory path
80
		 * @since 2019.04
81
		 * @category Developer
82
		 * @category User
83
		 * @see controller/jobs/product/import/xml/container/type
84
		 * @see controller/jobs/product/import/xml/container/content
85
		 * @see controller/jobs/product/import/xml/container/options
86
		 */
87
		$location = $config->get( 'controller/jobs/product/import/xml/location' );
88
89
		try
90
		{
91
			$logger->log( sprintf( 'Started product import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
92
93
			if( !file_exists( $location ) )
94
			{
95
				$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
96
				throw new \Aimeos\Controller\Jobs\Exception( $msg );
97
			}
98
99
			$files = [];
100
101
			if( is_dir( $location ) )
102
			{
103
				foreach( new \DirectoryIterator( $location ) as $entry )
104
				{
105
					if( strncmp( $entry->getFilename(), 'product', 7 ) === 0 && $entry->getExtension() === 'xml' ) {
106
						$files[] = $entry->getPathname();
107
					}
108
				}
109
			}
110
			else
111
			{
112
				$files[] = $location;
113
			}
114
115
			sort( $files );
116
			$context->__sleep();
117
118
			$fcn = function( $filepath ) {
119
				$this->import( $filepath );
120
			};
121
122
			foreach( $files as $filepath ) {
123
				$context->getProcess()->start( $fcn, [$filepath] );
124
			}
125
126
			$context->getProcess()->wait();
127
128
			$logger->log( sprintf( 'Finished product import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
129
		}
130
		catch( \Exception $e )
131
		{
132
			$logger->log( 'Product import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString() );
133
			throw $e;
134
		}
135
	}
136
137
138
	/**
139
	 * Imports the XML file given by its path
140
	 *
141
	 * @param string $filename Absolute or relative path to the XML file
142
	 */
143
	protected function import( $filename )
144
	{
145
		$context = $this->getContext();
146
		$config = $context->getConfig();
147
		$logger = $context->getLogger();
148
149
150
		$domains = ['attribute', 'media', 'price', 'product', 'product/property', 'text'];
151
152
		/** controller/jobs/product/import/xml/domains
153
		 * List of item domain names that should be retrieved along with the product items
154
		 *
155
		 * This configuration setting overwrites the shared option
156
		 * "controller/common/product/import/xml/domains" if you need a
157
		 * specific setting for the job controller. Otherwise, you should
158
		 * use the shared option for consistency.
159
		 *
160
		 * @param array Associative list of MShop item domain names
161
		 * @since 2019.04
162
		 * @category Developer
163
		 * @see controller/jobs/product/import/xml/backup
164
		 * @see controller/jobs/product/import/xml/max-query
165
		 */
166
		$domains = $config->get( 'controller/jobs/product/import/xml/domains', $domains );
167
168
		/** controller/jobs/product/import/xml/backup
169
		 * Name of the backup for sucessfully imported files
170
		 *
171
		 * After a XML file was imported successfully, you can move it to another
172
		 * location, so it won't be imported again and isn't overwritten by the
173
		 * next file that is stored at the same location in the file system.
174
		 *
175
		 * You should use an absolute path to be sure but can be relative path
176
		 * if you absolutely know from where the job will be executed from. The
177
		 * name of the new backup location can contain placeholders understood
178
		 * by the PHP strftime() function to create dynamic paths, e.g. "backup/%Y-%m-%d"
179
		 * which would create "backup/2000-01-01". For more information about the
180
		 * strftime() placeholders, please have a look into the PHP documentation of
181
		 * the {@link http://php.net/manual/en/function.strftime.php strftime() function}.
182
		 *
183
		 * '''Note:''' If no backup name is configured, the file or directory
184
		 * won't be moved away. Please make also sure that the parent directory
185
		 * and the new directory are writable so the file or directory could be
186
		 * moved.
187
		 *
188
		 * @param integer Name of the backup file, optionally with date/time placeholders
189
		 * @since 2019.04
190
		 * @category Developer
191
		 * @see controller/jobs/product/import/xml/domains
192
		 * @see controller/jobs/product/import/xml/max-query
193
		 */
194
		$backup = $config->get( 'controller/jobs/product/import/xml/backup' );
195
196
		/** controller/jobs/product/import/xml/max-query
197
		 * Maximum number of XML nodes processed at once
198
		 *
199
		 * Processing and fetching several product items at once speeds up importing
200
		 * the XML files. The more items can be processed at once, the faster the
201
		 * import. More items also increases the memory usage of the importer and
202
		 * thus, this parameter should be low enough to avoid reaching the memory
203
		 * limit of the PHP process.
204
		 *
205
		 * @param integer Number of XML nodes
206
		 * @since 2019.04
207
		 * @category Developer
208
		 * @category User
209
		 * @see controller/jobs/product/import/xml/domains
210
		 * @see controller/jobs/product/import/xml/backup
211
		 */
212
		$maxquery = $config->get( 'controller/jobs/product/import/xml/max-query', 1000 );
213
214
215
		$slice = 0;
216
		$nodes = [];
217
		$xml = new \XMLReader();
218
219
		if( $xml->open( $filename, LIBXML_COMPACT | LIBXML_PARSEHUGE ) === false ) {
220
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'No XML file "%1$s" found', $filename ) );
221
		}
222
223
		$logger->log( sprintf( 'Started product import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
224
225
		while( $xml->read() === true )
226
		{
227
			if( $xml->depth === 1 && $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'productitem' )
228
			{
229
				if( ( $dom = $xml->expand() ) === false )
230
				{
231
					$msg = sprintf( 'Expanding "%1$s" node failed', 'productitem' );
232
					throw new \Aimeos\Controller\Jobs\Exception( $msg );
233
				}
234
235
				$nodes[] = $dom;
236
237
				if( $slice++ >= $maxquery )
238
				{
239
					$this->importNodes( $nodes, $domains );
240
					unset( $nodes );
241
					$nodes = [];
242
					$slice = 0;
243
				}
244
			}
245
		}
246
247
		$this->importNodes( $nodes, $domains );
248
		unset( $nodes );
249
250
		$logger->log( sprintf( 'Finished product import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
251
252
		if( !empty( $backup ) && @rename( $filename, strftime( $backup ) ) === false )
253
		{
254
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, strftime( $backup ) );
255
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
256
		}
257
	}
258
259
260
	/**
261
	 * Imports the given DOM nodes
262
	 *
263
	 * @param \DomElement[] $nodes List of nodes to import
264
	 * @param string[] $ref List of domain names whose referenced items will be updated in the product items
265
	 */
266
	protected function importNodes( array $nodes, array $ref )
267
	{
268
		$codes = $map = [];
269
270
		foreach( $nodes as $node )
271
		{
272
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null ) {
273
				$codes[$attr->nodeValue] = null;
274
			}
275
		}
276
277
		$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
278
		$search = $manager->createSearch()->setSlice( 0, count( $codes ) );
279
		$search->setConditions( $search->compare( '==', 'product.code', array_keys( $codes ) ) );
280
281
		foreach( $manager->searchItems( $search, $ref ) as $item ) {
282
			$map[$item->getCode()] = $item;
283
		}
284
285
		foreach( $nodes as $node )
286
		{
287
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null && isset( $map[$attr->nodeValue] ) ) {
288
				$item = $this->process( $map[$attr->nodeValue], $node );
0 ignored issues
show
Unused Code introduced by
The assignment to $item is dead and can be removed.
Loading history...
289
			} else {
290
				$item = $this->process( $manager->createItem(), $node );
291
			}
292
		}
293
	}
294
295
296
	/**
297
	 * Updates the product item and its referenced items using the given DOM node
298
	 *
299
	 * @param \Aimeos\MShop\Product\Item\Iface $item Product item object to update
300
	 * @param \DomElement $node DOM node used for updateding the product item
301
	 * @return \Aimeos\MShop\Product\Item\Iface $item Updated product item object
302
	 */
303
	protected function process( \Aimeos\MShop\Product\Item\Iface $item, \DomElement $node )
304
	{
305
		$list = $subnodes = [];
306
307
		foreach( $node->attributes as $attr ) {
308
			$list[$attr->nodeName] = $attr->nodeValue;
309
		}
310
311
		foreach( $node->childNodes as $tag )
312
		{
313
			if( in_array( $tag->nodeName, ['lists', 'property'] ) ) {
314
				$item = $this->getProcessor( $tag->nodeName )->process( $item, $tag );
315
			} elseif( in_array( $tag->nodeName, ['catalog'] ) ) {
316
				$subnodes[$tag->nodeName] = $tag;
317
			} else {
318
				$list[$tag->nodeName] = $tag->nodeValue;
319
			}
320
		}
321
322
		$item = \Aimeos\MShop::create( $this->getContext(), 'product' )->saveItem( $item->fromArray( $list, true ) );
0 ignored issues
show
Bug introduced by
The method saveItem() does not exist on Aimeos\MShop\Common\Manager\Iface. Did you maybe mean saveItems()? ( Ignorable by Annotation )

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

322
		$item = \Aimeos\MShop::create( $this->getContext(), 'product' )->/** @scrutinizer ignore-call */ saveItem( $item->fromArray( $list, true ) );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
323
		$this->addType( 'product/type', 'product', $item->getType() );
324
325
		foreach( $subnodes as $name => $subnode ) {
326
			$item = $this->getProcessor( $name )->process( $item, $subnode );
327
		}
328
329
		return $item;
330
	}
331
}
332