Model and builders
The model layer is how a host turns its own data into ONE Record logistics
objects without writing JSON-LD by hand, and how it reads what partners send.
It depends on nothing but the vocabulary and
the RDF layer.
Logistics objects
Model\LogisticsObject is immutable: a URI and the graph of everything said
about it, including its embedded objects (cargo:Value, Party, Address,
… as blank nodes, or under internal: ids once a server has stored them).
Revision numbers and timestamps are not part of the object; a server keeps
them on the stored revision and sends them as headers.
$piece = LogisticsObject::fromJsonLd($json); // a GET response
$piece->types(); // ['https://onerecord.iata.org/ns/cargo#Piece']
$piece->mostSpecificType(); // for the Type header
$piece->literal(Cargo::goodsDescription); // 'Books'
$piece->values(Cargo::specialHandlingCodes); // list of Rdf\Term
$piece->toJson(); // compacted JSON-LD
$piece->isSameAs($other); // graph isomorphism
Building objects
ObjectBuilder checks every property against the ontology as you set it: the
class must be a logistics object class, the property must be accepted by the
class (own, inherited, or declared for any class), and the value must match
the property's kind. Give it a version-limited vocabulary to refuse terms a
partner on data model 3.2 would not understand.
add('waybill', ObjectBuilder::of(Cargo::Waybill)
->set(Cargo::waybillPrefix, '020')
->set(Cargo::waybillNumber, '12345675')
->set(Cargo::waybillType, Values::individual(Cargo::MASTER))
->set(Cargo::shipment, Values::ref('shipment')))
->add('shipment', ObjectBuilder::of(Cargo::Shipment)
->set(Cargo::goodsDescription, 'Machine parts')
->set(Cargo::totalGrossWeight, Values::quantity(190.5, MeasurementUnitCode::KGM))
->set(Cargo::waybill, Values::ref('waybill'))
->add(Cargo::pieces, Values::ref('piece-1'))
->add(Cargo::involvedParties, Embedded::of(Cargo::Party)
->set(Cargo::partyRole, Values::code('ParticipantIdentifier', 'SHP'))
->set(Cargo::partyDetails, Values::ref('shipper'))))
->add('piece-1', ObjectBuilder::of(Cargo::Piece)
->set(Cargo::grossWeight, Values::quantity(190.5, MeasurementUnitCode::KGM))
->set(Cargo::coload, false))
->add('shipper', ObjectBuilder::of(Cargo::Company)
->set(Cargo::name, 'ACME Machines'));
// Mint URIs under the host's base URL; the same seed always yields the same URIs.
$resolved = $graph->resolve(new UuidIriMinter('https://1r.example.com', seed: 'shipment-AER-1'));
echo $resolved->root()->toJson();
Value helpers cover the shapes every mapper needs:
| Helper | Produces |
| --- | --- |
| `Values::quantity(190.5, MeasurementUnitCode::KGM)` | `cargo:Value` with `numericalValue` and `unit` |
| `Values::money(5000, 'EUR')` | `cargo:CurrencyValue` with `currencyUnit` as `…/code-lists/CurrencyCode#EUR` (spec question 11) |
| `Values::code('ParticipantIdentifier', 'SHP')` | the code-list member IRI, checked against closed lists |
| `Values::codeListElement('ATH', 'IATA three-letter location code')` | `cargo:CodeListElement` for lists ONE Record does not publish |
| `Values::individual(Cargo::MASTER)` | a named individual, checked |
| `Values::dateTime($dt)`, `Values::date($dt)` | `xsd:dateTime` in UTC, `xsd:date` |
| `Values::ref('shipment')` | a reference to another object of the same graph |
| `Values::iri($uri)` | a reference to an object published elsewhere |
Plain PHP values are typed automatically: strings become `xsd:string`,
booleans `xsd:boolean`, integers `xsd:integer`, floats `xsd:double`,
`DateTimeInterface` becomes `xsd:dateTime`.
`Embedded::of(Cargo::Party)->set(...)` builds an embedded object; builders
refuse to embed a logistics object class (publish it on its own and reference
it) and refuse to build an embedded class as a logistics object.
`ObjectBuilder::unchecked()` skips the ontology for host extensions.
## Graphs of linked objects
A `LocalGraph` holds several objects that refer to each other by local key
(`local:` until resolution). `resolve()` mints a URI for each object with
an `IriMinter` and rewrites every reference. `UuidIriMinter` with a seed gives
the same graph the same URIs every time, so republishing is idempotent; keys
already published can be passed in so they keep their URIs.
$resolved = $graph->resolve(new UuidIriMinter('https://1r.example.com', seed: 'shipment-AER-1'));
$resolved->root(); // the waybill
$resolved->iris(); // ['waybill' => Iri, 'shipment' => Iri, ...]
Dangling references are refused before any URI is minted.
## Embedded object ids
Servers must give embedded objects ids that never change, because change
requests address them. `EmbeddedIdMinter` (default `Uuid5EmbeddedIdMinter`,
the spec's recommended `internal:` scheme) mints them when an object
is first stored, for every embedded node: a blank node, and equally a node the
client identified itself (an `https:` or `urn:` id, NE:ONE's `neone:` scheme),
since an id of its own makes a node no less embedded. The minted ids are kept
in the stored graph from then on; a client learns them from its next read.
A **typed link**, a reference that states only the class of what it points
to (`{"@id": "…/piece-2", "@type": "cargo:Piece"}`), is not an embedded node:
it keeps its id, its class must be one the ontology knows (a data model class
or a code list), and it survives changes to the rest of the object for as long
as something links to it. This holds whatever the class: a type-only node with
an id of its own is always read as a reference, so to embed an object under
your own id, give it at least one property, or make it a blank node. A change
cannot turn a reference into an embedded node; it adds a new embedded node
(`_:b0`) and deletes the link instead.
## Building events
Logistics events are not logistics objects, so `ObjectBuilder::of()` refuses
`cargo:LogisticsEvent`. Use `ObjectBuilder::ofEvent()` (or `ofEvent(Cargo::StatusUpdateEvent)`
on data model 3.3), which validates against the event class's properties,
requires `cargo:eventDate` and finishes with `buildEvent($iri, $object, $created)`;
`cargo:eventFor` is set to the object unless given. Stores rehydrate events
with `LogisticsEvent::fromStored()`, which validates nothing.
?>