Skip to content

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. ?>