citomni / danish-public-sector-data
Reusable CitOmni clients for authoritative data services provided by the Danish public sector.
Package info
github.com/citomni/danish-public-sector-data
pkg:composer/citomni/danish-public-sector-data
Requires
- php: ^8.5
- citomni/infrastructure: ^1.0
- citomni/kernel: ^1.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-01 23:50:05 UTC
README
Reusable CitOmni clients for authoritative data services provided by the Danish public sector.
Package
citomni/danish-public-sector-data
Repository: https://github.com/citomni/danish-public-sector-data
citomni/danish-public-sector-data is a CitOmni provider package for PHP 8.5+. It exposes small, normalized CitOmni services while keeping upstream API protocols, authentication details, and source-specific data models inside the provider.
The provider currently exposes normalized CVR, BBR, VAT-registration, and soil-contamination capabilities backed by the relevant Danish public-sector sources.
Requirements
- PHP 8.5+
- Composer
citomni/kernelcitomni/infrastructure- An enabled
CitOmni\Infrastructure\Boot\Registryin the host app - A Datafordeler IT-system with an API key for non-access-restricted data when using Datafordeler-backed lookups
Install
composer require citomni/danish-public-sector-data
Enable the infrastructure provider before this provider in the host app:
<?php declare(strict_types=1); return [ \CitOmni\Infrastructure\Boot\Registry::class, \CitOmni\DanishPublicSectorData\Boot\Registry::class, ];
Datafordeler API key
Store the API key in the normal app-local CitOmni secret store. Do not put the credential in application configuration, templates, JavaScript, or version control.
<?php declare(strict_types=1); return [ 'datafordeler.api_key' => 'YOUR_API_KEY', ];
The active file is selected by CITOMNI_ENVIRONMENT, for example
var/secrets/app.secret.dev.php during local development.
Credential safety
Datafordeleren authenticates API-key requests with the key in the request URL.
The internal Datafordeler client reuses the shared CitOmni curl service and marks
apiKey through sensitive_query_keys. The real value is preserved for the outbound
request, while URLs exposed through Curl metadata, logs, and exceptions are redacted.
Raw transport metadata is not returned by the public CVR service.
BBR property lookup
The provider exposes bbr for current property data from BBR/DAR through Datafordeler.
The service is BFE-centered internally and supports all three BBR property types:
- samlet fast ejendom (SFE)
- bygning på fremmed grund (BPFG)
- ejerlejlighed
Resolve human-entered auction-style addresses separately from loading the property:
$resolution = $this->app->bbr->resolveAddress('Nørmarkvej 29, 2 22, 7600 Struer'); if ($resolution['selectedBfeNumber'] !== null) { $property = $this->app->bbr->getPropertyByBfe($resolution['selectedBfeNumber']); }
resolveAddress() preserves trailing m.fl. as multiplePropertiesHint=true and returns
primary_resolved_scope_incomplete when the named primary property resolves but the source
explicitly indicates additional auction properties. The service never invents those additional
BFE numbers from the primary address.
Owner-apartment resolution uses the concrete DAR floor/door address and the corresponding BBR unit/property relation. This allows the selected owner apartment to be distinguished from the underlying SFE at the same house number.
Current property data is loaded explicitly by BFE:
$property = $this->app->bbr->getPropertyByBfe('4268969');
The normalized property graph includes relevant addresses, house numbers, grounds, buildings,
units, floors, entrances, and technical installations. Each physical BBR object includes a
scope value so context objects such as an owner apartment's host building or a BPFG property's
underlying ground are not presented as directly belonging to the requested property.
Bundled BBR code-list labels are returned next to their authoritative ...Code values,
including municipality names, floor types, elevator state, and house-number roles. Unknown codes
keep a null label so consumers can fall back to the raw value without guessing.
codeListSnapshotDate identifies the bundled code-list snapshot used for the labels.
Technical-installation history is opt-in:
$history = $this->app->bbr->getTechnicalInstallationHistory('4268969');
This keeps normal current-state lookups cheaper while still allowing due-diligence workflows to inspect historical BBR technical installations such as former tanks.
Address parsing and deterministic fuzzy-street matching have a local smoke test:
php tests/bbr_address_parser_test.php
Soil contamination lookup
The provider exposes soilContamination for structured soil-contamination classifications from DKjord. Parcel lookup uses Danmarks Miljøportal's anonymous WFS endpoint; BFE-centered lookup additionally uses Datafordeleren's current Matriklen data to resolve the physical cadastral parcels belonging to the property.
Lookup one exact cadastral parcel by official ejerlav code and matrikelnummer:
$parcel = $this->app->soilContamination->getByParcel(2005352, '311a');
The result always describes the requested parcel after a successful WFS lookup. hasDkJordMatch=false and an empty classifications list mean that none of the configured DKjord classification layers matched that exact ejerlav/matrikel pair; this must not be reworded as proof that the soil is uncontaminated.
The initial WFS contract classifies exact parcel matches through these public DKjord layers:
localizedfromDKJord:View_LokaliseretFladerv1fromDKJord:View_V1Fladerv2fromDKJord:View_V2Fladerremoved_after_mappingfromDKJord:View_UEKFladerremoved_before_mappingfromDKJord:View_UIKFlader
Matching is exact inside DKjord's semicolon-separated Lokalitetsmatrikler field. The service deliberately derives classification from layer membership rather than legacy Parcel API status codes or locality-level descriptive status text. Geometry is not requested.
For property workflows, resolve the physical parcel scope from a BFE number:
$result = $this->app->soilContamination->getByBfe(3208712);
The BFE lookup supports SFE, building-on-foreign-ground, and owner-apartment properties when an underlying SFE exists. It loads current Matriklen parcels, resolves their official ejerlav identifiers, attaches a normalized contamination result to every parcel, and returns a property-level union of matching classifications. A property without an underlying SFE can return parcelResolutionStatus=no_underlying_sfe without inventing a parcel association.
The local normalization/query-contract test does not make network requests:
php tests/soil_contamination_test.php
CVR company lookup
The public CVR service intentionally returns a normalized package-owned array rather than Datafordeler-specific GraphQL relation names.
$company = $this->app->cvr->getCompany('12345678'); if ($company === null) { // No matching CVR company exists. }
Result shape:
[ 'cvrNumber' => '12345678', 'name' => 'Example ApS', 'status' => 'aktiv', 'startDate' => '2020-01-01', 'endDate' => null, 'companyType' => [ 'code' => '80', 'name' => 'Anpartsselskab', ], 'address' => [ 'type' => 'beliggenhedsadresse', 'formatted' => 'Example Street 12, 7400 Herning', 'careOf' => null, 'street' => 'Example Street', 'houseNumberFrom' => '12', 'houseNumberTo' => null, 'floor' => null, 'door' => null, 'postalCode' => '7400', 'city' => 'Herning', 'supplementaryCity' => null, 'countryCode' => 'DK', 'freeText' => null, ], 'postalAddress' => null, 'contact' => [ 'email' => 'info@example.test', 'phone' => '12345678', 'marketingProtected' => false, ], 'industries' => [ 'primary' => [ 'code' => '000000', 'name' => 'Example industry', 'sequence' => 0, ], 'secondary' => [], ], ]
getCompany() requires exactly eight CVR digits and returns null when the
upstream query returns no company node. Integration failures throw exceptions
from CitOmni\DanishPublicSectorData\Exception. The address.formatted value is
built locally from the normalized CVR address fields; Datafordeler's upstream
Adresse value is a DAR address reference rather than formatted display text.
address continues to prefer the registered location address and falls back to the
postal address for backwards-compatible lookup behavior. postalAddress exposes the
postal address explicitly when CVR supplies one. Industry sequence 0 is normalized as
the primary industry; sequences 1 through 3 are returned as secondary industries.
The normalized getCompany() contract intentionally remains limited to fields exposed by
the selected Datafordeler GraphQL contract. Source-specific payloads are not part of
getCompany() and must not leak into application persistence contracts.
The current default uses Datafordeler flexibleCurrent/v3. Host applications can
override the endpoint selection through the normal CitOmni configuration flow:
return [ 'danish_public_sector_data' => [ 'cvr' => [ 'service' => 'flexibleCurrent', 'version' => 'v3', ], ], ];
A service-version change that also changes the GraphQL schema may require a package update; overriding the version does not make incompatible schemas compatible.
VAT registration lookup
The provider also exposes vatRegistration. Its current implementation checks one exact
CVR/SE number through SKAT's anonymous public VAT-number web lookup:
$status = $this->app->vatRegistration->getStatus('12345678');
The normalized result contains the queried number, the decisive current registration state,
the displayed verification date when available, and skat.dk as source. The web transport
is deliberately isolated because the public SKAT page is not a documented API contract.
No CAPTCHA, login, or access control is bypassed. The exact queried CVR/SE number is retained
because a legal CVR number may use separate administrative SE numbers for VAT registration.
The parser smoke test covers the observed CVR/SE search controls plus decisive positive and negative result wording without making a live network request:
php tests/skat_vat_web_parser_test.php
Internal Datafordeler client
Datafordeler authentication and GraphQL transport are internal package concerns.
Support\DatafordelerClient is instantiated by public package services as needed and
is deliberately not registered in the host application's service map.
The first release supports API-key authentication for non-access-restricted data. OAuth and access-restricted datasets are deliberately outside the initial scope.
Architecture
The provider keeps the boundaries intentionally small:
Service\Bbrresolves DAR addresses and returns normalized BBR property data.Service\SoilContaminationresolves physical Matriklen parcels and returns normalized DKjord soil-contamination data.Service\Cvris the public CVR capability and owns CVR-specific queries and normalization.Service\VatRegistrationis the public VAT-registration status capability.Support\DatafordelerClientowns Datafordeler authentication, GraphQL transport, response validation, and safe exception translation.Support\DkJordWfsClientisolates anonymous DKjord WFS transport and GeoJSON response validation.Support\SkatVatWebClientisolates the temporary public SKAT web-flow transport details.Exceptioncontains transport-agnostic integration failure semantics.- No SQL, HTTP controller behavior, or CLI output belongs in these services.
New public-sector sources should be added only when there is a concrete consumer. Do not force unrelated REST, GraphQL, geospatial, or file-download APIs behind one artificial generic abstraction.
Configuration and services
The provider contributes these shared service IDs:
bbrsoilContaminationcvrvatRegistration
Internal transport helpers are not registered as host-app services.
Package-owned defaults live under danish_public_sector_data in
src/Boot/Registry.php and may be overridden by the host app through normal
CitOmni configuration precedence.
Data and licensing
The package source code is released under the MIT License. Data retrieved from public-sector services remains subject to the terms, licences, access conditions, and other rules of the respective data provider. This package does not grant any rights to third-party or public-sector data.
Coding conventions
- PHP 8.5+
- PSR-1 / PSR-4
- PascalCase classes
- camelCase methods and variables
- UPPER_SNAKE_CASE constants
- K&R braces
- Tabs for indentation
- PHPDoc and inline comments in English
- Fail fast unless a failure is genuinely recoverable
License
citomni/danish-public-sector-data is released under the MIT License.
See LICENSE and NOTICE.
Trademarks
See TRADEMARKS.md for the CitOmni trademark notice
applicable to this package.