ReaderAbstract::toXML()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 6
nop 1
dl 0
loc 24
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class ReaderAbstract
4
 *
5
 * @filesource   ReaderAbstract.php
6
 * @created      05.01.2019
7
 * @package      codemasher\WildstarDB\Archive
8
 * @author       smiley <[email protected]>
9
 * @copyright    2019 smiley
10
 * @license      MIT
11
 */
12
13
namespace codemasher\WildstarDB\Archive;
14
15
use chillerlan\Database\Database;
16
use codemasher\WildstarDB\WSDBException;
17
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
18
use SimpleXMLElement;
19
20
use function array_column, array_values, count, dirname, fclose, file_put_contents, fopen, fputcsv, fread, is_file,
21
	is_readable, is_resource, is_writable, json_encode, mb_convert_encoding, memory_get_peak_usage, memory_get_usage,
22
	property_exists, realpath, rewind, stream_get_contents, strlen, trim, unpack;
23
24
use const LIBXML_BIGLINES, PHP_INT_SIZE;
25
26
/**
27
 * @property string $file
28
 * @property array  $header
29
 * @property string $name
30
 * @property array  $cols
31
 * @property array  $data
32
 * @property int    $headerSize
33
 */
34
abstract class ReaderAbstract implements ReaderInterface, LoggerAwareInterface{
35
	use LoggerAwareTrait;
36
37
	/**
38
	 * @see http://php.net/manual/function.pack.php
39
	 * @var string
40
	 * @internal
41
	 */
42
	protected $FORMAT_HEADER;
43
44
	/**
45
	 * @var string
46
	 */
47
	protected $file = '';
48
49
	/**
50
	 * @var string
51
	 */
52
	protected $name = '';
53
54
	/**
55
	 * @var array
56
	 */
57
	protected $header = [];
58
59
	/**
60
	 * @var array
61
	 */
62
	protected $cols = [];
63
64
	/**
65
	 * @var array
66
	 */
67
	protected $data = [];
68
69
	/**
70
	 * @var resource
71
	 * @internal
72
	 */
73
	protected $fh;
74
75
	/**
76
	 * @var int
77
	 * @internal
78
	 */
79
	protected $headerSize = 96;
80
81
	/**
82
	 * ReaderInterface constructor.
83
	 *
84
	 * @param \Psr\Log\LoggerInterface|null $logger
85
	 *
86
	 * @throws \codemasher\WildstarDB\WSDBException
87
	 */
88
	public function __construct(LoggerInterface $logger = null){
89
90
		if(PHP_INT_SIZE < 8){
91
			throw new WSDBException('64-bit PHP required');
92
		}
93
94
		$this->setLogger($logger ?? new NullLogger);
95
	}
96
97
	/**
98
	 * @return void
99
	 */
100
	public function __destruct(){
101
		$this->logger->info('memory usage: '.round(memory_get_usage(true) / 1048576, 3).'MB');
102
		$this->logger->info('peak memory usage: '.round(memory_get_peak_usage(true) / 1048576, 3).'MB');
103
104
		$this->close();
105
	}
106
107
	/**
108
	 * @return \codemasher\WildstarDB\Archive\ReaderInterface
109
	 */
110
	public function close():ReaderInterface{
111
112
		if(is_resource($this->fh)){
113
			fclose($this->fh);
114
115
			$this->fh = null;
116
		}
117
118
		$this->file   = '';
119
		$this->name   = '';
120
		$this->header = [];
121
		$this->cols   = [];
122
		$this->data   = [];
123
124
		return $this;
125
	}
126
127
	/**
128
	 * @param string $name
129
	 *
130
	 * @return mixed|null
131
	 */
132
	public function __get(string $name){
133
		return property_exists($this, $name) && $name !== 'fh' ? $this->{$name} : null;
134
	}
135
136
	/**
137
	 * @param string $filename
138
	 *
139
	 * @return void
140
	 * @throws \codemasher\WildstarDB\WSDBException
141
	 */
142
	protected function loadFile(string $filename):void{
143
		$this->close();
144
#		$filename = realpath($filename);
145
146
		if(!$filename || !is_file($filename) || !is_readable($filename)){
147
			throw new WSDBException('input file not readable');
148
		}
149
150
		$this->file = $filename;
151
		$this->fh   = fopen($this->file, 'rb');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($this->file, 'rb') can also be of type false. However, the property $fh is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
152
		$header     = fread($this->fh, $this->headerSize);
0 ignored issues
show
Bug introduced by
It seems like $this->fh can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

152
		$header     = fread(/** @scrutinizer ignore-type */ $this->fh, $this->headerSize);
Loading history...
153
154
		$this->logger->info('loading: '.$this->file);
155
156
		if(strlen($header) !== $this->headerSize){
157
			throw new WSDBException('cannot read header');
158
		}
159
160
		$this->header = unpack($this->FORMAT_HEADER, $header);
0 ignored issues
show
Documentation Bug introduced by
It seems like unpack($this->FORMAT_HEADER, $header) can also be of type false. However, the property $header is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
161
	}
162
163
	/**
164
	 * @param string $str
165
	 *
166
	 * @return string
167
	 */
168
	protected function decodeString(string $str):string{
169
		return trim(mb_convert_encoding($str, 'UTF-8', 'UTF-16LE'));
170
	}
171
172
	/**
173
	 * @return void
174
	 * @throws \codemasher\WildstarDB\WSDBException
175
	 */
176
	protected function checkData():void{
177
178
		if(empty($this->data)){
179
			throw new WSDBException('empty data, run ReaderInterface::read() first');
180
		}
181
182
	}
183
184
	/**
185
	 * @param string $data
186
	 * @param string $file
187
	 *
188
	 * @return bool
189
	 * @throws \codemasher\WildstarDB\WSDBException
190
	 */
191
	protected function saveToFile(string $data, string $file):bool{
192
193
		if(!is_writable(dirname($file))){
194
			throw new WSDBException('cannot write data to file: '.$file.', target directory is not writable');
195
		}
196
197
		$this->logger->info('writing data to file: '.$file);
198
199
		return (bool)file_put_contents($file, $data);
200
	}
201
202
	/**
203
	 * @param string|null $file
204
	 * @param int|null    $jsonOptions
205
	 *
206
	 * @return string
207
	 */
208
	public function toJSON(string $file = null, int $jsonOptions = 0):string{
209
		$this->checkData();
210
		$json = json_encode($this->data, $jsonOptions);
211
212
		if($file !== null){
213
			$this->saveToFile($json, $file);
214
		}
215
216
		return $json;
217
	}
218
219
	/**
220
	 * @param string|null $file
221
	 * @param string      $delimiter
222
	 * @param string      $enclosure
223
	 * @param string      $escapeChar
224
	 *
225
	 * @return string
226
	 */
227
	public function toCSV(string $file = null, string $delimiter = ',', string $enclosure = '"', string $escapeChar = '\\'):string{
228
		$this->checkData();
229
230
		$mh = fopen('php://memory', 'r+');
231
232
		fputcsv($mh, array_column($this->cols, 'name'), $delimiter, $enclosure, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $mh can also be of type false; however, parameter $handle of fputcsv() does only seem to accept resource, 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

232
		fputcsv(/** @scrutinizer ignore-type */ $mh, array_column($this->cols, 'name'), $delimiter, $enclosure, $escapeChar);
Loading history...
233
234
		foreach($this->data as $row){
235
			fputcsv($mh, array_values($row), $delimiter, $enclosure, $escapeChar);
236
		}
237
238
		rewind($mh);
0 ignored issues
show
Bug introduced by
It seems like $mh can also be of type false; however, parameter $handle of rewind() does only seem to accept resource, 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

238
		rewind(/** @scrutinizer ignore-type */ $mh);
Loading history...
239
240
		$csv = stream_get_contents($mh);
0 ignored issues
show
Bug introduced by
It seems like $mh can also be of type false; however, parameter $handle of stream_get_contents() does only seem to accept resource, 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

240
		$csv = stream_get_contents(/** @scrutinizer ignore-type */ $mh);
Loading history...
241
242
		fclose($mh);
0 ignored issues
show
Bug introduced by
It seems like $mh can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

242
		fclose(/** @scrutinizer ignore-type */ $mh);
Loading history...
243
244
		if($file !== null){
245
			$this->saveToFile($csv, $file);
246
		}
247
248
		return $csv;
249
	}
250
251
	/**
252
	 * ugh!
253
	 *
254
	 * @param string|null $file
255
	 *
256
	 * @return string
257
	 */
258
	public function toXML(string $file = null):string{
259
		$this->checkData();
260
261
		$sxe   = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>', LIBXML_BIGLINES);
262
		$types = [3 => 'uint32', 4 => 'float', 11 => 'bool', 20 => 'uint64', 130 => 'string'];
263
264
		foreach($this->data as $row){
265
			$item = $sxe->addChild('item');
266
267
			foreach(array_values($row) as $i => $value){
268
				$item
269
					->addChild($this->cols[$i]['name'], $value)
270
					->addAttribute('dataType', $types[$this->cols[$i]['header']['DataType']])
271
				;
272
			}
273
		}
274
275
		$xml = $sxe->asXML();
276
277
		if($file !== null){
278
			$this->saveToFile($xml, $file);
279
		}
280
281
		return $xml;
282
	}
283
284
	/**
285
	 * @param \chillerlan\Database\Database $db
286
	 *
287
	 * @return \codemasher\WildstarDB\Archive\ReaderInterface
288
	 */
289
	public function toDB(Database $db):ReaderInterface{
290
		// Windows: https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_lower_case_table_names
291
		$createTable = $db->create
292
			->table($this->name)
293
			->primaryKey($this->cols[0]['name'])
294
			->ifNotExists()
295
		;
296
297
		foreach($this->cols as $i => $col){
298
299
			switch($col['header']['DataType']){
300
				case 3:   $createTable->int($col['name'], 10, null, null, 'UNSIGNED'); break;
301
				case 4:   $createTable->decimal($col['name'], '7,3', 0); break;
302
				case 11:  $createTable->field($col['name'], 'BOOLEAN'); break;
303
				case 20:  $createTable->field($col['name'], 'BIGINT', null, 'UNSIGNED'); break;
304
				case 130: $createTable->text($col['name']); break;
305
			}
306
307
		}
308
309
		$this->logger->info($createTable->sql());
310
311
		$createTable->query();
312
313
		if(count($this->data) < 1){
314
			$this->logger->notice('no records available for table '.$this->name);
315
316
			return $this;
317
		}
318
319
		$db->insert->into($this->name)->values($this->data)->multi();
320
321
		return $this;
322
	}
323
324
}
325