byrokrat/id
byrokrat/id is a PHP library for Swedish identity numbers. It provides value objects to parse, validate, format, and compare personal identity numbers, coordination numbers, and organization numbers, including checksum verification, birth date handling, and age calculation.
Install via Composer:
composer require byrokrat/id
Autoload with PSR-4 is automatic; no service provider to register, no config file to publish. This is a pure value-object library, so "installation" is literally one line.
Look here first, in this order:
byrokrat\Id\IdInterface — the contract every id implements (format(), getRaw(), isValid(), getBirthdate(), etc.).byrokrat\Id\AbstractId — shared behaviour, including the formatting engine.byrokrat\Id\PersonalId — Swedish personnummer.byrokrat\Id\OrganizationId — Swedish organisationsnummer.byrokrat\Id\IdFactory — constructs the right concrete class from an arbitrary string, so you don't have to guess the type.Your first real use case is almost always form validation. Accept the raw input a user typed ("850101-1234", "198501011234", "850101 1234"), normalize it, and check the checksum:
use byrokrat\Id\PersonalId;
$id = new PersonalId('19850101-1234');
if ($id->isValid()) {
// checksum is correct
}
Use the factory when you accept either a person or an organization number in the same field (e.g. a generic "identifier" login or onboarding input):
use byrokrat\Id\IdFactory;
$id = IdFactory::createId($request->input('identifier'));
$id instanceof \byrokrat\Id\OrganizationId; // true for company numbers
$id instanceof \byrokrat\Id\PersonalId; // true for personal numbers
Treat the id classes as immutable domain value objects and never let the raw string leak past the boundary of your request/controller layer. A few patterns that pay off in a day-to-day Laravel codebase:
Custom validation rule. Wrap construction plus isValid() in a Rule class so the semantics are reusable:
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use byrokrat\Id\PersonalId;
use Throwable;
class SwedishPersonalId implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$id = new PersonalId((string) $value);
} catch (Throwable) {
$fail('The :attribute is not a valid Swedish personal identity number.');
return;
}
if (! $id->isValid()) {
$fail('The :attribute has an invalid checksum.');
}
}
}
Eloquent cast. Because the constructor accepts a string and the object converts back to string, a small custom cast keeps models clean:
class PersonalIdCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): ?PersonalId
{
return $value === null ? null : new PersonalId($value);
}
public function set($model, string $key, $value, array $attributes): ?string
{
return $value instanceof PersonalId
? (string) $value
: ($value === null ? null : (string) new PersonalId((string) $value));
}
}
Store the canonical, separator-stripped form in the database (getRaw() or (string) $id), and rehydrate on read. That keeps your schema portable and lets other tools (databases, exports, external APIs) consume it.
Normalizing before you compare. Two ids that are "the same person" can arrive with and without a century prefix and with -, + or no separator. Convert once at the boundary, compare the resulting object or its getRaw() string. Never compare user input to a stored id as plain strings — you will get false negatives.
Age and date gating. For features like account creation for 18+, getBirthdate() returning a DateTimeImmutable is more robust than arithmetic on the raw number:
$isAdult = $id->getBirthdate()->diff(new \DateTimeImmutable())->y >= 18;
Combine with isValid() first — a structurally invalid id may still throw when deriving the date.
Organization numbers for B2B. Use OrganizationId when you need to distinguish a sole trader from a limited company for pricing/tax purposes, or to derive a display label. In the byrokrat ecosystem these also interoperate with the payment/bank-giro packages, so the same identifier often becomes the anchor key for payment agreements.
Cohort / segmentation. getBirthdate() and isValid() are the only two calls you need for analytics or reporting (e.g. grouping users by birth year, flagging suspicious signups), while format() gives you consistent presentation for mail and PDFs.
+ is not cosmetic. The Swedish convention uses + as the separator for people aged 100 or over. If you strip separators before validation you may silently change the inferred century, because the century is not part of the number itself. Decide early whether you store full century (19850101…) or not, and never infer a century from +-less input without a policy.
Construction vs. validity. The constructor can accept a structurally-shaped but checksum-invalid number; isValid() is what confirms the checksum. Do not assume that "no exception thrown" means "valid id". Always call isValid() after construction (or wrap the two together in your rule/caster).
Coordination numbers are not personal numbers. Swedish samordningsnummer reuses the personnummer layout but adds 60 to the day-of-month, so the date portion can look impossible to a normal calendar check. Whether your use case accepts them depends on the domain — byrokrat/id provides the types to distinguish them; do not blindly reuse a PersonalId for a coordination number without making the choice explicit in code.
Organization numbers have reserved/test ranges. The last digit carries a check digit and specific digit ranges are reserved for test entities. If you seed the database with organization numbers for fixtures, generate values that pass isValid() and be explicit in the fixture comments that they are test values, so nobody mistakes them for real companies.
Immutability means no setters. You cannot "change the number" on an id object. Reassigning a field on the model must create a new object; treat the value as frozen and let the cast handle persistence. This is also why ids are safe to pass around in events, DTOs, and queues without defensive copying.
Serialization. (string) $id is the canonical form for display and storage. Do not rely on json_encode($id) unless you know it implements JsonSerializable; for API responses explicitly emit $id->format(...) or (string) $id so your contract is stable.
Formatting is your escape hatch. The formatting string lets you choose separators, century, and checksum inclusion. Escape literal characters — a format like Ymd is a specifier, not a literal pattern, so read the README carefully before building user-facing display strings, and centralise the format specifiers in a helper to avoid scattering them across Blade templates.
Exception handling. Structural problems throw; catch Throwable (or the specific byrokrat exception classes you see in the source) at the request boundary and translate to validation errors. Avoid catching inside domain services — let the controller decide the HTTP status.
Extensions and custom types. When you need a rule the library doesn't cover (for example, disallowing a specific serial range, or a private id format for an integration), extend AbstractId/implement the id interface rather than forking. That keeps the checksum and formatting logic reusable and means other byrokrat packages (payment, autogiro) still accept your objects.
Debugging. When validation is failing unexpectedly, log both the raw input and (string) $id — the mismatch is almost always leading zeros, whitespace, or a misplaced separator rather than the checksum algorithm itself. Test early with the four canonical input shapes: YYMMDD-XXXX, YYMMDDXXXX, YYYYMMDD-XXXX, YYYYMMDDXXXX.
How can I help you explore Laravel packages today?