kmukku/php-iso11649
Lightweight PHP library for ISO 11649 structured creditor references (RF references). Generate, validate, and format RF numbers with mod-97 checksum handling, plus helpers for converting existing references for use in SEPA credit transfers and payment workflows.
Install and inspect (5 minutes)
composer require kmukku/php-iso11649
There is no service provider, no config file, no facade, and no artisan command. The entire package is essentially one class in src/ plus a PHPUnit suite. Because the project has 12 stars and its last release was April 2020, treat the source as the documentation: open src/CreditorReference.php and tests/ first. It is small enough to read end-to-end, and that is the only reliable way to confirm method names and constructor signatures for the version you pinned.
First use case: turn an invoice number into a valid RF reference
use Kmukku\PhpIso11649\CreditorReference;
// The naked reference (no "RF", no check digits) is what you feed in.
$ref = new CreditorReference('INV201900042');
echo $ref->getReference(); // RF** INV201900042, check digits computed
echo $ref->getFormattedReference(); // same value grouped in fours, for print/pdf
First use case (the other direction): validate what came back from the bank
$incoming = 'RF18 5390 0754 7034'; // as printed on a statement, spaces included
$normalized = preg_replace('/\s+/', '', strtoupper($incoming));
if (CreditorReference::isValid($normalized)) {
// safe to match against your stored references
}
Confirm in the source whether validation is exposed as a static isValid() or as an instance check — this is the one API detail most likely to differ from what you expect. Build both flows (generate + validate) as a smoke test on day one so a future PHP upgrade fails loudly rather than silently.
1. Never call the library directly from controllers. Wrap it in your own value object so the rest of the app is insulated from a stale, low-activity dependency:
final class CreditorReference
{
private function __construct(private string $raw, private string $formatted) {}
public static function fromInvoice(string $invoiceNumber): self
{
$iso = new \Kmukku\PhpIso11649\CreditorReference(
self::sanitize($invoiceNumber)
);
return new self($iso->getReference(), $iso->getFormattedReference());
}
public static function fromBankPayload(string $raw): ?self
{
$candidate = self::sanitize($raw);
return \Kmukku\PhpIso11649\CreditorReference::isValid($candidate)
? new self($candidate, /* format for display */ $candidate)
: null;
}
public function raw(): string { return $this->raw; }
public function __toString(): string { return $this->formatted; }
private static function sanitize(string $v): string
{
return preg_replace('/[^A-Z0-9]/', '', strtoupper($v));
}
}
The fromBankPayload returning null instead of throwing is deliberate: inbound bank data is untrusted and "not a creditor reference" is a normal outcome, not an error.
2. Eloquent cast for storage. Store the canonical form (uppercase, no spaces); render the grouped form:
protected $casts = [
'creditor_reference' => CreditorReferenceCast::class, // implements CastsAttributes
];
Your set() should reject invalid values by throwing — bad references should never reach the DB. Your get() should return the wrapped VO so Blade can echo the formatted version.
3. Form request rule for customer-facing input.
class ValidCreditorReference implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$normalized = preg_replace('/\s+/', '', strtoupper((string) $value));
if (strlen($normalized) > 25
|| ! \Kmukku\PhpIso11649\CreditorReference::isValid($normalized)) {
$fail('The :attribute is not a valid ISO 11649 creditor reference.');
}
}
}
Note the explicit length check before delegating — do not assume the library rejects over-length input gracefully.
4. Reconciliation workflow. The typical daily loop is:
camt.053/MT940 remittance information into a bank_transactions table.RF... token from the free-text remittance field with a regex (/\bRF\d{2}[A-Z0-9]{1,21}\b/i).raw() against invoices.creditor_reference.5. Generate deterministically from the invoice. Since the check digits are a pure function of the input, you can regenerate a reference any time from the invoice number. Do that instead of storing an extra column if you're confident the invoice number is immutable; store it if it isn't.
6. Bind as a singleton in a service provider only if you have several consumers; there is no package-provided binding:
$this->app->singleton(ReferenceService::class, fn () => new ReferenceService());
INV123), validation takes the full string (RF18INV123). Mixing them up is the single most common mistake and produces check digits that look plausible but fail validation everywhere else. Make your wrapper's method names say which one they take.98 - (n mod 97). A 21-character reference becomes a ~45-digit number, well beyond PHP's 64-bit int. Verify how the package handles this (bcmath/gmp/string-based modulo) and confirm the relevant extension is installed in production. If it's string-based, fuzz it against your own reference implementation.RF and the two check digits, so the underlying reference is capped at 21. Many banks then truncate the remittance field further (often 140 structured characters) — the standard alone doesn't guarantee your reference survives the payment chain intact.RF. ISO 11649 excludes it to avoid ambiguity; enforce this in your sanitizer rather than relying on the package to complain.jschaedl/iban or similar and treat reference validation as one step in a larger pipeline.bcmod code is a morning's work, not a refactor.ValidationException or a skipped row. Do not let it bubble to a 500 in a bulk import.RF00, convert letters, mod 97, subtract from 98. Writing that five-line helper as a test oracle is the fastest way to confirm the package behaves as you expect and to catch normalization bugs.How can I help you explore Laravel packages today?