PHPackages                             husail/edi-sdk - 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. husail/edi-sdk

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

husail/edi-sdk
==============

Generic SDK for reading, writing and validating fixed-width EDI files.

v1.0.0(2w ago)241MITPHPPHP ^8.2

Since May 23Pushed 2w agoCompare

[ Source](https://github.com/husail/edi-sdk)[ Packagist](https://packagist.org/packages/husail/edi-sdk)[ Docs](https://github.com/husail/edi-sdk)[ RSS](/packages/husail-edi-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (4)Versions (2)Used By (1)

husail/edi-sdk
==============

[](#husailedi-sdk)

Generic PHP SDK for reading, writing and validating fixed-width EDI files.

[![PHP](https://camo.githubusercontent.com/2795c86d2dea6aba6285347c2adef64310db832ec1d2d634a32e3b51115a5c95/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d373737424234)](https://camo.githubusercontent.com/2795c86d2dea6aba6285347c2adef64310db832ec1d2d634a32e3b51115a5c95/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d373737424234)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)[![Status](https://camo.githubusercontent.com/b411594136144442e4c5e6e2e2d63ee7461762fbe5e2eb0e5abbbec3fd812845/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d6163746976652d73756363657373)](https://camo.githubusercontent.com/b411594136144442e4c5e6e2e2d63ee7461762fbe5e2eb0e5abbbec3fd812845/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d6163746976652d73756363657373)

---

📋 Requirements
--------------

[](#-requirements)

- PHP 8.2+
- `symfony/yaml` (for YAML layout driver)

---

📦 Installation
--------------

[](#-installation)

```
composer require husail/edi-sdk
```

---

🧠 Core concepts
---------------

[](#-core-concepts)

Define the file layout once and reuse it for:

- writing files
- parsing files
- validating files

The SDK handles:

- field positions and lengths
- padding and normalization
- numeric formatting
- line endings
- typed value casting
- structural validation

Layouts can be defined using:

- PHP
- YAML
- JSON

It also supports complex file structures such as:

- headers and trailers
- repeatable groups
- interleaved record types
- CNAB-style batch structures

---

📐 Defining a layout
-------------------

[](#-defining-a-layout)

### PHP Builder

[](#php-builder)

```
use Husail\EdiSdk\Schema\FieldType;
use Husail\EdiSdk\Schema\FileLayout;
use Husail\EdiSdk\Schema\RecordLayout;
use Husail\EdiSdk\Schema\Sequence\Record;
use Husail\EdiSdk\Schema\Sequence\Group;

$header = RecordLayout::define('header')
    ->lineLength(50)
    ->addField(name: 'type',    pos: 1,  len: 1,  type: FieldType::ALPHA,   const: 'H')
    ->addField(name: 'company', pos: 2,  len: 20, type: FieldType::ALPHA)
    ->addField(name: 'date',    pos: 22, len: 8,  type: FieldType::NUMERIC)
    ->addField(name: 'filler',  pos: 30, len: 21, type: FieldType::ALPHA,   required: false)
    ->build();

$detail = RecordLayout::define('detail')
    ->lineLength(50)
    ->addField(name: 'type',     pos: 1,  len: 1,  type: FieldType::ALPHA,   const: 'D')
    ->addField(name: 'invoice',  pos: 2,  len: 10, type: FieldType::NUMERIC)
    ->addField(name: 'customer', pos: 12, len: 20, type: FieldType::ALPHA)
    ->addField(name: 'amount',   pos: 32, len: 15, type: FieldType::NUMERIC, cast: 'float', decimalPlaces: 2)
    ->build();

$trailer = RecordLayout::define('trailer')
    ->lineLength(50)
    ->addField(name: 'type',          pos: 1, len: 1,  type: FieldType::ALPHA,   const: 'T')
    ->addField(name: 'total_records', pos: 2, len: 6,  type: FieldType::NUMERIC, cast: 'int')
    ->addField(name: 'total_amount',  pos: 8, len: 15, type: FieldType::NUMERIC, cast: 'float', decimalPlaces: 2)
    ->build();

$layout = FileLayout::define('my-edi')
    ->lineLength(50)
    ->lineEnding("\r\n")
    ->addRecord($header)
    ->addRecord($detail)
    ->addRecord($trailer)
    ->withSequence([
        Record::one($header),
        Record::many($detail),
        Record::one($trailer),
    ])
    ->build();
```

### YAML

[](#yaml)

```
name: my-edi
line_length: 50
line_ending: '\r\n'

records:
  - name: header
    fields:
      - { name: type,    pos: 1,  len: 1,  type: alpha,   const: H }
      - { name: company, pos: 2,  len: 20, type: alpha }
      - { name: date,    pos: 22, len: 8,  type: numeric }
      - { name: filler,  pos: 30, len: 21, type: alpha,   required: false }

  - name: detail
    fields:
      - { name: type,     pos: 1,  len: 1,  type: alpha,   const: D }
      - { name: invoice,  pos: 2,  len: 10, type: numeric }
      - { name: customer, pos: 12, len: 20, type: alpha }
      - { name: amount,   pos: 32, len: 15, type: numeric, decimal_places: 2, cast: float }

  - name: trailer
    fields:
      - { name: type,          pos: 1, len: 1,  type: alpha,   const: T }
      - { name: total_records, pos: 2, len: 6,  type: numeric, cast: int }
      - { name: total_amount,  pos: 8, len: 15, type: numeric, decimal_places: 2, cast: float }

sequence:
  - { type: record, record: header }
  - { type: many,   record: detail }
  - { type: record, record: trailer }
```

```
use Husail\EdiSdk\Drivers\YamlDriver;

$layout = (new YamlDriver())->load('/path/to/my-edi.yaml');
```

> **`line_ending`** must use single-quoted escape sequences (`'\r\n'`, `'\n'`). Double quotes cause YAML to interpret the escape before the SDK processes it.

### JSON

[](#json)

Same structure as YAML.

```
use Husail\EdiSdk\Drivers\JsonDriver;

$layout = (new JsonDriver())->load('/path/to/my-edi.json');
```

---

📝 Writing a file
----------------

[](#-writing-a-file)

```
use Husail\EdiSdk\Edi;

// Write to string
$content = Edi::write($layout)
    ->add('header', [
        'company' => 'ACME LTDA',
        'date'    => '06052026',
    ])
    ->add('detail', [
        'invoice'  => 1001,
        'customer' => 'JOAO SILVA',
        'amount'   => 150.75,
    ])
    ->add('detail', [
        'invoice'  => 1002,
        'customer' => 'MARIA SOUZA',
        'amount'   => 89.90,
    ])
    ->add('trailer', [
        'total_records' => 4,
        'total_amount'  => 240.65,
    ])
    ->toString();

// Save to file
Edi::write($layout)->add(...)->toFile('/path/to/file.txt');
```

---

📂 Reading a file
----------------

[](#-reading-a-file)

```
use Husail\EdiSdk\Edi;

$result = Edi::parse(file_get_contents('/path/to/file.txt'), $layout);

// Access a single record
$header = $result->first('header');
echo $header?->get('company'); // 'ACME LTDA          '
echo $header?->get('nonexistent', default: 'fallback'); // 'fallback'

// Access a collection of records
$details = $result->records('detail');
$details->count();
$details->first()?->get('amount');  // 150.75 (float, when cast: float is set)
$details->last()?->get('customer');
$details->nth(1)?->get('invoice');

// Filter
$highValue = $details->filter(fn ($r) => $r->get('amount') > 100);
$highValue->count(); // 1

// Iterate
$details->each(fn ($r) => process($r));

// Retrocompatibility — returns array of arrays
$details->toArray();
$result->toArray();
```

> Without `cast` defined on the field, the parser returns raw strings. Add `cast: int`, `cast: float` or `cast: date` to the field definition for automatic conversion.

---

✅ Validating a file
-------------------

[](#-validating-a-file)

```
use Husail\EdiSdk\Edi;

$result = Edi::validate(file_get_contents('/path/to/file.txt'), $layout);

if ($result->passes()) {
    // file is valid
}

foreach ($result->errors() as $error) {
    echo "Line {$error->line} [{$error->record}] {$error->field}: {$error->message}";
}

$result->errorsForLine(3);
$result->errorsForRecord('detail');
$result->errorCount();
```

---

🌳 Sequence nodes
----------------

[](#-sequence-nodes)

The sequence tree describes how records are ordered and grouped in the file.

NodeFactoryDescription`RecordNode``Record::one($layout)`Exactly one required record`RecordNode``Record::optional($layout)`One optional record`ManyNode``Record::many($layout)`Zero or more records of the same type`GroupNode``Group::repeat($identifyBy, $children)`Repeatable group of records (e.g. batches)`AmbiguousNode``Group::ambiguous($identifyBy, $children)`Interleaved record types at the same positionThe `identifyBy` closure receives the raw line and returns the record name it belongs to, or `null` to close the group.

### Example: repeatable batches

[](#example-repeatable-batches)

```
use Husail\EdiSdk\Schema\Sequence\Record;
use Husail\EdiSdk\Schema\Sequence\Group;

$layout = FileLayout::define('cnab-like')
    ->lineLength(240)
    ->addRecord($fileHeader)
    ->addRecord($batchHeader)
    ->addRecord($segmentA)
    ->addRecord($segmentB)
    ->addRecord($batchTrailer)
    ->addRecord($fileTrailer)
    ->withSequence([
        Record::one($fileHeader),

        Group::repeat(
            identifyBy: fn (string $line): ?string => match ($line[7]) {
                '1' => 'batch_header',
                '3' => 'detail',
                '5' => 'batch_trailer',
                default => null,
            },
            children: [
                Record::one($batchHeader),

                Group::ambiguous(
                    identifyBy: fn (string $line): ?string => match ($line[13]) {
                        'A' => 'segment_a',
                        'B' => 'segment_b',
                        default => null,
                    },
                    children: [
                        Record::many($segmentA),
                        Record::optional($segmentB),
                    ]
                ),

                Record::one($batchTrailer),
            ]
        ),

        Record::one($fileTrailer),
    ])
    ->build();
```

### Composite identify\_by in YAML

[](#composite-identify_by-in-yaml)

Some formats use the same character at a given position for multiple record types. The YAML driver supports composite `identify_by` rules with multiple match conditions. More specific rules must come first — the first matching rule wins.

```
- type: ambiguous
  identify_by:
    # segment_b_pix shares 'B' at pos 14 with segment_b
    - record: segment_b_pix
      match:
        - { pos: 14, len: 1, value: "B" }
        - { pos: 15, len: 2, in: ["01", "02", "03", "04"] }

    - record: segment_b
      match:
        - { pos: 14, len: 1, value: "B" }

    # segment_j52 shares 'J' at pos 14 with segment_j
    - record: segment_j52
      match:
        - { pos: 14, len: 1, value: "J" }
        - { pos: 18, len: 2, value: "52" }

    - record: segment_j
      match:
        - { pos: 14, len: 1, value: "J" }
```

Each match supports `value` (exact equality) and `in` (list of accepted values). `children` is optional when `identify_by` is present — the driver automatically infers a `ManyNode`for each record declared in the rules, preserving order. If neither `children` nor `identify_by`is present, a `LayoutException` is thrown.

---

📑 Field definition reference
----------------------------

[](#-field-definition-reference)

PropertyTypeDescription`name``string`Field key in parser output`pos``int`Start position, 1-based`len``int`Length in characters`type``alpha|numeric`Determines default padding`const``?string`Fixed value — writer ignores input, validator enforces`default``?string`Fallback when value is null or empty`required``bool`Validator emits error when ALPHA field is empty (default: `true`)`cast``?string`Parser cast: `int`, `float`, `date``decimal_places``int`Implicit decimal places for numeric values (requires `cast: float`)`format``?string`Date format, required when `cast: date` (e.g. `dmY`)`padding_char``?string`Overrides default padding char for the type`padding_side``left|right`Overrides default padding side for the type### Default padding

[](#default-padding)

TypeCharSide`alpha`spaceright`numeric``0`left> `required` only applies to ALPHA fields. For NUMERIC, zeros are valid values and cannot be distinguished from unfilled fields in a fixed-width format.

---

✔️ Custom validators
--------------------

[](#️-custom-validators)

```
$record = RecordLayout::define('detail')
    ->lineLength(50)
    ->addField(...)
    ->addValidator(function (array $data): ?string {
        if ($data['amount'] === '000000000000000') {
            return 'amount cannot be zero';
        }

        return null;
    })
    ->build();
```

---

⚙️ Custom layout driver
-----------------------

[](#️-custom-layout-driver)

Implement `LayoutDriverInterface` to load layouts from any source. Your driver is responsible only for parsing the format — the `ArrayLayoutMapper`handles building the `FileLayout` from the normalized array.

```
use Husail\EdiSdk\Contracts\LayoutDriverInterface;
use Husail\EdiSdk\Schema\FileLayout;
use Husail\EdiSdk\Schema\Mapping\ArrayLayoutMapper;

class XmlLayoutDriver implements LayoutDriverInterface
{
    private ArrayLayoutMapper $mapper;

    public function __construct()
    {
        $this->mapper = new ArrayLayoutMapper();
    }

    public function load(mixed $source): FileLayout
    {
        $data = $this->parseXml($source); // convert XML → normalized array
        return $this->mapper->map($data);
    }
}
```

---

🧪 Testing
---------

[](#-testing)

```
composer install
composer test
```

---

🤝 Contributing
--------------

[](#-contributing)

Contributions, issues and pull requests are welcome.
If you find a bug or have a suggestion, feel free to open an issue.

---

📜 License
---------

[](#-license)

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

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community8

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

17d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d639b59331f31fc8422a8172c75d8b7ff59c37dd60a16035c1a468aacb9b1823?d=identicon)[victordanilo](/maintainers/victordanilo)

---

Top Contributors

[![victordanilo](https://avatars.githubusercontent.com/u/4293184?v=4)](https://github.com/victordanilo "victordanilo (14 commits)")

---

Tags

edicnabedi-parseredi-writer

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/husail-edi-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/husail-edi-sdk/health.svg)](https://phpackages.com/packages/husail-edi-sdk)
```

###  Alternatives

[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1155.2k](/packages/rcsofttech-audit-trail-bundle)[andersondanilo/cnab_yaml

Especificação do formato Cnab240 e Cnab400 traduzida para Yaml

72288.0k6](/packages/andersondanilo-cnab-yaml)[friendsoftypo3/content-blocks

TYPO3 CMS Content Blocks - Content Types API | Define reusable components via YAML

101466.4k44](/packages/friendsoftypo3-content-blocks)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1715.6k12](/packages/2lenet-crudit-bundle)

PHPackages © 2026

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