PHPackages                             webwingscz/bank-statements - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PDF &amp; Document Generation](/categories/documents)
4. /
5. webwingscz/bank-statements

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

webwingscz/bank-statements
==========================

Parser of bank account statements: the Czech and Slovak ABO/GPC format and Wise CSV exports. No runtime dependencies.

1.1.0(today)06↑2400%MITPHPPHP ^8.3

Since Jul 28Pushed todayCompare

[ Source](https://github.com/webwingscz/bank-statements)[ Packagist](https://packagist.org/packages/webwingscz/bank-statements)[ Docs](https://github.com/webwingscz/bank-statements)[ RSS](/packages/webwingscz-bank-statements/feed)WikiDiscussions master Synced today

READMEChangelog (2)Dependencies (5)Versions (3)Used By (0)

webwingscz/bank-statements
==========================

[](#webwingsczbank-statements)

Parser of bank account statements: the Czech and Slovak **ABO/GPC** format and **Wise CSV** exports.

*Česká verze: [README.cs.md](README.cs.md)*

- **No runtime dependencies at all** — PHP 8.3+, not even an extension.
- **Immutable, typed value objects** — `readonly` classes, enums, no magic.
- **Amounts in minor units** — no floating point rounding between the file and your application.
- **Two formats, one shape** — a GPC file and a Wise export come out as the same `Statement` and `Transaction`; what only one of them carries lives behind `Transaction::$detail`.
- **Bank dialects** — the ABO format is only nominally standard; the deviations are explicit instead of guessed.
- PHPStan **level max**, 215 unit tests.

Installation
------------

[](#installation)

```
composer require webwingscz/bank-statements
```

Usage — ABO/GPC
---------------

[](#usage--abogpc)

```
use Webwings\BankStatements\Abo\AboDialect;
use Webwings\BankStatements\Abo\AboParser;

$parser = new AboParser(AboDialect::fio());

foreach ($parser->parseFile('statement.gpc') as $statement) {
    echo $statement->accountNumber?->toString(), "\n";   // 8310192897/2010
    echo $statement->closingBalance->toDecimalString(), "\n";

    foreach ($statement as $transaction) {
        printf(
            "%s  %10s  %-20s  VS %s\n",
            $transaction->date()?->format('Y-m-d'),
            $transaction->amount->toDecimalString(),   // "-24.20"
            $transaction->payerOrPayeeName() ?? '',
            $transaction->variableSymbol ?? '-',
        );
    }
}
```

Parsing from a string works the same way:

```
$statements = $parser->parseString(file_get_contents('statement.gpc'));
```

### Pick the dialect by bank code

[](#pick-the-dialect-by-bank-code)

A GPC file does **not** contain the bank code of the account it belongs to, yet banks disagree on two details. Tell the parser which bank produced the file — you always know that, the file does not.

```
new AboParser(AboDialect::forBankCode('0800'));   // Česká spořitelna
new AboParser(AboDialect::fio());                 // 2010
new AboParser(AboDialect::csob());                // 0300
new AboParser(AboDialect::standard());            // anything without a known deviation
```

What differs:

Reversal posting codesISO 4217 currency in bytes 118–122Most banks (FIO, ČSOB, KB, …)`4` = debit, `5` = creditonly FIO and ČSOBČeská spořitelna`3` = debit, `4` = creditno — those bytes hold other fieldsGetting this wrong is not harmless: with the wrong dialect the value `4` flips a reversal from debit to credit, and reading the currency where it does not exist invents one. Unknown posting codes therefore raise `UnknownPostingCode` rather than being silently skipped.

Own variant? Build one:

```
AboDialect::custom(
    ['1' => PostingCode::Debit, '2' => PostingCode::Credit],
    hasCurrencyInTransaction: true,
    bankCode: '6210',
);
```

### Strict mode

[](#strict-mode)

By default unknown record types (such as Komerční banka's `UHL1` header) and orphaned detail records are skipped, so that real-world exports parse. Pass `strict: true` to reject them instead:

```
new AboParser(AboDialect::fio(), strict: true);
```

What is parsed from ABO/GPC
---------------------------

[](#what-is-parsed-from-abogpc)

RecordMeaningMapped to`074`statement header`Statement` — account, client name, opening/closing balance, turnovers, serial number, dates`075`transaction`Transaction` — signed amount, posting code, counter-account, VS/KS/SS, document id, note, value and due date, currency`076`deduction date and counter-party name`Transaction::$deductionDate`, `Transaction::$counterPartyName``078`free text AV1, AV2`Transaction::$descriptionLines``079`free text AV3, AV4`Transaction::$descriptionLines`A file may contain several `074` blocks; parsing therefore returns a `StatementList`.

The full field layout with 1-based byte positions is documented in the class docblock of [`AboParser`](src/Abo/AboParser.php).

Usage — Wise CSV
----------------

[](#usage--wise-csv)

A Wise (formerly TransferWise) export is read into the same objects as a GPC file:

```
use Webwings\BankStatements\AccountNumber;
use Webwings\BankStatements\Wise\WiseParser;

$parser = new WiseParser(
    accountNumber: AccountNumber::fromParts('', '8310192897', '2010'),
    clientName: 'Webwings s.r.o.',
);

$statement = $parser->parseFile('statement_3807780_EUR_2026-06-01_2026-06-30.csv')->first();

echo $statement->currency;                            // EUR
echo $statement->closingBalance->toDecimalString();    // 1177.41

foreach ($statement as $transaction) {
    printf(
        "%s  %10s  %s\n",
        $transaction->date()?->format('Y-m-d'),
        $transaction->amount->toDecimalString(),       // "-1083.35"
        $transaction->payerOrPayeeName() ?? '',        // "Ovhcloud Dublin"
    );
}
```

ColumnMapped to`Amount``Transaction::$amount`, already signed by Wise`Transaction Type``Transaction::$postingCode`, cross-checked against the sign`Date``Transaction::$valueDate` and `$dueDate`, at midnight`TransferWise ID``Transaction::$documentId``Description``Transaction::$note``Payment Reference`, `Note``Transaction::$descriptionLines``Currency``Transaction::$currency` and `Statement::$currency``Payer Name`, `Payee Name`, `Merchant``Transaction::$counterPartyName``Payee Account Number``Transaction::$counterAccount`, for outgoing items`Running Balance`opening and closing balance of the `Statement`everything else`WiseTransactionDetail` behind `Transaction::$detail`### The account number is not in the file

[](#the-account-number-is-not-in-the-file)

A Wise CSV names no account at all. The export file name carries a *balance id* — a Wise profile holds one balance per currency — and which real account number that stands for is knowledge only you have. `WiseFileName` reads the name so that you can look the account up:

```
use Webwings\BankStatements\Wise\WiseFileName;

$name = WiseFileName::tryParse($path);   // balanceId "3807780", currency "EUR", from, to
$parser = new WiseParser(accountNumber: $accountsByBalanceId[$name->balanceId] ?? null);
```

The same name gives the statement its period. Without it — when parsing from a string, or from a renamed file — the period is taken from the oldest and newest movement instead.

There is no variable, constant or specific symbol in a Wise statement either. Deriving one from `Payment Reference`, which holds free text such as `VF2 26002 WEBWINGS`, would invent data, so the symbols stay `null` and the reference is kept as free text.

### Columns are found by name

[](#columns-are-found-by-name)

Wise reorders and extends its column set between exports; the header therefore decides what a cell means, never its position. Unknown columns are ignored, and a missing amount or date column raises `MissingColumn`.

### What only Wise has

[](#what-only-wise-has)

An exchange rate, a running balance, a card number, a merchant, a payment reference, the exact timestamp — none of that exists in a GPC file. It is kept in a `WiseTransactionDetail`:

```
use Webwings\BankStatements\Wise\WiseDetailsType;
use Webwings\BankStatements\Wise\WiseTransactionDetail;

if ($transaction->detail instanceof WiseTransactionDetail) {
    $transaction->detail->exchangeRate;                              // "20.96460", a string on purpose
    $transaction->detail->exchangeToAmount?->toDecimalString();      // "150000.00"
    $transaction->detail->bookedAt?->format('Y-m-d H:i:s.v');        // the time of day, too
    $transaction->detail->runningBalance?->toDecimalString();
    $transaction->detail->cardLastFourDigits;
    $transaction->detail->detailsType === WiseDetailsType::Conversion;
}
```

An exchange rate stays a **string**: `20.96460` is not money, has five decimal places and would be the one value in the library that a float could silently change.

### Charges are separate items

[](#charges-are-separate-items)

Wise books its own charges as items of their own, whose id is that of the charged item prefixed with `FEE-`. A transfer of 150 000 CZK with a 22,16 CZK fee is two rows, and this library keeps them as the two movements the account actually saw — nothing is netted off:

```
$transaction->detail->isFee();      // true for the FEE- item
$transaction->detail->feeOf();      // "TRANSFER-2185636734"
$transaction->detail->fees;         // on the charged item: the 22.16 it reports
```

### Order, balances and turnovers are derived

[](#order-balances-and-turnovers-are-derived)

Wise exports newest first and writes no statement header. The rows are therefore reordered to chronological order — the way a statement reads and the way the ABO parser returns them — and the header is computed: the closing balance is the running balance of the newest item, the opening balance is the oldest item's running balance minus that item, and the turnovers are the sums of the negative and the positive amounts. On the three production exports this library was built against, `closingBalance - openingBalance` equals `transactionSum()` to the heller.

### Strict mode

[](#strict-mode-1)

By default an odd row is still worth reading. `strict: true` rejects three things instead: a row whose `Transaction Type` contradicts the sign of its amount, a `Transaction Details Type` this version does not know, and a second currency in one file.

```
new WiseParser(accountNumber: $account, strict: true);
```

Without strict mode the sign of the amount wins over a contradicting `Transaction Type` — the sign *is* the movement — an unknown details type becomes `null`, and the statement takes the currency of its first row.

Design notes
------------

[](#design-notes)

**Amounts never become floats.** `Amount` holds minor units (hellers) as an integer and offers `toDecimalString()`; `toFloat()` exists but is documented as lossy. ABO stores an unsigned amount and carries the direction in the posting code — `Transaction::$amount` is already signed, negative for money leaving the account.

**Missing values are `null`, not zero.** An all-zero counter-account or variable symbol means "not filled in". Returning `0` or `"0"` would be indistinguishable from a real zero and makes any hash built over a transaction depend on the parser used.

**Text is decoded, not assumed.** The ABO specification allows UTF-8 (files without diacritics) and Windows-1250 (files with them). `TextDecoder` detects UTF-8 by content and converts otherwise, with ISO-8859-2 as a last resort. Byte offsets are always taken before decoding, because the format is byte-oriented.

**Conversion works with iconv, with mbstring, or with neither.** `TextConverter` picks whatever the installation offers. This is not a nicety: **mbstring cannot convert Windows-1250** — that code page is not among the encodings it supports — and Windows-1250 is precisely what Czech banks use for statements with diacritics. The library therefore ships built-in translation tables (`SingleByteEncoding`) for Windows-1250 and ISO-8859-2, generated with iconv and verified against it byte by byte in the test suite. An mbstring-only or extension-free installation gets identical results to an iconv one.

```
// Force a specific mechanism, e.g. to reproduce a production environment
new AboParser(AboDialect::fio(), decoder: new TextDecoder(converter: TextConverter::BuiltIn));
```

**Short lines are tolerated.** Reading past the end of a truncated record yields an empty field rather than an error; several exporters omit the trailing filler. A CSV column the export does not have reads as empty for the same reason.

**CSV is not split by commas.** A statement description regularly contains the delimiter — `Dekujeme, Rohlik.cz Prague 8` — and may contain a doubled quote or a line break. `CsvReader` leaves the work to `fgetcsv()` and disables its non-standard backslash escaping, which would otherwise swallow the rest of a record ending in a backslash.

**One statement, one currency.** `Statement::$currency` exists because a Wise export states the currency once for the whole balance; a GPC statement leaves it null and carries the currency per transaction, if at all.

Development
-----------

[](#development)

```
composer install
composer test        # PHPUnit
composer phpstan     # PHPStan level max
composer cs-check    # PHP-CS-Fixer, dry run
composer ci          # all of the above
```

The GPC fixtures in [`tests/fixtures`](tests/fixtures) are byte-exact files built from the published specifications. They cover: FIO with a debit item and a currency code, ČSOB with a credit item plus `076`/`078`/`079` details and a credit reversal, Česká spořitelna reversal codes, two statements in one file preceded by a `UHL1` header, and a Windows-1250 encoded file.

The Wise fixtures mirror the structure of real exports — the full 23-column header, the newest-first order, a charge next to the item it belongs to, a conversion, a cross-currency card payment, a cashback of type `UNKNOWN`, an IBAN counter-account and a merchant name containing a comma — with invented names, accounts and amounts. There is also a reordered header with an unknown column and a Windows-1250 encoded CSV.

Sources
-------

[](#sources)

- [Česká spořitelna — Technický popis struktury ABO formátu](https://www.csas.cz/banka/content/inet/internet/cs/ABO_format.pdf)
- [FIO banka — Struktura GPC formátu](https://www.fio.cz/docs/cz/struktura-gpc.pdf)
- [Komerční banka — Klientský formát ABO](https://mojebanka.kb.cz/file/cs/bdsk_format_abo_sk.pdf)
- [Wise — Statements and transaction reports](https://wise.com/help/articles/2932777/how-do-i-download-my-statement)

Credits
-------

[](#credits)

The knowledge of the ABO field layout originates from [jakubzapletal/bank-statements](https://github.com/jakubzapletal/bank-statements) and its fork [ifm24/bank-statements](https://github.com/ifm24/bank-statements), both MIT licensed. This library is a fresh implementation written against the bank specifications, with no dependency on `symfony/dom-crawler`, immutable value objects and explicit bank dialects.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4532277?v=4)[Jiří Dorazil](/maintainers/juradee)[@juradee](https://github.com/juradee)

---

Tags

parsercsvBankBankingczechabostatementslovakgpcWisetransferwise

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/webwingscz-bank-statements/health.svg)

```
[![Health](https://phpackages.com/badges/webwingscz-bank-statements/health.svg)](https://phpackages.com/packages/webwingscz-bank-statements)
```

###  Alternatives

[faisalman/simple-excel-php

Easily parse / convert / write between Microsoft Excel XML / CSV / TSV / HTML / JSON / etc formats

578610.1k1](/packages/faisalman-simple-excel-php)[rodenastyle/stream-parser

PHP Multiformat Streaming Parser

447204.3k2](/packages/rodenastyle-stream-parser)[avadim/fast-excel-reader

Lightweight and very fast XLSX Excel Spreadsheet and CSV Reader in PHP

107737.8k11](/packages/avadim-fast-excel-reader)[shuchkin/simplecsv

Parse and retrieve data from CSV files. Export data to CSV.

52100.9k1](/packages/shuchkin-simplecsv)[csanquer/colibri-csv

Lightweight and performant CSV reader and writer library

16164.8k5](/packages/csanquer-colibri-csv)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
