Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ofxparser Laravel Package

asgrim/ofxparser

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer (the package is abandoned but still installable):

composer require asgrim/ofxparser

The library has one job: turn an OFX file (the format most banks export from their "download transactions" button) into PHP objects. Start by looking at src/Parser.php — it is the single entry point — and then src/Entities/ where Ofx, BankAccount, CreditCardAccount, Statement, and Transaction live. The Parser exposes two methods: parse($filePath) and parseString($contents).

A first, realistic use case: a customer uploads a .ofx export and you want the transactions in a database.

use OfxParser\Parser;

$parser = new Parser();
$ofx    = $parser->parse('/tmp/statement.ofx');

foreach ($ofx->bankAccounts as $account) {
    foreach ($account->statement->transactions as $txn) {
        printf(
            "%s  %-30s  %10.2f  %s\n",
            $txn->date->format('Y-m-d'),
            $txn->name,
            $txn->amount,
            $txn->uniqueId
        );
    }
}

That is essentially the whole API surface. $txn->date is a \DateTime, $txn->amount is a float, and $txn->name/$txn->memo are the strings the bank felt like providing.

In Laravel, wrap this in a small service and never touch the parser directly from a controller:

// app/Services/OfxImporter.php
final class OfxImporter
{
    public function import(string $path): array
    {
        $ofx = (new \OfxParser\Parser())->parse($path);

        return collect($ofx->bankAccounts)
            ->flatMap(fn ($account) => $account->statement->transactions)
            ->map(fn ($txn) => [
                'external_id' => $txn->uniqueId,
                'posted_on'   => $txn->date->format('Y-m-d'),
                'payee'       => $txn->name,
                'memo'        => $txn->memo,
                'amount'      => $txn->amount,
            ])
            ->all();
    }
}

Implementation Patterns

Branch on account type, not on a generic account. The parser gives you two separate collections — $ofx->bankAccounts and $ofx->creditCardAccounts — and you must iterate both. A common bug is only reading bankAccounts and silently dropping credit card data.

$accounts = array_merge($ofx->bankAccounts, $ofx->creditCardAccounts);

Use uniqueId as the idempotency key. OFX's FITID (exposed as $txn->uniqueId) is the bank's stable transaction identifier. Store it with a unique index so re-importing the same statement is a no-op. In Laravel:

Transaction::upsert($rows, ['external_id'], ['payee', 'memo', 'amount', 'posted_on']);

Beware that some institutions emit empty or duplicate FITIDs; fall back to a composite hash (sha1(rtrim($txn->name).$txn->date->format('c').$txn->amount)) when the value is null/blank.

Normalize the sign convention before persisting. Bank statements treat debits as negative (-42.10). Credit card statements in OFX often report a purchase as positive and a payment as negative — the opposite mental model. Decide once, in your importer, whether your DB stores "signed cash flow" or "positive for spend", and convert on the way in:

$amount = $account instanceof CreditCardAccount ? -$txn->amount : $txn->amount;

Work from the statement window. $account->statement->startDate and ->endDate are useful for choosing a reconciliation period, and $account->balance gives you the closing balance for a sanity check against your ledger.

Queue large imports. The parser builds the entire object graph in memory, so a multi-year export from a busy account can be tens of thousands of transaction objects. Dispatch a ImportOfxStatement job, store the raw file on a private disk, and parse inside the job. Keep the original file around — it is your audit trail for "where did this number come from?"

Accept both uploads and stored files. Because parseString exists, you can skip Storage::disk()->path() gymnastics and feed the contents directly:

$ofx = (new Parser())->parseString($request->file('ofx')->get());

Abstract behind an interface if you care about the future. The package is archived (last release 2018) and does not accept fixes. If OFX imports are load-bearing for you, put a StatementParser contract in front of it so you can swap in a maintained fork without touching call sites.

Gotchas and Tips

  • It is abandoned. No bug fixes, no PHP 8.x guarantees, no security patches. It works because OFX 1.x/2.x has not changed, but pin the version and be prepared to fork. The MIT licence means that is fine.
  • SGML vs. XML. OFX 1.x files are SGML (unquoted attribute-like tags, missing closing tags) and are not valid XML. The library handles both, but oddities in bank exports — unescaped &, stray control characters — can still throw. Catch \OfxParser\ParseException and log the raw file; you will need it.
  • Amounts are floats, not money. Do not do arithmetic on them. Immediately convert with (int) round($txn->amount * 100) or Money::of($txn->amount, $currency) from brick/money before storing.
  • Dates are naive DateTime objects. There is no timezone handling; the date is whatever the bank sent (usually the posting date, sometimes a timezone-shifted one). Store as a date column, not a timestamp, or you will fight midnight-offset bugs.
  • name and memo are inconsistent. Some banks put the merchant in name and nothing in memo; others invert it; some put the cardholder's name in both. Treat them as free text and normalize (trim, collapse whitespace, strip terminal-location noise) rather than assuming a structure.
  • Empty statements are valid. A freshly opened account may yield an account object with zero transactions. Your importer should handle that without treating it as a failure — distinguish "parsed, no rows" from "parse failed".
  • The API is small, which is the point. There is nothing to configure, no options, no custom mappers, no event hooks. "Extension points" are whatever you build around it. That means the real design work sits in your importer, not in the library.
  • Debug by dumping the Ofx entity. If a field you expect is missing, dd($ofx->bankAccounts[0]->statement) — the entity shape tells you exactly what that particular bank export actually contained, which is faster than reading the spec.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib