PHPackages                             entelix/edilink - 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. [PSR &amp; Standards](/categories/psr-standards)
4. /
5. entelix/edilink

ActiveLibrary[PSR &amp; Standards](/categories/psr-standards)

entelix/edilink
===============

EDILink — Laravel package for building, dispatching and parsing shipping EDI messages. Pluggable carrier profiles. Zero framework coupling in the core.

v1.0.0(1mo ago)01↑2900%MITPHPPHP ^8.2

Since Mar 23Pushed 1mo agoCompare

[ Source](https://github.com/HiralRajgor/edilink)[ Packagist](https://packagist.org/packages/entelix/edilink)[ Docs](https://github.com/HiralRajgor/edilink)[ RSS](/packages/entelix-edilink/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (6)Versions (2)Used By (0)

EDILink by Hiral Rajgor | Entelix
=================================

[](#edilink-by-hiral-rajgor--entelix)

**Laravel package for generating shipping EDI files from container lifecycle events.**

EDILink gives you a clean, framework-agnostic pipeline to turn your container movement data into carrier-ready EDI output — with zero database coupling, typed data objects, and a pluggable carrier profile system that makes adding new shipping lines straightforward.

Built and maintained by [Hiral Rajgor](https://github.com/HiralRajgor) | [Entelix Technologies](https://entelix.in).

---

Features
--------

[](#features)

- **Typed input objects** — `MovementRecord` with a fluent builder; hydrate from any array or DB row
- **Pluggable carrier profiles** — MSC ships built-in; add HLL, KMTC, OOCL etc. by implementing one interface
- **Dual output modes** — fixed-width text (for EDI files) or structured array (for Excel / OVA / API)
- **Chronological validation** — each event is checked against the full arrival→survey→repair→departure chain before being included
- **DB feedback loop** — every `EdiOutput` carries the `includedIds` of records written, so you update your own DB
- **Zero framework coupling in the core** — `MovementRecord`, `EdiOutput`, and all profile logic are pure PHP; Laravel is only needed for the Facade, service provider, and Artisan commands

---

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

[](#requirements)

- PHP 8.2+
- Laravel 10, 11, 12, or 13

---

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

[](#installation)

```
composer require entelix/edilink
```

Laravel auto-discovers the service provider. To publish the config file:

```
php artisan vendor:publish --tag=edilink-config
```

---

Core concepts
-------------

[](#core-concepts)

### MovementRecord

[](#movementrecord)

The single input object that flows through EDILink. Represents one container's state at the time you want to generate EDI.

**Fluent builder:**

```
use Entelix\EdiLink\DTOs\MovementRecord;

$record = MovementRecord::build()
    ->identity('CMAU1234560', '20GP', 'MSC', reportingParty: 'ADEPOT')
    ->depot('ADN01', zone: 26)
    ->arrival('2024-06-01 08:00:00', movementType: 'FULL_IN', vehicleNo: 'GJ05TX1234')
    ->deliveryOrder('MSCUDO123456', validity: '2024-06-10', grace: false)
    ->survey('2024-06-01 10:00:00')
    ->repairCycle(sentAt: '2024-06-02 09:00:00', returnedAt: '2024-06-05 14:00:00')
    ->departure('2024-06-08 11:00:00', movementType: 'FULL_OUT', vehicleNo: 'MH04CD5678')
    ->booking('MSCUBOOK001', validity: '2024-06-15', consignee: 'ACME EXPORTS', sealRef: 'MSC987654')
    ->ediFlags(gateIn: '', survey: '', mnrIn: '', mnrOut: '', gateOut: '')
    ->id('1001')
    ->make();
```

**From a DB row array:**

```
$record = MovementRecord::fromArray($dbRow);
```

`fromArray()` accepts both snake\_case and camelCase keys, and maps common column name variants automatically — `gate_in`, `arrived_at`, `gate_in_at`, `arrivedAt` all map to the same field.

### EdiOutput

[](#edioutput)

Every `build*()` method returns an `EdiOutput`:

```
$output->content;       // string  — the EDI text (or JSON for array format)
$output->includedIds;   // array   — your PKs for DB update
$output->eventType;     // string  — 'gate_in', 'survey', etc.
$output->rows;          // array   — structured rows for Excel/OVA
$output->hasContent();  // bool
$output->lineCount();   // int
$output->recordCount(); // int
```

---

Usage
-----

[](#usage)

### Generate a single event

[](#generate-a-single-event)

```
use Entelix\EdiLink\Facades\EdiLink;

$output = EdiLink::carrier('MSC')->buildGateIn($records);

file_put_contents(storage_path('app/edilink/gatein.txt'), $output->content);

// Update your DB for the records that were included
Container::whereIn('id', $output->includedIds)
         ->where('edi_gate_in', '')
         ->update(['edi_gate_in' => 'gatein.txt']);
```

### Generate all events at once

[](#generate-all-events-at-once)

```
$results = EdiLink::carrier('MSC')->buildAll($records);

// $results is keyed by event type:
// ['gate_in' => EdiOutput, 'survey' => EdiOutput, 'repair_dispatch' => EdiOutput, ...]

$filename = 'MSC_EDI_' . now()->format('d_M_Y_H_i') . '.txt';
$buffer   = '';

foreach ($results as $eventType => $output) {
    $buffer .= $output->content;

    if (! empty($output->includedIds)) {
        $column = match($eventType) {
            'gate_in'        => 'edi_gate_in',
            'survey'         => 'edi_survey',
            'repair_dispatch'=> 'edi_mnr_in',
            'repair_return'  => 'edi_mnr_out',
            'gate_out'       => 'edi_gate_out',
            default          => null,
        };

        if ($column) {
            Container::whereIn('id', $output->includedIds)
                     ->where($column, '')
                     ->update([$column => $filename]);
        }
    }
}

file_put_contents(storage_path("app/edilink/{$filename}"), $buffer);
```

### Shorthand — full EDI string in one call

[](#shorthand--full-edi-string-in-one-call)

```
$ediContent = EdiLink::generate('MSC', $records);
file_put_contents($path, $ediContent);
```

### Array / OVA output mode

[](#array--ova-output-mode)

```
$output = EdiLink::carrier('MSC', 'array')->buildGateIn($records);

$rows = json_decode($output->content, true);
// Each row is an associative array: ['carrier_code', 'container_number', 'event_code', ...]

// Export to Excel
foreach ($rows as $row) {
    $sheet->appendRow(array_values($row));
}
```

---

Laravel scheduler integration
-----------------------------

[](#laravel-scheduler-integration)

The example below shows the recommended pattern: a dedicated scope on your model resolves the pending records, `buildAll()` generates the EDI in one pass, and `includedIds` gives you the exact IDs to stamp without a second query.

**Your model scope** — add this to whatever Eloquent model holds your container data:

```
// app/Models/ContainerUnit.php

public function scopePendingEdiFor(Builder $query, string $carrier, Carbon $from, Carbon $to): Builder
{
    // Adapt column names to match your own schema
    return $query
        ->where('shipping_line', $carrier)
        ->where(function (Builder $q) use ($from, $to) {
            $events = [
                ['flag' => 'edi_arrival',  'event_col' => 'arrived_at'],
                ['flag' => 'edi_survey',   'event_col' => 'surveyed_at'],
                ['flag' => 'edi_mnr_out',  'event_col' => 'repair_sent_at'],
                ['flag' => 'edi_mnr_in',   'event_col' => 'repair_done_at'],
                ['flag' => 'edi_departed', 'event_col' => 'departed_at'],
            ];

            foreach ($events as $e) {
                $q->orWhere(fn(Builder $sub) =>
                    $sub->whereNull($e['flag'])
                        ->whereBetween($e['event_col'], [$from, $to])
                );
            }
        });
}
```

**The Artisan command:**

```
// app/Console/Commands/DispatchCarrierEdi.php

namespace App\Console\Commands;

use App\Models\ContainerUnit;
use Entelix\EdiLink\Core\EdiOutput;
use Entelix\EdiLink\DTOs\MovementRecord;
use Entelix\EdiLink\Facades\EdiLink;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;

class DispatchCarrierEdi extends Command
{
    protected $signature   = 'edi:dispatch {carrier}';
    protected $description = 'Build and email a carrier EDI file covering the previous hour';

    /**
     * Maps EDILink event slugs to the dispatch-flag column in your table.
     * Adjust the right-hand values to match your actual column names.
     */
    private const DISPATCH_FLAGS = [
        'gate_in'         => 'edi_arrival',
        'survey'          => 'edi_survey',
        'repair_dispatch' => 'edi_mnr_out',
        'repair_return'   => 'edi_mnr_in',
        'gate_out'        => 'edi_departed',
    ];

    public function handle(): int
    {
        $carrier  = strtoupper($this->argument('carrier'));
        $window   = $this->reportingWindow();
        $filename = sprintf('%s_EDI_%s.txt', $carrier, $window['from']->format('Ymd_Hi'));

        $units = ContainerUnit::with(['depot', 'inboundOrder', 'outboundBooking', 'activeSeal'])
            ->pendingEdiFor($carrier, $window['from'], $window['to'])
            ->get();

        if ($units->isEmpty()) {
            $this->info("No pending {$carrier} EDI events in window.");
            return self::SUCCESS;
        }

        // Hydrate MovementRecord objects from your model collection.
        // fromArray() accepts any key names — map yours here once.
        $records = $units->map(fn($unit) => MovementRecord::fromArray([
            'id'                      => $unit->id,
            'container_number'        => $unit->unit_number,
            'iso_type'                => $unit->size_type,
            'carrier_code'            => $unit->shipping_line,
            'reporting_party'         => $unit->depot->edi_party_code,
            'depot_code'              => $unit->depot->location_code,
            'zone_id'                 => $unit->depot->zone_id,
            'arrived_at'              => $unit->arrived_at,
            'arrival_movement_type'   => $unit->arrival_type,
            'arrival_vehicle'         => $unit->arrival_vehicle_ref,
            'delivery_order_ref'      => $unit->inboundOrder?->reference,
            'delivery_order_expiry'   => $unit->inboundOrder?->expires_at,
            'delivery_order_overdue'  => $unit->arrival_after_do_expiry,
            'surveyed_at'             => $unit->surveyed_at,
            'sent_for_repair_at'      => $unit->repair_sent_at,
            'returned_from_repair_at' => $unit->repair_done_at,
            'departed_at'             => $unit->departed_at,
            'departure_movement_type' => $unit->departure_type,
            'departure_vehicle'       => $unit->departure_vehicle_ref,
            'destination_location'    => $unit->departure_destination,
            'booking_ref'             => $unit->outboundBooking?->reference,
            'booking_expiry'          => $unit->outboundBooking?->expires_at,
            'booking_overdue'         => $unit->departure_after_booking_expiry,
            'consignee_name'          => $unit->outboundBooking?->consignee,
            'seal_reference'          => $unit->activeSeal?->full_number,
            // Dispatch flags — empty string = pending, filename = already sent
            'dispatched_gate_in'      => $unit->edi_arrival     ?? '',
            'dispatched_survey'       => $unit->edi_survey       ?? '',
            'dispatched_mnr_in'       => $unit->edi_mnr_out      ?? '',
            'dispatched_mnr_out'      => $unit->edi_mnr_in       ?? '',
            'dispatched_gate_out'     => $unit->edi_departed     ?? '',
        ]))->all();

        // Generate all event types in a single pass
        $results = EdiLink::carrier($carrier)->buildAll($records);

        // Concatenate content + stamp dispatched flags in one loop
        $ediContent = collect($results)
            ->filter(fn(EdiOutput $o) => $o->hasContent())
            ->each(function (EdiOutput $output) use ($filename) {
                $column = self::DISPATCH_FLAGS[$output->eventType] ?? null;
                if ($column && $output->includedIds) {
                    ContainerUnit::whereIn('id', $output->includedIds)
                        ->whereNull($column)
                        ->update([$column => $filename]);
                }
            })
            ->implode('content');

        if (empty(trim($ediContent))) {
            $this->info("EDI generated but all lines were filtered. Nothing to send.");
            return self::SUCCESS;
        }

        Storage::put("edi/outbound/{$filename}", $ediContent);

        // Send — adapt to your mail setup (Mailable, raw, notification, etc.)
        Mail::send([], [], fn($msg) => $msg
            ->to(config("services.edi.{$carrier}.recipients"))
            ->subject("{$carrier} EDI — {$filename}")
            ->text("{$carrier} EDI file attached. Period: {$window['from']} to {$window['to']}.")
            ->attachData($ediContent, $filename, ['mime' => 'text/plain'])
        );

        $this->info("Dispatched {$filename} — {$units->count()} unit(s), " . strlen($ediContent) . " bytes.");
        return self::SUCCESS;
    }

    private function reportingWindow(): array
    {
        return [
            'from' => Carbon::now()->subHour()->startOfHour(),
            'to'   => Carbon::now()->startOfHour(),
        ];
    }
}
```

Register in your scheduler:

```
// routes/console.php  (Laravel 11+)
Schedule::command('edi:dispatch MSC')->hourly();
```

---

Adding a new carrier
--------------------

[](#adding-a-new-carrier)

1. Create a profile class in your app (or a separate package):

```
// app/EdiLink/HllCarrierProfile.php

namespace App\EdiLink;

use Entelix\EdiLink\Builders\AbstractCarrierProfile;
use Entelix\EdiLink\Core\EdiLine;
use Entelix\EdiLink\Core\EdiOutput;
use Entelix\EdiLink\DTOs\MovementRecord;
use DateTimeImmutable;

class HllCarrierProfile extends AbstractCarrierProfile
{
    public function carrierCode(): string { return 'HLL'; }
    public function carrierName(): string { return 'Hapag-Lloyd'; }

    public function buildGateIn(array $records): EdiOutput
    {
        $lines       = [];
        $includedIds = [];

        foreach ($records as $record) {
            if (! $this->isPending($record->dispatchedGateIn)) continue;
            if ($record->arrivedAt === null) continue;

            $line = EdiLine::make()
                // HLL has its own field layout — define it here
                ->add('carrier',       4,  $record->carrierCode)
                ->add('container',     11, $record->containerNumber)
                ->add('event',         6,  'RCVD')
                ->add('timestamp',     12, $this->ediTimestamp($record->arrivedAt))
                ->add('location',      5,  $record->depotCode);

            $lines[]     = $line->toText();
            $includedIds[] = $record->recordId;
        }

        return new EdiOutput(
            content:     implode('', $lines),
            includedIds: $includedIds,
            eventType:   'gate_in',
            generatedAt: new DateTimeImmutable()
        );
    }

    // Implement buildSurvey, buildRepairDispatch, buildRepairReturn, buildGateOut
    // Inherit no-op buildCfsArrival, buildStuffing, buildDestuffing from AbstractCarrierProfile
}
```

2. Register in `config/edilink.php`:

```
'carriers' => [
    'HLL' => \App\EdiLink\HllCarrierProfile::class,
],
```

3. Use it:

```
EdiLink::carrier('HLL')->buildAll($records);
```

---

Artisan commands
----------------

[](#artisan-commands)

```
# Check registered carriers and usage hint
php artisan edilink:generate MSC

# Validate an EDI file against a carrier schema
php artisan edilink:validate /path/to/file.txt --carrier=MSC
```

---

Running tests
-------------

[](#running-tests)

```
composer install
./vendor/bin/phpunit
```

---

MSC event reference
-------------------

[](#msc-event-reference)

MethodEvent type slugEDI code`buildGateIn()``gate_in`DEV / MCY / MPI / ERM (zone-aware)`buildSurvey()``survey`DAM`buildRepairDispatch()``repair_dispatch`TBR`buildRepairReturn()``repair_return`REP`buildGateOut()``gate_out`FST / MPO / MSH (zone-aware)`buildCfsArrival()``cfs_arrival`DVAN`buildStuffing()``stuffing`CST`buildDestuffing()``destuffing`DST---

Roadmap
-------

[](#roadmap)

- MSC fixed-width EDI generator
- MSC array / OVA output
- Zone-aware event code resolution (Hazira, Mundra, Nhava Sheva)
- Chronological chain validation
- `EdiOutput.includedIds` for DB feedback loop
- `MovementRecord` fluent builder + `fromArray()` factory
- HLL carrier profile
- KMTC carrier profile
- OOCL carrier profile
- Inbound EDI parser (raw EDI text → `MovementRecord[]`)
- Schema validator with field-level error messages
- Hosted API tier — subscribe at entelix.com for an API key

---

License
-------

[](#license)

MIT — free to use in any project.

© Entelix Technologies. Contributions welcome via GitHub.

###  Health Score

40

—

FairBetter than 87% of packages

Maintenance97

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

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/64be2851c822103aaac4f5da39e0e0b3f7cebede5f25693cf19c36d1e9caa378?d=identicon)[HiralRajgor](/maintainers/HiralRajgor)

---

Top Contributors

[![HiralRajgor](https://avatars.githubusercontent.com/u/34230139?v=4)](https://github.com/HiralRajgor "HiralRajgor (2 commits)")

---

Tags

adanicontainerdepoediedifactgatehappagkmtclaravellloydmnrmscovaphpportshippingshippinglinevesselcontainerlaravelediedifactshippinglogisticsdepotmscgate-ingate-outmnrKMTCAdaniHLLHappag LloydOOCL

###  Code Quality

TestsPHPUnit

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/entelix-edilink/health.svg)

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

###  Alternatives

[dvdoug/boxpacker

An implementation of the 3D (actually 4D) bin packing/knapsack problem (aka creating parcels by putting items into boxes)

6522.7M18](/packages/dvdoug-boxpacker)[daylerees/container-debug

Inspect the Laravel IoC Container from Artisan.

582.8k](/packages/daylerees-container-debug)[nunomaduro/laravel-pot

Set of commands to inspect Laravel's container

991.6k](/packages/nunomaduro-laravel-pot)[godruoyi/easy-container

A small PHP 5.3 dependency injection container extended from Laravel container.

334.9k2](/packages/godruoyi-easy-container)

PHPackages © 2026

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