Passed
Push — master ( f45c30...3fc913 )
by Robin
04:20 queued 02:45
created

Engine   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 620
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 181
dl 0
loc 620
rs 2
c 0
b 0
f 0
wmc 85

39 Methods

Rating   Name   Duplication   Size   Complexity  
A loadString() 0 3 1
A __getInstance() 0 6 1
A isApplicable() 0 3 1
A detectBank() 0 13 3
A resetEngines() 0 3 1
A registerEngine() 0 18 4
A parseStatementEndPrice() 0 3 1
A getRawData() 0 3 1
A parse() 0 37 5
A parseStatementData() 0 10 1
A parseTransactionCode() 0 10 3
A parseTransactionEntryTimestamp() 0 3 1
A parseTransactionValueTimestamp() 0 3 1
A sanitizeDebitCredit() 0 9 3
A sanitizeDescription() 0 3 1
A parseStatementBank() 0 3 1
B sanitizeAccount() 0 29 7
A parseTransactionData() 0 6 2
A getCurrentStatementData() 0 3 1
A parseStatementStartPrice() 0 3 1
A sanitizeTimestamp() 0 9 2
A parseStatementAccount() 0 17 5
A sanitizeAccountName() 0 3 1
A sanitizePrice() 0 5 1
A getCurrentTransactionData() 0 3 1
A parseTransactionDescription() 0 10 3
A parseStatementPrice() 0 12 5
A parseTransactionTimestamp() 0 10 3
A parseTransactionDebitCredit() 0 10 3
A parseStatementTimestamp() 0 7 1
A parseStatementEndTimestamp() 0 3 1
A parseTransactionCancellation() 0 3 1
A parseTransactionAccount() 0 10 3
A parseTimestampFromStatement() 0 10 3
A parseTransactionPrice() 0 10 3
A parseStatementNumber() 0 10 3
A parseTransactionAccountName() 0 10 3
A parseStatementStartTimestamp() 0 3 1
A parseStatementCurrency() 0 7 2

How to fix   Complexity   

Complex Class

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.

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
2
3
namespace Kingsquare\Parser\Banking\Mt940;
4
5
use Kingsquare\Banking\Statement;
6
use Kingsquare\Banking\Transaction;
7
use Kingsquare\Parser\Banking\Mt940;
8
9
/**
10
 * @author Kingsquare ([email protected])
11
 * @license http://opensource.org/licenses/MIT MIT
12
 */
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)
52
    {
53
        if (!is_int($priority)) {
0 ignored issues
show
introduced by
The condition is_int($priority) is always true.
Loading history...
54
            trigger_error('Priority must be integer', E_USER_WARNING);
55
56
            return;
57
        }
58
        if (array_key_exists($priority, self::$registeredEngines)) {
59
            trigger_error('Priority already taken', E_USER_WARNING);
60
61
            return;
62
        }
63
        if (!class_exists($engineClass)) {
64
            trigger_error('Engine does not exist', E_USER_WARNING);
65
66
            return;
67
        }
68
        self::$registeredEngines[$priority] = $engineClass;
69
    }
70
71
    /**
72
     * Unregisters all Engines.
73
     */
74
    public static function resetEngines()
75
    {
76
        self::$registeredEngines = [];
77
    }
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)
87
    {
88
        return true;
89
    }
90
91
    /**
92
     * @param string $string
93
     *
94
     * @return Engine
95
     */
96
    private static function detectBank($string)
97
    {
98
        ksort(self::$registeredEngines, SORT_NUMERIC);
99
100
        foreach (self::$registeredEngines as $engineClass) {
101
            if ($engineClass::isApplicable($string)) {
102
                return new $engineClass();
103
            }
104
        }
105
106
        trigger_error('Unknown mt940 parser loaded, thus reverted to default', E_USER_NOTICE);
107
108
        return new Engine\Unknown();
109
    }
110
111
    /**
112
     * loads the $string into _rawData
113
     * this could be used to move it into handling of streams in the future.
114
     *
115
     * @param string $string
116
     */
117
    public function loadString($string)
118
    {
119
        $this->rawData = trim($string);
120
    }
121
122
    /**
123
     * actual parsing of the data.
124
     *
125
     * @return Statement[]
126
     */
127
    public function parse()
128
    {
129
        $results = [];
130
        foreach ($this->parseStatementData() as $this->currentStatementData) {
131
            $statement = new Statement();
132
            if ($this->debug) {
133
                $statement->rawData = $this->currentStatementData;
134
            }
135
            $statement->setBank($this->parseStatementBank());
136
            $statement->setAccount($this->parseStatementAccount());
137
            $statement->setStartPrice($this->parseStatementStartPrice());
138
            $statement->setEndPrice($this->parseStatementEndPrice());
139
            $statement->setStartTimestamp($this->parseStatementStartTimestamp());
140
            $statement->setEndTimestamp($this->parseStatementEndTimestamp());
141
            $statement->setNumber($this->parseStatementNumber());
142
            $statement->setCurrency($this->parseStatementCurrency());
143
144
            foreach ($this->parseTransactionData() as $this->currentTransactionData) {
0 ignored issues
show
Comprehensibility Bug introduced by
$this is overwriting a variable from outer foreach loop.
Loading history...
145
                $transaction = new Transaction();
146
                if ($this->debug) {
147
                    $transaction->rawData = $this->currentTransactionData;
148
                }
149
                $transaction->setAccount($this->parseTransactionAccount());
150
                $transaction->setAccountName($this->parseTransactionAccountName());
151
                $transaction->setPrice($this->parseTransactionPrice());
152
                $transaction->setDebitCredit($this->parseTransactionDebitCredit());
153
                $transaction->setCancellation($this->parseTransactionCancellation());
154
                $transaction->setDescription($this->parseTransactionDescription());
155
                $transaction->setValueTimestamp($this->parseTransactionValueTimestamp());
156
                $transaction->setEntryTimestamp($this->parseTransactionEntryTimestamp());
157
                $transaction->setTransactionCode($this->parseTransactionCode());
158
                $statement->addTransaction($transaction);
159
            }
160
            $results[] = $statement;
161
        }
162
163
        return $results;
164
    }
165
166
    /**
167
     * split the rawdata up into statementdata chunks.
168
     *
169
     * @return array
170
     */
171
    protected function parseStatementData()
172
    {
173
        $results = preg_split(
174
            '/(^:20:|^-X{,3}$|\Z)/m',
175
            $this->getRawData(),
176
            -1,
177
            PREG_SPLIT_NO_EMPTY
178
        );
179
        array_shift($results); // remove the header
0 ignored issues
show
Bug introduced by
It seems like $results can also be of type false; however, parameter $array of array_shift() does only seem to accept array, 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

179
        array_shift(/** @scrutinizer ignore-type */ $results); // remove the header
Loading history...
180
        return $results;
181
    }
182
183
    /**
184
     * split the statement up into transaction chunks.
185
     *
186
     * @return array
187
     */
188
    protected function parseTransactionData()
189
    {
190
        $results = [];
191
        preg_match_all('/^:61:(.*?)(?=^:61:|^-X{,3}$|\Z)/sm', $this->getCurrentStatementData(), $results);
192
193
        return !empty($results[0]) ? $results[0] : [];
194
    }
195
196
    /**
197
     * return the actual raw data string.
198
     *
199
     * @return string _rawData
200
     */
201
    public function getRawData()
202
    {
203
        return $this->rawData;
204
    }
205
206
    /**
207
     * return the actual raw data string.
208
     *
209
     * @return string currentStatementData
210
     */
211
    public function getCurrentStatementData()
212
    {
213
        return $this->currentStatementData;
214
    }
215
216
    /**
217
     * return the actual raw data string.
218
     *
219
     * @return string currentTransactionData
220
     */
221
    public function getCurrentTransactionData()
222
    {
223
        return $this->currentTransactionData;
224
    }
225
226
    // statement parsers, these work with currentStatementData
227
228
    /**
229
     * return the actual raw data string.
230
     *
231
     * @return string bank
232
     */
233
    protected function parseStatementBank()
234
    {
235
        return '';
236
    }
237
238
    /**
239
     * uses field 25 to gather accoutnumber.
240
     *
241
     * @return string accountnumber
242
     */
243
    protected function parseStatementAccount()
244
    {
245
        $results = [];
246
        if (preg_match('/:25:([\d\.]+)*/', $this->getCurrentStatementData(), $results)
247
            && !empty($results[1])
248
        ) {
249
            return $this->sanitizeAccount($results[1]);
250
        }
251
252
        // SEPA / IBAN
253
        if (preg_match('/:25:([A-Z0-9]{8}[\d\.]+)*/', $this->getCurrentStatementData(), $results)
254
            && !empty($results[1])
255
        ) {
256
            return $this->sanitizeAccount($results[1]);
257
        }
258
259
        return '';
260
    }
261
262
    /**
263
     * uses field 60F to gather starting amount.
264
     *
265
     * @return float price
266
     */
267
    protected function parseStatementStartPrice()
268
    {
269
        return $this->parseStatementPrice('60F');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseStatementPrice('60F') also could return the type string which is incompatible with the documented return type double.
Loading history...
270
    }
271
272
    /**
273
     * uses the 62F field to return end price of the statement.
274
     *
275
     * @return float price
276
     */
277
    protected function parseStatementEndPrice()
278
    {
279
        return $this->parseStatementPrice('62F');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseStatementPrice('62F') also could return the type string which is incompatible with the documented return type double.
Loading history...
280
    }
281
282
    /**
283
     * The actual pricing parser for statements.
284
     *
285
     * @param string $key
286
     *
287
     * @return float|string
288
     */
289
    protected function parseStatementPrice($key)
290
    {
291
        $results = [];
292
        if (preg_match('/:' . $key . ':([CD])?.*[A-Z]{3}([\d,\.]+)*/', $this->getCurrentStatementData(), $results)
293
            && !empty($results[2])
294
        ) {
295
            $sanitizedPrice = $this->sanitizePrice($results[2]);
296
297
            return (!empty($results[1]) && $results[1] === 'D') ? -$sanitizedPrice : $sanitizedPrice;
298
        }
299
300
        return '';
301
    }
302
303
    /**
304
     * The currency initials parser for statements.
305
     * @param string $key
306
     * @return string currency initials
307
     */
308
    protected function parseStatementCurrency($key = '60[FM]')
309
    {
310
        $results = [];
311
        if (preg_match('/:' . $key . ':[CD]?.*([A-Z]{3})([\d,\.]+)*/', $this->getCurrentStatementData(), $results)) {
312
            return $results[1];
313
        }
314
        return '';
315
    }
316
317
    /**
318
     * uses the 60F field to determine the date of the statement.
319
     *
320
     * @deprecated will be removed in the next major release and replaced by startTimestamp / endTimestamps
321
     *
322
     * @return int timestamp
323
     */
324
    protected function parseStatementTimestamp()
325
    {
326
        trigger_error('Deprecated in favor of splitting the start and end timestamps for a statement. ' .
327
            'Please use parseStatementStartTimestamp($format) or parseStatementEndTimestamp($format) instead. ' .
328
            'parseStatementTimestamp is now parseStatementStartTimestamp', E_USER_DEPRECATED);
329
330
        return $this->parseStatementStartTimestamp();
331
    }
332
333
    /**
334
     * uses the 60F field to determine the date of the statement.
335
     *
336
     * @return int timestamp
337
     */
338
    protected function parseStatementStartTimestamp()
339
    {
340
        return $this->parseTimestampFromStatement('60F');
341
    }
342
343
    /**
344
     * uses the 62F field to determine the date of the statement.
345
     *
346
     * @return int timestamp
347
     */
348
    protected function parseStatementEndTimestamp()
349
    {
350
        return $this->parseTimestampFromStatement('62F');
351
    }
352
353
    protected function parseTimestampFromStatement($key)
354
    {
355
        $results = [];
356
        if (preg_match('/:' . $key . ':[C|D](\d{6})*/', $this->getCurrentStatementData(), $results)
357
            && !empty($results[1])
358
        ) {
359
            return $this->sanitizeTimestamp($results[1], 'ymd');
360
        }
361
362
        return 0;
363
    }
364
365
    /**
366
     * uses the 28C field to determine the statement number.
367
     *
368
     * @return string
369
     */
370
    protected function parseStatementNumber()
371
    {
372
        $results = [];
373
        if (preg_match('/:28C?:(.*)/', $this->getCurrentStatementData(), $results)
374
            && !empty($results[1])
375
        ) {
376
            return trim($results[1]);
377
        }
378
379
        return '';
380
    }
381
382
    // transaction parsers, these work with getCurrentTransactionData
383
384
    /**
385
     * uses the 86 field to determine account number of the transaction.
386
     *
387
     * @return string
388
     */
389
    protected function parseTransactionAccount()
390
    {
391
        $results = [];
392
        if (preg_match('/^:86: ?([\d\.]+)\s/im', $this->getCurrentTransactionData(), $results)
393
            && !empty($results[1])
394
        ) {
395
            return $this->sanitizeAccount($results[1]);
396
        }
397
398
        return '';
399
    }
400
401
    /**
402
     * uses the 86 field to determine accountname of the transaction.
403
     *
404
     * @return string
405
     */
406
    protected function parseTransactionAccountName()
407
    {
408
        $results = [];
409
        if (preg_match('/:86: ?[\d\.]+ (.+)/', $this->getCurrentTransactionData(), $results)
410
            && !empty($results[1])
411
        ) {
412
            return $this->sanitizeAccountName($results[1]);
413
        }
414
415
        return '';
416
    }
417
418
    /**
419
     * uses the 61 field to determine amount/value of the transaction.
420
     *
421
     * @return float
422
     */
423
    protected function parseTransactionPrice()
424
    {
425
        $results = [];
426
        if (preg_match('/^:61:.*?[CD]([\d,\.]+)N/i', $this->getCurrentTransactionData(), $results)
427
            && !empty($results[1])
428
        ) {
429
            return $this->sanitizePrice($results[1]);
430
        }
431
432
        return 0;
433
    }
434
435
    /**
436
     * uses the 61 field to determine debit or credit of the transaction.
437
     *
438
     * @return string
439
     */
440
    protected function parseTransactionDebitCredit()
441
    {
442
        $results = [];
443
        if (preg_match('/^:61:\d+([CD])\d+/', $this->getCurrentTransactionData(), $results)
444
            && !empty($results[1])
445
        ) {
446
            return $this->sanitizeDebitCredit($results[1]);
447
        }
448
449
        return '';
450
    }
451
452
    /**
453
     * Parses the Cancellation flag of a Transaction
454
     *
455
     * @return boolean
456
     */
457
    protected function parseTransactionCancellation()
458
    {
459
        return false;
460
    }
461
462
    /**
463
     * uses the 86 field to determine retrieve the full description of the transaction.
464
     *
465
     * @return string
466
     */
467
    protected function parseTransactionDescription()
468
    {
469
        $results = [];
470
        if (preg_match_all('/[\n]:86:(.*?)(?=\n(:6(1|2))|$)/s', $this->getCurrentTransactionData(), $results)
471
            && !empty($results[1])
472
        ) {
473
            return $this->sanitizeDescription(implode(PHP_EOL, $results[1]));
474
        }
475
476
        return '';
477
    }
478
479
    /**
480
     * uses the 61 field to determine the entry timestamp.
481
     *
482
     * @return int
483
     */
484
    protected function parseTransactionEntryTimestamp()
485
    {
486
        return $this->parseTransactionTimestamp('61');
487
    }
488
489
    /**
490
     * uses the 61 field to determine the value timestamp.
491
     *
492
     * @return int
493
     */
494
    protected function parseTransactionValueTimestamp()
495
    {
496
        return $this->parseTransactionTimestamp('61');
497
    }
498
499
    /**
500
     * This does the actual parsing of the transaction timestamp for given $key.
501
     *
502
     * @param string $key
503
     * @return int
504
     */
505
    protected function parseTransactionTimestamp($key)
506
    {
507
        $results = [];
508
        if (preg_match('/^:' . $key . ':(\d{6})/', $this->getCurrentTransactionData(), $results)
509
            && !empty($results[1])
510
        ) {
511
            return $this->sanitizeTimestamp($results[1], 'ymd');
512
        }
513
514
        return 0;
515
    }
516
517
    /**
518
     * uses the 61 field to get the bank specific transaction code.
519
     *
520
     * @return string
521
     */
522
    protected function parseTransactionCode()
523
    {
524
        $results = [];
525
        if (preg_match('/^:61:.*?N(.{3}).*/', $this->getCurrentTransactionData(), $results)
526
            && !empty($results[1])
527
        ) {
528
            return trim($results[1]);
529
        }
530
531
        return '';
532
    }
533
534
    /**
535
     * @param string $string
536
     *
537
     * @return string
538
     */
539
    protected function sanitizeAccount($string)
540
    {
541
        static $crudeReplacements = [
542
            '.' => '',
543
            ' ' => '',
544
            'GIRO' => 'P',
545
        ];
546
547
        // crude IBAN to 'old' converter
548
        if (Mt940::$removeIBAN
549
            && preg_match('#[A-Z]{2}[\d]{2}[A-Z]{4}(.*)#', $string, $results)
550
            && !empty($results[1])
551
        ) {
552
            $string = $results[1];
553
        }
554
555
        $account = ltrim(
556
            str_replace(
557
                array_keys($crudeReplacements),
558
                array_values($crudeReplacements),
559
                strip_tags(trim($string))
560
            ),
561
            '0'
562
        );
563
        if ($account !== '' && strlen($account) < 9 && strpos($account, 'P') === false) {
564
            $account = 'P' . $account;
565
        }
566
567
        return $account;
568
    }
569
570
    /**
571
     * @param string $string
572
     *
573
     * @return string
574
     */
575
    protected function sanitizeAccountName($string)
576
    {
577
        return preg_replace('/[\r\n]+/', '', trim($string));
578
    }
579
580
    /**
581
     * @param string $string
582
     * @param string $inFormat
583
     *
584
     * @return int
585
     */
586
    protected function sanitizeTimestamp($string, $inFormat = 'ymd')
587
    {
588
        $date = \DateTime::createFromFormat($inFormat, $string);
589
        $date->setTime(0, 0, 0);
590
        if ($date !== false) {
591
            return (int)$date->format('U');
592
        }
593
594
        return 0;
595
    }
596
597
    /**
598
     * @param string $string
599
     *
600
     * @return string
601
     */
602
    protected function sanitizeDescription($string)
603
    {
604
        return preg_replace('/[\r\n]+/', '', trim($string));
605
    }
606
607
    /**
608
     * @param string $string
609
     *
610
     * @return string
611
     */
612
    protected function sanitizeDebitCredit($string)
613
    {
614
        $debitOrCredit = strtoupper(substr((string)$string, 0, 1));
615
        if ($debitOrCredit !== Transaction::DEBIT && $debitOrCredit !== Transaction::CREDIT) {
616
            trigger_error('wrong value for debit/credit (' . $string . ')', E_USER_ERROR);
617
            $debitOrCredit = '';
618
        }
619
620
        return $debitOrCredit;
621
    }
622
623
    /**
624
     * @param string $string
625
     *
626
     * @return float
627
     */
628
    protected function sanitizePrice($string)
629
    {
630
        $floatPrice = ltrim(str_replace(',', '.', strip_tags(trim($string))), '0');
631
632
        return (float)$floatPrice;
633
    }
634
}
635