Passed
Push — master ( bdfaf8...931c46 )
by smiley
01:33
created

ReaderAbstract::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 0
dl 0
loc 15
rs 9.9666
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
8
 * @author       smiley <[email protected]>
9
 * @copyright    2019 smiley
10
 * @license      MIT
11
 */
12
13
namespace codemasher\WildstarDB;
14
15
use chillerlan\Database\Database;
16
use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger};
17
use SimpleXMLElement;
18
19
/**
20
 * @property string $file
21
 * @property array  $header
22
 * @property string $name
23
 * @property array  $cols
24
 * @property array  $data
25
 * @property int    $headerSize
26
 */
27
abstract class ReaderAbstract implements ReaderInterface, LoggerAwareInterface{
28
	use LoggerAwareTrait;
29
30
	/**
31
	 * @see http://php.net/manual/function.pack.php
32
	 * @var string
33
	 */
34
	protected $FORMAT_HEADER;
35
36
	/**
37
	 * @var string
38
	 */
39
	protected $file = '';
40
41
	/**
42
	 * @var string
43
	 */
44
	protected $name = '';
45
46
	/**
47
	 * @var array
48
	 */
49
	protected $header = [];
50
51
	/**
52
	 * @var array
53
	 */
54
	protected $cols = [];
55
56
	/**
57
	 * @var array
58
	 */
59
	protected $data = [];
60
61
	/**
62
	 * @var resource
63
	 */
64
	protected $fh;
65
66
	/**
67
	 * @var int
68
	 */
69
	protected $headerSize = 96;
70
71
	/**
72
	 * ReaderInterface constructor.
73
	 *
74
	 * @param \Psr\Log\LoggerInterface|null $logger
75
	 *
76
	 * @throws \codemasher\WildstarDB\WSDBException
77
	 */
78
	public function __construct(LoggerInterface $logger = null){
79
80
		if(\PHP_INT_SIZE < 8){
81
			throw new WSDBException('64-bit PHP required');
82
		}
83
84
		$this->setLogger($logger ?? new NullLogger);
85
	}
86
87
	/**
88
	 * @return void
89
	 */
90
	public function __destruct(){
91
		$this->logger->info('memory usage: '.(\memory_get_usage(true)/1048576).'MB');
92
		$this->logger->info('peak memory usage: '.(\memory_get_peak_usage(true)/1048576).'MB');
93
94
		$this->close();
95
	}
96
97
	/**
98
	 * @return \codemasher\WildstarDB\ReaderInterface
99
	 */
100
	public function close():ReaderInterface{
101
102
		if(\is_resource($this->fh)){
103
			\fclose($this->fh);
104
105
			$this->fh = null;
106
		}
107
108
		$this->file   = '';
109
		$this->name   = '';
110
		$this->header = [];
111
		$this->cols   = [];
112
		$this->data   = [];
113
114
		return $this;
115
	}
116
117
	/**
118
	 * @param string $name
119
	 *
120
	 * @return mixed|null
121
	 */
122
	public function __get(string $name){
123
		return \property_exists($this, $name) && $name !== 'fh' ? $this->{$name} : null;
124
	}
125
126
	/**
127
	 * @param string $filename
128
	 *
129
	 * @return void
130
	 * @throws \codemasher\WildstarDB\WSDBException
131
	 */
132
	protected function loadFile(string $filename):void{
133
		$this->close();
134
		$filename = \realpath($filename);
135
136
		if(!$filename || !\is_file($filename) || !\is_readable($filename)){
137
			throw new WSDBException('input file not readable');
138
		}
139
140
		$this->file = $filename;
141
		$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...
142
		$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

142
		$header     = \fread(/** @scrutinizer ignore-type */ $this->fh, $this->headerSize);
Loading history...
143
144
		$this->logger->info('loading: '.$this->file);
145
146
		if(\strlen($header) !== $this->headerSize){
147
			throw new WSDBException('cannot read header');
148
		}
149
150
		$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...
151
	}
152
153
	/**
154
	 * @param string $str
155
	 *
156
	 * @return string
157
	 */
158
	protected function decodeString(string $str):string{
159
		return \trim(\mb_convert_encoding($str, 'UTF-8', 'UTF-16LE'));
160
	}
161
162
	/**
163
	 * @return void
164
	 * @throws \codemasher\WildstarDB\WSDBException
165
	 */
166
	protected function checkData():void{
167
168
		if(empty($this->data)){
169
			throw new WSDBException('empty data, run ReaderInterface::read() first');
170
		}
171
172
	}
173
174
	/**
175
	 * @param string $data
176
	 * @param string $file
177
	 *
178
	 * @return bool
179
	 * @throws \codemasher\WildstarDB\WSDBException
180
	 */
181
	protected function saveToFile(string $data, string $file):bool{
182
183
		if(!\is_writable(\dirname($file))){
184
			throw new WSDBException('cannot write data to file: '.$file.', target directory is not writable');
185
		}
186
187
		$this->logger->info('writing data to file: '.$file);
188
189
		return (bool)\file_put_contents($file, $data);
190
	}
191
192
	/**
193
	 * @param string|null $file
194
	 * @param int|null    $jsonOptions
195
	 *
196
	 * @return string
197
	 */
198
	public function toJSON(string $file = null, int $jsonOptions = 0):string{
199
		$this->checkData();
200
201
		$json = \json_encode($this->data, $jsonOptions);
202
203
		if($file !== null){
204
			$this->saveToFile($json, $file);
205
		}
206
207
		return $json;
208
	}
209
210
	/**
211
	 * @param string|null $file
212
	 * @param string      $delimiter
213
	 * @param string      $enclosure
214
	 * @param string      $escapeChar
215
	 *
216
	 * @return string
217
	 */
218
	public function toCSV(string $file = null, string $delimiter = ',', string $enclosure = '"', string $escapeChar = '\\'):string{
219
		$this->checkData();
220
221
		$mh = \fopen('php://memory', 'r+');
222
223
		\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

223
		\fputcsv(/** @scrutinizer ignore-type */ $mh, \array_column($this->cols, 'name'), $delimiter, $enclosure, $escapeChar);
Loading history...
224
225
		foreach($this->data as $row){
226
			\fputcsv($mh, \array_values($row), $delimiter, $enclosure, $escapeChar);
227
		}
228
229
		\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

229
		\rewind(/** @scrutinizer ignore-type */ $mh);
Loading history...
230
231
		$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

231
		$csv = \stream_get_contents(/** @scrutinizer ignore-type */ $mh);
Loading history...
232
233
		\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

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