1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Ksdev\Mt940Parser; |
4
|
|
|
|
5
|
|
|
class Mt940Parser |
6
|
|
|
{ |
7
|
|
|
/** |
8
|
|
|
* Parse the MT940 file format and return it as an readable array |
9
|
|
|
* |
10
|
|
|
* @param string $filePath |
11
|
|
|
* |
12
|
|
|
* @return array |
13
|
|
|
* |
14
|
|
|
* @throws \Exception |
15
|
|
|
*/ |
16
|
|
|
public function parse($filePath) |
17
|
|
|
{ |
18
|
|
|
$preparedArray = $this->prepareFile($filePath); |
19
|
|
|
$statement = $this->parseContent($preparedArray); |
20
|
|
|
|
21
|
|
|
return $statement; |
22
|
|
|
} |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Convert the file content into an array structure |
26
|
|
|
* |
27
|
|
|
* @param string $filePath |
28
|
|
|
* |
29
|
|
|
* @return array |
30
|
|
|
* |
31
|
|
|
* @throws \RuntimeException If the file cannot be opened |
32
|
|
|
*/ |
33
|
|
|
private function prepareFile($filePath) |
34
|
|
|
{ |
35
|
|
|
$file = new \SplFileObject($filePath); |
36
|
|
|
|
37
|
|
|
$i = 0; |
38
|
|
|
$isBlockOpen = false; |
39
|
|
|
$preparedArray = []; |
40
|
|
|
foreach ($file as $line) { |
41
|
|
|
$strippedLine = str_replace(["\r", "\n"], '', $line); |
42
|
|
|
|
43
|
|
|
if ($strippedLine == chr(45) . chr(3)) { |
44
|
|
|
$i++; |
45
|
|
|
$isBlockOpen = false; |
46
|
|
|
continue; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
if ($strippedLine == chr(1)) { |
50
|
|
|
$isBlockOpen = true; |
51
|
|
|
continue; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
if ($isBlockOpen) { |
55
|
|
|
if (preg_match('/^:.{2,3}:/', $line)) { |
56
|
|
|
$preparedArray[$i][] = $line; |
57
|
|
|
} |
58
|
|
|
else { |
59
|
|
|
$arrayKeys = array_keys($preparedArray[$i]); |
60
|
|
|
$lastKey = end($arrayKeys); |
61
|
|
|
$preparedArray[$i][$lastKey] .= $line; |
62
|
|
|
} |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
return $preparedArray; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Convert the prepared array into an easily readable form |
71
|
|
|
* |
72
|
|
|
* @param array $preparedArray |
73
|
|
|
* |
74
|
|
|
* @return array |
75
|
|
|
* |
76
|
|
|
* @throws \Exception |
77
|
|
|
*/ |
78
|
|
|
private function parseContent($preparedArray) |
79
|
|
|
{ |
80
|
|
|
$statement = []; |
81
|
|
|
foreach ($preparedArray as $key => $accountBlock) { |
82
|
|
|
foreach ($accountBlock as $tagLine) { |
83
|
|
|
if (preg_match('/^:(.{2,3}):(.*)/s', $tagLine, $matches)) { |
84
|
|
|
$tagNum = $matches[1]; |
85
|
|
|
$tagContent = $matches[2]; |
86
|
|
|
|
87
|
|
|
switch ($tagNum) { |
88
|
|
|
case '20': |
89
|
|
|
$generationDate = $this->parseStatementIdentifier($tagContent); |
90
|
|
|
$statement[$key]['generationDate'] = $generationDate; |
91
|
|
|
break; |
92
|
|
|
case '25': |
93
|
|
|
$accountNumber = $this->parseAccountNumber($tagContent); |
94
|
|
|
$statement[$key]['accountNumber'] = $accountNumber; |
95
|
|
|
break; |
96
|
|
|
case '28C': |
97
|
|
|
$statementNumber = $this->parseStatementNumber($tagContent); |
98
|
|
|
$statement[$key]['statementNumber'] = $statementNumber; |
99
|
|
|
break; |
100
|
|
|
case '60F': |
101
|
|
|
$openingBalance = $this->parseBalance($tagContent); |
102
|
|
|
$statement[$key]['openingBalance'] = $openingBalance; |
103
|
|
|
break; |
104
|
|
|
case '62F': |
105
|
|
|
$closingBalance = $this->parseBalance($tagContent); |
106
|
|
|
$statement[$key]['closingBalance'] = $closingBalance; |
107
|
|
|
break; |
108
|
|
|
case '64': |
109
|
|
|
$availableBalance = $this->parseBalance($tagContent); |
110
|
|
|
$statement[$key]['availableBalance'] = $availableBalance; |
111
|
|
|
break; |
112
|
|
|
case '61': |
113
|
|
|
$transaction = $this->parseTransaction($tagContent); |
114
|
|
|
$statement[$key]['transactions'][] = $transaction; |
115
|
|
|
break; |
116
|
|
|
case '86': |
117
|
|
|
$details = $this->parseTransactionDetails($tagContent); |
118
|
|
|
$arrayKeys = array_keys($statement[$key]['transactions']); |
119
|
|
|
$lastKey = end($arrayKeys); |
120
|
|
|
$statement[$key]['transactions'][$lastKey]['details'] = $details; |
121
|
|
|
break; |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
else { |
125
|
|
|
throw new \Exception('Invalid format of tag line'); |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return $statement; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @param string $tagContent |
135
|
|
|
* |
136
|
|
|
* @return string |
137
|
|
|
* |
138
|
|
|
* @throws \Exception |
139
|
|
|
*/ |
140
|
|
View Code Duplication |
private function parseStatementIdentifier($tagContent) |
141
|
|
|
{ |
142
|
|
|
if (preg_match('/ST(\d{6})/', $tagContent, $matches)) { |
143
|
|
|
$generationDate = $matches[1]; |
144
|
|
|
|
145
|
|
|
return $generationDate; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
throw new \Exception('Invalid format of statement identifier'); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @param string $tagContent |
153
|
|
|
* |
154
|
|
|
* @return string |
155
|
|
|
* |
156
|
|
|
* @throws \Exception |
157
|
|
|
*/ |
158
|
|
View Code Duplication |
private function parseAccountNumber($tagContent) |
159
|
|
|
{ |
160
|
|
|
if (preg_match('/(\d{26})/', $tagContent, $matches)) { |
161
|
|
|
$accountNumber = $matches[1]; |
162
|
|
|
|
163
|
|
|
return $accountNumber; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
throw new \Exception('Invalid format of account number'); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @param string $tagContent |
171
|
|
|
* |
172
|
|
|
* @return string |
173
|
|
|
* |
174
|
|
|
* @throws \Exception |
175
|
|
|
*/ |
176
|
|
View Code Duplication |
private function parseStatementNumber($tagContent) |
177
|
|
|
{ |
178
|
|
|
if (preg_match('/(\d+)\//', $tagContent, $matches)) { |
179
|
|
|
$statementNumber = $matches[1]; |
180
|
|
|
|
181
|
|
|
return $statementNumber; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
throw new \Exception('Invalid format of statement number'); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* @param string $tagContent |
189
|
|
|
* |
190
|
|
|
* @return array |
191
|
|
|
* |
192
|
|
|
* @throws \Exception |
193
|
|
|
*/ |
194
|
|
|
private function parseBalance($tagContent) |
195
|
|
|
{ |
196
|
|
|
if (preg_match('/([CD])(\d{6})(\w{3})([\d,]+)/', $tagContent, $matches)) { |
197
|
|
|
$balance = $matches[1]; |
198
|
|
|
$date = $matches[2]; |
199
|
|
|
$currency = $matches[3]; |
200
|
|
|
$amount = $matches[4]; |
201
|
|
|
|
202
|
|
|
return compact('balance', 'date', 'currency', 'amount'); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
throw new \Exception('Invalid format of balance'); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* @param string $tagContent |
210
|
|
|
* |
211
|
|
|
* @return array |
212
|
|
|
* |
213
|
|
|
* @throws \Exception |
214
|
|
|
*/ |
215
|
|
|
private function parseTransaction($tagContent) |
216
|
|
|
{ |
217
|
|
|
if (preg_match('/(\d{6})(\d{4})([CD])([A-Z])([\d,]+)N(\w{3}).*\n(.+)/', $tagContent, $matches)) { |
218
|
|
|
$valueDate = $matches[1]; |
219
|
|
|
$bookingDate = $matches[2]; |
220
|
|
|
$balance = $matches[3]; |
221
|
|
|
$currencyLetter = $matches[4]; |
222
|
|
|
$amount = $matches[5]; |
223
|
|
|
$code = $matches[6]; |
224
|
|
|
$description = mb_convert_encoding($matches[7], 'UTF-8', 'ISO-8859-2'); |
225
|
|
|
|
226
|
|
|
$currencies = [ |
227
|
|
|
'N' => 'PLN', |
228
|
|
|
'R' => 'EUR', |
229
|
|
|
'D' => 'USD' |
230
|
|
|
]; |
231
|
|
|
if (isset($currencies[$currencyLetter])) { |
232
|
|
|
$currency = $currencies[$currencyLetter]; |
233
|
|
|
} |
234
|
|
|
else { |
235
|
|
|
throw new \Exception('Invalid format of transaction currency'); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
return compact('valueDate', 'bookingDate', 'balance', 'currency', 'amount', 'code', 'description'); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
throw new \Exception('Invalid format of transaction'); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param string $tagContent |
246
|
|
|
* |
247
|
|
|
* @return string |
248
|
|
|
*/ |
249
|
|
|
private function parseTransactionDetails($tagContent) |
250
|
|
|
{ |
251
|
|
|
return mb_convert_encoding(str_replace(["\r", "\n"], '', $tagContent), 'UTF-8', 'ISO-8859-2'); |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|