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:
Complex classes like Engine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Engine, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
13 | abstract class Engine |
||
14 | { |
||
15 | private $rawData = ''; |
||
16 | protected $currentStatementData = ''; |
||
17 | protected $currentTransactionData = ''; |
||
18 | |||
19 | public $debug = false; |
||
20 | |||
21 | protected static $registeredEngines = [ |
||
22 | 100 => Engine\Abn::class, |
||
23 | 200 => Engine\Ing::class, |
||
24 | 300 => Engine\Rabo::class, |
||
25 | 400 => Engine\Spk::class, |
||
26 | 500 => Engine\Triodos::class, |
||
27 | 600 => Engine\Knab::class, |
||
28 | ]; |
||
29 | |||
30 | /** |
||
31 | * reads the firstline of the string to guess which engine to use for parsing. |
||
32 | * |
||
33 | * @param string $string |
||
34 | * |
||
35 | * @return Engine |
||
36 | */ |
||
37 | public static function __getInstance($string) |
||
38 | { |
||
39 | $engine = self::detectBank($string); |
||
40 | $engine->loadString($string); |
||
41 | |||
42 | return $engine; |
||
43 | } |
||
44 | |||
45 | /** |
||
46 | * Register a new Engine. |
||
47 | * |
||
48 | * @param string $engineClass Class name of Engine to be registered |
||
49 | * @param int $priority |
||
50 | */ |
||
51 | public static function registerEngine($engineClass, $priority) |
||
70 | |||
71 | /** |
||
72 | * Unregisters all Engines. |
||
73 | */ |
||
74 | public static function resetEngines() |
||
78 | |||
79 | /** |
||
80 | * Checks whether the Engine is applicable for the given string. |
||
81 | * |
||
82 | * @param string $string |
||
83 | * |
||
84 | * @return bool |
||
85 | */ |
||
86 | public static function isApplicable($string) |
||
90 | |||
91 | /** |
||
92 | * @param string $string |
||
93 | * |
||
94 | * @return Engine |
||
95 | */ |
||
96 | private static function detectBank($string) |
||
97 | { |
||
98 | foreach (self::$registeredEngines as $engineClass) { |
||
99 | if ($engineClass::isApplicable($string)) { |
||
100 | return new $engineClass(); |
||
101 | } |
||
102 | } |
||
103 | |||
104 | trigger_error('Unknown mt940 parser loaded, thus reverted to default', E_USER_NOTICE); |
||
105 | |||
106 | return new Engine\Unknown(); |
||
107 | } |
||
108 | |||
109 | /** |
||
110 | * loads the $string into _rawData |
||
111 | * this could be used to move it into handling of streams in the future. |
||
112 | * |
||
113 | * @param string $string |
||
114 | */ |
||
115 | public function loadString($string) |
||
119 | |||
120 | /** |
||
121 | * actual parsing of the data. |
||
122 | * |
||
123 | * @return Statement[] |
||
124 | */ |
||
125 | public function parse() |
||
126 | { |
||
127 | $results = []; |
||
128 | foreach ($this->parseStatementData() as $this->currentStatementData) { |
||
129 | $statement = new Statement(); |
||
130 | if ($this->debug) { |
||
131 | $statement->rawData = $this->currentStatementData; |
||
132 | } |
||
133 | $statement->setBank($this->parseStatementBank()); |
||
134 | $statement->setAccount($this->parseStatementAccount()); |
||
135 | $statement->setStartPrice($this->parseStatementStartPrice()); |
||
136 | $statement->setEndPrice($this->parseStatementEndPrice()); |
||
137 | $statement->setStartTimestamp($this->parseStatementStartTimestamp()); |
||
138 | $statement->setEndTimestamp($this->parseStatementEndTimestamp()); |
||
139 | $statement->setNumber($this->parseStatementNumber()); |
||
140 | |||
141 | foreach ($this->parseTransactionData() as $this->currentTransactionData) { |
||
142 | $transaction = new Transaction(); |
||
143 | if ($this->debug) { |
||
144 | $transaction->rawData = $this->currentTransactionData; |
||
145 | } |
||
146 | $transaction->setAccount($this->parseTransactionAccount()); |
||
147 | $transaction->setAccountName($this->parseTransactionAccountName()); |
||
148 | $transaction->setPrice($this->parseTransactionPrice()); |
||
149 | $transaction->setDebitCredit($this->parseTransactionDebitCredit()); |
||
150 | $transaction->setCancellation($this->parseTransactionCancellation()); |
||
151 | $transaction->setDescription($this->parseTransactionDescription()); |
||
152 | $transaction->setValueTimestamp($this->parseTransactionValueTimestamp()); |
||
153 | $transaction->setEntryTimestamp($this->parseTransactionEntryTimestamp()); |
||
154 | $transaction->setTransactionCode($this->parseTransactionCode()); |
||
155 | $statement->addTransaction($transaction); |
||
156 | } |
||
157 | $results[] = $statement; |
||
158 | } |
||
159 | |||
160 | return $results; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * split the rawdata up into statementdata chunks. |
||
165 | * |
||
166 | * @return array |
||
167 | */ |
||
168 | protected function parseStatementData() |
||
169 | { |
||
170 | $results = preg_split( |
||
171 | '/(^:20:|^-X{,3}$|\Z)/sm', |
||
172 | $this->getRawData(), |
||
173 | -1, |
||
174 | PREG_SPLIT_NO_EMPTY |
||
175 | ); |
||
176 | array_shift($results); // remove the header |
||
177 | return $results; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * split the statement up into transaction chunks. |
||
182 | * |
||
183 | * @return array |
||
184 | */ |
||
185 | protected function parseTransactionData() |
||
186 | { |
||
187 | $results = []; |
||
188 | preg_match_all('/^:61:(.*?)(?=^:61:|^-X{,3}$|\Z)/sm', $this->getCurrentStatementData(), $results); |
||
189 | |||
190 | return (!empty($results[0])) ? $results[0] : []; |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * return the actual raw data string. |
||
195 | * |
||
196 | * @return string _rawData |
||
197 | */ |
||
198 | public function getRawData() |
||
202 | |||
203 | /** |
||
204 | * return the actual raw data string. |
||
205 | * |
||
206 | * @return string currentStatementData |
||
207 | */ |
||
208 | public function getCurrentStatementData() |
||
212 | |||
213 | /** |
||
214 | * return the actual raw data string. |
||
215 | * |
||
216 | * @return string currentTransactionData |
||
217 | */ |
||
218 | public function getCurrentTransactionData() |
||
222 | |||
223 | // statement parsers, these work with currentStatementData |
||
224 | |||
225 | /** |
||
226 | * return the actual raw data string. |
||
227 | * |
||
228 | * @return string bank |
||
229 | */ |
||
230 | protected function parseStatementBank() |
||
234 | |||
235 | /** |
||
236 | * uses field 25 to gather accoutnumber. |
||
237 | * |
||
238 | * @return string accountnumber |
||
239 | */ |
||
240 | View Code Duplication | protected function parseStatementAccount() |
|
|
|||
241 | { |
||
242 | $results = []; |
||
243 | if (preg_match('/:25:([\d\.]+)*/', $this->getCurrentStatementData(), $results) |
||
244 | && !empty($results[1]) |
||
245 | ) { |
||
246 | return $this->sanitizeAccount($results[1]); |
||
247 | } |
||
248 | |||
249 | // SEPA / IBAN |
||
250 | if (preg_match('/:25:([A-Z0-9]{8}[\d\.]+)*/', $this->getCurrentStatementData(), $results) |
||
251 | && !empty($results[1]) |
||
252 | ) { |
||
253 | return $this->sanitizeAccount($results[1]); |
||
254 | } |
||
255 | |||
256 | return ''; |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * uses field 60F to gather starting amount. |
||
261 | * |
||
262 | * @return float price |
||
263 | */ |
||
264 | protected function parseStatementStartPrice() |
||
268 | |||
269 | /** |
||
270 | * uses the 62F field to return end price of the statement. |
||
271 | * |
||
272 | * @return float price |
||
273 | */ |
||
274 | protected function parseStatementEndPrice() |
||
278 | |||
279 | /** |
||
280 | * The actual pricing parser for statements. |
||
281 | * |
||
282 | * @param $key |
||
283 | * |
||
284 | * @return float|string |
||
285 | */ |
||
286 | protected function parseStatementPrice($key) |
||
287 | { |
||
288 | $results = []; |
||
289 | if (preg_match('/:'.$key.':([CD])?.*EUR([\d,\.]+)*/', $this->getCurrentStatementData(), $results) |
||
290 | && !empty($results[2]) |
||
291 | ) { |
||
292 | $sanitizedPrice = $this->sanitizePrice($results[2]); |
||
293 | |||
294 | return (!empty($results[1]) && $results[1] === 'D') ? -$sanitizedPrice : $sanitizedPrice; |
||
295 | } |
||
296 | |||
297 | return ''; |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * uses the 60F field to determine the date of the statement. |
||
302 | * |
||
303 | * @deprecated will be removed in the next major release and replaced by startTimestamp / endTimestamps |
||
304 | * |
||
305 | * @return int timestamp |
||
306 | */ |
||
307 | protected function parseStatementTimestamp() |
||
308 | { |
||
309 | trigger_error('Deprecated in favor of splitting the start and end timestamps for a statement. '. |
||
310 | 'Please use parseStatementStartTimestamp($format) or parseStatementEndTimestamp($format) instead. '. |
||
311 | 'setTimestamp is now parseStatementStartTimestamp', E_USER_DEPRECATED); |
||
312 | |||
313 | return $this->parseStatementStartTimestamp(); |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * uses the 60F field to determine the date of the statement. |
||
318 | * |
||
319 | * @return int timestamp |
||
320 | */ |
||
321 | protected function parseStatementStartTimestamp() |
||
325 | |||
326 | /** |
||
327 | * uses the 62F field to determine the date of the statement. |
||
328 | * |
||
329 | * @return int timestamp |
||
330 | */ |
||
331 | protected function parseStatementEndTimestamp() |
||
335 | |||
336 | protected function parseTimestampFromStatement($key) |
||
337 | { |
||
338 | $results = []; |
||
339 | if (preg_match('/:'.$key.':[C|D](\d{6})*/', $this->getCurrentStatementData(), $results) |
||
340 | && !empty($results[1]) |
||
341 | ) { |
||
342 | return $this->sanitizeTimestamp($results[1], 'ymd'); |
||
343 | } |
||
347 | |||
348 | /** |
||
349 | * uses the 28C field to determine the statement number. |
||
350 | * |
||
351 | * @return string |
||
352 | */ |
||
353 | protected function parseStatementNumber() |
||
364 | |||
365 | // transaction parsers, these work with getCurrentTransactionData |
||
366 | /** |
||
367 | * uses the 86 field to determine account number of the transaction. |
||
368 | * |
||
369 | * @return string |
||
370 | */ |
||
371 | View Code Duplication | protected function parseTransactionAccount() |
|
382 | |||
383 | /** |
||
384 | * uses the 86 field to determine accountname of the transaction. |
||
385 | * |
||
386 | * @return string |
||
387 | */ |
||
388 | View Code Duplication | protected function parseTransactionAccountName() |
|
399 | |||
400 | /** |
||
401 | * uses the 61 field to determine amount/value of the transaction. |
||
402 | * |
||
403 | * @return float |
||
404 | */ |
||
405 | View Code Duplication | protected function parseTransactionPrice() |
|
416 | |||
417 | /** |
||
418 | * uses the 61 field to determine debit or credit of the transaction. |
||
419 | * |
||
420 | * @return string |
||
421 | */ |
||
422 | View Code Duplication | protected function parseTransactionDebitCredit() |
|
433 | |||
434 | /** |
||
435 | * Parses the Cancellation flag of a Transaction |
||
436 | * |
||
437 | * @return boolean |
||
438 | */ |
||
439 | protected function parseTransactionCancellation () { |
||
442 | |||
443 | /** |
||
444 | * uses the 86 field to determine retrieve the full description of the transaction. |
||
445 | * |
||
446 | * @return string |
||
447 | */ |
||
448 | protected function parseTransactionDescription() |
||
459 | |||
460 | /** |
||
461 | * uses the 61 field to determine the entry timestamp. |
||
462 | * |
||
463 | * @return int |
||
464 | */ |
||
465 | protected function parseTransactionEntryTimestamp() |
||
469 | |||
470 | /** |
||
471 | * uses the 61 field to determine the value timestamp. |
||
472 | * |
||
473 | * @return int |
||
474 | */ |
||
475 | protected function parseTransactionValueTimestamp() |
||
479 | |||
480 | /** |
||
481 | * This does the actual parsing of the transaction timestamp for given $key. |
||
482 | * |
||
483 | * @param string $key |
||
484 | * @return int |
||
485 | */ |
||
486 | protected function parseTransactionTimestamp($key) |
||
497 | |||
498 | /** |
||
499 | * uses the 61 field to get the bank specific transaction code. |
||
500 | * |
||
501 | * @return string |
||
502 | */ |
||
503 | View Code Duplication | protected function parseTransactionCode() |
|
514 | |||
515 | /** |
||
516 | * @param string $string |
||
517 | * |
||
518 | * @return string |
||
519 | */ |
||
520 | protected function sanitizeAccount($string) |
||
550 | |||
551 | /** |
||
552 | * @param string $string |
||
553 | * |
||
554 | * @return string |
||
555 | */ |
||
556 | protected function sanitizeAccountName($string) |
||
560 | |||
561 | /** |
||
562 | * @param string $string |
||
563 | * @param string $inFormat |
||
564 | * |
||
565 | * @return int |
||
566 | */ |
||
567 | protected function sanitizeTimestamp($string, $inFormat = 'ymd') |
||
577 | |||
578 | /** |
||
579 | * @param string $string |
||
580 | * |
||
581 | * @return string |
||
582 | */ |
||
583 | protected function sanitizeDescription($string) |
||
587 | |||
588 | /** |
||
589 | * @param string $string |
||
590 | * |
||
591 | * @return string |
||
592 | */ |
||
593 | protected function sanitizeDebitCredit($string) |
||
603 | |||
604 | /** |
||
605 | * @param string $string |
||
606 | * |
||
607 | * @return float |
||
608 | */ |
||
609 | protected function sanitizePrice($string) |
||
615 | } |
||
616 |
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.