Passed
Push — master ( c9c1dd...eb7532 )
by Aimeos
18:11
created

Xml::checkConfigBE()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2019-2023
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\Base\Critera\Attribute\Iface
86
	 */
87
	public function getConfigBE() : array
88
	{
89
		return $this->getConfigItems( $this->beConfig );
90
	}
91
92
93
	/**
94
	 * Sends the details of all orders to the ERP system for further processing
95
	 *
96
	 * @param \Aimeos\MShop\Order\Item\Iface[] $orders List of order invoice objects
97
	 * @return \Aimeos\MShop\Order\Item\Iface[] Updated order items
98
	 */
99
	public function push( iterable $orders ) : \Aimeos\Map
100
	{
101
		$this->createFile( $this->createXml( $orders ) );
102
103
		foreach( $orders as $key => $order ) {
104
			$orders[$key] = $order->setStatusDelivery( \Aimeos\MShop\Order\Item\Base::STAT_PROGRESS );
105
		}
106
107
		return map( $orders );
108
	}
109
110
111
	/**
112
	 * Looks for new update files and updates the orders for which status updates were received.
113
	 * If batch processing of files isn't supported, this method can be empty.
114
	 *
115
	 * @return bool True if the update was successful, false if async updates are not supported
116
	 * @throws \Aimeos\MShop\Service\Exception If updating one of the orders failed
117
	 */
118
	public function updateAsync() : bool
119
	{
120
		$context = $this->context();
121
		$logger = $context->logger();
122
		$location = $this->require( 'xml.updatedir' );
123
124
		if( !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

124
		if( !file_exists( /** @scrutinizer ignore-type */ $location ) )
Loading history...
125
		{
126
			$msg = sprintf( 'File or directory "%1$s" doesn\'t exist', $location );
127
			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...
128
		}
129
130
		$msg = sprintf( 'Started order status import from "%1$s"', $location );
131
		$logger->info( $msg, 'core/service' );
132
133
		$files = [];
134
135
		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

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

229
				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...
230
					$nodes[$attr->nodeValue] = $dom;
231
				}
232
			}
233
		}
234
235
		$this->importNodes( $nodes );
236
		unset( $nodes );
237
238
		$msg = sprintf( 'Finished order status import from file "%1$s"', $filename );
239
		$logger->info( $msg, 'core/service' );
240
241
		$backup = \Aimeos\Base\Str::strtime( $this->getConfigValue( 'xml.backupdir', '' ) );
242
243
		if( !empty( $backup ) && @rename( $filename, $backup ) === false )
244
		{
245
			$msg = sprintf( 'Unable to move imported file "%1$s" to "%2$s"', $filename, $backup );
246
			throw new \Aimeos\Controller\Jobs\Exception( $msg );
247
		}
248
249
		return $this;
250
	}
251
252
253
	/**
254
	 * Imports the orders from the given XML nodes
255
	 *
256
	 * @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...
257
	 * @return \Aimeos\MShop\Service\Provider\Delivery\Iface Same object for fluent interface
258
	 */
259
	protected function importNodes( array $nodes ) : \Aimeos\MShop\Service\Provider\Delivery\Iface
260
	{
261
		$manager = \Aimeos\MShop::create( $this->context(), 'order' );
262
		$search = $manager->filter()->slice( 0, count( $nodes ) );
263
		$search->setConditions( $search->compare( '==', 'order.id', array_keys( $nodes ) ) );
264
		$items = $manager->search( $search );
265
266
		foreach( $nodes as $node )
267
		{
268
			$list = [];
269
270
			foreach( $node->childNodes as $childNode ) {
271
				$list[$childNode->nodeName] = $childNode->nodeValue;
272
			}
273
274
			if( ( $attr = $node->attributes->getNamedItem( 'ref' ) ) !== null
275
				&& ( $item = $items->get( $attr->nodeValue ) ) !== null
276
			) {
277
				$item->fromArray( $list );
278
			}
279
		}
280
281
		$manager->save( $items->toArray() );
282
		return $this;
283
	}
284
}
285