jongotlin/bank-account-bundle
A Symfony bundle for working with bank account details, offering value objects and validation for IBAN, BIC, and related data so you can store and verify payment information cleanly across your application.
The most important thing to understand on day one: jongotlin/bank-account-bundle is a Symfony bundle, not a Laravel package. Composer will install it without complaint, but nothing in it will ever execute in a Laravel app — there is no bundle registry, no Kernel, no Symfony Validator component wiring. The bundle contributes a Bundle class, a DI extension, a config tree, and service definitions. None of that is reusable here.
What you actually want is the framework-agnostic domain library that the bundle wraps:
composer require jongotlin/bank-account
Where to look first in that library:
BankAccountFactory — the entry point; takes a string and returns a parsed account (or throws).First use case: validating a Swedish bank account number (clearing number + account number) captured during seller onboarding, payout setup, or Autogiro subscription billing. Validate at capture time so the user gets immediate feedback, and again at payout execution as a second gate.
A 15-minute spike to confirm viability:
use Jongotlin\BankAccount\BankAccountFactory;
$factory = new BankAccountFactory();
try {
$account = $factory->createFromString('500 123 456');
echo $account->getFormatted(); // canonical display form
} catch (\Throwable $e) {
// invalid — surface as a validation message, never a 500
}
Run 50–100 real account numbers (a mix of known-good and known-bad from your treasury team) through it before writing a single line of integration code. If the false-negative rate on your own known-good data is non-zero, stop and fix the clearing table first.
Do not install the bundle alongside the library "just in case." It is a dead dependency that will confuse the next maintainer and add nothing.
Never call BankAccountFactory inline in a controller. Wrap it in a ValidationRule so it composes with FormRequests, Validator::make(), and the string-rule registry:
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Jongotlin\BankAccount\BankAccountFactory;
final class SwedishBankAccount implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
(new BankAccountFactory())->createFromString((string) $value);
} catch (\Throwable) {
$fail('The :attribute is not a valid Swedish bank account number.');
}
}
}
Note the \Throwable catch — the library's error contract may be exceptions rather than booleans. Catch broadly at the boundary and convert to a validation message; an uncaught exception here is a 500 on a form submission.
Register a short string alias in a service provider if you use this in many places ('bank_account'), so FormRequests stay readable.
Three distinct representations, and mixing them up is the most common source of bugs:
500 123 456, 500-123456, 500123456). Strip spaces and hyphens once, at the start.Don't let the library type leak across your codebase. One interface, one adapter:
namespace App\Support\BankAccount;
interface BankAccountValidator
{
public function isValid(string $raw): bool;
public function normalise(string $raw): string;
public function format(string $raw): string;
}
Bind it in AppServiceProvider, inject it into your rule and your value object. This is the seam that lets you fork, replace, or add an ops-override without touching call sites — and given the maintenance risk, you want that seam from day one.
For models that carry an account number, use a cast that reconstitutes a value object exposing normalised() and formatted(). This keeps the clearing/account split in one place and makes formatting a method call rather than a helper sprawled across Blade templates.
If you're forking or extending, put the accepted clearing-number ranges in config/bank-accounts.php rather than hardcoding them. Swedish clearing ranges do get reassigned, and you want operations to adjust the table without a deploy. Cache the config in production as usual.
Validate at input (onboarding/payout-details forms) and at payout execution. Input validation gives good UX; execution-time validation catches accounts that were valid when stored but have drifted into a reassigned range, and catches any path that bypassed the form. Instrument both: count rejections by reason and alert on anomalous spikes.
If you're replacing existing regex or manual checks, run the new validator in shadow mode against live data for two to four weeks. Log disagreements, do not block. Cut over only when the false-negative rate against known-good accounts is zero, and keep the legacy path one flag-flip away for a full release cycle.
Build a golden-file corpus from published Bankgirot sources and your treasury team's records: valid accounts, invalid accounts, and boundary clearing numbers for each bank. Include 3300-prefixed personal account numbers, which behave differently. This corpus is your Swedish payments domain knowledge — it outlives the library.
The bundle is a trap. If you run composer require jongotlin/bank-account-bundle, it installs silently, resolves, and does nothing. No error, no warning. You'll discover this weeks later when someone wonders why the "validator" isn't catching bad input. Require the library explicitly and add a composer.json comment or README note explaining why the bundle is absent.
Check the PHP constraints before you plan anything. The library is small and may cap PHP at a version you no longer run. Confirm the matrix against your actual runtime (Laravel 9/10/11, PHP 8.1/8.2/8.3) before estimating the integration. If it caps below your floor, you're forking, not integrating — budget accordingly.
Pin exactly and commit the lockfile. This is a 0-star, 0-dependent, single-maintainer package on the payment path. Treat upstream releases as reviewed changes, not Dependabot auto-merges. An unreviewed bump to the clearing table is a silent behavioural change to money movement.
False negatives are the high-severity failure. A valid account rejected blocks a user's payout setup and generates a support ticket. A stale or incomplete clearing-range table is the usual cause. Mitigate with the shadow-mode cutover and an ops override path — someone in treasury must be able to force-accept an account, with an audit trail.
False positives are cheap but not free. An invalid account accepted means the bank returns the payment, which incurs fees and reconciliation work. That's why you validate at execution as well as input.
Mask account numbers in logs and error trackers from day one. Account numbers are financial PII. Exception handlers, Laravel Telescope, Sentry breadcrumbs, and Log::info debugging will all happily capture them. Configure scrubbing before you write the first test, not after.
Bankgiro and Plusgiro are not bank accounts. The library validates domestic Swedish account numbers; it does not distinguish Bankgiro/Plusgiro from clearing+account, does not produce IBAN/BIC, and does not initiate payments. Decide explicitly which account types your flow accepts and document it. If your payout rail is SEPA rather than Bankgirot, this package solves the data-entry problem only — you still need an IBAN library or your PSP's SDK for the payment path.
Handle the error contract deliberately. The validator may throw rather than return false. Find out which, and make your rule the single place that translates it. Don't let library exceptions escape into middleware or the exception handler.
try/catch (\Throwable) is correct here, not lazy. Validation failures should never produce a 500. If the library throws something unexpected — a type error on odd input, say — you still want the user to see a validation message.
Plan for the fork. MIT licence means you can fork; that is the correct mitigation for a bus-factor-of-one dependency. Budget an annual review of the clearing-number table and checksum rules against Bankgirot publications, plus a quarterly glance at upstream activity. Assign a named owner, or it becomes anonymous infrastructure nobody dares touch.
Write a one-page runbook. How to inspect a rejection, how to override, how to add a clearing range. Escalation is internal engineering, with treasury/payments ops as the authority on whether a rejected account is genuinely invalid.
Performance is a non-issue. String arithmetic, stateless, deterministic, microseconds. Call it at request time, in queue workers, and across batch payout generation without a second thought. It will never be the bottleneck — spend your optimisation budget elsewhere.
Extension point if you do fork: the clearing-number range table and the checksum dispatch are the two places you'll edit. Keep them in one class with the range table as a data structure, not scattered if branches, and the fork stays cheap to maintain.
How can I help you explore Laravel packages today?