Passed
Push — master ( 659531...28cbe9 )
by Aimeos
03:02
created

Standard::process()   B

Complexity

Conditions 7
Paths 59

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 1
b 0
f 0
nc 59
nop 2
dl 0
loc 29
rs 8.8333
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2019-2021
6
 * @package Controller
7
 * @subpackage Jobs
8
 */
9
10
11
namespace Aimeos\Controller\Jobs\Catalog\Import\Xml;
12
13
14
/**
15
 * Job controller for XML catalog 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\Xml\Traits;
25
26
27
	/**
28
	 * Returns the localized name of the job.
29
	 *
30
	 * @return string Name of the job
31
	 */
32
	public function getName() : string
33
	{
34
		return $this->context()->translate( 'controller/jobs', 'Catalog import XML' );
35
	}
36
37
38
	/**
39
	 * Returns the localized description of the job.
40
	 *
41
	 * @return string Description of the job
42
	 */
43
	public function getDescription() : string
44
	{
45
		return $this->context()->translate( 'controller/jobs', 'Imports new and updates existing categories from XML files' );
46
	}
47
48
49
	/**
50
	 * Executes the job.
51
	 *
52
	 * @throws \Aimeos\Controller\Jobs\Exception If an error occurs
53
	 */
54
	public function run()
55
	{
56
		$context = $this->context();
57
		$config = $context->config();
58
		$logger = $context->logger();
59
60
		/** controller/jobs/catalog/import/xml/location
61
		 * File or directory where the content is stored which should be imported
62
		 *
63
		 * You need to configure the XML file or directory with the XML files that
64
		 * should be imported. It should be an absolute path to be sure but can be
65
		 * relative path if you absolutely know from where the job will be executed
66
		 * from.
67
		 *
68
		 * @param string Absolute file or directory path
69
		 * @since 2019.04
70
		 * @category Developer
71
		 * @category User
72
		 * @see controller/jobs/catalog/import/xml/container/type
73
		 * @see controller/jobs/catalog/import/xml/container/content
74
		 * @see controller/jobs/catalog/import/xml/container/options
75
		 */
76
		$location = $config->get( 'controller/jobs/catalog/import/xml/location' );
77
78
		try
79
		{
80
			$logger->info( sprintf( 'Started catalog import from "%1$s"', $location ), 'import/xml/catalog' );
81
82
			if( !file_exists( $location ) )
83
			{
84
				$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
85
				throw new \Aimeos\Controller\Jobs\Exception( $msg );
86
			}
87
88
			$files = [];
89
90
			if( is_dir( $location ) )
91
			{
92
				foreach( new \DirectoryIterator( $location ) as $entry )
93
				{
94
					if( strncmp( $entry->getFilename(), 'catalog', 7 ) === 0 && $entry->getExtension() === 'xml' ) {
95
						$files[] = $entry->getPathname();
96
					}
97
				}
98
			}
99
			else
100
			{
101
				$files[] = $location;
102
			}
103
104
			sort( $files );
105
			$context->__sleep();
106
107
			$fcn = function( $filepath ) {
108
				$this->import( $filepath );
109
			};
110
111
			foreach( $files as $filepath ) {
112
				$context->process()->start( $fcn, [$filepath] );
113
			}
114
115
			$context->process()->wait();
116
117
			$logger->info( sprintf( 'Finished catalog import from "%1$s"', $location ), 'import/xml/catalog' );
118
		}
119
		catch( \Exception $e )
120
		{
121
			$logger->error( 'Catalog import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString(), 'import/xml/catalog' );
122
			$this->mail( 'Catalog XML import error', $e->getMessage() . "\n" . $e->getTraceAsString() );
123
			throw $e;
124
		}
125
	}
126
127
128
	/**
129
	 * Imports the XML file given by its path
130
	 *
131
	 * @param string $filename Absolute or relative path to the XML file
132
	 */
133
	protected function import( string $filename )
134
	{
135
		$context = $this->context();
136
		$config = $context->config();
137
		$logger = $context->logger();
138
139
140
		/** controller/jobs/catalog/import/xml/domains
141
		 * List of item domain names that should be retrieved along with the catalog items
142
		 *
143
		 * This configuration setting overwrites the shared option
144
		 * "controller/common/catalog/import/xml/domains" if you need a
145
		 * specific setting for the job controller. Otherwise, you should
146
		 * use the shared option for consistency.
147
		 *
148
		 * @param array Associative list of MShop item domain names
149
		 * @since 2019.04
150
		 * @category Developer
151
		 * @see controller/jobs/catalog/import/xml/backup
152
		 * @see controller/jobs/catalog/import/xml/max-query
153
		 */
154
		$domains = $config->get( 'controller/jobs/catalog/import/xml/domains', [] );
155
156
		/** controller/jobs/catalog/import/xml/backup
157
		 * Name of the backup for sucessfully imported files
158
		 *
159
		 * After a XML file was imported successfully, you can move it to another
160
		 * location, so it won't be imported again and isn't overwritten by the
161
		 * next file that is stored at the same location in the file system.
162
		 *
163
		 * You should use an absolute path to be sure but can be relative path
164
		 * if you absolutely know from where the job will be executed from. The
165
		 * name of the new backup location can contain placeholders understood
166
		 * by the PHP DateTime::format() method (with percent signs prefix) to
167
		 * create dynamic paths, e.g. "backup/%Y-%m-%d" which would create
168
		 * "backup/2000-01-01". For more information about the date() placeholders,
169
		 * please have a look  into the PHP documentation of the
170
		 * {@link https://www.php.net/manual/en/datetime.format.php format() method}.
171
		 *
172
		 * **Note:** If no backup name is configured, the file or directory
173
		 * won't be moved away. Please make also sure that the parent directory
174
		 * and the new directory are writable so the file or directory could be
175
		 * moved.
176
		 *
177
		 * @param integer Name of the backup file, optionally with date/time placeholders
178
		 * @since 2019.04
179
		 * @category Developer
180
		 * @see controller/jobs/catalog/import/xml/domains
181
		 * @see controller/jobs/catalog/import/xml/max-query
182
		 */
183
		$backup = $config->get( 'controller/jobs/catalog/import/xml/backup' );
184
185
186
		$xml = new \XMLReader();
187
188
		if( $xml->open( $filename, LIBXML_COMPACT | LIBXML_PARSEHUGE ) === false ) {
189
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( 'No XML file "%1$s" found', $filename ) );
190
		}
191
192
		$logger->info( sprintf( 'Started catalog import from file "%1$s"', $filename ), 'import/xml/catalog' );
193
194
		$this->importTree( $xml, $domains );
195
196
		foreach( $this->getProcessors() as $proc ) {
197
			$proc->finish();
198
		}
199
200
		$logger->info( sprintf( 'Finished catalog import from file "%1$s"', $filename ), 'import/xml/catalog' );
201
202
		if( !empty( $backup ) && @rename( $filename, $backup = \Aimeos\MW\Str::strtime( $backup ) ) === false )
203
		{
204
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, $backup );
205
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
206
		}
207
	}
208
209
210
	/**
211
	 * Imports a single category node
212
	 *
213
	 * @param \DomElement $node DOM node of "catalogitem" element
214
	 * @param string[] $domains List of domain names whose referenced items will be updated in the catalog items
215
	 * @param string|null $parentid ID of the parent catalog node
216
	 * @param array &$map Will contain the associative list of code/ID pairs of the child categories
217
	 * @return string Catalog ID of the imported category
218
	 */
219
	protected function importNode( \DomElement $node, array $domains, string $parentid = null, array &$map ) : string
220
	{
221
		$manager = \Aimeos\MShop::create( $this->context(), 'catalog' );
222
223
		if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null )
0 ignored issues
show
Bug introduced by
The method getNamedItem() does not exist on null. ( Ignorable by Annotation )

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

223
		if( ( $attr = $node->attributes->/** @scrutinizer ignore-call */ getNamedItem( 'ref' ) ) !== null )

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...
224
		{
225
			try
226
			{
227
				$item = $manager->find( $attr->nodeValue, $domains );
228
				$manager->move( $item->getId(), $item->getParentId(), $parentid );
229
230
				$item = $this->process( $item, $node );
231
				$currentid = $manager->save( $item )->getId();
232
				unset( $item );
233
234
				$tree = $manager->getTree( $currentid, [], \Aimeos\MW\Tree\Manager\Base::LEVEL_LIST );
235
236
				foreach( $tree->getChildren() as $child ) {
237
					$map[$child->getCode()] = $child->getId();
238
				}
239
240
				return $currentid;
241
			}
242
			catch( \Aimeos\MShop\Exception $e ) {} // not found, create new
243
		}
244
245
		$item = $this->process( $manager->create(), $node );
246
		return $manager->insert( $item, $parentid )->getId();
247
	}
248
249
250
	/**
251
	 * Imports the catalog document
252
	 *
253
	 * @param \XMLReader $xml Catalog document to import
254
	 * @param string[] $domains List of domain names whose referenced items will be updated in the catalog items
255
	 * @param string|null $parentid ID of the parent catalog node
256
	 * @param array $map Associative list of catalog code as keys and category ID as values
257
	 */
258
	protected function importTree( \XMLReader $xml, array $domains, string $parentid = null, array $map = [] )
259
	{
260
		$total = 0;
261
		$childMap = [];
262
		$currentid = $parentid;
263
264
		while( $xml->read() === true )
265
		{
266
			if( $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'catalogitem' )
267
			{
268
				if( ( $node = $xml->expand() ) === false )
269
				{
270
					$msg = sprintf( 'Expanding "%1$s" node failed', 'catalogitem' );
271
					throw new \Aimeos\Controller\Jobs\Exception( $msg );
272
				}
273
274
				if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null ) {
275
					unset( $map[$attr->nodeValue] );
276
				}
277
278
				$currentid = $this->importNode( $node, $domains, $parentid, $childMap );
279
				$total++;
280
			}
281
			elseif( $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'catalog' )
282
			{
283
				$this->importTree( $xml, $domains, $currentid, $childMap );
284
				$childMap = [];
285
			}
286
			elseif( $xml->nodeType === \XMLReader::END_ELEMENT && $xml->name === 'catalog' )
287
			{
288
				\Aimeos\MShop::create( $this->context(), 'catalog' )->delete( $map );
289
				break;
290
			}
291
		}
292
	}
293
294
295
	/**
296
	 * Updates the catalog item and its referenced items using the given DOM node
297
	 *
298
	 * @param \Aimeos\MShop\Catalog\Item\Iface $item Catalog item object to update
299
	 * @param \DomElement $node DOM node used for updateding the catalog item
300
	 * @return \Aimeos\MShop\Catalog\Item\Iface $item Updated catalog item object
301
	 */
302
	protected function process( \Aimeos\MShop\Catalog\Item\Iface $item, \DomElement $node ) : \Aimeos\MShop\Catalog\Item\Iface
303
	{
304
		try
305
		{
306
			$list = [];
307
308
			foreach( $node->attributes as $attr ) {
309
				$list[$attr->nodeName] = $attr->nodeValue;
310
			}
311
312
			foreach( $node->childNodes as $tag )
313
			{
314
				if( $tag->nodeName === 'lists' ) {
315
					$item = $this->getProcessor( $tag->nodeName )->process( $item, $tag );
316
				} elseif( $tag->nodeName[0] !== '#' ) {
317
					$list[$tag->nodeName] = $tag->nodeValue;
318
				}
319
			}
320
321
			$list['catalog.config'] = isset( $list['catalog.config'] ) ? json_decode( $list['catalog.config'], true ) : [];
322
			$item->fromArray( $list, true );
323
		}
324
		catch( \Exception $e )
325
		{
326
			$msg = 'Catalog import error: ' . $e->getMessage() . "\n" . $e->getTraceAsString();
327
			$this->context()->logger()->error( $msg, 'import/xml/catalog' );
328
		}
329
330
		return $item;
331
	}
332
}
333