Passed
Push — master ( 4cd074...0929aa )
by smiley
04:07
created

QRCode   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 309
Duplicated Lines 7.44 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 12
Bugs 0 Features 1
Metric Value
dl 23
loc 309
rs 9.3999
c 12
b 0
f 1
wmc 33
lcom 1
cbo 9

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setOptions() 0 20 4
A render() 0 3 1
B getMatrix() 0 24 4
A getBestMaskPattern() 0 17 2
A initDataInterface() 0 19 3
B initOutputInterface() 0 16 5
A isNumber() 12 12 3
A isAlphaNum() 11 11 3
B isKanji() 0 16 6
A isByte() 0 3 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * Class QRCode
4
 *
5
 * @filesource   QRCode.php
6
 * @created      26.11.2015
7
 * @package      chillerlan\QRCode
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\QRCode;
14
15
use chillerlan\QRCode\Data\{
16
	AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix
17
};
18
use chillerlan\QRCode\Output\{
19
	QRCodeOutputException, QRImage, QRMarkup, QROutputInterface, QRString
20
};
21
use chillerlan\Traits\ClassLoader;
22
23
/**
24
 * Turns a text string into a Model 2 QR Code
25
 *
26
 * @link https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
27
 * @link http://www.qrcode.com/en/codes/model12.html
28
 * @link http://www.thonky.com/qr-code-tutorial/
29
 */
30
class QRCode{
31
	use ClassLoader;
32
33
	/**
34
	 * API constants
35
	 */
36
	const OUTPUT_MARKUP_HTML  = 'html';
37
	const OUTPUT_MARKUP_SVG   = 'svg';
38
#	const OUTPUT_MARKUP_XML   = 'xml'; // anyone?
39
40
	const OUTPUT_IMAGE_PNG    = 'png';
41
	const OUTPUT_IMAGE_JPG    = 'jpg';
42
	const OUTPUT_IMAGE_GIF    = 'gif';
43
44
	const OUTPUT_STRING_JSON  = 'json';
45
	const OUTPUT_STRING_TEXT  = 'text';
46
47
	const OUTPUT_CUSTOM       = 'custom';
48
49
	const VERSION_AUTO        = -1;
50
	const MASK_PATTERN_AUTO   = -1;
51
52
	const ECC_L         = 0b01; // 7%.
53
	const ECC_M         = 0b00; // 15%.
54
	const ECC_Q         = 0b11; // 25%.
55
	const ECC_H         = 0b10; // 30%.
56
57
	const DATA_NUMBER   = 0b0001;
58
	const DATA_ALPHANUM = 0b0010;
59
	const DATA_BYTE     = 0b0100;
60
	const DATA_KANJI    = 0b1000;
61
62
	const ECC_MODES = [
63
		self::ECC_L => 0,
64
		self::ECC_M => 1,
65
		self::ECC_Q => 2,
66
		self::ECC_H => 3,
67
	];
68
69
	const DATA_MODES = [
70
		self::DATA_NUMBER   => 0,
71
		self::DATA_ALPHANUM => 1,
72
		self::DATA_BYTE     => 2,
73
		self::DATA_KANJI    => 3,
74
	];
75
76
	const OUTPUT_MODES = [
77
		QRMarkup::class => [
78
			self::OUTPUT_MARKUP_SVG,
79
			self::OUTPUT_MARKUP_HTML,
80
		],
81
		QRImage::class => [
82
			self::OUTPUT_IMAGE_PNG,
83
			self::OUTPUT_IMAGE_GIF,
84
			self::OUTPUT_IMAGE_JPG,
85
		],
86
		QRString::class => [
87
			self::OUTPUT_STRING_JSON,
88
			self::OUTPUT_STRING_TEXT,
89
		]
90
	];
91
92
	/**
93
	 * @var \chillerlan\QRCode\QROptions
94
	 */
95
	protected $options;
96
97
	/**
98
	 * @var \chillerlan\QRCode\Data\QRDataInterface
99
	 */
100
	protected $dataInterface;
101
102
	/**
103
	 * QRCode constructor.
104
	 *
105
	 * @param \chillerlan\QRCode\QROptions|null $options
106
	 */
107
	public function __construct(QROptions $options = null){
108
		mb_internal_encoding('UTF-8');
109
110
		$this->setOptions($options ?? new QROptions);
111
	}
112
113
	/**
114
	 * Sets the options, called internally by the constructor
115
	 *
116
	 * @param \chillerlan\QRCode\QROptions $options
117
	 *
118
	 * @return \chillerlan\QRCode\QRCode
119
	 * @throws \chillerlan\QRCode\QRCodeException
120
	 */
121
	public function setOptions(QROptions $options):QRCode{
122
123
		if(!array_key_exists($options->eccLevel, $this::ECC_MODES)){
124
			throw new QRCodeException('Invalid error correct level: '.$options->eccLevel);
125
		}
126
127
		if(!is_array($options->imageTransparencyBG) || count($options->imageTransparencyBG) < 3){
128
			$options->imageTransparencyBG = [255, 255, 255];
129
		}
130
131
		$options->version = (int)$options->version;
132
133
		// clamp min/max version number
134
		$options->versionMin = (int)min($options->versionMin, $options->versionMax);
135
		$options->versionMax = (int)max($options->versionMin, $options->versionMax);
136
137
		$this->options = $options;
138
139
		return $this;
140
	}
141
142
	/**
143
	 * Renders a QR Code for the given $data and QROptions
144
	 *
145
	 * @param string $data
146
	 *
147
	 * @return mixed
148
	 */
149
	public function render(string $data){
150
		return $this->initOutputInterface($data)->dump();
151
	}
152
153
	/**
154
	 * Returns a QRMatrix object for the given $data and current QROptions
155
	 *
156
	 * @param string $data
157
	 *
158
	 * @return \chillerlan\QRCode\Data\QRMatrix
159
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
160
	 */
161
	public function getMatrix(string $data):QRMatrix {
162
		$data = trim($data);
163
164
		if(empty($data)){
165
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
166
		}
167
168
		$this->dataInterface = $this->initDataInterface($data);
169
170
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
171
			? $this->getBestMaskPattern()
172
			: max(7, min(0, (int)$this->options->maskPattern));
173
174
		$matrix = $this
175
			->dataInterface
176
			->initMatrix($maskPattern)
177
		;
178
179
		if((bool)$this->options->addQuietzone){
180
			$matrix->setQuietZone($this->options->quietzoneSize);
181
		}
182
183
		return $matrix;
184
	}
185
186
	/**
187
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
188
	 *
189
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
190
	 *
191
	 * @return int
192
	 */
193
	protected function getBestMaskPattern():int{
194
		$penalties = [];
195
196
		$tester = new MaskPatternTester;
197
198
		for($testPattern = 0; $testPattern < 8; $testPattern++){
199
			$matrix = $this
200
				->dataInterface
201
				->initMatrix($testPattern, true);
202
203
			$tester->setMatrix($matrix);
204
205
			$penalties[$testPattern] = $tester->testPattern();
206
		}
207
208
		return array_search(min($penalties), $penalties, true);
209
	}
210
211
	/**
212
	 * returns a fresh QRDataInterface for the given $data
213
	 *
214
	 * @param string                       $data
215
	 *
216
	 * @return \chillerlan\QRCode\Data\QRDataInterface
217
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
218
	 */
219
	public function initDataInterface(string $data):QRDataInterface{
220
221
		$DATA_MODES = [
222
			Number::class   => 'Number',
223
			AlphaNum::class => 'AlphaNum',
224
			Kanji::class    => 'Kanji',
225
			Byte::class     => 'Byte',
226
		];
227
228
		foreach($DATA_MODES as $dataInterface => $mode){
229
230
			if(call_user_func_array([$this, 'is'.$mode], [$data]) === true){
231
				return $this->loadClass($dataInterface, QRDataInterface::class, $this->options, $data);
232
			}
233
234
		}
235
236
		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
237
	}
238
239
	/**
240
	 * returns a fresh (built-in) QROutputInterface
241
	 *
242
	 * @param string $data
243
	 *
244
	 * @return \chillerlan\QRCode\Output\QROutputInterface
245
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
246
	 */
247
	protected function initOutputInterface(string $data):QROutputInterface{
248
249
		if($this->options->outputType === $this::OUTPUT_CUSTOM && $this->options->outputInterface !== null){
250
			return $this->loadClass($this->options->outputInterface, QROutputInterface::class, $this->options, $this->getMatrix($data));
251
		}
252
253
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
254
255
			if(in_array($this->options->outputType, $modes, true)){
256
				return $this->loadClass($outputInterface, QROutputInterface::class, $this->options, $this->getMatrix($data));
257
			}
258
259
		}
260
261
		throw new QRCodeOutputException('invalid output type');
262
	}
263
264
	/**
265
	 * checks if a string qualifies as numeric
266
	 *
267
	 * @param string $string
268
	 *
269
	 * @return bool
270
	 */
271 View Code Duplication
	public function isNumber(string $string):bool {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
		$len = strlen($string);
273
		$map = str_split('0123456789');
274
275
		for($i = 0; $i < $len; $i++){
276
			if(!in_array($string[$i], $map, true)){
277
				return false;
278
			}
279
		}
280
281
		return true;
282
	}
283
284
	/**
285
	 * checks if a string qualifies as alphanumeric
286
	 *
287
	 * @param string $string
288
	 *
289
	 * @return bool
290
	 */
291 View Code Duplication
	public function isAlphaNum(string $string):bool {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
		$len = strlen($string);
293
294
		for($i = 0; $i < $len; $i++){
295
			if(!in_array($string[$i], AlphaNum::CHAR_MAP, true)){
296
				return false;
297
			}
298
		}
299
300
		return true;
301
	}
302
303
	/**
304
	 * checks if a string qualifies as Kanji
305
	 *
306
	 * @param string $string
307
	 *
308
	 * @return bool
309
	 */
310
	public function isKanji(string $string):bool {
311
		$i   = 0;
312
		$len = strlen($string);
313
314
		while($i + 1 < $len){
315
			$c = ((0xff&ord($string[$i])) << 8)|(0xff&ord($string[$i + 1]));
316
317
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
318
				return false;
319
			}
320
321
			$i += 2;
322
		}
323
324
		return !($i < $len);
325
	}
326
327
	/**
328
	 * a dummy
329
	 *
330
	 * @param $data
331
	 *
332
	 * @return bool
333
	 */
334
	protected function isByte(string $data):bool{
335
		return !empty($data);
336
	}
337
338
}
339