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

Standard::getItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 16
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\Attribute\Import\Xml;
12
13
14
/**
15
 * Job controller for XML attribute 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', 'Attribute 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 attributes 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
		/** controller/jobs/attribute/import/xml/location
62
		 * File or directory where the content is stored which should be imported
63
		 *
64
		 * You need to configure the XML file or directory with the XML files that
65
		 * should be imported. It should be an absolute path to be sure but can be
66
		 * relative path if you absolutely know from where the job will be executed
67
		 * from.
68
		 *
69
		 * @param string Absolute file or directory path
70
		 * @since 2019.04
71
		 * @category Developer
72
		 * @category User
73
		 * @see controller/jobs/attribute/import/xml/container/type
74
		 * @see controller/jobs/attribute/import/xml/container/content
75
		 * @see controller/jobs/attribute/import/xml/container/options
76
		 */
77
		$location = $config->get( 'controller/jobs/attribute/import/xml/location' );
78
79
		try
80
		{
81
			$logger->log( sprintf( 'Started attribute import from "%1$s"', $location ), \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(), 'attribute', 8 ) === 0 && $entry->getExtension() === 'xml' ) {
96
						$files[] = $entry->getPathname();
97
					}
98
				}
99
			}
100
			else
101
			{
102
				$files[] = $location;
103
			}
104
105
			sort( $files );
106
			$context->__sleep();
107
108
			$fcn = function( $filepath ) {
109
				$this->import( $filepath );
110
			};
111
112
			foreach( $files as $filepath ) {
113
				$context->getProcess()->start( $fcn, [$filepath] );
114
			}
115
116
			$context->getProcess()->wait();
117
118
			$logger->log( sprintf( 'Finished attribute import from "%1$s"', $location ), \Aimeos\MW\Logger\Base::INFO );
119
		}
120
		catch( \Exception $e )
121
		{
122
			$logger->log( 'Attribute import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString() );
123
			throw $e;
124
		}
125
	}
126
127
128
	/**
129
	 * Returns the attribute items for the given nodes
130
	 *
131
	 * @param \DomElement[] $nodes List of XML attribute item nodes
132
	 * @param string[] $ref Domain names of referenced items that should be fetched too
133
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] Associative list of attribute items with IDs as keys
134
	 */
135
	protected function getItems( array $nodes, array $ref )
136
	{
137
		$keys = [];
138
139
		foreach( $nodes as $node )
140
		{
141
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null ) {
142
				$keys[] = md5( $attr->nodeValue );
143
			}
144
		}
145
146
		$manager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
147
		$search = $manager->createSearch()->setSlice( 0, count( $keys ) );
148
		$search->setConditions( $search->compare( '==', 'attribute.key', $keys ) );
149
150
		return $manager->searchItems( $search, $ref );
151
	}
152
153
154
	/**
155
	 * Imports the XML file given by its path
156
	 *
157
	 * @param string $filename Absolute or relative path to the XML file
158
	 */
159
	protected function import( $filename )
160
	{
161
		$context = $this->getContext();
162
		$config = $context->getConfig();
163
		$logger = $context->getLogger();
164
165
166
		$domains = ['attribute/property', 'media', 'price', 'text'];
167
168
		/** controller/jobs/attribute/import/xml/domains
169
		 * List of item domain names that should be retrieved along with the attribute items
170
		 *
171
		 * This configuration setting overwrites the shared option
172
		 * "controller/common/attribute/import/xml/domains" if you need a
173
		 * specific setting for the job controller. Otherwise, you should
174
		 * use the shared option for consistency.
175
		 *
176
		 * @param array Associative list of MShop item domain names
177
		 * @since 2019.04
178
		 * @category Developer
179
		 * @see controller/jobs/attribute/import/xml/backup
180
		 * @see controller/jobs/attribute/import/xml/max-query
181
		 */
182
		$domains = $config->get( 'controller/jobs/attribute/import/xml/domains', $domains );
183
184
		/** controller/jobs/attribute/import/xml/backup
185
		 * Name of the backup for sucessfully imported files
186
		 *
187
		 * After a XML file was imported successfully, you can move it to another
188
		 * location, so it won't be imported again and isn't overwritten by the
189
		 * next file that is stored at the same location in the file system.
190
		 *
191
		 * You should use an absolute path to be sure but can be relative path
192
		 * if you absolutely know from where the job will be executed from. The
193
		 * name of the new backup location can contain placeholders understood
194
		 * by the PHP strftime() function to create dynamic paths, e.g. "backup/%Y-%m-%d"
195
		 * which would create "backup/2000-01-01". For more information about the
196
		 * strftime() placeholders, please have a look into the PHP documentation of
197
		 * the {@link http://php.net/manual/en/function.strftime.php strftime() function}.
198
		 *
199
		 * '''Note:''' If no backup name is configured, the file or directory
200
		 * won't be moved away. Please make also sure that the parent directory
201
		 * and the new directory are writable so the file or directory could be
202
		 * moved.
203
		 *
204
		 * @param integer Name of the backup file, optionally with date/time placeholders
205
		 * @since 2019.04
206
		 * @category Developer
207
		 * @see controller/jobs/attribute/import/xml/domains
208
		 * @see controller/jobs/attribute/import/xml/max-query
209
		 */
210
		$backup = $config->get( 'controller/jobs/attribute/import/xml/backup' );
211
212
		/** controller/jobs/attribute/import/xml/max-query
213
		 * Maximum number of XML nodes processed at once
214
		 *
215
		 * Processing and fetching several attribute items at once speeds up importing
216
		 * the XML files. The more items can be processed at once, the faster the
217
		 * import. More items also increases the memory usage of the importer and
218
		 * thus, this parameter should be low enough to avoid reaching the memory
219
		 * limit of the PHP process.
220
		 *
221
		 * @param integer Number of XML nodes
222
		 * @since 2019.04
223
		 * @category Developer
224
		 * @category User
225
		 * @see controller/jobs/attribute/import/xml/domains
226
		 * @see controller/jobs/attribute/import/xml/backup
227
		 */
228
		$maxquery = $config->get( 'controller/jobs/attribute/import/xml/max-query', 1000 );
229
230
231
		$slice = 0;
232
		$nodes = [];
233
		$xml = new \XMLReader();
234
235
		if( $xml->open( $filename, LIBXML_COMPACT | LIBXML_PARSEHUGE ) === false ) {
236
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'No XML file "%1$s" found', $filename ) );
237
		}
238
239
		$logger->log( sprintf( 'Started attribute import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
240
241
		while( $xml->read() === true )
242
		{
243
			if( $xml->depth === 1 && $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'attributeitem' )
244
			{
245
				if( ( $dom = $xml->expand() ) === false )
246
				{
247
					$msg = sprintf( 'Expanding "%1$s" node failed', 'attributeitem' );
248
					throw new \Aimeos\Controller\Jobs\Exception( $msg );
249
				}
250
251
				$nodes[] = $dom;
252
253
				if( $slice++ >= $maxquery )
254
				{
255
					$this->importNodes( $nodes, $domains );
256
					unset( $nodes );
257
					$nodes = [];
258
					$slice = 0;
259
				}
260
			}
261
		}
262
263
		$this->importNodes( $nodes, $domains );
264
		unset( $nodes );
265
266
		$this->saveTypes();
267
268
		foreach( $this->getProcessors() as $proc ) {
269
			$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

269
			$proc->/** @scrutinizer ignore-call */ 
270
          finish();
Loading history...
270
		}
271
272
		$logger->log( sprintf( 'Finished attribute import from file "%1$s"', $filename ), \Aimeos\MW\Logger\Base::INFO );
273
274
		if( !empty( $backup ) && @rename( $filename, strftime( $backup ) ) === false )
275
		{
276
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, strftime( $backup ) );
277
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
278
		}
279
	}
280
281
282
	/**
283
	 * Imports the given DOM nodes
284
	 *
285
	 * @param \DomElement[] $nodes List of nodes to import
286
	 * @param string[] $ref List of domain names whose referenced items will be updated in the attribute items
287
	 */
288
	protected function importNodes( array $nodes, array $ref )
289
	{
290
		$map = [];
291
		$manager = \Aimeos\MShop::create( $this->getContext(), 'attribute' );
292
293
		foreach( $this->getItems( $nodes, $ref ) as $item ) {
294
			$map[$item->getKey()] = $item;
295
		}
296
297
		foreach( $nodes as $node )
298
		{
299
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null && isset( $map[md5( $attr->nodeValue )] ) ) {
300
				$item = $this->process( $map[md5( $attr->nodeValue )], $node );
301
			} else {
302
				$item = $this->process( $manager->createItem(), $node );
303
			}
304
305
			$manager->saveItem( $item );
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

305
			$manager->/** @scrutinizer ignore-call */ 
306
             saveItem( $item );

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...
306
			$this->addType( 'attribute/type', $item->getDomain(), $item->getType() );
307
		}
308
	}
309
310
311
	/**
312
	 * Updates the attribute item and its referenced items using the given DOM node
313
	 *
314
	 * @param \Aimeos\MShop\Attribute\Item\Iface $item Attribute item object to update
315
	 * @param \DomElement $node DOM node used for updateding the attribute item
316
	 * @return \Aimeos\MShop\Attribute\Item\Iface $item Updated attribute item object
317
	 */
318
	protected function process( \Aimeos\MShop\Attribute\Item\Iface $item, \DomElement $node )
319
	{
320
		$list = [];
321
322
		foreach( $node->attributes as $attr ) {
323
			$list[$attr->nodeName] = $attr->nodeValue;
324
		}
325
326
		foreach( $node->childNodes as $tag )
327
		{
328
			if( in_array( $tag->nodeName, ['lists', 'property'] ) ) {
329
				$item = $this->getProcessor( $tag->nodeName )->process( $item, $tag );
330
			} else {
331
				$list[$tag->nodeName] = $tag->nodeValue;
332
			}
333
		}
334
335
		return $item->fromArray( $list, true );
336
	}
337
}
338