PHPackages                             roundingwell/hl7 - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. roundingwell/hl7

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

roundingwell/hl7
================

An ADT/HL7 parser providing structured messages

0.10.0(2w ago)1338MITPHPPHP ^8.4CI passing

Since Jul 19Pushed 1mo agoCompare

[ Source](https://github.com/RoundingWell/hl7)[ Packagist](https://packagist.org/packages/roundingwell/hl7)[ RSS](/packages/roundingwell-hl7/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (8)Versions (12)Used By (0)

HL7
===

[](#hl7)

An ADT/HL7 parser for PHP that turns raw HL7 messages into strongly-typed, structured objects.

Instead of hand-splitting pipe-delimited strings, you parse a message once and read its segments, fields, and data-type components through named accessors.

#### AI Note

[](#ai-note)

This project contains code written by both humans and agentic tools. The core functionality was developed entirely by humans. Agentic tools have been used to expand the functionality and test coverage of the project. All code is reviewed by a human before being merged.

Requirements
------------

[](#requirements)

- PHP 8.4 or newer

Installation
------------

[](#installation)

```
composer require roundingwell/hl7
```

Quick start
-----------

[](#quick-start)

```
use RoundingWell\HL7\MessageFactory;

$factory = new MessageFactory();

// Parse from a string...
$message = $factory->parse($rawHl7);

// ...or straight from a file.
$message = $factory->parseFile('/path/to/message.hl7');
```

The factory reads the delimiter and encoding characters from the `MSH` segment, detects the line ending (`\r`, `\n`, or `\r\n`), and returns a `Message`. When the message type maps to a known trigger event (for example `A01`), a message subclass is returned; otherwise a generic `Message` is used.

### Reading segments and fields

[](#reading-segments-and-fields)

```
$msh = $message->getMSH();
echo $msh->getMessageControlId()->getValue(); // "599102"
echo $msh->getMessageType()->getTriggerEvent()->getValue(); // "A01"

// Assuming the message is an A01
$pid = $message->getPID();

echo $pid->getDateOfBirth()->getValue();

// Repeating fields return a list of data-type instances.
foreach ($pid->getPatientName() as $name) {
    echo $name->getGivenName()->getValue();               // "DONALD"
    echo $name->getFamilyName()->getSurname()->getValue(); // "DUCK"
}

// Multiple occurrences of the same segment (e.g. DG1) are available too.
$diagnoses = $message->listDG1();
```

Some messages nest repeating groups of segments (e.g. the `PROCEDURE` group in an `A01`, which bundles a `PR1` with its `ROL` segments). Groups expose the same lookup helpers as a message:

```
foreach ($message->getAll('PROCEDURE') as $procedure) {
    $pr1 = $procedure->get('PR1');
    $roles = $procedure->getAll('ROL');
}
```

Composite data types expose their components through named accessors, and those components may themselves be composites (sub-components), so you can drill down as far as the data type defines:

```
foreach ($pid->getIdentifierList() as $cx) {
    echo $cx->getId()->getValue();                              // "10006579"
    echo $cx->getAssigningAuthority()->getNamespaceId()->getValue(); // "1"
    echo $cx->getIdentifierTypeCode()->getValue();              // "MRN"
}
```

Every composite also exposes its components positionally via `getComponent(int $index)`(0-based) and `getComponents()`, which the named accessors are built on.

### Generating acknowledgments

[](#generating-acknowledgments)

Any parsed message can produce an `ACK` response. Supply an acknowledgment code, a [PSR-20](https://www.php-fig.org/psr/psr-20/) clock (for `MSH-7`), and an `IdGenerator`(for the acknowledgment's own `MSH-10`):

```
use RoundingWell\HL7\AcknowledgmentCode;
use RoundingWell\HL7\SymfonyUidGenerator;
use Symfony\Component\Clock\NativeClock;

$ack = $message->generateACK(
    AcknowledgmentCode::AA,
    new NativeClock(),
    new SymfonyUidGenerator(),
);
```

`generateACK()` swaps the sender/receiver, echoes the request's control ID into `MSA-2`, and writes the acknowledgment code to `MSA-1`. The returned `ACK` is a `Message` object, which can be serialized back to HL7 (see below).

> `SymfonyUidGenerator` and `NativeClock` require the optional `symfony/uid` and `symfony/clock` packages. Any PSR-20 clock and any `IdGenerator` implementation work.

### Serializing back to HL7

[](#serializing-back-to-hl7)

Any `Message` can be turned back into a wire string with `serialize()`:

```
$wire = $message->serialize($encoding);
```

`parse()` followed by `serialize()` reproduces the original message, with two deviations from a byte-for-byte round-trip: trailing empty fields, components, and subcomponents are trimmed (HL7 treats trailing delimiters as optional), and segments are joined by the line ending rather than terminated by it (no trailing line ending is appended).

### Debugging message structure

[](#debugging-message-structure)

When you need to see where a value sits in a parsed message, `debug()` returns an indented dump of its populated structure. Each element is labelled with its access path and schema name, descending through composites to their subcomponents:

```
echo $message->debug();
// ADT_A01
//   MSH
//     MSH.1 (Field Separator): |
//     MSH.2 (Encoding Characters): ^~\&
//     MSH.9 (Message Type)
//       MSH.9.1 (Message Type): ADT
//       MSH.9.2 (Trigger Event): A01
//   PID
//     PID.1 (Set ID): 1
//     PID.5 (Patient Name)
//       PID.5.1 (Family Name)
//         PID.5.1.1 (Surname): SMITH
//       PID.5.2 (Given Name): JOHN
```

Empty fields are omitted, and a repeating field is indexed (`PID.3[0]`, `PID.3[1]`) only when it has more than one repetition. Untyped content is shown too: values held by an untyped segment or field (see [Untyped fields](#untyped-fields)) live in extra components, which the dump renders with no schema name. A lone untyped value collapses onto its field line, while multiple parts expand:

```
echo $message->debug();
// ADT_A01
//   MSH
//     MSH.1 (Field Separator): |
//     MSH.2 (Encoding Characters): ^~\&
//   ZPD
//     ZPD.1: foo
//     ZPD.2
//       ZPD.2.1: bar
//       ZPD.2.2: baz
```

Concepts
--------

[](#concepts)

TypeResponsibility`MessageFactory`Parses raw HL7 into a `Message`, resolving encoding and message type.`Message`Interface for a whole message: a `Group` plus `getMSH()`, `getVersion()`, `parse()`, `serialize()`, and `debug()`.`Group`Interface for a named collection of `Structure`s (segments and nested groups) with lookup helpers (`get`, `getAll`, `getRepetition`, `getStructures`, `getNames`, `isRequired`, `isRepeating`).`Message\ADT\Axx`Specific ADT message subclasses (e.g. `A01`) add named accessors for message-specific segments.`Segment`Interface for a collection of numbered fields (each a `Type`), read with `getField()` / `getFieldRepetition()`. Typed subclasses (e.g. `PID`, `MSH`) add named accessors.`Type`An HL7 data type — a `Primitive` scalar (`ST`, `NM`, `DTM`, …), a `Composite` of other types, or a `Varies` placeholder for undefined fields.`Encoding`The field, component, repetition, and sub-component separators, plus the escape and truncation characters and line ending.`AcknowledgmentCode`Enum of the HL7 table 0008 acknowledgment codes (`AA`, `AE`, `AR`) accepted by `generateACK()`.`IdGenerator`Interface for generating unique message control IDs (`MSH-10`), e.g. for a generated `ACK`.`SymfonyUidGenerator``IdGenerator` implementation backed by `symfony/uid`, producing time-ordered UUIDv7 identifiers.### Untyped fields

[](#untyped-fields)

Fields that are not defined for a segment are still parsed so no data is lost. Whole segments that have no typed subclass (for example `GT1` in an ADT message) are exposed as `GenericSegment`s, and their fields are `GenericComposite` instances — a schema-less composite that preserves any component (`^`) structure instead of flattening it.

A `GenericComposite` has no defined components, so every parsed component lands in its extra components (`getExtraComponents()`), each a `Varies` wrapping a `GenericPrimitive`. Read a scalar field through its single component:

```
$gt1 = $message->get('GT1');

$field = $gt1->getFieldRepetition(2, 0);                        // a GenericComposite
echo $field->getExtraComponents()->getComponent(0)->getData()->getValue(); // "8291"
```

Component structure is retained: an undefined field `a^b^c` keeps three components (one extra component per `^`), and a component carrying subcomponents (`a&b`) keeps `b` as a subcomponent of that component rather than promoting it to its own component.

### Unmatched segments

[](#unmatched-segments)

Typed messages never silently drop a segment. Any segment the schema cannot place, wherever it appears in the message, is recovered rather than dropped, so parse → serialize round trips do not lose data. A segment name the schema declares is parsed into its declared, typed slot even when it reappears after its slot has already been consumed, so it stays readable through `get()`/ `getAll()` there like any other occurrence — `getAll()` may then return more than one match even for a non-repeating definition. Anything the schema does not declare at all — a site-defined Z-segment, or any other unrecognized name — is parsed as a `GenericSegment`:

```
$zds = $message->get('ZDS'); // a GenericSegment, readable like any untyped segment
```

Supported types
---------------

[](#supported-types)

**Messages:** `A01`, `A03`, `A04`, `A06`, `A07`, `A08`, `A13`, `ACK`

**Segments:** `DG1`, `DRG`, `EVN`, `MSA`, `MSH`, `NK1`, `OBX`, `PID`, `PV1`, `PV2`

**Data types:** `CE`, `CNE`, `CP`, `CWE`, `CX`, `DLD`, `DR`, `DT`, `DTM`, `EI`, `FC`, `FNx`, `Generic`, `HD`, `ID`, `IS`, `JCC`, `MO`, `MSG`, `NM`, `PL`, `PT`, `SAD`, `SI`, `SNM`, `ST`, `TS`, `TX`, `VID`, `Varies`, `XAD`, `XCN`, `XON`, `XPN`, `XTN`

Error handling
--------------

[](#error-handling)

Parsing failures throw exceptions extending `RoundingWell\HL7\Exception\HL7Exception`:

- `InvalidFile` — the file does not exist or cannot be read.
- `InvalidMessage` — the message is missing its `MSH` segment, delimiter, or encoding characters.
- `InvalidValue`, `InvalidDateTime` — a field value fails validation.

Looking up structures on a parsed message throws standard SPL exceptions:

- `InvalidArgumentException` — requesting a segment, field, or group structure that was never registered.
- `OutOfBoundsException` — requesting a repetition of a non-repeating structure, or a negative repetition.

Development
-----------

[](#development)

```
composer lint      # check code style
composer format    # fix code style
composer analyze   # static analysis
composer test      # run tests and enforce 100% coverage
composer verify    # lint + analyze + test
```

License
-------

[](#license)

Released under the [MIT License](LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance95

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

10

Last Release

16d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/38203?v=4)[Woody Gilk](/maintainers/shadowhand)[@shadowhand](https://github.com/shadowhand)

---

Top Contributors

[![shadowhand](https://avatars.githubusercontent.com/u/38203?v=4)](https://github.com/shadowhand "shadowhand (106 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/roundingwell-hl7/health.svg)

```
[![Health](https://phpackages.com/badges/roundingwell-hl7/health.svg)](https://phpackages.com/packages/roundingwell-hl7)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.4M2.2k](/packages/symfony-symfony)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k21](/packages/tempest-framework)[nesbot/carbon

An API extension for DateTime that supports 281 different languages.

194716.3M6.0k](/packages/nesbot-carbon)[spomky-labs/otphp

A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator

1.5k51.8M192](/packages/spomky-labs-otphp)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

568591.1k63](/packages/ecotone-ecotone)[simplesamlphp/saml2

SAML2 PHP library from SimpleSAMLphp

30418.3M44](/packages/simplesamlphp-saml2)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
