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

Standard::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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