survos / ark-bundle
Symfony bundle for ARK identifier minting and resolution
Fund package maintenance!
Requires
- php: ^8.4
- daniel-km/noid: ^1.4
- doctrine/dbal: ^3.9||^4.2
- doctrine/doctrine-bundle: ^3.2
- doctrine/orm: ^3.3
- symfony/config: ^8.0
- symfony/console: ^8.0
- symfony/dependency-injection: ^8.0
- symfony/framework-bundle: ^8.0
- symfony/http-foundation: ^8.0
- symfony/http-kernel: ^8.0
- symfony/routing: ^8.0
- symfony/uid: ^8.0
- twig/twig: ^3.21
Requires (Dev)
- api-platform/core: ^4.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.0|^14.0
Suggests
- api-platform/core: Exposes ArkBinding as an ApiResource with collection/item endpoints.
- survos/field-bundle: Registers ArkBinding in the admin navbar via #[EntityMeta].
- survos/jsonl-bundle: Enables ark:export --output and ark:import --input with .jsonl/.jsonl.gz files.
- dev-main
- 2.1.2
- 2.1.1
- 2.0.220
- 2.0.219
- 2.0.218
- 2.0.217
- 2.0.216
- 2.0.215
- 2.0.214
- 2.0.213
- 2.0.212
- 2.0.211
- 2.0.210
- 2.0.209
- 2.0.208
- 2.0.207
- 2.0.206
- 2.0.205
- 2.0.204
- 2.0.203
- 2.0.202
- 2.0.201
- 2.0.200
- 2.0.199
- 2.0.198
- 2.0.197
- 2.0.196
- 2.0.195
- 2.0.194
- 2.0.193
- 2.0.192
- 2.0.191
- 2.0.190
- 2.0.189
- 2.0.188
- 2.0.187
- 2.0.186
- 2.0.185
- 2.0.184
- 2.0.183
- 2.0.182
- 2.0.181
- 2.0.180
- 2.0.179
- 2.0.178
- 2.0.177
- 2.0.176
- 2.0.175
- 2.0.174
- 2.0.173
- 2.0.172
- 2.0.171
- 2.0.170
- 2.0.169
- 2.0.168
- 2.0.167
- 2.0.166
- 2.0.165
- 2.0.164
- 2.0.163
- 2.0.162
- 2.0.161
- 2.0.160
- 2.0.159
- 2.0.158
- 2.0.156
- 2.0.155
- 2.0.154
- 2.0.146
- 2.0.145
- 2.0.144
- 2.0.143
- 2.0.142
- 2.0.141
- 2.0.140
- 2.0.139
- 2.0.138
- 2.0.137
- 2.0.136
- 2.0.135
- 2.0.134
- 2.0.133
- 2.0.132
- 2.0.131
- 2.0.130
- 2.0.129
- 2.0.128
- 2.0.127
- 2.0.126
- 1.0.0
This package is auto-updated.
Last update: 2026-05-10 14:52:27 UTC
README
Symfony bundle for ARK (Archival Resource Key) minting, binding, and resolution.
Requires PHP 8.4+ and Symfony 8.
What it does
- Mints opaque ARK identifiers via the NOID algorithm (
daniel-km/noid) - Stores bindings in a Doctrine
ark_bindingpivot table (one SQL lookup to resolve any ARK) - Derives deterministic 22-character ARK names from ULIDs
- Auto-mints on
prePersistfor entities implementingArkableInterface - Redirects
GET /ark/{naan}/{name}→ entity's target URL (301) - Serves ERC metadata via
?infoquery parameter - Exposes an admin browse entry via
#[EntityMeta](requiressurvos/field-bundle) - Exports/imports bindings as JSONL for backups (requires
survos/jsonl-bundle)
Why ARK
- ARK Standard: https://arks.org
- ARK Spec (IETF draft): https://www.ietf.org/archive/id/draft-kunze-ark-34.html
- N2T Global Resolver: https://n2t.net
Get a NAAN
Request your own NAAN before going to production:
- Application form: https://n2t.net/e/naan_request
- Registry: https://n2t.net/e/pub/naan_registry.txt
Resolver rule for N2T registration
Use the template form so N2T constructs the correct path:
https://your-domain.org/ark/${content}
For ARK ark:12345/x8rd9, ${content} = 12345/x8rd9, producing
https://your-domain.org/ark/12345/x8rd9 which matches the bundle's route exactly.
Test ARK
Use _probe as the identifier string. The bundle serves GET /ark/{naan}/_probe
with HTTP 200 independently of any database state, so N2T's periodic checks
always pass:
ark:YOUR_NAAN/_probe
Install
composer require survos/ark-bundle
Optional:
composer require survos/field-bundle # admin navbar entry for ArkBinding composer require survos/jsonl-bundle # ark:export / ark:import commands
Configuration
config/packages/survos_ark.yaml:
survos_ark: naan: '12345' resolver_base_url: 'https://your-domain.org' shoulder: '' template: 'fk.reedeeedk' local_path: '/ark' db_type: 'sqlite' db_path: '%kernel.project_dir%/var/noid' auto_mint: true n2t_resolve: false
Making an entity ARK-enabled
Implement ArkableInterface and use ArkableTrait:
use Survos\ArkBundle\Contract\ArkableInterface; use Survos\ArkBundle\Doctrine\ArkableTrait; class Item implements ArkableInterface { use ArkableTrait; public function getArkTarget(): string { return '/items/' . $this->id; // relative URLs are prefixed with resolver_base_url } public function getArkObjectType(): string { return 'item'; } }
An ARK is minted automatically on first persist(). For entities with pre-generated
IDs (ULID/UUID), an ArkBinding row is also created in the same flush cycle.
Auto-increment entities are indexed lazily on first resolution or via ark:reindex.
ULID ARKs
ArkUlidCodec converts a ULID to a fixed 22-character URL-safe name and back.
It uses a base58 alphabet that avoids 0, O, 1, I, and l; a true
base64url alphabet cannot avoid those characters.
$name = $codec->name($ulid); $ark = $codec->ark($ulid); $url = $codec->url($ulid); $n2tUrl = $codec->n2tUrl($ulid); $ulid = $codec->ulid($name);
The existing route GET /ark/{naan}/{name} matches these names, so an app can
decode the {name} segment and resolve it without storing the ARK separately.
For QR codes and public links, encode the N2T URL. N2T resolves
https://n2t.net/ark:/12345/{name} to the configured resolver URL,
for example https://your-domain.org/ark/12345/{name}:
{{ ark_ulid_n2t_url(item.id) }}
Twig functions:
| Function | Description |
|---|---|
ark_ulid_name(ulid) |
Deterministic 22-character name |
ark_ulid_url(ulid) |
Local resolver URL |
ark_ulid_n2t_url(ulid) |
Public N2T ARK URL for QR codes |
ark_ulid(name) |
Decode the name back to a canonical ULID string |
Resolution order
ark_bindingtable — single SQL query (primary path)- NOID database — in-process binary store (legacy/fallback)
- Entity scan — iterates all
ArkableInterfaceclasses; lazily populates both stores on hit
ArkBinding entity
ark_binding is a pivot table that links every minted ARK to its owning entity:
| Column | Description |
|---|---|
ark |
Full ARK string, e.g. ark:/12345/ab12345 (unique) |
entityClass |
Short class name, e.g. Item, Scan |
entityId |
Entity identifier as string |
targetUrl |
Cached redirect URL |
label |
Optional human-readable label |
With survos/field-bundle installed, ArkBinding appears in the admin navbar
under the ARK group.
Commands
| Command | Description |
|---|---|
ark:mint [count] |
Mint N ARKs |
ark:bind <name> <url> |
Manually bind a name to a URL |
ark:resolve <name> |
Look up a binding |
ark:validate <name> |
Check the check character |
ark:bulk-mint |
Mint ARKs for all unminted ArkableInterface entities |
ark:reindex |
Re-sync all NOID and ArkBinding entries from entity targets |
ark:report |
Report ARK coverage across entities |
ark:export |
Export ArkBinding rows as JSONL (stdout or --output file.jsonl.gz) |
ark:import |
Import ArkBinding rows from JSONL (stdin or --input file.jsonl.gz) |
Routes
| Route | Description |
|---|---|
GET /ark/{naan}/{name} |
Resolve and redirect (301); append ?info for ERC metadata |
GET /ark/{naan}/_probe |
Service health probe — always 200 |
Persistence
| Layer | What it stores | Authoritative? |
|---|---|---|
ark_binding Doctrine table |
ARK → entity class, ID, target URL | Yes — back this up |
NOID SQLite file (var/noid/) |
Minting sequence + URL cache | No — rebuild with ark:reindex |
The NOID file is semi-ephemeral: if lost, run ark:reindex to repopulate it from
the ark_binding table. Available backends: sqlite (default), mysql, pdo,
bdb (BerkeleyDB), lmdb, xml.
ULID compression
A ULID is 128 bits. Its canonical Crockford Base32 form is 26 characters; the same bytes can be represented more compactly:
| Format | Length | Notes |
|---|---|---|
| Binary | 16 bytes | Storage (Postgres uuid, BINARY(16)) |
| Base64url | 22 chars | Shortest sensible ASCII form |
| Base58 | 22 chars | No ambiguous chars |
| Base32 (Crockford) | 26 chars | Canonical, case-insensitive, sortable |
| RFC 4122 UUID | 36 chars | Interop with UUID tooling |
Round-trip with symfony/uid
use Symfony\Component\Uid\Ulid; $ulid = new Ulid('01ARZ3NDEKTSV4RRFFQ69G5FAV'); $short = $ulid->toBase64(); // 22 chars, URL-safe $back = Ulid::fromBase64($short); // round-trips
Recommendation
- Database: 16-byte binary (
uuidcolumn). - URLs / compact tokens: Base64url, 22 chars.
- ARK identifiers / human-transcribable IDs: keep the 26-char Crockford Base32 — case-insensitive, no
0/Oor1/I/lconfusion, lexicographically sortable. The 4 saved characters aren't worth losing those properties.
22 chars is the practical floor for ASCII. Base85 reaches 20 but includes characters (", ', \) that are awkward in URLs and JSON.