Passed
Push — master ( 44dd5c...d886ca )
by Aimeos
02:13
created

Standard::getDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
	 * Returns the localized name of the job.
30
	 *
31
	 * @return string Name of the job
32
	 */
33
	public function getName()
34
	{
35
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Product import XML' );
36
	}
37
38
39
	/**
40
	 * Returns the localized description of the job.
41
	 *
42
	 * @return string Description of the job
43
	 */
44
	public function getDescription()
45
	{
46
		return $this->getContext()->getI18n()->dt( 'controller/jobs', 'Imports new and updates existing products from XML files' );
47
	}
48
49
50
	/**
51
	 * Executes the job.
52
	 *
53
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
54
	 */
55
	public function run()
56
	{
57
		$context = $this->getContext();
58
		$config = $context->getConfig();
59
		$logger = $context->getLogger();
60
61
62
		/** controller/jobs/product/import/xml/location
63
		 * File or directory where the content is stored which should be imported
64
		 *
65
		 * You need to configure the XML file or directory with the XML files that
66
		 * should be imported. It should be an absolute path to be sure but can be
67
		 * relative path if you absolutely know from where the job will be executed
68
		 * from.
69
		 *
70
		 * @param string Absolute file or directory path
71
		 * @since 2019.04
72
		 * @category Developer
73
		 * @category User
74
		 * @see controller/jobs/product/import/xml/container/type
75
		 * @see controller/jobs/product/import/xml/container/content
76
		 * @see controller/jobs/product/import/xml/container/options
77
		 */
78
		$location = $config->get( 'controller/jobs/product/import/xml/location' );
79
80
		try
81
		{
82
			$logger->log( sprintf( 'Started product import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
83
84
			if( !file_exists( $location ) )
85
			{
86
				$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
87
				throw new \Aimeos\Controller\Jobs\Exception( $msg );
88
			}
89
90
			$files = [];
91
92
			if( is_dir( $location ) )
93
			{
94
				foreach( new \DirectoryIterator( $location ) as $entry )
95
				{
96
					if( strncmp( $entry->getFilename(), 'product', 7 ) === 0 && $entry->getExtension() === 'xml' ) {
97
						$files[] = $entry->getPathname();
98
					}
99
				}
100
			}
101
			else
102
			{
103
				$files[] = $location;
104
			}
105
106
			sort( $files );
107
			$context->__sleep();
108
109
			$fcn = function( $filepath ) {
110
				$this->import( $filepath );
111
			};
112
113
			foreach( $files as $filepath ) {
114
				$context->getProcess()->start( $fcn, [$filepath] );
115
			}
116
117
			$context->getProcess()->wait();
118
119
			$logger->log( sprintf( 'Finished product import from "%1$s"', $location ), \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
	 * Imports the XML file given by its path
131
	 *
132
	 * @param string $filename Absolute or relative path to the XML file
133
	 */
134
	protected function import( $filename )
135
	{
136
		$context = $this->getContext();
137
		$config = $context->getConfig();
138
		$logger = $context->getLogger();
139
140
141
		$domains = ['attribute', 'media', 'price', 'product', 'product/property', 'text'];
142
143
		/** controller/jobs/product/import/xml/domains
144
		 * List of item domain names that should be retrieved along with the product items
145
		 *
146
		 * This configuration setting overwrites the shared option
147
		 * "controller/common/product/import/xml/domains" if you need a
148
		 * specific setting for the job controller. Otherwise, you should
149
		 * use the shared option for consistency.
150
		 *
151
		 * @param array Associative list of MShop item domain names
152
		 * @since 2019.04
153
		 * @category Developer
154
		 * @see controller/jobs/product/import/xml/backup
155
		 * @see controller/jobs/product/import/xml/max-query
156
		 */
157
		$domains = $config->get( 'controller/jobs/product/import/xml/domains', $domains );
158
159
		/** controller/jobs/product/import/xml/backup
160
		 * Name of the backup for sucessfully imported files
161
		 *
162
		 * After a XML file was imported successfully, you can move it to another
163
		 * location, so it won't be imported again and isn't overwritten by the
164
		 * next file that is stored at the same location in the file system.
165
		 *
166
		 * You should use an absolute path to be sure but can be relative path
167
		 * if you absolutely know from where the job will be executed from. The
168
		 * name of the new backup location can contain placeholders understood
169
		 * by the PHP strftime() function to create dynamic paths, e.g. "backup/%Y-%m-%d"
170
		 * which would create "backup/2000-01-01". For more information about the
171
		 * strftime() placeholders, please have a look into the PHP documentation of
172
		 * the {@link http://php.net/manual/en/function.strftime.php strftime() function}.
173
		 *
174
		 * '''Note:''' If no backup name is configured, the file or directory
175
		 * won't be moved away. Please make also sure that the parent directory
176
		 * and the new directory are writable so the file or directory could be
177
		 * moved.
178
		 *
179
		 * @param integer Name of the backup file, optionally with date/time placeholders
180
		 * @since 2019.04
181
		 * @category Developer
182
		 * @see controller/jobs/product/import/xml/domains
183
		 * @see controller/jobs/product/import/xml/max-query
184
		 */
185
		$backup = $config->get( 'controller/jobs/product/import/xml/backup' );
186
187
		/** controller/jobs/product/import/xml/max-query
188
		 * Maximum number of XML nodes processed at once
189
		 *
190
		 * Processing and fetching several product items at once speeds up importing
191
		 * the XML files. The more items can be processed at once, the faster the
192
		 * import. More items also increases the memory usage of the importer and
193
		 * thus, this parameter should be low enough to avoid reaching the memory
194
		 * limit of the PHP process.
195
		 *
196
		 * @param integer Number of XML nodes
197
		 * @since 2019.04
198
		 * @category Developer
199
		 * @category User
200
		 * @see controller/jobs/product/import/xml/domains
201
		 * @see controller/jobs/product/import/xml/backup
202
		 */
203
		$maxquery = $config->get( 'controller/jobs/product/import/xml/max-query', 1000 );
204
205
206
		$slice = 0;
207
		$nodes = [];
208
		$xml = new \XMLReader();
209
210
		if( $xml->open( $filename, LIBXML_COMPACT | LIBXML_PARSEHUGE ) === false ) {
211
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'No XML file "%1$s" found', $filename ) );
212
		}
213
214
		$logger->log( sprintf( 'Started product import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
215
216
		while( $xml->read() === true )
217
		{
218
			if( $xml->depth === 1 && $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'productitem' )
219
			{
220
				if( ( $dom = $xml->expand() ) === false )
221
				{
222
					$msg = sprintf( 'Expanding "%1$s" node failed', 'productitem' );
223
					throw new \Aimeos\Controller\Jobs\Exception( $msg );
224
				}
225
226
				$nodes[] = $dom;
227
228
				if( $slice++ >= $maxquery )
229
				{
230
					$this->importNodes( $nodes, $domains );
231
					unset( $nodes );
232
					$nodes = [];
233
					$slice = 0;
234
				}
235
			}
236
		}
237
238
		$this->importNodes( $nodes, $domains );
239
		unset( $nodes );
240
241
		$this->saveTypes();
242
243
		foreach( $this->getProcessors() as $proc ) {
244
			$proc->finish();
0 ignored issues
show
Bug introduced by
The method finish() does not exist on Aimeos\Controller\Common...ort\Xml\Processor\Iface. Since it exists in all sub-types, consider adding an abstract or default implementation to Aimeos\Controller\Common...ort\Xml\Processor\Iface. ( Ignorable by Annotation )

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

244
			$proc->/** @scrutinizer ignore-call */ 
245
          finish();
Loading history...
245
		}
246
247
		$logger->log( sprintf( 'Finished product import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
248
249
		if( !empty( $backup ) && @rename( $filename, strftime( $backup ) ) === false )
250
		{
251
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, strftime( $backup ) );
252
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
253
		}
254
	}
255
256
257
	/**
258
	 * Imports the given DOM nodes
259
	 *
260
	 * @param \DomElement[] $nodes List of nodes to import
261
	 * @param string[] $ref List of domain names whose referenced items will be updated in the product items
262
	 */
263
	protected function importNodes( array $nodes, array $ref )
264
	{
265
		$codes = $map = [];
266
267
		foreach( $nodes as $node )
268
		{
269
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null ) {
270
				$codes[$attr->nodeValue] = null;
271
			}
272
		}
273
274
		$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
275
		$search = $manager->createSearch()->setSlice( 0, count( $codes ) );
276
		$search->setConditions( $search->compare( '==', 'product.code', array_keys( $codes ) ) );
277
278
		foreach( $manager->searchItems( $search, $ref ) as $item ) {
279
			$map[$item->getCode()] = $item;
1 ignored issue
show
Bug introduced by
The method getCode() does not exist on Aimeos\MShop\Common\Item\Iface. It seems like you code against a sub-type of said class. However, the method does not exist in Aimeos\MAdmin\Log\Item\Iface or Aimeos\MShop\Order\Item\Base\Product\Iface or Aimeos\MShop\Text\Item\Iface or Aimeos\MAdmin\Cache\Item\Iface or Aimeos\MShop\Common\Item\Status\Iface or Aimeos\MShop\Stock\Item\Iface or Aimeos\MShop\Media\Item\Iface or Aimeos\MShop\Order\Item\Iface or Aimeos\MShop\Subscription\Item\Iface or Aimeos\MShop\Coupon\Item\Iface or Aimeos\MShop\Common\Item\ListRef\Iface or Aimeos\MAdmin\Job\Item\Iface or Aimeos\MShop\Order\Item\Status\Iface or Aimeos\MShop\Common\Item\Lists\Iface or Aimeos\MShop\Price\Item\Iface or Aimeos\MShop\Locale\Item\Iface or Aimeos\MShop\Common\Item\PropertyRef\Iface or Aimeos\MShop\Tag\Item\Iface or Aimeos\MShop\Order\Item\Base\Iface or Aimeos\MShop\Supplier\Item\Address\Iface or Aimeos\MShop\Common\Item\Address\Iface or Aimeos\MShop\Common\Item\Property\Iface or Aimeos\MShop\Common\Item\AddressRef\Iface or Aimeos\MShop\Plugin\Item\Iface or Aimeos\MShop\Order\Item\Base\Product\Iface or Aimeos\MShop\Text\Item\Iface or Aimeos\MShop\Media\Item\Iface or Aimeos\MShop\Coupon\Item\Iface or Aimeos\MAdmin\Job\Item\Iface or Aimeos\MShop\Common\Item\Lists\Iface or Aimeos\MShop\Price\Item\Iface or Aimeos\MShop\Locale\Item\Iface or Aimeos\MShop\Plugin\Item\Iface or Aimeos\MShop\Text\Item\Iface or Aimeos\MShop\Media\Item\Iface or Aimeos\MShop\Price\Item\Iface or Aimeos\MShop\Media\Item\Iface or Aimeos\MShop\Price\Item\Iface or Aimeos\MShop\Order\Item\Base\Base or Aimeos\MShop\Order\Item\Base\Standard or Aimeos\MShop\Customer\Item\Address\Iface or Aimeos\MShop\Order\Item\Base\Address\Iface or Aimeos\MShop\Supplier\Item\Address\Iface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

279
			$map[$item->/** @scrutinizer ignore-call */ getCode()] = $item;
Loading history...
280
		}
281
282
		foreach( $nodes as $node )
283
		{
284
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null && isset( $map[$attr->nodeValue] ) ) {
285
				$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...
286
			} else {
287
				$item = $this->process( $manager->createItem(), $node );
288
			}
289
		}
290
	}
291
292
293
	/**
294
	 * Updates the product item and its referenced items using the given DOM node
295
	 *
296
	 * @param \Aimeos\MShop\Product\Item\Iface $item Product item object to update
297
	 * @param \DomElement $node DOM node used for updateding the product item
298
	 * @return \Aimeos\MShop\Product\Item\Iface $item Updated product item object
299
	 */
300
	protected function process( \Aimeos\MShop\Product\Item\Iface $item, \DomElement $node )
301
	{
302
		$list = $subnodes = [];
303
304
		foreach( $node->attributes as $attr ) {
305
			$list[$attr->nodeName] = $attr->nodeValue;
306
		}
307
308
		foreach( $node->childNodes as $tag )
309
		{
310
			if( in_array( $tag->nodeName, ['lists', 'property'] ) ) {
311
				$item = $this->getProcessor( $tag->nodeName )->process( $item, $tag );
312
			} elseif( in_array( $tag->nodeName, ['catalog'] ) ) {
313
				$subnodes[$tag->nodeName] = $tag;
314
			} else {
315
				$list[$tag->nodeName] = $tag->nodeValue;
316
			}
317
		}
318
319
		$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

319
		$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...
320
		$this->addType( 'product/type', 'product', $item->getType() );
321
322
		foreach( $subnodes as $name => $subnode ) {
323
			$item = $this->getProcessor( $name )->process( $item, $subnode );
324
		}
325
326
		return $item;
327
	}
328
}
329