Passed
Push — master ( f5c4dd...5b94e3 )
by Aimeos
10:52
created

Xml::createXml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2019-2022
6
 * @package MShop
7
 * @subpackage Service
8
 */
9
10
11
namespace Aimeos\MShop\Service\Provider\Delivery;
12
13
14
/**
15
 * XML delivery provider implementation
16
 *
17
 * @package MShop
18
 * @subpackage Service
19
 */
20
class Xml
21
	extends \Aimeos\MShop\Service\Provider\Delivery\Base
22
	implements \Aimeos\MShop\Service\Provider\Delivery\Iface
23
{
24
	private $num = 0;
25
26
	private $beConfig = [
27
		'xml.backupdir' => [
28
			'code' => 'xml.backupdir',
29
			'internalcode' => 'xml.backupdir',
30
			'label' => 'Relative or absolute path of the backup directory (with date() placeholders)',
31
			'type' => 'string',
32
			'internaltype' => 'string',
33
			'default' => '',
34
			'required' => false,
35
		],
36
		'xml.exportpath' => [
37
			'code' => 'xml.exportpath',
38
			'internalcode' => 'xml.exportpath',
39
			'label' => 'Relative or absolute path and name of the XML files (with date() placeholders)',
40
			'type' => 'string',
41
			'internaltype' => 'string',
42
			'default' => './order_%Y-%m-%d_%H:%i:%s_%v.xml',
43
			'required' => true,
44
		],
45
		'xml.template' => [
46
			'code' => 'xml.template',
47
			'internalcode' => 'xml.template',
48
			'label' => 'Relative path of the template file name',
49
			'type' => 'string',
50
			'internaltype' => 'string',
51
			'default' => 'service/provider/delivery/xml-body',
52
			'required' => false,
53
		],
54
		'xml.updatedir' => [
55
			'code' => 'xml.updatedir',
56
			'internalcode' => 'xml.updatedir',
57
			'label' => 'Relative or absolute path and name of the order update XML files',
58
			'type' => 'string',
59
			'internaltype' => 'string',
60
			'default' => '',
61
			'required' => false,
62
		],
63
	];
64
65
66
	/**
67
	 * Checks the backend configuration attributes for validity
68
	 *
69
	 * @param array $attributes Attributes added by the shop owner in the administraton interface
70
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
71
	 * 	known by the provider but aren't valid
72
	 */
73
	public function checkConfigBE( array $attributes ) : array
74
	{
75
		$errors = parent::checkConfigBE( $attributes );
76
77
		return array_merge( $errors, $this->checkConfig( $this->beConfig, $attributes ) );
78
	}
79
80
81
	/**
82
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
83
	 * rules for the value of each field in the administration interface.
84
	 *
85
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
86
	 */
87
	public function getConfigBE() : array
88
	{
89
		return $this->getConfigItems( $this->beConfig );
90
	}
91
92
93
	/**
94
	 * Creates the XML files and updates the delivery status
95
	 *
96
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order instance
97
	 * @return \Aimeos\MShop\Order\Item\Iface Updated order item
98
	 */
99
	public function process( \Aimeos\MShop\Order\Item\Iface $order ) : \Aimeos\MShop\Order\Item\Iface
100
	{
101
		$this->createFile( $this->createXml( [$order] ) );
102
		return $order->setStatusDelivery( \Aimeos\MShop\Order\Item\Base::STAT_PROGRESS );
103
	}
104
105
106
	/**
107
	 * Sends the details of all orders to the ERP system for further processing
108
	 *
109
	 * @param \Aimeos\MShop\Order\Item\Iface[] $orders List of order invoice objects
110
	 * @return \Aimeos\MShop\Order\Item\Iface[] Updated order items
111
	 */
112
	public function processBatch( iterable $orders ) : \Aimeos\Map
113
	{
114
		$this->createFile( $this->createXml( $orders ) );
115
116
		foreach( $orders as $key => $order ) {
117
			$orders[$key] = $order->setStatusDelivery( \Aimeos\MShop\Order\Item\Base::STAT_PROGRESS );
118
		}
119
120
		return map( $orders );
121
	}
122
123
124
	/**
125
	 * Looks for new update files and updates the orders for which status updates were received.
126
	 * If batch processing of files isn't supported, this method can be empty.
127
	 *
128
	 * @return bool True if the update was successful, false if async updates are not supported
129
	 * @throws \Aimeos\MShop\Service\Exception If updating one of the orders failed
130
	 */
131
	public function updateAsync() : bool
132
	{
133
		$context = $this->context();
134
		$logger = $context->logger();
135
		$location = $this->getConfigValue( 'xml.updatedir' );
136
137
		if( $location === '' || !file_exists( $location ) )
0 ignored issues
show
Bug introduced by
It seems like $location can also be of type null; however, parameter $filename of file_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

137
		if( $location === '' || !file_exists( /** @scrutinizer ignore-type */ $location ) )
Loading history...
138
		{
139
			$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
140
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
0 ignored issues
show
Bug introduced by
The type Aimeos\Controller\Jobs\Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
141
		}
142
143
		$msg = sprintf( 'Started order status import from "%1$s"', $location );
144
		$logger->info( $msg, 'core/service' );
145
146
		$files = [];
147
148
		if( is_dir( $location ) )
0 ignored issues
show
Bug introduced by
It seems like $location can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

148
		if( is_dir( /** @scrutinizer ignore-type */ $location ) )
Loading history...
149
		{
150
			foreach( new \DirectoryIterator( $location ) as $entry )
151
			{
152
				if( !strncmp( $entry->getFilename(), 'order', 5 ) && $entry->getExtension() === 'xml' ) {
153
					$files[] = $entry->getPathname();
154
				}
155
			}
156
		}
157
		else
158
		{
159
			$files[] = $location;
160
		}
161
162
		sort( $files );
163
164
		foreach( $files as $filepath ) {
165
			$this->importFile( $filepath );
166
		}
167
168
		$msg = sprintf( 'Finished order status import from "%1$s"', $location );
169
		$logger->info( $msg, 'core/service' );
170
171
		return true;
172
	}
173
174
175
	/**
176
	 * Stores the content into the file
177
	 *
178
	 * @param string $content XML content
179
	 * @return \Aimeos\MShop\Service\Provider\Delivery\Iface Same object for fluent interface
180
	 */
181
	protected function createFile( string $content ) : \Aimeos\MShop\Service\Provider\Delivery\Iface
182
	{
183
		$filepath = $this->getConfigValue( 'xml.exportpath', './order_%Y-%m-%d_%H:%i:%s_%v.xml' );
184
		$filepath = sprintf( \Aimeos\Base\Str::strtime( $filepath ), $this->num++ );
185
186
		if( file_put_contents( $filepath, $content ) === false )
187
		{
188
			$msg = sprintf( 'Unable to create order XML file "%1$s"', $filepath );
189
			throw new \Aimeos\MShop\Service\Exception( $msg );
190
		}
191
192
		return $this;
193
	}
194
195
196
	/**
197
	 * Creates the XML file for the given orders
198
	 *
199
	 * @param \Aimeos\MShop\Order\Item\Iface[] $orderItems List of order items to export
200
	 * @return string Generated XML
201
	 */
202
	protected function createXml( iterable $orderItems ) : string
203
	{
204
		$view = $this->context()->view();
205
		$template = $this->getConfigValue( 'xml.template', 'service/provider/delivery/xml-body' );
206
207
		return $view->assign( ['orderItems' => $orderItems] )->render( $template );
208
	}
209
210
211
	/**
212
	 * Imports all orders from the given XML file name
213
	 *
214
	 * @param string $filename Relative or absolute path to the XML file
215
	 * @return \Aimeos\MShop\Service\Provider\Delivery\Iface Same object for fluent interface
216
	 */
217
	protected function importFile( string $filename ) : \Aimeos\MShop\Service\Provider\Delivery\Iface
218
	{
219
		$nodes = [];
220
		$xml = new \XMLReader();
221
		$logger = $this->context()->logger();
222
223
		if( $xml->open( $filename, LIBXML_COMPACT | LIBXML_PARSEHUGE ) === false )
224
		{
225
			$msg = $this->context()->translate( 'mshop', 'No XML file "%1$s" found' );
226
			throw new \Aimeos\Controller\Jobs\Exception( sprintf( $msg, $filename ) );
227
		}
228
229
		$msg = sprintf( 'Started order status import from file "%1$s"', $filename );
230
		$logger->info( $msg, 'core/service' );
231
232
		while( $xml->read() === true )
233
		{
234
			if( $xml->depth === 1 && $xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'orderitem' )
235
			{
236
				if( ( $dom = $xml->expand() ) === false )
237
				{
238
					$msg = sprintf( 'Expanding "%1$s" node failed', 'orderitem' );
239
					throw new \Aimeos\Controller\Jobs\Exception( $msg );
240
				}
241
242
				if( ( $attr = $dom->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

242
				if( ( $attr = $dom->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...
243
					$nodes[$attr->nodeValue] = $dom;
244
				}
245
			}
246
		}
247
248
		$this->importNodes( $nodes );
249
		unset( $nodes );
250
251
		$msg = sprintf( 'Finished order status import from file "%1$s"', $filename );
252
		$logger->info( $msg, 'core/service' );
253
254
		$backup = \Aimeos\Base\Str::strtime( $this->getConfigValue( 'xml.backupdir', '' ) );
255
256
		if( !empty( $backup ) && @rename( $filename, $backup ) === false )
257
		{
258
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, $backup );
259
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
260
		}
261
262
		return $this;
263
	}
264
265
266
	/**
267
	 * Imports the orders from the given XML nodes
268
	 *
269
	 * @param \DomElement[] List of order DOM nodes
0 ignored issues
show
Bug introduced by
The type Aimeos\MShop\Service\Provider\Delivery\List was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
270
	 * @return \Aimeos\MShop\Service\Provider\Delivery\Iface Same object for fluent interface
271
	 */
272
	protected function importNodes( array $nodes ) : \Aimeos\MShop\Service\Provider\Delivery\Iface
273
	{
274
		$manager = \Aimeos\MShop::create( $this->context(), 'order' );
275
		$search = $manager->filter()->slice( 0, count( $nodes ) );
276
		$search->setConditions( $search->compare( '==', 'order.id', array_keys( $nodes ) ) );
277
		$items = $manager->search( $search );
278
279
		foreach( $nodes as $node )
280
		{
281
			$list = [];
282
283
			foreach( $node->childNodes as $childNode ) {
284
				$list[$childNode->nodeName] = $childNode->nodeValue;
285
			}
286
287
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null
288
				&& ( $item = $items->get( $attr->nodeValue ) ) !== null
289
			) {
290
				$item->fromArray( $list );
291
			}
292
		}
293
294
		$manager->save( $items->toArray() );
295
		return $this;
296
	}
297
}
298