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

Swedish Entity Laravel Package

olssonm/swedish-entity

Validate and format Swedish organization numbers (organisationsnummer) in Laravel and PHP. Includes validation rules, helpers, and entity types to keep Swedish company data clean and accurate in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package with Composer and let Laravel's package discovery register the service provider:

composer require olssonm/swedish-entity

The package is small, so the first thing to do is open the README on GitHub and skim the Person / Organization classes plus the two validation rules. Everything is namespaced under Olssonm\SwedishEntity, so you can grep the vendor directory for class Person to see the public surface (valid(), format(), isPerson(), isCompany(), getAge(), getBirthDate(), getGender(), etc.).

The first, most useful integration is using the bundled rule on a signup form. That alone covers ~80% of day-to-day use — rejecting badly typed personnummer before they hit your DB:

use Olssonm\SwedishEntity\Rules\Personnummer;

$request->validate([
    'ssn' => ['required', 'string', new Personnummer()],
]);

If you're not using the validation rule (e.g. importing a CSV, or an API endpoint with a custom response shape), call the static helpers instead:

use Olssonm\SwedishEntity\SwedishEntity;

if (! SwedishEntity::isValid($raw)) {
    // handle invalid input
}

For display/parsing you'll usually reach for the object wrapper:

use Olssonm\SwedishEntity\Person;

$person = new Person('198005011234');
$person->format();       // '19800501-1234'
$person->valid();        // true
$person->getAge();       // int

Look first at Person for private individuals, Organization for company numbers (organisationsnummer), and the exception class (PersonException / OrganizationException) if you prefer throw-on-invalid semantics over a boolean return.

Implementation Patterns

Form request validation. Wrap the rule in a reusable form request so the same rule applies across registration, checkout, and account-update endpoints. Combine it with 'digits:10' or a regex if you want to reject whitespace before the package has to strip it — the rule handles -, +, and plain digits, but rejecting obvious garbage early gives better error messages.

Normalise before persist. Always store the formatted value, not the raw user input. The dash placement differs between 10- and 12-digit forms, and mixing them in a column is a debugging nightmare:

$person = new Person($request->input('ssn'));
$user->personal_identity_number = $person->format();
$user->save();

Use the same rule to strip the + century separator during hydration if you support users over ~100 years old.

Eloquent accessors/mutators. Encapsulate formatting in the model rather than sprinkling it across controllers:

public function setSsnAttribute($value)
{
    $this->attributes['ssn'] = (new Person($value))->format();
}

That way mass-assignment, factories, and seeders all produce consistent data. Pair it with a cast/accessor on the read side if you want the display format (with dash) versus the stored format (digits only) to diverge.

Extract demographics instead of duplicating them. A personnummer encodes birth date and legal gender (from the third digit of the sequence). Rather than asking users to enter their age and gender separately, derive them:

$person = new Person($user->ssn);
$profile->birth_date = $person->getBirthDate();
$profile->gender     = $person->getGender(); // 'M' / 'F'
$profile->age        = $person->getAge();

Cache the derived values — don't call getAge() at render time on every request, since age is a function of "now."

Handle coordination numbers explicitly. Sweden uses samordningsnummer (day-of-month +60) for people without a full personnummer. If your app serves non-residents (staffing, universities, healthcare), don't just reject on isPerson() === false — check isCoordinationNumber() and store it in a separate column or a type flag. Otherwise you'll silently lock out legitimate users.

Company numbers as a separate code path. Organisationsnummer follow a different checksum and prefix rules than personnummer. Keep them in distinct columns and separate request classes; trying to use one "isValid" for both will accept numbers you don't want. SwedishEntity::isCompany() vs SwedishEntity::isPerson() is the branch point.

Bulk/import flows. Wrap the loop in a try/catch when validating rows from a CSV — one bad SSN shouldn't abort the batch. Collect failures with row numbers and continue:

try {
    $person = new Person($row['ssn']);
    if (! $person->valid()) { throw new PersonException(); }
} catch (PersonException $e) {
    $errors[] = ['row' => $i, 'ssn' => $row['ssn']];
}

Gotchas and Tips

Format ≠ correctness of the checksum alone. format() will happily normalise strings whose Luhn check digit is wrong; only valid() (or a catch on the constructor) tells you the number is real-looking. Never store a formatted value without validating first.

Century separators are the #1 source of bugs. Personnummer can appear as YYMMDD-XXXX, YYYYMMDD-XXXX, and with + for people over 100. Round-tripping + through your DB (which probably stores digits only) loses the century information. Decide policy early: either reject + inputs or expand them to 12-digit form at ingest and document the assumption.

Coordination numbers look valid but aren't personnummer. A samordningsnummer will pass some naive isPerson checks depending on version, and getBirthDate() will return a date offset by 60 days. If you compute age for tax/reporting, verify isCoordinationNumber() and document how you handle it — otherwise someone will file a tax report on a fake birthday.

Luhn check digits differ between types. Personnummer use Luhn over the 9-digit body; organisationsnummer use Luhn over their 9-digit body (leading two digits constrained to known prefixes). Don't assume a single valid() method exists for both — call the type-appropriate method.

Whitespace and Unicode dashes. Copy-paste from Word yields en-dashes (–) and non-breaking spaces that the parser may not strip. Normalise with preg_replace('/[^\d]/', '', $value) (while preserving a leading + if you care) before handing input to the package, especially for webhook/API payloads.

Gender is legal/birth-registered, not identity. getGender() returns the digit-derived gender, which may not match a user's current identity. Don't surface it in user profiles without a strong reason, and don't gate features on it.

Validation rule messages are English/uncustomisable by default. Publish the package config (php artisan vendor:publish --tag=swedish-entity) if available, or override translations in lang/. For a Swedish-facing app you'll want Swedish error messages; consider a custom Rule that wraps the package and returns :attribute är inte ett giltigt personnummer.

Testing with real numbers. Use well-known test fixtures (198005011234 is not valid — check the check digit). The safest pattern is to generate valid SSNs in a factory by computing the Luhn digit yourself, so tests don't break when someone "fixes" a hard-coded fixture.

Constructor throws on garbage, valid() doesn't on good input. If you're inside a pipeline that may legitimately receive invalid data (e.g. analytics import), prefer SwedishEntity::isValid($raw) and branch, instead of constructing and catching — exceptions are slow and noisy in hot paths.

No DB migrations, no schema. This is a pure validator/formatter. If you need uniqueness ("one account per personnummer"), enforce it in your migration with a unique index on the normalised column — the package won't help you there.

Extension point: wrap it. The library is intentionally thin. Create a small App\Support\SwedishIdentity service around it that (a) normalises input, (b) validates, (c) exposes isCompany / isPerson / isCoordination semantics for your domain, and (d) returns consistent DTOs. That single seam makes swapping the underlying package trivial if maintenance slows (it's a 15-star project — bus factor matters).

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels