kubinyete/edi-php

A standard library for reading EDI files and declaring your own EDI parsers

1.0.9 2024-05-03 12:33 UTC

This package is auto-updated.

Last update: 2024-05-03 13:01:15 UTC


README

A standard library for declaring EDI parsers, because that's not a fun thing to do.

Why should I use it?

This is a very common use-case for many services that output data using a defined text-file layout, for example, many services (Ex: Payment providers) output some sort of layout file that follows a simplified column separated values that has a fixed size set.

For example, the following line (25 bytes):

120240503Hello World

Can be interpreted as

Range Size Column Value
0 - 0 1 Reg. type 1
1 - 8 8 Date 2024-05-03
9 - 25 16 Message Hello World

How it works

To provide a better developer experience while creating these parsers, this library provides some useful property attributes and a base class for declaring these types of data.

For example, to declare the previous line, we can declare it in code as the following:

final class ExampleLayoutType extends Registry {
    #[Number(1)]
    public int $type;

    #[Date(8, format: '!Ymd')]
    public DateTimeInterface $type;

    #[Text(16)]
    public string $message;
}

// We can parse the data directly
$example = ExampleLayoutType::from('120240503Hello World     ')

// Or we can hydrate it
$example = new ExampleLayoutType();
$example->hydrate('120240503Hello World     ');

Declaring dynamic parsers

Using our built in line parser, we can create dynamically parsed (by line) types, with the power of generators, these types are parsed on demand:

// Using a standard line parser, iterates over each line
class EDIParser extends LineParser
{
    // Ran on each line iteration
    protected function parse(LineContext $ctx): ?Registry
    {
        [$contents, $number] = $ctx->unwrap();
        // For this EDI file, we can deduce which type it is based on
        // the first 2 letters provided each line.
        $code = substr($contents, 0, 2);

        try {
            return match ($code) {
                EDIRegistry::TYPE_HEADER_START => EDIHeader::from($contents),
                EDIRegistry::TYPE_TRANSACTION_BATCH_START => EDITransactionBatch::from($contents),
                EDIRegistry::TYPE_SALE_RECEIPT => EDISaleReceipt::from($contents),
                // Our LineContext can provide a more verbose message to inform
                // where in our data the error ocurred.
                default => $ctx->raise("Cannot parse EDI of type '$code'", 0),
            };
        } catch (FieldException $e) {
            $ctx->raise($e->getMessage(), $e->getCursor());
        }
    }
}

// Using our parser directly
// Reading directly from stdin
$buffer = Stream::file('php://stdin', 'rb');
$parser = EDIParser::loadFromStream($buffer);

foreach ($parser as $registry) {
    /** @var Registry $registry */
    // We have direct access to our parsed objects on demand
    dump($registry);
}

Understanding parsing errors

This feature is work-in-progress, there are a some things we can do to make it better

We enable out-of-the-box support for friendly contextual messages on any errors that ocurred.

PHP Fatal error:  Uncaught Kubinyete\Edi\Parser\Exception\ParseException: Line 1: Failed to parse field '202d1001025840' as a date with format 'YmdHis'
Contents: "A00003.1202d1001025840000442ADIQ SOLUCOES PAGAMENTOS S.A  0040002783189N000001"
           --------^
 in /home/vitorkubinyete/code/edi-php/src/Parser/LineContext.php:38