PHPackages                             bambamboole/gaeb - 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. bambamboole/gaeb

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

bambamboole/gaeb
================

A small PHP library to parse, write and validate GAEB DA XML 3.3 files.

v0.1.0(today)01↑2900%[1 PRs](https://github.com/bambamboole/gaeb-php/pulls)MITPHPPHP ^8.4CI passing

Since Jul 31Pushed todayCompare

[ Source](https://github.com/bambamboole/gaeb-php)[ Packagist](https://packagist.org/packages/bambamboole/gaeb)[ RSS](/packages/bambamboole-gaeb/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (4)Versions (10)Used By (0)

gaeb-php
========

[](#gaeb-php)

A small PHP library that parses [GAEB DA XML](https://www.gaeb.de/) 3.3 files (exchange phases X80–X87 plus X89 invoices) into a typed, readonly PHP object graph, and writes schema-valid X84 bids, X87 order confirmations, and X89 invoices back out. Reading is lenient — missing optional elements simply become `null` instead of throwing. Writing is strict — see ["Writing a bid (X84)"](#writing-a-bid-x84) and ["Writing an X89 invoice from a contract"](#writing-an-x89-invoice-from-a-contract) below.

Install
-------

[](#install)

```
composer require bambamboole/gaeb
```

Requires PHP `^8.4` and the `ext-dom` extension (usually bundled) plus `brick/math` for decimal-exact money handling in the write path. No other runtime dependencies. Built on PHP 8.4's native `Dom\XMLDocument` API for lightweight XML processing.

Usage
-----

[](#usage)

```
$gaeb = GaebParser::fromString(file_get_contents('tender.x83'));
// returns a GaebFile; the library itself never touches the filesystem

$gaeb->info;      // GaebInfo
$gaeb->project;   // ProjectInfo
$gaeb->boq;       // ?BoQ

foreach ($gaeb->boq->allItems() as $item) { ... }   // lazy, flattened
```

All money and quantity fields in the object graph are [`Brick\Math\BigDecimal`](https://github.com/brick/math) (`?BigDecimal`, never floats) — values stay decimal-exact end to end, and `json_encode()`emits them as strings (`"45.50"`). Non-numeric content in a numeric element reads as `null` (lenient), like every other unparseable field.

`$gaeb->info` exposes the GAEB version, exchange phase (`80`–`89`), date, and generating program. `$gaeb->project` exposes the project name, label, and currency. `$gaeb->boq` is `null` when the file has no bill of quantities; otherwise it holds the BoQ label/currency/totals plus the top-level category/item tree, and `allItems()` lazily yields every `Item` depth-first with its full position number (`rNo`, e.g. `01.02.0030`) resolved. An item's `descriptionXml` holds the raw XML (the serialized `Description` element, self-contained with its own `xmlns="…"` declaration).

The read model covers the money-relevant LV elements beyond plain items: markup/surcharge positions (`markupItems` on `BoQ`/`BoQCategory`, with type, IDREF references, sub-quantities, and — on the priced side — percent and totals), `remarks` and `performanceDescriptions`, category `totals` and `notApplicable` (`NotApplBoQ`), unit-price components (`Item->upComponents`plus the `BoQ->noUpComponents`/`upComponentLabels` breakdown labels), item-level `vat` and `discountPercent`, per-rate `Totals->vatParts`, `Totals->netUpComponents`, and the `notOffered`/`qtyToBeDetermined` flags (a not-offered position is distinct from a 0.00 unit price).

X84/X86 files also expose the parties and award data:

```
$gaeb->owner;      // ?Party — client (Auftraggeber, OWN)
$gaeb->contractor; // ?Party — contractor/bidder (Auftragnehmer, CTR)
$gaeb->award;      // ?AwardData — populated from AwardInfo on any phase; meaningfully filled (dates, duration) on X86
```

Nachträge (change orders) are exposed on every level that carries them: `$gaeb->award->changeOrders` lists the `AwardInfo/COInfo` entries (`ChangeOrder` — number, phase, `ChangeOrderStatus`, initiator, reason, date), and `BoQ`, `BoQCategory` and `Item` each expose `changeOrderNo`/`changeOrderStatus` from their `CONo`/`COStatus` pair (`null` on main-contract elements). `createInvoice()` refuses to bill a Nachtrag position whose status is not `Approved` and carries the `CONo`/`COStatus` pair into the written X89.

Item classification &amp; totals
--------------------------------

[](#item-classification--totals)

Each `Item` also carries: `provisional` (`Provisional::WithoutTotal|WithTotal`— a Bedarfsposition, `null` when the item isn't one), `hourlyWork` (Stundenlohnarbeiten), `notApplicable` (a dropped/void position), and `alternativeGroupNo`/`alternativeSerialNo`(the `ALNGroupNo`/`ALNSerNo` pair linking a base position to its alternatives). Sum semantics are the caller's responsibility: for an alternative group only the awarded serial number counts, `Provisional::WithoutTotal` and `notApplicable`items are excluded from BoQ totals, and `hourlyWork` items are typically listed separately from the main sum.

`$gaeb->boq->totals` (`?Totals`) holds the full breakdown when present: `total`, `discountPercent`, `discountAmount`, `totalAfterDiscount`, `vat`, `vatAmount`, `totalNet`, `totalGross`.

Bid data
--------

[](#bid-data)

Each `Item` also carries three bid-related fields. `textComplements`(`list`) are the Bieterlücken — gaps embedded in the short/long text that either the tendering office fills in (`TextComplementKind::Owner`) or the bidder must fill in (`TextComplementKind::Bidder`). `bidderComment` is every `BidComm` on the item flattened and joined with `"\n"` (`null` when absent). `subDescriptions` (`list`) are the item's `SubDescr` children, each with its own `subDNo`, `shortText`/`longText`/ `descriptionXml`, `qty`, `unit`, and `unitPrice`.

Find the gaps a bidder still needs to fill:

```
$gaps = array_values(array_filter($item->textComplements, fn ($c) => $c->kind === TextComplementKind::Bidder));
```

Writing a bid (X84)
-------------------

[](#writing-a-bid-x84)

`GaebDocument` opens a received tender (X81/X83) and turns it into a new, schema-valid X84 bid — the source document is never mutated. Collect the bid's prices, filled bidder text gaps, and comments on a `Bid` builder, then transform:

```
use Bambamboole\Gaeb\GaebDocument;
use Bambamboole\Gaeb\Write\Bid;
use Bambamboole\Gaeb\Dto\Party;

$tender = GaebDocument::fromString(file_get_contents('tender.x83'));

$contractor = new Party(
    name: 'ACME Bau GmbH',
    street: 'Musterstraße 1',
    zip: '12345',
    city: 'Berlin',
    email: 'info@acme.example',
    phone: '+49 30 1234567',
);

$bid = new Bid($contractor, currency: 'EUR', date: '2026-07-31');
// currency defaults to the source's currency, date defaults to today —
// pass both explicitly for a byte-deterministic output.
// progSystem: 'my-erp 1.0' overrides the  stamp
// (default 'bambamboole/gaeb'); Invoice takes the same option.

$bid->setUnitPrice('01.02.0010', '12.50')   // decimal strings are exact, floats convenient
    ->fillGap('01.02.0010', 1, 'Musterhersteller GmbH')
    ->setComment('01.02.0010', 'Lieferzeit 4 Wochen')
    ->setNotOffered('01.02.0020');          // spec 4.6.4: distinct from UP 0.00
// ...one setUnitPrice() or setNotOffered() per priceable item (every item
// that isn't marked notApplicable) — createBid() throws GaebWriteException
// naming any that's missing a price, any rNo it doesn't recognize, or any
// position that is both priced and marked not offered.

$award = $tender->createBid($bid);

$errors = $award->validate();   // [] means schema-valid
if ($errors !== []) {
    throw new RuntimeException(implode("\n", $errors));
}

file_put_contents('bid.x84', (string) $award);
```

The computed `BoQ` total sums each emitted item's `IT`, excluding `Provisional::WithoutTotal` items and non-base alternatives (`alternativeGroupNo` set with `alternativeSerialNo !== 1`); `notApplicable`items are never emitted at all. Reading tolerates schema deviations and wild-file spellings; writing refuses to guess — anything that would corrupt or invalidate the bid (a missing price, an unknown `rNo`) throws `GaebWriteException` instead of silently producing a bad file.

Writing an X89 invoice from a contract
--------------------------------------

[](#writing-an-x89-invoice-from-a-contract)

```
use Bambamboole\Gaeb\Dto\InvoiceType;
use Bambamboole\Gaeb\Dto\Payment;
use Bambamboole\Gaeb\GaebDocument;
use Bambamboole\Gaeb\Write\Invoice;

$contract = GaebDocument::fromString(file_get_contents('contract.x86'));

$invoice = new Invoice(
    invoiceNo: 'RE-2026-001',
    invoiceDate: '2026-10-31',
    type: InvoiceType::Deduction,           // Abschlagsrechnung
    servicePeriodStart: '2026-09-01',
    servicePeriodEnd: '2026-10-31',
    creatorTaxNo: 'DE123456789',
    vatPercent: '19',                       // falls back to the contract's Totals/VAT
);
$invoice->billQty('01.0010', '30')          // CUMULATIVE billed quantities
    ->addPayment(new Payment(               // prior payments, emitted as PaymentMade
        total: '1190.00', totalVat: '190.00',
        discountAmount: null,
        paymentDate: '2026-10-05', invoiceNo: 'RE-2026-000',
    ));

$x89 = $contract->createInvoice($invoice);  // new GaebDocument, source untouched
$x89->validate();                           // [] — schema-valid against the official X89 XSD
```

Reading X89 files works through the same parser: `$gaeb->invoice` carries the header, parties (with tax number), payments and gross total; billed items appear in the BoQ with `Item->billedQty`.

E-invoice attachment (X86 → X89B)
---------------------------------

[](#e-invoice-attachment-x86--x89b)

Under Germany's e-invoicing mandate the commercial invoice travels as XRechnung/ZUGFeRD — the GAEB X89B ("Rechnungsbegründende Unterlage") is the audit attachment carrying the billed LV alongside it. It applies the same strict billing rules as `createInvoice()` (cumulative quantities, Nachtrag approval, exact money), but by design carries **no** commercial data: no recipient, shares, payments, or invoice-level `TotalGross` — those live in the e-invoice. Its header holds only `RefInvoiceNo` (the e-invoice's number, taken from `Invoice::$invoiceNo`) and the service period, so the same `Invoice` builder produces both documents:

```
$contract = GaebDocument::fromString(file_get_contents('contract.x86'));
$x89  = $contract->createInvoice($invoice);            // the full GAEB invoice
$x89b = $contract->createSupportingDocument($invoice); // the e-invoice attachment
$x89b->validate();  // [] — against the bundled X89B XSD
```

Payments and invoice type/date on the `Invoice` are used by the X89 twin and simply have no representation in the X89B.

Confirming an order (X86 → X87)
-------------------------------

[](#confirming-an-order-x86--x87)

The contractor's order confirmation is a re-stamp of the received contract — same content under the DA87 namespace with `DP` 87 and a fresh `GAEBInfo`:

```
$contract = GaebDocument::fromString(file_get_contents('contract.x86'));
$x87 = $contract->createOrderConfirmation(date: '2026-06-20');  // source untouched
$x87->validate();  // []
```

Together with `createBid()` and `createInvoice()` this closes the contractor workflow: bid (X84) → confirm (X87) → invoice (X89).

`GaebDocument::validate(?string $xsdDir = null): array` schema-checks the document against the XSDs bundled in the package, resolved per DP token and phase family under `docs/gaeb/3.3/` — `2021-05_Leistungsverzeichnis/` for X80–X87, `2021-05_Rechnung/` for X89/X89B; pass `$xsdDir` to validate against a different XSD set instead. An empty array means valid; otherwise it's the flattened list of libxml error strings.

Custom drivers / instance API
-----------------------------

[](#custom-drivers--instance-api)

`GaebParser::fromString()` is a shortcut for `new GaebParser`. Use the instance API directly to inject your own driver(s):

```
$parser = new GaebParser([new MyDriver, new GaebXmlDriver]);
$gaeb = $parser->parse($content);
```

The library performs no file I/O on documents: it takes XML strings (or an existing `Dom\XMLDocument` via `GaebDocument::fromDocument()`) and returns objects/strings — reading from and writing to disk is the caller's concern. The only files it touches are its own bundled XSDs in `validate()`.

Drivers are tried in order; the first one whose `supports(string $content): bool`returns `true` handles the parse. Implement `Bambamboole\Gaeb\Driver\Driver`to add support for another format without touching this library.

Out of scope
------------

[](#out-of-scope)

- Writing formats other than the X84 bid, X87 confirmation, X89 invoice, and X89B attachment transforms above: from-scratch X81/X83 authoring, X86 award/rejection writing
- Multiple `InvoiceShare`s (only a single `basic amount` share is emitted), `COInfo`/Nachträge, cash-discount `Terms` when writing invoices
- Per-item `VAT`, `UPComp1–6` price components, sub-description prices, `TimeQu`, `Product`, and `MarkupItem` pricing when writing bids
- Legacy formats (GAEB 90, GAEB 2000)

These may be added later without breaking the public API.

Testing
-------

[](#testing)

All fixtures under `tests/fixtures/` are synthetic, hand-crafted XML files written for this project to exercise specific parsing paths — see `tests/fixtures/README.md`.

License
-------

[](#license)

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

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

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/547137a6d80cad01ed1dd065b1c6af329d9a23a4134a895cff01e078cc155500?d=identicon)[bambamboole](/maintainers/bambamboole)

---

Top Contributors

[![bambamboole](https://avatars.githubusercontent.com/u/8823695?v=4)](https://github.com/bambamboole "bambamboole (87 commits)")

---

Tags

xmlparserwriterAVAgaebbambambooleboq

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/bambamboole-gaeb/health.svg)

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

###  Alternatives

[masterminds/html5

An HTML5 parser and serializer.

1.8k280.1M346](/packages/masterminds-html5)[imangazaliev/didom

Simple and fast HTML parser

2.2k2.5M73](/packages/imangazaliev-didom)[orchestra/parser

XML Document Parser for Laravel and PHP

4671.8M7](/packages/orchestra-parser)[veewee/xml

XML without worries

1837.3M39](/packages/veewee-xml)[laravie/parser

XML Document Parser for PHP

2292.2M8](/packages/laravie-parser)[goetas-webservices/xsd-reader

Read any XML Schema (XSD) programmatically with PHP

625.3M23](/packages/goetas-webservices-xsd-reader)

PHPackages © 2026

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