Parsing QIF files
From a file
Section titled “From a file”Parser::parseFile() reads the file, parses it and returns the parser. getTransactions() then returns the
transactions, in the order of the file:
!AccountNCheckingTBank^!Type:BankD03/01/2026T-1,250.00CXPExample Property RentalsA12 Sample StreetASpringfieldMJanuary rentLHousing:Rent^D05/01/2026T2,400.00C*PAcme CorpMSalaryLIncome:Salary^D07/01/2026T-84.30PDemo GroceryLFoodSFood:Groceries$-64.30EWeekly groceriesSHousehold$-20.00^<?php
use Silarhi\Qif\Parser;
$parser = Parser::parseFile('export.qif');
foreach ($parser->getTransactions() as $transaction) { echo json_encode([ 'type' => $transaction->getType()?->name, 'date' => $transaction->getDate()?->format('Y-m-d'), 'amount' => $transaction->getAmount(), 'status' => $transaction->getStatus()->name, 'payee' => $transaction->getDescription(), 'address' => $transaction->getAddress(), 'memo' => $transaction->getMemo(), 'category' => $transaction->getCategory(), 'splits' => $transaction->getSplits(), ], JSON_PRETTY_PRINT), PHP_EOL;}{ "type": "Bank", "date": "2026-01-03", "amount": -1250, "status": "Reconciled", "payee": "Example Property Rentals", "address": "12 Sample Street\nSpringfield", "memo": "January rent", "category": "Housing:Rent", "splits": []}{ "type": "Bank", "date": "2026-01-05", "amount": 2400, "status": "Cleared", "payee": "Acme Corp", "address": null, "memo": "Salary", "category": "Income:Salary", "splits": []}{ "type": "Bank", "date": "2026-01-07", "amount": -84.3, "status": "NotCleared", "payee": "Demo Grocery", "address": null, "memo": null, "category": "Food", "splits": [ { "category": "Food:Groceries", "amount": -64.3, "memo": "Weekly groceries" }, { "category": "Household", "amount": -20, "memo": null } ]}The !Account list at the top of the file holds the account, not a transaction: the parser skips it.
parseFile() throws a RuntimeException when the file does not exist or cannot be read, and a ParseException on a
value it cannot read without guessing: see Error handling.
From a string
Section titled “From a string”To parse content you already have in memory (an upload, an HTTP response…), use the constructor and call parse(),
which returns the transactions:
use Silarhi\Qif\Parser;
$transactions = (new Parser($content))->parse();getTransactions() returns the same array afterwards, and an empty array until parse() has run. Calling parse()
again parses the content again and replaces the transactions: parseFile() has already called it. When parse()
throws a ParseException, getTransactions() returns the transactions read before the error: see
Error handling.
The second argument of the constructor is the line separator, "\r\n" by default. Each of its characters ends a
line, so the default reads Windows (\r\n), Unix (\n) and old Mac (\r) files, and skips empty lines: you never
need to change it for these files.
The whole content is held in memory, and so are the transactions.
Options
Section titled “Options”parseFile() and the constructor take the date format and the decimal separator of the file:
use Silarhi\Qif\Enums\DateFormat;use Silarhi\Qif\Enums\DecimalSeparator;use Silarhi\Qif\Parser;
$parser = Parser::parseFile('/path/to/export.qif', DateFormat::MonthFirst, DecimalSeparator::Point);
$transactions = (new Parser($content, dateFormat: DateFormat::DayFirst, decimalSeparator: DecimalSeparator::Comma)) ->parse();| Option | Default | Values |
|---|---|---|
$dateFormat |
DateFormat::DayFirst |
DayFirst (25/12/2023) or MonthFirst (12/25/2023): Dates |
$decimalSeparator |
null, detected |
DecimalSeparator::Point (1,234.50) or Comma (1.234,50): Amounts |
What you get
Section titled “What you get”| Getter | Type | QIF line | Notes |
|---|---|---|---|
getType() |
?Types |
!Type: |
Bank, Cash, CreditCard… null for an unknown type |
getDate() |
?DateTimeImmutable |
D |
At midnight, null when missing or empty: see Dates |
getAmount() |
?float |
T, U |
null when missing or empty: see Amounts |
getMemo() |
?string |
M |
|
getStatus() |
Status |
C |
Reconciled (X, R), Cleared (*, c), else NotCleared |
getDescription() |
?string |
P |
Payee |
getAddress() |
?string |
A |
Every A line of the record, joined with "\n" |
getCategory() |
?string |
L |
|
getSplits() |
list<array> |
S, E, $ |
[['category' => string, 'amount' => float, 'memo' => ?string], ...]: see Splits |
The values are the rest of the line after its code, with the spaces around the line trimmed. An empty field (a bare
P, D or T line) is skipped like a missing one: the getter returns null, and an empty C line leaves the
NotCleared status. The only exception is the S line, which starts a split even when empty: see Splits.
How a file is read
Section titled “How a file is read”- Records end with a
^line. The last record of the file is kept even without its closing^, and so is a record followed by a new header. A record without any value (a stray^, or only empty fields) is not a transaction: it is skipped. - Headers: a
!Type:line sets the type of the records that follow it, until the next header. An unknown type (!Type:Memorized) gives them anulltype. The type values are listed in Types. - Lists that are not transactions are skipped, up to the next header: accounts (
!Account), categories (!Type:Cat), classes (!Type:Class), securities (!Type:Security) and prices (!Type:Prices). - Options:
!Option:and!Clear:lines (!Option:AutoSwitch) are ignored, and leave the type of the records that follow them unchanged. - Amounts: when a record has both a
Tand aUline, the last one wins. - The UTF-8 BOM is removed, wherever it is.
- Ignored lines: the check number (
N), the split percentage (%), the investment fields (Y,I,Q,O), the reimbursable flag (F), the budget (B) and the invoice fields (X…). The field reference lists what is read and what is written.
Splits
Section titled “Splits”Each S line starts a split of the transaction: its category. The E line after it is its memo, the $ line its
amount, read like the T amount. getSplits() returns them as a list, in the order of the file, as several splits can
share a category:
[ ['category' => 'Food:Groceries', 'amount' => -64.3, 'memo' => 'Weekly groceries'], ['category' => 'Household', 'amount' => -20.0, 'memo' => null],]A split without a $ line, or with an empty one, throws a ParseException; an empty E line leaves the memo
null. An empty S line still starts a split, with an empty string as category, as the $ and E lines that follow
it belong to it. A $ or E line that does not follow an S line is ignored: in investment records, $ is the
amount transferred.
The parser replaces the ' year separator with / and spaces with 0 (1/ 1/ 0 becomes 1/01/00), then reads the
date with two-digit or four-digit years, day first by default. ISO dates (2026-01-31, 26-01-31) are read whatever
the date format. For the month-first dates of US versions of Quicken, pass DateFormat::MonthFirst:
<?php
use Silarhi\Qif\Enums\DateFormat;use Silarhi\Qif\Exception\ParseException;use Silarhi\Qif\Parser;
$dates = ['2026-01-31', '31/01/2026', '01/31/2026', '26-01-31', '31/01/26', "31/01'26", '1/ 1/ 0', '31/02/2026', 'Jan 31', ''];
printf("%-12s %-18s %s\n", 'D line', 'DayFirst', 'MonthFirst');foreach ($dates as $date) { $results = []; foreach ([DateFormat::DayFirst, DateFormat::MonthFirst] as $format) { try { [$transaction] = (new Parser("!Type:Bank\nD{$date}\nT-10\n^\n", dateFormat: $format))->parse(); $results[] = $transaction->getDate()?->format('Y-m-d H:i') ?? 'null'; } catch (ParseException) { $results[] = 'ParseException'; } }
printf("%-12s %-18s %s\n", "D{$date}", ...$results);}D line DayFirst MonthFirstD2026-01-31 2026-01-31 00:00 2026-01-31 00:00D31/01/2026 2026-01-31 00:00 ParseExceptionD01/31/2026 ParseException 2026-01-31 00:00D26-01-31 2026-01-31 00:00 2026-01-31 00:00D31/01/26 2026-01-31 00:00 ParseExceptionD31/01'26 2026-01-31 00:00 ParseExceptionD1/ 1/ 0 2000-01-01 00:00 2000-01-01 00:00D31/02/2026 ParseException ParseExceptionDJan 31 ParseException ParseExceptionD null null- Dates are at midnight, in the default timezone of PHP.
- A date that does not exist with the chosen format throws a
ParseExceptioninstead of rolling over:31/02/2026, or01/31/2026read day first. So does a value that is not a date (Jan 31). - An empty
Dline leaves the datenull. - Transactions with the same date string share one
DateTimeImmutableinstance.
Amounts
Section titled “Amounts”Amounts can group their thousands with ,, . or spaces (including the no-break spaces of French amounts). By
default, the decimal separator is detected for each amount:
- a plain number (
-1234.50,1.234) uses a decimal point, as in the QIF specification; - when an amount has both
,and., the last one separates the decimals (1,234.50,1.234,50); - a separator repeated groups thousands (
1,234,567); - a single
,is a decimal comma (12,50), except after 1 to 3 digits and before exactly 3 (1,234): that amount is ambiguous,1234or1.234, and throws aParseException.
Pass DecimalSeparator::Point or DecimalSeparator::Comma to read every amount of the file the same way. An amount
written in the other convention then throws a ParseException, and so does a value that is not a number (12 EUR).
The messages show the amount as written in the file, spaces included:
<?php
use Silarhi\Qif\Enums\DecimalSeparator;use Silarhi\Qif\Exception\ParseException;use Silarhi\Qif\Parser;
$amounts = ['-1234.50', '1,234.50', '1.234,50', '12,50', '1,234,567', '1.234', '1 234,50', '1,234', '12 EUR'];
printf("%-12s %-16s %-16s %s\n", 'T line', 'Detected', 'Point', 'Comma');foreach ($amounts as $amount) { $results = []; foreach ([null, DecimalSeparator::Point, DecimalSeparator::Comma] as $separator) { try { [$transaction] = (new Parser("!Type:Bank\nT{$amount}\n^\n", decimalSeparator: $separator))->parse(); $results[] = var_export($transaction->getAmount(), true); } catch (ParseException) { $results[] = 'ParseException'; } }
printf("%-12s %-16s %-16s %s\n", "T{$amount}", ...$results);}T line Detected Point CommaT-1234.50 -1234.5 -1234.5 ParseExceptionT1,234.50 1234.5 1234.5 ParseExceptionT1.234,50 1234.5 ParseException 1234.5T12,50 12.5 ParseException 12.5T1,234,567 1234567.0 1234567.0 ParseExceptionT1.234 1.234 1.234 1234.0T1 234,50 1234.5 ParseException 1234.5T1,234 ParseException 1234.0 1.234T12 EUR ParseException ParseException ParseExceptionAn empty T line leaves the amount null.
Raw record
Section titled “Raw record”Each parsed transaction keeps the lines it was read from in its public $_raw property, for debugging and logs. Its
header line comes first, on a line of its own (!Type:Bank, or !Type:Memorized for an unknown type). Every line
ends with the line break of the file (its first one: "\r\n", "\n" or "\r"), the last one included, and empty
lines are left out. It is null on the transactions you build yourself. Its exact format is not part of the API.
