Repository navigation
Profiles
php-fhir can generate classes for FHIR profiles (for example US Core) as an extra
step after the normal schema-based generation. The XSD schemas do not describe profiles, so they are read from FHIR
implementation guide packages (package.tgz files or extracted package directories) containing StructureDefinition
JSON.
Profile classes are placed in a Profiles namespace (configurable) beneath each version's namespace, for example
<Prefix>\Versions\R4\Profiles\USCore\FHIRResource\FHIRDomainResource\USCorePatientProfile. Beneath its package's
namespace, a profile class sits at the same relative path as the type it constrains has beneath Types (here
Types\FHIRResource\FHIRDomainResource\FHIRPatient), and the directory structure follows. Package-wide classes such as
PackageMap and PackageAutoloader live directly in the package namespace. Each class:
-
extends the type it constrains (
FHIRPatient), so it can be used anywhere the base type is accepted. A profile derived from another generated profile extends that profile instead. - is named from the StructureDefinition
name, converted to PascalCase. Name collisions fail generation. - exposes
PROFILE_URL,PROFILE_VERSION,PROFILE_NAME,PROFILE_BASE_TYPEandPROFILE_INVARIANTSconstants. - is validated through the type's existing
_getValidationErrors(), which also reports the profile's own failures. - implements the core
Types\ProfileTypeInterface(_getProfileUrl(),_getProfileVersion()and_getProfileValidationErrors(), which runs only the rules the profile and its parent profiles add). - has typed accessors for top-level slices, named
get<SliceName><Element>(), e.g.getRaceExtension(). They return a single object when the slice's maximum is 1, otherwise an array.
Alongside the classes, each version gets a ProfileMap and ProfileAutoloader. Each package also gets its own PackageMap
(canonical url to class) and PackageAutoloader inside its namespace, implementing the shared PackageMapInterface and PackageAutoloaderInterface from the root Profiles namespace. The root ProfileMap only lists the package
maps and queries them in turn, and ProfileAutoloader loads and registers a package's autoloader the first time one of
that package's classes is requested, so nothing is loaded for packages you do not use. Package namespaces may be nested
(e.g. Vendor and Vendor\IG); every package whose namespace a requested class falls under is tried until one loads it.
Add profiles to a version:
[
'name' => 'R4',
'schemaPath' => __DIR__ . '/input/R4',
'profiles' => [
'namespace' => 'Profiles',
'packages' => [
[
'name' => 'hl7.fhir.us.core', // required, must match the package's own name
'source' => __DIR__ . '/input/hl7.fhir.us.core.tgz', // required, .tgz or extracted directory
'namespace' => 'USCore', // default derived from name
'dependencies' => ['hl7.fhir.uv.sdc'], // names of other configured packages, see below
'strict' => true, // fail instead of warn+skip on unresolvable content
],
],
],
],dependencies lists the names of other packages configured in the same version's packages. An unknown name or a
circular dependency is a configuration error. Dependencies are built first, and the dependent package then:
- resolves base profiles, extensions and ValueSets through them (including their dependencies);
- can have its classes
extendthe dependency's classes.
A package's strict setting applies to its own profiles only; a dependency is checked, and its warnings logged, under
its own setting.
Every profile in each configured package is generated. When using --profilePackages, the requested packages'
dependencies are written too, as their classes are extended.
Only local files are supported; nothing is downloaded. A package must target the FHIR version it is configured under
(a mismatch fails with a clear error), so R4, R4B and R5 each need their own IG builds. Packages are generated by default
along with the library whenever they are configured; see CLI Usage for --profiles, --noProfiles,
--onlyProfiles and --profilePackages.
Register the profile autoloader (the main library Autoloader does not load profile classes). Its unregister()
also unregisters the package autoloaders it registered:
\Acme\Fhir\Versions\R4\Profiles\ProfileAutoloader::register();
$patient = \Acme\Fhir\Versions\R4\Profiles\USCore\FHIRResource\FHIRDomainResource\USCorePatientProfile::jsonUnserialize($decoded);
foreach ($patient->_getValidationErrors() as $path => $rules) {
foreach ($rules as $rule => $message) {
echo "{$path} [{$rule}]: {$message}\n"; // e.g. "identifier [min_occurs]: Field ... must have at least 1 elements"
}
}
$race = $patient->getRaceExtension();Set profileResolution to auto (see Configuration Reference)
and ResourceParser will pick the profile class from meta.profile:
$version = new \Acme\Fhir\Versions\R4\Version([
'unserializeConfig' => ['profileResolution' => 'auto', 'onProfileViolation' => 'fallback'],
]);
$resource = \Acme\Fhir\Encoding\ResourceParser::parse($version, $json); // USCorePatientProfile or FHIRPatientonProfileViolation acts on _getProfileValidationErrors() only, so a resource that conforms to the profile is not
rejected for problems in the base type or in nested elements the profile does not constrain.
When several declared profiles apply, the most specific one (the one refining all the others) wins. meta.profile
is written back unchanged on serialization. If ProfileAutoloader is not registered, resolution silently falls back to
the base types.
Profiles use the library's regular validation system. Each profile class carries a generated rule map
(_PROFILE_VALIDATION_RULES) that _getCombinedValidationRules() merges into the base type's rules, so
_getValidationErrors() is not overridden and the usual field => [rule => message] map is returned. Rule keys are
field names, or dotted paths for nested elements (name.family, contact.relationship, deceased[x]). Errors are
keyed by the concrete path, e.g. name.0.family or contact.1.relationship, using the standard rule names
(min_occurs, max_occurs, fixed_value, pattern_value, code_binding, choice_occurs, slice_occurs).
_getProfileValidationErrors() returns the same map restricted to the rules set by the profile classes, without
descending into the elements' own validation.
A profile may only tighten the base type: the merged minimum is the larger of the two, the merged maximum the
smaller, and any other rule declared by the profile replaces the base type's. A profile that loosens cardinality is
silently ignored in favour of the base type. min_occurs / max_occurs on a nested path apply per parent instance;
a maximum of 0 means the element must be absent.
It checks:
- cardinality (
min/max, including prohibited elements), per parent instance - narrowed choice types (
value[x]) - fixed and pattern values, on every occurrence of a repeating element. A fixed value must match exactly: a complex
value may not have additional fields or list entries, and lists must be in the same order. A primitive is compared
on its value; its id and extensions are not part of the fixed value. Numbers are compared by value, so
1.50,1.5and15e-1are equal. A pattern is a subset match: every field it names must match, and each entry of a list in the pattern must match some entry of the instance's list. - required bindings, when the ValueSet can be fully enumerated from the configured packages
- slice cardinality. With
value/patterndiscriminators, a slice's members are matched on the fixed / pattern values at the discriminator paths (or on an element above them), and on the extension profile url for aurldiscriminator. Without discriminators, they are matched on all of the slice's fixed / pattern values and its extension profile url.
Rules that cannot be resolved against the generated types are skipped, and counted in the generation log. Fixed,
pattern and binding constraints on a [x] choice element are skipped too.
Profile packages are large, and generation is the most memory hungry part of php-fhir. Every package a profile package depends on is loaded and parsed along with it, and the terminology, extensions and cross-version packages are huge.
As a reference, generating US Core for R4 requires nine packages (US Core, SDC, SMART App Launch, PHINVADS, HL7 Terminology, HL7 Extensions, the R5-to-R4 cross-version package, HL7 UV Tools and the R4 core package) and peaks at roughly 550 MB. Allow PHP at least 1 GB:
php -d memory_limit=1G bin/generate.php --onlyProfiles --versions R4
To keep this down, packages are processed one at a time in dependency order, only StructureDefinition, ValueSet and
CodeSystem files are read (recognised by their conventional name, the package's .index.json, or their content),
each package is loaded once however many packages depend on it, narrative text is dropped, resources are held compressed, and everything no remaining
package depends on is released as soon as its classes are written. Generating several versions in a single run is
sequential, but add headroom if you do.
Generated classes use the classes they reference rather than writing fully qualified names inline.
-
Invariants are metadata only. FHIRPath constraints are exposed in
PROFILE_INVARIANTSbut never evaluated. - Required bindings to ValueSets that cannot be enumerated locally (filters, other packages, incomplete code systems,
paged or truncated expansions without a compose definition) are skipped and logged at
info, as are ValueSets that enumerate to no codes at all. An expansion is only trusted when it starts at offset 0, is not markedtoocostlyorunclosed, and the distinct codes it lists account for itstotal(required when acountparameter was used). Base-specification bindings are mostly enforced by the base types' own enums. - Slices that cannot be matched, other discriminator types (
type,profile,exists), discriminator paths using functions such asresolve()orextension(), constraints inside slices, and closed slicing are not supported. Skipped rules are logged atinfo. - There are no typed
value[x]accessors for extension profiles.