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

Standards Laravel Package

prinsfrank/standards

A collection of international standards as native PHP 8.1+ enums, pulled from sources like ISO, IANA, and the US Library of Congress. Automatically updated daily so reference data such as country, currency, and language codes stays accurate, with no manual maintenance required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (PHP 8.1+ required, enums are backed so from()/tryFrom()/cases() all work):

composer require prinsfrank/standards

Require it as a production dependency, not --dev, as soon as any enum appears in a typehint, cast, or validation rule — a dev-only install will explode in prod the moment a CountryAlpha2 parameter is autoloaded.

Where to look first, in this order:

  1. The mermaid ER diagram in the README — it tells you which conversions exist and, more importantly, which are not bidirectional (a Country cannot become a LanguageTag; a LanguageTag can resolve to a Country).
  2. The per-standard section you actually need (Country, Currency, Language, HttpStatusCode, TLD). Each has an "At a glance" block with the exact method names.
  3. src/ in the repo — for any method not shown in the README, the enum source is the documentation. Case names and helper methods change between releases, so read the file rather than guessing.
  4. UPGRADING.md before any major bump, and the release notes before any minor bump (see Gotchas — upstream standards drift).

First use case: sanitise and enrich a country at a form boundary. This is the smallest thing that demonstrates the whole value proposition — validation, canonical codes, and free enrichment:

use PrinsFrank\Standards\Country\CountryAlpha2;
use PrinsFrank\Standards\Country\CountryAlpha3;
use PrinsFrank\Standards\Country\Groups\EU;

$country = CountryAlpha2::tryFrom(strtoupper($request->input('country', '')))
    ?? abort(422, 'Unknown country');

$country->value;                                   // 'NL'  — canonical, uppercase
$country->toCountryAlpha3()->value;                // 'NLD'
$country->toCountryNumeric()->value;               // '528' (string!)
$country->getFlagEmoji();                          // '🇳🇱'
$country->getCountryCallingCodes()[0]->value;      // '+31' or 31 depending on enum
$country->getCurrenciesAlpha3()[0]->value;         // 'EUR'
$country->isMemberOf(EU::class);                   // true

If that works, you already have 80% of what most apps need from this package: an enum type you can thread through the domain instead of magic strings.

Implementation Patterns

Validate at the boundary, typehint everywhere else. The package's real payoff is signature-level guarantees: function shipTo(CountryAlpha2 $country, CurrencyAlpha3 $currency, int $minorAmount). Never pass raw ISO strings past the controller. Convert once with tryFrom(), return a 422 on null, and let the rest of the codebase stop re-validating.

Eloquent casts do the conversion for you. Because these are backed enums, Laravel casts them natively:

protected $casts = [
    'country'      => CountryAlpha2::class,
    'currency'     => CurrencyAlpha3::class,
    'language'     => LanguageAlpha2::class,
];

Migrations should match the backing value exactly: char(2) for alpha-2, char(3) for alpha-3, char(3) for numeric codes (not integer — see Gotchas), char(3) for currencies.

API Resources: emit ->value, never ->name.

return [
    'country'       => $this->country?->value,              // 'NL'
    'country_name'  => $this->country?->toCountryName()->value, // 'Netherlands (the)'
    'flag'          => $this->country?->getFlagEmoji(),
];

For JSON responses, a backed enum already serialises to its value when json_encoded, but being explicit in resources keeps the contract visible to reviewers and prevents accidental case-name leaks.

Dropdowns: build from cases(), sort by the display name, not the case name.

$options = collect(CountryAlpha2::cases())
    ->map(fn (CountryAlpha2 $c) => [
        'value' => $c->value,
        'label' => $c->toCountryName()->value,
        'flag'  => $c->getFlagEmoji(),
    ])
    ->sortBy('label')
    ->values();

Cache this (config cache, a small repository class, or an in-request static memo) if it appears on a hot page — it isn't slow, but cases() re-materialises thousands of enum objects on every call.

Currency + amount as a pair. Store the ISO code plus an integer minor amount; if you need the exponent for formatting, reach for the currency minor-unit enums rather than hardcoding 2 (JPY, KWD, ISK will bite you). CurrencyAlpha3::from($code)->toCurrencyName()->value gives you the human label; keep symbol lookup (CurrencySymbol) out of your DB entirely.

Language negotiation from Accept-Language. Parse the header into tags, then walk them in preference order through LanguageTag/LanguageAlpha2::tryFrom() and fall back to a default. This is exactly the gif-demo use case and replaces a fiddly hand-rolled parser. Keep the fallback explicit — an unmatched header shouldn't throw.

HTTP layer. HttpStatusCode::from($code) in an exception renderer maps framework exceptions to a canonical status; HttpMethod::from($request->method()) is useful for CORS/allow-list checks. Wrap both in tryFrom() in any public-facing path — non-standard codes (499, 444) come from proxies, not RFC 9110.

Country groups for policy gating. isMemberOf(EU::class) and friends (EEA, EFTA, Schengen, EuroZone, NATO, BRICS, WorldTradeOrganization) are cleaner than a hardcoded ISO array for VAT rules, shipping zones, or feature flags — and they update with the standard instead of with your PR backlog.

Wrap it for anything non-trivial. Enum members can't be extended and vendor code shouldn't carry your business logic. Put a thin service or value object in front (CountryRepository, Money, LocaleNegotiator) that exposes only the methods you use; when the package shifts a case name or you swap the data source, you patch one class instead of 40 call sites.

Tests. Enum cases make excellent data providers, and asserting on CountryAlpha2::Netherlands reads far better than 'NL'. Seed factories with enum cases — the cast handles persistence.

Gotchas and Tips

Case names are not values, and they are not safe to persist. CountryAlpha2::Netherlands, CountryCallingCode::Netherlands_Kingdom_of_the, CountrySubdivision::Vanuatu_province_Malampa. These names are sanitised from upstream display names and do change when a country/currency renames or the sanitisation rules shift. Store ->value. Same for ->name in logs, cache keys, and queues.

Never match() on a package enum without a default. This is the single most common way this package breaks a build: a minor release adds a new country group member, currency, or language, and every exhaustive match without a default arm now throws UnhandledMatchError in production. Either add default => ..., or pin the version and treat upgrades as a reviewed task.

Numeric codes are strings with significant leading zeros. CountryNumeric::from('004'), CountryNumeric::fromInt(...) exists for the int form, but the backing value is a string. A CountryNumeric column typed INT will silently lose Afghanistan. Same applies to currency numeric codes.

from() throws ValueError; tryFrom() returns null. Untrusted input (forms, API payloads, headers, CSV imports) must use tryFrom(). from() is only acceptable on data you generated yourself. ValueError is not an HttpException, so it surfaces as a 500 by default.

Country codes are UPPERCASE, language codes are lowercase. CountryAlpha2::from('NL') works; CountryAlpha2::from('nl') throws. Normalise case at the edge (strtoupper for countries, strtolower for languages) before calling tryFrom(), or you'll ship a bug that only shows up with user-typed input.

Enums cannot be extended. No class MyCountry extends CountryAlpha2. If you need extra behaviour, implement an interface on your own wrapper enum or value object, or compose. Don't plan an architecture around subclassing.

Flag emoji render as letter pairs on Windows. getFlagEmoji() returns regional-indicator pairs; Chrome/Edge on Windows shows NL instead of 🇳🇱 unless the Segoe UI Emoji flag font workaround is installed. Don't rely on flags as the only country affordance in a UI.

HTTP status enums only know registered codes. RFC-registered codes only — Cloudflare/nginx pseudo-codes (520, 499) will throw. Use tryFrom() and fall back to a generic reason phrase in your error renderer.

TLD enums are data, not validators. GenericTLD::from('com') tells you the TLD exists in IANA's list; it does not validate a domain, and it can't know about xn-- IDN or a second-level structure. Keep real validation in filter_var/a validator rule.

getNameInLanguage() may not have every country in every language. Guard the result before rendering, and treat it as a nicety layered on top of toCountryName()->value, not the primary label.

Pinning vs. freshness is a real trade-off. The package regenerates from ISO/IANA/SIX/LoC daily, so composer update will pull in upstream renames, retirements, and new subdivisions. Pin a minor version in composer.json and schedule the bump — review the release notes and the daily workflow diffs, run your test suite, and check for match exhaustiveness before merging. Composer's lockfile is what actually protects you here, so commit it.

Laravel integration shortcuts: use Rule::enum(CountryAlpha2::class) in FormRequests instead of in: lists, CountryAlpha2::tryFrom() in custom rules for nullable fields, and Illuminate\Validation\Rules\Enum for array/CSV validation. Don't reach for ->name in Blade — $country->value for storage, $country->toCountryName()->value for display.

When something breaks after an upgrade, the debugging path is: UPGRADING.md → the release notes for the version you crossed → src/<Standard>/ to see the actual case list → grep your own codebase for the removed case name (not the code). Errors will present as ValueError at from(), UnhandledMatchError in a match, or an Eloquent cast exception on hydration — that last one usually means stored data now fails tryFrom(), so plan a data migration, not just a code fix.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route