├── .github
└── workflows
│ └── ci.yml
├── CONTRIBUTORS.md
├── EXAMPLES.md
├── LICENSE
├── README.md
├── composer.json
├── ecs.php
└── src
└── EDI
├── Analyser.php
├── Encoder.php
├── Interpreter.php
├── Parser.php
├── Reader.php
└── ReaderException.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | types:
7 | - opened
8 | - synchronize
9 |
10 | jobs:
11 | build-test:
12 | runs-on: ubuntu-24.04
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Cache Composer dependencies
16 | uses: actions/cache@v4
17 | with:
18 | path: /tmp/composer-cache
19 | key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
20 | - uses: php-actions/composer@v6
21 | - name: PHPUnit tests
22 | run: ./vendor/bin/phpunit
23 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | Contributors
2 | -------------
3 |
4 | * Stefano Sabatini https://github.com/sabas
5 | * Uldis Nelsons https://github.com/uldisn
6 | * Marius Teller https://github.com/Xiro
--------------------------------------------------------------------------------
/EXAMPLES.md:
--------------------------------------------------------------------------------
1 | ##Examples
2 |
3 | Load a message and dump a json
4 | ------------------------------
5 | Loading from file:
6 | ```php
7 | use EDI\Parser;
8 | $filePath = "example.edi";
9 | $p = new EDI\Parser();
10 | $p->load($filePath)->parse();
11 | $p->checkEncoding(); // optional
12 | if (count($p->errors()) > 0) {
13 | echo json_encode($p->errors());
14 | return;
15 | }
16 | echo json_encode($p->get());
17 | ```
18 | To load a single-line string (`"seg'seg"`) use `$p->loadString()`.
19 | To load an array of lines (`["seg","seg"]`) use `$p->loadArray()`.
20 |
21 | Convert a formatted array to EDIFACT message
22 | --------------------------------------------
23 | Loading from a PHP array:
24 | ```php
25 | use EDI\Encoder;
26 | $arr = []; //array
27 | $enc = new EDI\Encoder($arr, false); //one segment per line
28 | echo $enc->get();
29 | ```
30 |
31 | Create human-readable file with comments from EDI file
32 | ------------------------------------------------------
33 |
34 | ```php
35 | $filePath = 'demo.edi';
36 | $parser = new EDI\Parser();
37 | $parser->load($filePath);
38 | $segments = $parser->getRawSegments();
39 |
40 | $analyser = new EDI\Analyser();
41 | $analyser->loadSegmentsXml('edifact/src/EDI/Mapping/d95b/segments.xml');
42 |
43 | $text = $analyser->process($parsed, $parser->get());
44 | ```
45 |
46 | EDI data reading from extracted group
47 | -------------------------------------
48 |
49 | As not to have to go through the indexes for extracted groups, just use a reader with a different parser.
50 |
51 | E.g. inventory messages (snippet, not a valid EDI message!):
52 |
53 | ```
54 | INV+2++1'QTY+156:1000:PCE'QTY+145:3000:PCE'LOC+18+YA:::567'DTM+179:20180509:102'RFF+AAK:TEST'DTM+171:20180509:102'
55 | INV+1++11'QTY+156:200:PCE'QTY+145:2800:PCE'LOC+18+YA:::567'DTM+179:20180509:102'RFF+ALO:4916165350'DTM+171:20180509:102'
56 | INV+1++11'QTY+156:200:PCE'QTY+145:2600:PCE'LOC+18+YA:::567'DTM+179:20180509:102'RFF+ALO:4916165351'DTM+171:20180509:102'
57 | INV+1++11'QTY+156:200:PCE'QTY+145:2400:PCE'LOC+18+YA:::567'DTM+179:20180509:102'RFF+ALO:4916165352'DTM+171:20180509:102'
58 | INV+1++11'QTY+156:100:PCE'QTY+145:2300:PCE'LOC+18+YA:::567'DTM+179:20180510:102'RFF+ALO:4916165359'DTM+171:20180510:102'
59 | ```
60 |
61 | ```php
62 | $parser = new EDI\Parser();
63 | $parser->load($filePath);
64 | $reader = new EDI\Reader($parser);
65 | $groups = $reader->groupsExtract('INV');
66 |
67 | foreach ($groups as $record) {
68 | $parser->loadArray($record, false);
69 | $r = new EDI\Reader($parser);
70 | $records[] = [
71 | 'storageLocation' => $r->readEdiDataValue(['LOC', ['2.0' => 'YA']], 2, 3),
72 | 'bookingDate' => $r->readEdiSegmentDTM(179),
73 | 'enteredOn' => $r->readEdiSegmentDTM(171),
74 | 'quantity' => $r->readEdiDataValue(['QTY', ['1.0' => 156]], 1, 1),
75 | 'actualStock' => $r->readEdiDataValue(['QTY', ['1.0' => 145]], 1, 1)
76 | ];
77 | }
78 | ```
79 |
80 | Readable EDI file
81 | -----------------
82 | ```
83 | UNB+UNOE:2+RIXCT++141028:0746+NBFILE027747'
84 | ```
85 |
86 | ```
87 | UNB - InterchangeHeader
88 | (To start, identify and specify an interchange)
89 | [1] UNOE,2
90 | unb1 - syntaxIdentifier
91 | Syntax identifier
92 | [0] UNOE
93 | id: unb11 - syntaxIdentifier
94 | Syntax identifier
95 | type: a
96 | required: true
97 | length: 4
98 | [1] 2
99 | id: unb12 - syntaxVersionNumber
100 | Syntax version number
101 | type: n
102 | required: true
103 | length: 1
104 | [2] RIXCT
105 | unb2 - interchangeSender
106 | Interchange sender
107 | [3]
108 | unb3 - interchangeRecipient
109 | Interchange recipient
110 | [4] 141028,0746
111 | unb4 - dateTimePreparation
112 | Date Time of preparation
113 | [0] 141028
114 | id: unb41 - date
115 | type: n
116 | required: true
117 | length: 6
118 | [1] 0746
119 | id: unb42 - time
120 | type: n
121 | required: true
122 | length: 4
123 | [5] NBFILE027747
124 | unb5 - interchangeControlReference
125 | ```
126 |
127 | EDI data element reading
128 | ------------------------
129 |
130 | ```php
131 | $filePath = 'files/truck_out_176699.edi';
132 | $parser = new EDI\Parser();
133 | $parser->load($filePath);
134 | $reader = new EDI\Reader($parser);
135 |
136 | $record = [
137 | 'interchangeSender' => $reader->readEdiDataValue('UNB', 2),
138 | 'arrivalDateTimeEstimated' => $reader->$EdiReader->readEdiSegmentDTM('132'),
139 | 'messageReferenceNumber' => $reader->readEdiDataValue('UNH', 1),
140 | 'TareWeight' => $reader->readEdiDataValue(['MEA', ['2' => 'T']], 3, 0)
141 | . ' '
142 | . $reader->readEdiDataValue(['MEA', ['2' => 'T']], 3, 1),
143 | 'GrossWeight' => $reader->readEdiDataValue(['MEA', ['2' => 'G']], 3, 0)
144 | . ' '
145 | . $reader->readEdiDataValue(['MEA', ['2' => 'G']], 3, 1),
146 | ];
147 |
148 | //error processing
149 | $readerErrors = $EdiReader->errors();
150 | if (!empty($readerErrors)) {
151 | var_dump($readerErrors);
152 | }
153 | var_dump($record);
154 | ```
155 |
156 | Demo
157 | -------
158 |
159 | Message from Wikipedia page http://en.wikipedia.org/wiki/EDIFACT#Example
160 | ```
161 | UNA:+.? '
162 | UNB+IATB:1+6XPPC+LHPPC+940101:0950+1'
163 | UNH+1+PAORES:93:1:IA'
164 | MSG+1:45'
165 | IFT+3+XYZCOMPANY AVAILABILITY'
166 | ERC+A7V:1:AMD'
167 | IFT+3+NO MORE FLIGHTS'
168 | ODI'
169 | TVL+240493:1000::1220+FRA+JFK+DL+400+C'
170 | PDI++C:3+Y::3+F::1'
171 | APD+74C:0:::6++++++6X'
172 | TVL+240493:1740::2030+JFK+MIA+DL+081+C'
173 | PDI++C:4'
174 | APD+EM2:0:1630::6+++++++DA'
175 | UNT+13+1'
176 | UNZ+1+1'
177 | ```
178 |
179 | Gets converted in json as
180 | ```
181 | [["UNB",["IATB","1"],"6XPPC","LHPPC",["940101","0950"],"1"],["UNH","1",["PAORES","93","1","IA"]],["MSG",["1","45"]],["IFT","3","XYZCOMPANY AVAILABILITY"],["ERC",["A7V","1","AMD"]],["IFT","3","NO MORE FLIGHTS"],["ODI"],["TVL",["240493","1000","","1220"],"FRA","JFK","DL","400","C"],["PDI","",["C","3"],["Y","","3"],["F","","1"]],["APD",["74C","0","","","6"],"","","","","","6X"],["TVL",["240493","1740","","2030"],"JFK","MIA","DL","081","C"],["PDI","",["C","4"]],["APD",["EM2","0","1630","","6"],"","","","","","","DA"],["UNT","13","1"],["UNZ","1","1"]]
182 | ```
183 |
184 | Converting back the message to EDIFACT (and enabling newlines)
185 | ```
186 | UNB+IATB:1+6XPPC+LHPPC+940101:0950+1'
187 | UNH+1+PAORES:93:1:IA'
188 | MSG+1:45'
189 | IFT+3+XYZCOMPANY AVAILABILITY'
190 | ERC+A7V:1:AMD'
191 | IFT+3+NO MORE FLIGHTS'
192 | ODI'
193 | TVL+240493:1000::1220+FRA+JFK+DL+400+C'
194 | PDI++C:3+Y::3+F::1'
195 | APD+74C:0:::6++++++6X'
196 | TVL+240493:1740::2030+JFK+MIA+DL+081+C'
197 | PDI++C:4'
198 | APD+EM2:0:1630::6+++++++DA'
199 | UNT+13+1'
200 | UNZ+1+1'
201 | ```
202 | Disabling newlines (passing true to encoder)
203 | ```
204 |
205 | UNB+IATB:1+6XPPC+LHPPC+940101:0950+1'UNH+1+PAORES:93:1:IA'MSG+1:45'IFT+3+XYZCOMPANY AVAILABILITY'ERC+A7V:1:AMD'IFT+3+NO MORE FLIGHTS'ODI'TVL+240493:1000::1220+FRA+JFK+DL+400+C'PDI++C:3+Y::3+F::1'APD+74C:0:::6++++++6X'TVL+240493:1740::2030+JFK+MIA+DL+081+C'PDI++C:4'APD+EM2:0:1630::6+++++++DA'UNT+13+1'UNZ+1+1'
206 | ```
207 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | edifact
2 | =======
3 |
4 | Tools to process EDI messages in UN/EDIFACT format
5 |
6 | Supported syntax is version 3.
7 |
8 | It's provided in a Composer package:
9 |
10 | `composer require sabas/edifact`
11 |
12 | The mapping XML files are provided in a separate package:
13 |
14 | `composer require php-edifact/edifact-mapping`
15 |
16 | EDI\Parser
17 | ------------------
18 | Given an EDI message checks the syntax, outputs errors and returns the message as a multidimensional array.
19 |
20 | **INPUT**
21 | ```php
22 | $p = new EDI\Parser();
23 | $p->load($location); // a local path to a file or a URL (if file_get_contents() allows for remote access)
24 | $p->loadString($string); // a full message (single line text)
25 | $p->loadArray($array); // an array of strings (one segment per entry)
26 | ```
27 |
28 | **OUTPUT**
29 |
30 | Errors
31 | ```php
32 | $c->errors();
33 | ```
34 | Array
35 | ```php
36 | $c->get();
37 | ```
38 |
39 |
40 | EDI\Encoder
41 | ------------------
42 | Given a multidimensional array (formatted as the output of the parser), returns an EDI string, optionally one segment per line.
43 |
44 | **INPUT**
45 | ```php
46 | $c = new EDI\Encoder($x, $compact = true);
47 | ```
48 | `$x` is a multidimensional array where first dimension is the EDI segment, second contains elements:
49 | * single value
50 | * array (representing composite elements)
51 |
52 | `$compact` is a boolean, if you need a segment per line. Set to false to enable wrapped lines.
53 |
54 | OR
55 | ```php
56 | $c = new EDI\Encoder();
57 | $c->encode($array, false);
58 | ```
59 |
60 | **OUTPUT**
61 | ```php
62 | $c->get(); // returns String
63 | ```
64 |
65 | EDI\Analyser
66 | ------------------
67 | Create human-readable, structured text with comments from `segments.xml`.
68 | Requires the EDI\Mapping package.
69 |
70 | ```php
71 | $parser = new EDI\Parser();
72 | $parser->load($file);
73 | $segments = $parser->getRawSegments();
74 | $parsed = $parser->get();
75 | $analyser = new EDI\Analyser();
76 | $mapping = new EDI\MappingProvider('D95B');
77 | $analyser->loadSegmentsXml($mapping->getSegments());
78 | $analyser->loadSegmentsXml($mapping->getServiceSegments(), false);
79 | $analyser->loadMessageXml($mapping->getMessage('coparn'));
80 | $analyser->loadCodesXml($mapping->getCodes());
81 | $analyser->directory = 'D95B';
82 | $result = $analyser->process($parsed, $segments);
83 |
84 | ```
85 | * `$file` is the path to orginal EDI message file
86 |
87 | ### Example INPUT
88 | ```text
89 | UNA:+,? '
90 | UNB+UNOA:1+MAEU+LVRIXBCT+200813:0816+1412605'
91 | UNH+141260500001+COPARN:D:95B:UN'
92 | BGM+12+20200813081626+9'
93 | RFF+BN:204549739'
94 | NAD+CA+MAE:172:20'
95 | EQD+CN++45G1:102:5+2+2+4'
96 | RFF+BN:204549739'
97 | RFF+SQ:7G3JTL39O0M3B'
98 | TMD+++2'
99 | DTM+201:202008130000:203'
100 | LOC+98+LVRIX:139:6+RIGA TERMINAL:TER:ZZZ'
101 | CNT+16:1'
102 | UNT+12+141260500001'
103 | UNZ+1+1412605'
104 | ```
105 |
106 | ### Example Output
107 | ```text
108 |
109 | UNA:+,? '
110 | UNB - InterchangeHeader
111 | (To start, identify and specify an interchange)
112 | [0] UNOA,1
113 | unb1 - syntaxIdentifier
114 | Syntax identifier
115 | [0] UNOA
116 | id: unb11 - syntaxIdentifier
117 | Syntax identifier
118 | type: a
119 | required: true
120 | length: 4
121 | [1] 1
122 | id: unb12 - syntaxVersionNumber
123 | Syntax version number
124 | type: n
125 | required: true
126 | length: 1
127 | [1] MAEU
128 | unb2 - interchangeSender
129 | Interchange sender
130 | [2] LVRIXBCT
131 | unb3 - interchangeRecipient
132 | Interchange recipient
133 | [3] 200813,0816
134 | unb4 - dateTimePreparation
135 | Date Time of preparation
136 | [0] 200813
137 | id: unb41 - date
138 |
139 | type: n
140 | required: true
141 | length: 6
142 | [1] 0816
143 | id: unb42 - time
144 |
145 | type: n
146 | required: true
147 | length: 4
148 | [4] 1412605
149 | unb5 - interchangeControlReference
150 |
151 |
152 | UNB+UNOA:1+MAEU+LVRIXBCT+200813:0816+1412605'
153 | UNH - messageHeader http://www.unece.org/trade/untdid/d95b/trsd/trsdunh.htm
154 | (To head, identify and specify a message.)
155 | [0] 141260500001
156 | unh1 - messageReferenceNumber
157 |
158 | [1] COPARN,D,95B,UN
159 | unh2 - messageType
160 |
161 | [0] COPARN
162 | id: unh21 - messageType
163 |
164 | type: an
165 | maxlen: 6
166 | required: true
167 | [1] D
168 | id: unh22 - messageVersion
169 |
170 | type: an
171 | maxlen: 3
172 | required: true
173 | [2] 95B
174 | id: unh23 - messageRelease
175 |
176 | type: an
177 | maxlen: 3
178 | required: true
179 | [3] UN
180 | id: unh24 - controllingAgency
181 |
182 | type: an
183 | maxlen: 3
184 | required: true
185 |
186 | UNH+141260500001+COPARN:D:95B:UN'
187 | BGM - beginningOfMessage http://www.unece.org/trade/untdid/d95b/trsd/trsdbgm.htm
188 | (To indicate the type and function of a message and to transmit the
189 | identifying number.)
190 | [0] 12
191 | C002 - documentmessageName
192 | Identification of a type of document/message by code or name. Code
193 | preferred.
194 | [1] 20200813081626
195 | 1004 - documentmessageNumber
196 | Reference number assigned to the document/message by the issuer.
197 | [2] 9
198 | 1225 - messageFunctionCoded
199 | Code indicating the function of the message.
200 |
201 | BGM+12+20200813081626+9'
202 | RFF - reference http://www.unece.org/trade/untdid/d95b/trsd/trsdrff.htm
203 | (To specify a reference.)
204 | [0] BN,204549739
205 | C506 - reference
206 | Identification of a reference.
207 | [0] BN - Booking reference number
208 | id: 1153 - referenceQualifier
209 | Code giving specific meaning to a reference segment or a reference
210 | number.
211 | type: an
212 | maxlen: 3
213 | required: true
214 | [1] 204549739
215 | id: 1154 - referenceNumber
216 | Identification number the nature and function of which can be
217 | qualified by an entry in data element 1153 Reference qualifier.
218 | type: an
219 | maxlen: 35
220 |
221 | RFF+BN:204549739'
222 | NAD - nameAndAddress http://www.unece.org/trade/untdid/d95b/trsd/trsdnad.htm
223 | (To specify the name/address and their related function, either by CO82 only
224 | and/or unstructured by CO58 or structured by CO80 thru 3207. be permitted
225 | to use the ADR segment and/or the PNA segment. After the conclusion of the
226 | Brazil JRT (scheduled for April 1996), this NAD segment shall NO LONGER BE
227 | PERMITTED FOR USE IN ANY NEW MESSAGES submitted for Status 1 in the
228 | UN/EDIFACT Directories. This means that either the ADR and/or the PNA
229 | segments shall be used in place of this NAD segment by the message
230 | designers. (See Rapporteurs' decision on the proposed ADR & PNA segments
231 | and the existing NAD segment - Sydney, April 1995).)
232 | [0] CA
233 | 3035 - partyQualifier
234 | Code giving specific meaning to a party.
235 | [1] MAE,172,20
236 | C082 - partyIdentificationDetails
237 | Identification of a transaction party by code.
238 | [0] MAE
239 | id: 3039 - partyIdIdentification
240 | Code identifying a party involved in a transaction.
241 | type: an
242 | maxlen: 35
243 | required: true
244 | [1] 172 - Carrier code
245 | id: 1131 - codeListQualifier
246 | Identification of a code list.
247 | type: an
248 | maxlen: 3
249 | [2] 20 - BIC (Bureau International des Containeurs)
250 | id: 3055 - codeListResponsibleAgencyCoded
251 | Code identifying the agency responsible for a code list.
252 | type: an
253 | maxlen: 3
254 |
255 | NAD+CA+MAE:172:20'
256 | EQD - equipmentDetails http://www.unece.org/trade/untdid/d95b/trsd/trsdeqd.htm
257 | (To identify a unit of equipment.)
258 | [0] CN
259 | 8053 - equipmentQualifier
260 | Code identifying type of equipment.
261 | [1]
262 | C237 - equipmentIdentification
263 | Marks (letters and/or numbers) identifying equipment used for transport
264 | such as a container.
265 | [2] 45G1,102,5
266 | C224 - equipmentSizeAndType
267 | Code and/or name identifying size and type of equipment used in
268 | transport. Code preferred.
269 | [0] 45G1
270 | id: 8155 - equipmentSizeAndTypeIdentification
271 | Coded description of the size and type of equipment e.g. unit load
272 | device.
273 | type: an
274 | maxlen: 10
275 | [1] 102 - Size and type
276 | id: 1131 - codeListQualifier
277 | Identification of a code list.
278 | type: an
279 | maxlen: 3
280 | [2] 5 - ISO (International Organization for Standardization)
281 | id: 3055 - codeListResponsibleAgencyCoded
282 | Code identifying the agency responsible for a code list.
283 | type: an
284 | maxlen: 3
285 | [3] 2
286 | 8077 - equipmentSupplierCoded
287 | To indicate the party that is the supplier of the equipment.
288 | [4] 2
289 | 8249 - equipmentStatusCoded
290 | Indication of the action related to the equipment.
291 | [5] 4
292 | 8169 - fullemptyIndicatorCoded
293 | To indicate the extent to which the equipment is full or empty.
294 |
295 | EQD+CN++45G1:102:5+2+2+4'
296 | RFF - reference http://www.unece.org/trade/untdid/d95b/trsd/trsdrff.htm
297 | (To specify a reference.)
298 | [0] BN,204549739
299 | C506 - reference
300 | Identification of a reference.
301 | [0] BN - Booking reference number
302 | id: 1153 - referenceQualifier
303 | Code giving specific meaning to a reference segment or a reference
304 | number.
305 | type: an
306 | maxlen: 3
307 | required: true
308 | [1] 204549739
309 | id: 1154 - referenceNumber
310 | Identification number the nature and function of which can be
311 | qualified by an entry in data element 1153 Reference qualifier.
312 | type: an
313 | maxlen: 35
314 |
315 | RFF+BN:204549739'
316 | RFF - reference http://www.unece.org/trade/untdid/d95b/trsd/trsdrff.htm
317 | (To specify a reference.)
318 | [0] SQ,7G3JTL39O0M3B
319 | C506 - reference
320 | Identification of a reference.
321 | [0] SQ - Container sequence number
322 | id: 1153 - referenceQualifier
323 | Code giving specific meaning to a reference segment or a reference
324 | number.
325 | type: an
326 | maxlen: 3
327 | required: true
328 | [1] 7G3JTL39O0M3B
329 | id: 1154 - referenceNumber
330 | Identification number the nature and function of which can be
331 | qualified by an entry in data element 1153 Reference qualifier.
332 | type: an
333 | maxlen: 35
334 |
335 | RFF+SQ:7G3JTL39O0M3B'
336 | TMD - transportMovementDetails http://www.unece.org/trade/untdid/d95b/trsd/trsdtmd.htm
337 | (To specify transport movement details for a goods item or equipment.)
338 | [0]
339 | C219 - movementType
340 | Description of type of service for movement of cargo.
341 | [1]
342 | 8332 - equipmentPlan
343 | Description indicating equipment plan, e.g. FCL or LCL.
344 | [2] 2
345 | 8341 - haulageArrangementsCoded
346 | Specification of the type of equipment haulage arrangements.
347 |
348 | TMD+++2'
349 | DTM - datetimeperiod http://www.unece.org/trade/untdid/d95b/trsd/trsddtm.htm
350 | (To specify date, and/or time, or period.)
351 | [0] 201,202008130000,203
352 | C507 - datetimeperiod
353 | Date and/or time, or period relevant to the specified date/time/period
354 | type.
355 | [0] 201 - Pick-up date/time of equipment
356 | id: 2005 - datetimeperiodQualifier
357 | Code giving specific meaning to a date, time or period.
358 | type: an
359 | maxlen: 3
360 | required: true
361 | [1] 202008130000
362 | id: 2380 - datetimeperiod
363 | The value of a date, a date and time, a time or of a period in a
364 | specified representation.
365 | type: an
366 | maxlen: 35
367 | [2] 203 - CCYYMMDDHHMM
368 | id: 2379 - datetimeperiodFormatQualifier
369 | Specification of the representation of a date, a date and time or of
370 | a period.
371 | type: an
372 | maxlen: 3
373 |
374 | DTM+201:202008130000:203'
375 | LOC - placelocationIdentification http://www.unece.org/trade/untdid/d95b/trsd/trsdloc.htm
376 | (To identify a country/place/location/related location one/related location
377 | two.)
378 | [0] 98
379 | 3227 - placelocationQualifier
380 | Code identifying the function of a location.
381 | [1] LVRIX,139,6
382 | C517 - locationIdentification
383 | Identification of a location by code or name.
384 | [0] LVRIX
385 | id: 3225 - placelocationIdentification
386 | Identification of the name of place/location, other than 3164 City
387 | name.
388 | type: an
389 | maxlen: 25
390 | [1] 139 - Port
391 | id: 1131 - codeListQualifier
392 | Identification of a code list.
393 | type: an
394 | maxlen: 3
395 | [2] 6 - UN/ECE (United Nations - Economic Commission for Europe)
396 | id: 3055 - codeListResponsibleAgencyCoded
397 | Code identifying the agency responsible for a code list.
398 | type: an
399 | maxlen: 3
400 | [2] RIGA TERMINAL,TER,ZZZ
401 | C519 - relatedLocationOneIdentification
402 | Identification the first related location by code or name.
403 | [0] RIGA TERMINAL
404 | id: 3223 - relatedPlacelocationOneIdentification
405 | Specification of the first related place/location by code.
406 | type: an
407 | maxlen: 25
408 | [1] TER
409 | id: 1131 - codeListQualifier
410 | Identification of a code list.
411 | type: an
412 | maxlen: 3
413 | [2] ZZZ - Mutually defined
414 | id: 3055 - codeListResponsibleAgencyCoded
415 | Code identifying the agency responsible for a code list.
416 | type: an
417 | maxlen: 3
418 |
419 | LOC+98+LVRIX:139:6+RIGA TERMINAL:TER:ZZZ'
420 | CNT - controlTotal http://www.unece.org/trade/untdid/d95b/trsd/trsdcnt.htm
421 | (To provide control total.)
422 | [0] 16,1
423 | C270 - control
424 | Control total for checking integrity of a message or part of a message.
425 | [0] 16 - Total number of equipment
426 | id: 6069 - controlQualifier
427 | Determines the source data elements in the message which forms the
428 | basis for 6066 Control value.
429 | type: an
430 | maxlen: 3
431 | required: true
432 | [1] 1
433 | id: 6066 - controlValue
434 | Value obtained from summing the values specified by the Control
435 | Qualifier throughout the message (Hash total).
436 | type: n
437 | maxlen: 18
438 | required: true
439 |
440 | CNT+16:1'
441 | UNT - MessageTrailer http://www.unece.org/trade/untdid/d95b/trsd/trsdunt.htm
442 | (MessageTrailer)
443 | [0] 12
444 | 9900 - segmentsNumber
445 | segmentsNumber
446 | [1] 141260500001
447 | 9901 - msgRefNumber
448 | msgRefNumber
449 |
450 | UNT+12+141260500001'
451 | UNZ - InterchangeTrailer http://www.unece.org/trade/untdid/d95b/trsd/trsdunz.htm
452 | (InterchangeTrailer)
453 | [0] 1
454 | 9902 - interchangeControlCount
455 | interchangeControlCount
456 | [1] 1412605
457 | 9903 - interchangeControlRef
458 | interchangeControlRef
459 | ```
460 |
461 | EDI\Reader
462 | ------------------
463 | Read specific segment element values from parsed EDI file.
464 | Pass the reader a parser with a message already loaded.
465 |
466 | **INPUT**
467 | ```php
468 | $p = new EDI\Parser();
469 | $p->load($x);
470 | $r = new EDI\Reader($p);
471 | $sender = $r->readEdiDataValue('UNB', 2);
472 | $Dt = $r->readUNBDateTimeOfPreparation();
473 |
474 | ```
475 | See section about EDI\Parser above on how to load a file into a parser.
476 |
477 | **OUTPUT**
478 |
479 | Errors
480 | ```php
481 | $r->errors();
482 | ```
483 | Array
484 | ```php
485 | $r->getParsedFile();
486 | ```
487 |
488 | EDI\Interpreter
489 | ---------------
490 | Organizes the data parsed by EDI/Parser using the XML description of the message and the XML segments.
491 |
492 | **INPUT**
493 | ```php
494 | $p = new EDI\Parser();
495 | $p->load($edifile);
496 | $edi = $p->get();
497 |
498 | $mapping = new EDI\Mapping\MappingProvider('D95B');
499 |
500 | $analyser = new EDI\Analyser();
501 | $segs = $analyser->loadSegmentsXml($mapping->getSegments());
502 | $svc = $analyser->loadSegmentsXml($mapping->getServiceSegments(3));
503 |
504 | $interpreter = new EDI\Interpreter($mapping->getMessage('CODECO'), $segs, $svc);
505 | $prep = $interpreter->prepare($edi);
506 | ```
507 |
508 | **OUTPUT**
509 |
510 | JSON
511 | ```php
512 | $interpreter->getJson()
513 | ```
514 |
515 | JSON for interchange service segments (UNB / UNZ)
516 | ```php
517 | $interpreter->getJsonServiceSegments()
518 | ```
519 |
520 | Errors (per message)
521 | ```php
522 | $interpreter->getErrors()
523 | ```
524 |
525 | Example
526 | -------
527 |
528 | **Edifact**
529 |
530 | `DTM+7:201309200717:203'`
531 |
532 | **Array**
533 | ```php
534 | ['DTM',['7','201309200717','203']]
535 | ```
536 |
537 | Testing
538 | -------
539 | The package should be required with composer, alongside edifact-mapping. The tests then can be run simply with phpunit in the root of the package.
540 |
541 | Notes
542 | ------
543 | Valid characters are: A-Za-z0-9.,-()/'+:=?!"%&*;<> [UNECE](http://www.unece.org/trade/untdid/texts/d422_d.htm#p5.1)
544 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sabas/edifact",
3 | "description": "Tools in PHP for UN/EDIFACT",
4 | "keywords": [
5 | "EDI",
6 | "EDIFACT",
7 | "message",
8 | "container"
9 | ],
10 | "homepage": "https://github.com/php-edifact/edifact",
11 | "license": "LGPL-3.0-or-later",
12 | "authors": [
13 | {
14 | "name": "Stefano Sabatini",
15 | "email": "sabas88@gmail.com",
16 | "homepage": "http://stefanosabatini.com",
17 | "role": "Developer"
18 | },
19 | {
20 | "name": "Uldis Nelsons",
21 | "role": "Developer"
22 | },
23 | {
24 | "name": "Emil Vikström",
25 | "role": "Developer"
26 | }
27 | ],
28 | "require": {
29 | "php": "~8",
30 | "ext-json": "*",
31 | "ext-mbstring": "*",
32 | "ext-simplexml": "*"
33 | },
34 | "require-dev": {
35 | "php-edifact/edifact-mapping": "dev-master",
36 | "phpunit/phpunit": "~11.0",
37 | "symplify/easy-coding-standard": "^12.0"
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "EDI\\": "src/EDI/"
42 | }
43 | },
44 | "repositories": [
45 | {
46 | "type": "vcs",
47 | "url": "https://github.com/php-edifact/edifact"
48 | }
49 | ],
50 | "support": {
51 | "issues": "https://github.com/php-edifact/edifact/issues"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
11 | __DIR__ . '/src',
12 | __DIR__ . '/tests',
13 | ]);
14 |
15 | // this way you add a single rule
16 | $ecsConfig->rules([
17 | NoUnusedImportsFixer::class,
18 | ]);
19 |
20 | // this way you can add sets - group of rules
21 | $ecsConfig->sets([
22 | // run and fix, one by one
23 | SetList::SPACES,
24 | SetList::ARRAY,
25 | // SetList::DOCBLOCK,
26 | // SetList::NAMESPACES,
27 | // SetList::COMMENTS,
28 | SetList::PSR_12,
29 | SetList::LARAVEL,
30 | SetList::CLEAN_CODE,
31 | ]);
32 | };
33 |
--------------------------------------------------------------------------------
/src/EDI/Analyser.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public $segments;
17 |
18 | /**
19 | * @var string
20 | */
21 | public $directory;
22 |
23 | /**
24 | * @var array
25 | */
26 | private $jsonedi;
27 |
28 | /**
29 | * @var array
30 | */
31 | private $codes;
32 |
33 | public function setXml($segments, $codes)
34 | {
35 | $this->segments = $segments;
36 | $this->codes = $codes;
37 | }
38 |
39 | /**
40 | * @return array|false
41 | */
42 | public function loadMessageXml(string $message_xml_file)
43 | {
44 | $messageXmlString = \file_get_contents($message_xml_file);
45 | if ($messageXmlString === false) {
46 | return false;
47 | }
48 |
49 | $messageXml = new \SimpleXMLIterator($messageXmlString);
50 |
51 | return [
52 | 'defaults' => $this->readMessageDefaults($messageXml),
53 | 'segments' => $this->readXmlNodes($messageXml),
54 | ];
55 | }
56 |
57 | /**
58 | * get all data element codes
59 | *
60 | *
61 | * @return array|false
62 | */
63 | public function loadCodesXml(string $codesXml)
64 | {
65 | $codesXmlString = \file_get_contents($codesXml);
66 | if ($codesXmlString === false) {
67 | return false;
68 | }
69 |
70 | $codesXml = new \SimpleXMLIterator($codesXmlString);
71 | $this->codes = [];
72 | foreach ($codesXml as $codeCollection) {
73 | \assert($codeCollection instanceof \SimpleXMLIterator);
74 |
75 | $codeCollectionAttributes = $codeCollection->attributes();
76 | if ($codeCollectionAttributes === null) {
77 | continue;
78 | }
79 |
80 | $id = (string) $codeCollectionAttributes->id;
81 | $this->codes[$id] = [];
82 | foreach ($codeCollection as $codeNode) {
83 | \assert($codeNode instanceof \SimpleXMLIterator);
84 |
85 | $codeAttributes = $codeNode->attributes();
86 | if ($codeAttributes !== null) {
87 | $code = (string) $codeAttributes->id;
88 | $this->codes[$id][$code] = (string) $codeAttributes->desc;
89 | }
90 | }
91 | }
92 |
93 | return $this->codes;
94 | }
95 |
96 | /**
97 | * convert segment definition from XML to array. Sequence of data_elements and
98 | * composite_data_element same as in XML
99 | * For single message, it's vailable also in (new EDI\Mapping\MappingProvider($version))->loadSegmentsXml()
100 | *
101 | *
102 | * @return array|false
103 | */
104 | public function loadSegmentsXml(string $segmentXmlFile, bool $discardOldSegments = true)
105 | {
106 | if ($discardOldSegments) {
107 | $this->segments = [];
108 | }
109 |
110 | $segmentsXml = \file_get_contents($segmentXmlFile);
111 | if ($segmentsXml === false) {
112 | return false;
113 | }
114 |
115 | $xml = \simplexml_load_string($segmentsXml);
116 | if ($xml === false) {
117 | return false;
118 | }
119 | // free memory
120 | unset($segmentsXml);
121 |
122 | foreach ($xml as $segmentNode) {
123 | \assert($segmentNode instanceof \SimpleXMLElement);
124 |
125 | $segmentNodeAttributes = $segmentNode->attributes();
126 | if ($segmentNodeAttributes === null) {
127 | continue;
128 | }
129 |
130 | $qualifier = (string) $segmentNodeAttributes->id;
131 | $segment = [];
132 | $segment['attributes'] = $this->readAttributesArray($segmentNode);
133 | $details = $this->readXmlNodes($segmentNode);
134 | if (! empty($details)) {
135 | $segment['details'] = $details;
136 | }
137 | $this->segments[$qualifier] = $segment;
138 | }
139 |
140 | return $this->segments;
141 | }
142 |
143 | /**
144 | * Load segment definitions from multiple files
145 | *
146 | * @see Analyser::loadSegmentsXml()
147 | *
148 | * @param string[] $segmentXmlFiles
149 | * @return array|false
150 | */
151 | public function loadMultiSegmentsXml(array $segmentXmlFiles)
152 | {
153 | foreach ($segmentXmlFiles as $xmlFile) {
154 | if (! $result = $this->loadSegmentsXml($xmlFile, false)) {
155 | return $result;
156 | }
157 | }
158 |
159 | return $this->segments;
160 | }
161 |
162 | /**
163 | * create readable EDI MESSAGE with comments
164 | *
165 | * @param array $data by EDI\Parser:parse() created array from plain EDI message
166 | * @param array|null $rawSegments (optional) List of raw segments from EDI\Parser::getRawSegments
167 | * @return string file
168 | */
169 | public function process(array $data, ?array $rawSegments = null): string
170 | {
171 | $r = [];
172 | foreach ($data as $nrow => $segment) {
173 | $id = $segment[0];
174 |
175 | $r[] = '';
176 | $jsonsegment = [];
177 | if (isset($rawSegments[$nrow])) {
178 | $r[] = \trim($rawSegments[$nrow]);
179 | }
180 |
181 | if (isset($this->segments[$id])) {
182 | $attributes = $this->segments[$id]['attributes'];
183 | $details_desc = $this->segments[$id]['details'];
184 |
185 | $idHeader = $id.' - '.$attributes['name'];
186 | if ($this->directory && $id !== 'UNB') {
187 | $idHeader .= ' https://service.unece.org/trade/untdid/'.strtolower($this->directory).'/trsd/trsd'.strtolower($id).'.htm';
188 | }
189 | $r[] = $idHeader;
190 | $r[] = ' ('.\wordwrap($attributes['desc'], 75, \PHP_EOL.' ').')';
191 |
192 | $jsonelements = [
193 | 'segmentCode' => $id,
194 | ];
195 | foreach ($segment as $idx => $detail) {
196 | $n = $idx - 1;
197 | if ($idx == 0 || ! isset($details_desc[$n])) {
198 | continue;
199 | }
200 | $d_desc_attr = $details_desc[$n]['attributes'];
201 | $l1 = ' '.$d_desc_attr['id'].' - '.$d_desc_attr['name'];
202 | $l2 = ' '.\wordwrap($d_desc_attr['desc'], 71, \PHP_EOL.' ');
203 |
204 | if (\is_array($detail)) {
205 | $r[] = ' ['.$n.'] '.\implode(',', $detail);
206 | $r[] = $l1;
207 | $r[] = $l2;
208 |
209 | $jsoncomposite = [];
210 | if (isset($details_desc[$n]['details'])) {
211 | $sub_details_desc = $details_desc[$n]['details'];
212 |
213 | foreach ($detail as $d_n => $d_detail) {
214 | $d_sub_desc_attr = $sub_details_desc[$d_n]['attributes'];
215 | $codeElementId = $d_sub_desc_attr['id'];
216 | $line = ' ['.$d_n.'] '.$d_detail;
217 | if (isset($this->codes[(int) $codeElementId][$d_detail])) {
218 | $line .= ' - '.\wordwrap($this->codes[$codeElementId][$d_detail], 69, \PHP_EOL.' ');
219 | }
220 | $r[] = $line;
221 |
222 | $r[] = ' id: '.$codeElementId.' - '.$d_sub_desc_attr['name'];
223 | if (isset($d_sub_desc_attr['desc'])) {
224 | $r[] = ' '.\wordwrap($d_sub_desc_attr['desc'], 69, \PHP_EOL.' ');
225 | }
226 | $r[] = ' type: '.$d_sub_desc_attr['type'];
227 |
228 | if (isset($jsoncomposite[$d_sub_desc_attr['name']])) {
229 | if (is_array($jsoncomposite[$d_sub_desc_attr['name']])) {
230 | $jsoncomposite[$d_sub_desc_attr['name']][] = $d_detail;
231 | } else {
232 | $jsoncomposite[$d_sub_desc_attr['name']] = [
233 | $jsoncomposite[$d_sub_desc_attr['name']],
234 | $d_detail
235 | ];
236 | }
237 | } else {
238 | $jsoncomposite[$d_sub_desc_attr['name']] = $d_detail;
239 | }
240 |
241 | if (isset($d_sub_desc_attr['maxlength'])) {
242 | $r[] = ' maxlen: '.$d_sub_desc_attr['maxlength'];
243 | }
244 | if (isset($d_sub_desc_attr['required'])) {
245 | $r[] = ' required: '.$d_sub_desc_attr['required'];
246 | }
247 | if (isset($d_sub_desc_attr['length'])) {
248 | $r[] = ' length: '.$d_sub_desc_attr['length'];
249 | }
250 |
251 | // check for skipped data
252 | unset(
253 | $d_sub_desc_attr['id'],
254 | $d_sub_desc_attr['name'],
255 | $d_sub_desc_attr['desc'],
256 | $d_sub_desc_attr['type'],
257 | $d_sub_desc_attr['maxlength'],
258 | $d_sub_desc_attr['required'],
259 | $d_sub_desc_attr['length']
260 | );
261 |
262 | /*
263 | if (!empty($d_sub_desc_attr)) {
264 | var_dump($d_sub_desc_attr);
265 | }
266 | */
267 | }
268 | }
269 | $jsonelements[$d_desc_attr['name']] = $jsoncomposite;
270 | } else {
271 | $codeElementId = $d_desc_attr['id'];
272 | $line = ' ['.$n.'] '.$detail;
273 | if (isset($this->codes[(int) $codeElementId][$detail])) {
274 | /*
275 | * for retrieving code element description when first element of the segment
276 | * is a data element and not a composite one. Ex: NAD segment.
277 | * We rewrite also l1 line for adding 'id:' prefix before data element id.
278 | * It's just a cosmetic fix
279 | */
280 | $line .= ' - '.\wordwrap($this->codes[$codeElementId][$detail], 71, \PHP_EOL.' ');
281 | $l1 = ' id: '.$d_desc_attr['id'].' - '.$d_desc_attr['name'];
282 | }
283 | $r[] = $line;
284 | $r[] = $l1;
285 | $r[] = $l2;
286 | $jsonelements[$d_desc_attr['name']] = $detail;
287 | }
288 | }
289 | $jsonsegment[$attributes['name']] = $jsonelements;
290 | } else {
291 | $r[] = $id;
292 | $jsonsegment['UnrecognisedType'] = $segment;
293 | }
294 | $this->jsonedi[] = $jsonsegment;
295 | }
296 |
297 | return \implode(\PHP_EOL, $r);
298 | }
299 |
300 | /**
301 | * return the processed EDI in json format
302 | *
303 | * @return false|string
304 | */
305 | public function getJson()
306 | {
307 | return \json_encode($this->jsonedi);
308 | }
309 |
310 | /**
311 | * read default values in given message xml
312 | */
313 | protected function readMessageDefaults(\SimpleXMLElement $message): array
314 | {
315 | // init
316 | $defaults = [];
317 |
318 | /** @var \SimpleXMLElement $defaultValueNode */
319 | foreach ($message->defaults[0] ?? [] as $defaultValueNode) {
320 | $attributes = $defaultValueNode->attributes();
321 | $id = (string) $attributes->id;
322 | $defaults[$id] = (string) $attributes->value;
323 | }
324 |
325 | return $defaults;
326 | }
327 |
328 | /**
329 | * read message segments and groups
330 | */
331 | protected function readXmlNodes(\SimpleXMLElement $element): array
332 | {
333 | $arrayElements = [];
334 | foreach ($element as $name => $node) {
335 | if ($name == 'defaults') {
336 | continue;
337 | }
338 | $arrayElement = [];
339 | $arrayElement['type'] = $name;
340 | $arrayElement['attributes'] = $this->readAttributesArray($node);
341 | $details = $this->readXmlNodes($node);
342 | if (! empty($details)) {
343 | $arrayElement['details'] = $details;
344 | }
345 | $arrayElements[] = $arrayElement;
346 | }
347 |
348 | return $arrayElements;
349 | }
350 |
351 | /**
352 | * return an xml elements attributes in as array
353 | */
354 | protected function readAttributesArray(\SimpleXMLElement $element): array
355 | {
356 | $attributes = [];
357 | foreach ($element->attributes() ?? [] as $attrName => $attr) {
358 | $attributes[(string) $attrName] = (string) $attr;
359 | }
360 |
361 | return $attributes;
362 | }
363 | }
364 |
--------------------------------------------------------------------------------
/src/EDI/Encoder.php:
--------------------------------------------------------------------------------
1 | setUNA(":+.? '", false);
72 | if ($array === null) {
73 | return;
74 | }
75 |
76 | /** @noinspection UnusedFunctionResultInspection */
77 | $this->encode($array, $compact);
78 | }
79 |
80 | /**
81 | * @param array[] $array
82 | * @param bool $compact All segments on a single line?
83 | */
84 | public function encode(array $array, $compact = true): string
85 | {
86 | $this->originalArray = $array;
87 | $this->compact = $compact;
88 |
89 | $edistring = '';
90 | $count = \count($array);
91 | $k = 0;
92 | foreach ($array as $row) {
93 | $k++;
94 | $row = \array_values($row);
95 | $edistring .= $this->encodeSegment($row);
96 | if (! $compact && $k < $count) {
97 | $edistring .= "\n";
98 | }
99 | }
100 | $this->output = $edistring;
101 |
102 | return $edistring;
103 | }
104 |
105 | public function encodeSegment(array $row): string
106 | {
107 | // init
108 | $str = '';
109 | $t = \count($row);
110 |
111 | /** @noinspection AlterInForeachInspection */
112 | foreach ($row as $i => &$iValue) {
113 | if (\is_array($iValue)) {
114 | if (
115 | \count($iValue) === 1
116 | &&
117 | \is_array(\reset($iValue))
118 | ) {
119 | $iValue = \array_pop($iValue);
120 | }
121 |
122 | /** @noinspection NotOptimalIfConditionsInspection */
123 | if (\is_array($iValue)) {
124 | foreach ($iValue as &$temp) {
125 | $temp = $this->escapeValue($temp);
126 | }
127 | unset($temp);
128 | }
129 |
130 | $elm = \implode($this->sepComp, $iValue);
131 | } else {
132 | $elm = $this->escapeValue($iValue);
133 | }
134 |
135 | $str .= $elm;
136 | if ($i == $t - 1) {
137 | break;
138 | }
139 | $str .= $this->sepData;
140 | }
141 |
142 | $str .= $this->symbEnd;
143 |
144 | return $str;
145 | }
146 |
147 | public function get(): string
148 | {
149 | if ($this->UNAActive) {
150 | $una = 'UNA'.$this->sepComp.
151 | $this->sepData.
152 | $this->sepDec.
153 | $this->symbRel.
154 | $this->symbRep.
155 | $this->symbEnd;
156 | if ($this->compact === false) {
157 | $una .= "\n";
158 | }
159 |
160 | return $una.$this->output;
161 | }
162 |
163 | return $this->output;
164 | }
165 |
166 | public function setUNA(string $chars, bool $user_call = true): bool
167 | {
168 | if (\strlen($chars) == 6) {
169 | $this->sepComp = $chars[0];
170 | $this->sepData = $chars[1];
171 | $this->sepDec = $chars[2];
172 | $this->symbRel = $chars[3];
173 | $this->symbRep = $chars[4];
174 | $this->symbEnd = $chars[5];
175 |
176 | if ($user_call) {
177 | $this->enableUNA();
178 | }
179 |
180 | if ($this->output != '') {
181 | $this->output = $this->encode($this->originalArray);
182 | }
183 |
184 | return true;
185 | }
186 |
187 | return false;
188 | }
189 |
190 | /**
191 | * @return void
192 | */
193 | public function enableUNA()
194 | {
195 | $this->UNAActive = true;
196 | }
197 |
198 | /**
199 | * @return void
200 | */
201 | public function disableUNA()
202 | {
203 | $this->UNAActive = false;
204 | }
205 |
206 | /**
207 | * @param int|string $str
208 | */
209 | private function escapeValue(&$str): string
210 | {
211 | $search = [
212 | $this->symbRel,
213 | $this->sepComp,
214 | $this->sepData,
215 | $this->symbEnd,
216 | ];
217 | $replace = [
218 | $this->symbRel.$this->symbRel,
219 | $this->symbRel.$this->sepComp,
220 | $this->symbRel.$this->sepData,
221 | $this->symbRel.$this->symbEnd,
222 | ];
223 |
224 | return \str_replace($search, $replace, (string) $str);
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/EDI/Interpreter.php:
--------------------------------------------------------------------------------
1 | 'Missing required segment',
18 | 'MISSINGREQUIREDGROUP' => 'Missing required group',
19 | 'NOTCONFORMANT' => "It looks like that this message isn't conformant to the mapping provided. (Not all segments were added)",
20 | 'TOOMANYELEMENTS_COMPOSITE' => 'This composite data element has more elements than expected',
21 | 'TOOMANYELEMENTS' => 'This segment has more data elements than expected',
22 | 'MISSINGINTERCHANGEDELIMITER' => 'The file has at least one UNB or UNZ missing',
23 | 'MISSINGMESSAGEDELIMITER' => 'The message has at least one UNH or UNT missing',
24 | 'TOOMANYSEGMENTS' => 'The message has some additional segments beyond the maximum repetition allowed',
25 | 'TOOMANYGROUPS' => 'The message has some additional groups beyond the maximum repetition allowed',
26 | 'SPURIOUSSEGMENT' => 'This segment is spurious',
27 | ];
28 |
29 | /**
30 | * @var array
31 | */
32 | public $segmentTemplates = [
33 | 'DTM' => ['DTM', '999', 'XXX'],
34 | ];
35 |
36 | /**
37 | * @var array
38 | */
39 | public $groupTemplates = [
40 | 'SG1' => [['TDT', '20', 'XXX']],
41 | ];
42 |
43 | /**
44 | * @var bool
45 | */
46 | private $patchFiles = true;
47 |
48 | /**
49 | * @var bool
50 | */
51 | private $segmentGroup = true;
52 |
53 | /**
54 | * @var bool
55 | */
56 | private $forceArrayWhenRepeatable = false;
57 |
58 | /**
59 | * @var \SimpleXMLElement
60 | */
61 | private $xmlMsg;
62 |
63 | /**
64 | * @var array
65 | */
66 | private $xmlSeg;
67 |
68 | /**
69 | * @var array
70 | */
71 | private $xmlSvc;
72 |
73 | /**
74 | * @var array
75 | */
76 | private $ediGroups;
77 |
78 | /**
79 | * @var array
80 | */
81 | private $errors;
82 |
83 | /**
84 | * @var array
85 | */
86 | private $msgs;
87 |
88 | /**
89 | * @var array
90 | */
91 | private $serviceSeg;
92 |
93 | /**
94 | * @var array
95 | */
96 | private $codes;
97 |
98 | /**
99 | * @var callable
100 | */
101 | private $comparisonFunction;
102 |
103 | /**
104 | * @var callable
105 | */
106 | private $replacementFunction;
107 |
108 | /**
109 | * @var string
110 | */
111 | private $outputKey = 'name';
112 |
113 | /**
114 | * @var string
115 | */
116 | private $currentGroup = '';
117 |
118 | /**
119 | * @var string
120 | */
121 | private $currentGroupHeader = '';
122 |
123 | /**
124 | * Split multiple messages and process
125 | *
126 | * @param string $xmlMsg Path to XML Message representation
127 | * @param array $xmlSeg Segments processed by EDI\Analyser::loadSegmentsXml or EDI\Mapping\MappingProvider
128 | * @param array $xmlSvc Service segments processed by EDI\Analyser::loadSegmentsXml or EDI\Mapping\MappingProvider
129 | * @param array|null $messageTextConf Personalization of error messages
130 | */
131 | public function __construct(string $xmlMsg, array $xmlSeg, array $xmlSvc, ?array $messageTextConf = null)
132 | {
133 | // simplexml_load_file: This can be affected by a PHP bug #62577 (https://bugs.php.net/bug.php?id=62577)
134 | $xmlData = \file_get_contents($xmlMsg);
135 | if ($xmlData === false) {
136 | throw new \InvalidArgumentException('file_get_contents for "'.$xmlMsg.'" failed');
137 | }
138 |
139 | $xmlMsgTmp = \simplexml_load_string($xmlData);
140 | if ($xmlMsgTmp === false) {
141 | throw new \InvalidArgumentException('simplexml_load_string for "'.$xmlMsg.'" failed');
142 | }
143 |
144 | $this->xmlMsg = $xmlMsgTmp;
145 | $this->xmlSeg = $xmlSeg;
146 | $this->xmlSvc = $xmlSvc;
147 | if ($messageTextConf !== null) {
148 | $this->messageTextConf = \array_replace($this->messageTextConf, $messageTextConf);
149 | }
150 | $this->errors = [];
151 |
152 | $this->comparisonFunction = static function ($segment, $elm) {
153 | return $segment[0] == $elm['id'];
154 | };
155 | }
156 |
157 | /**
158 | * @return void
159 | */
160 | public function togglePatching(bool $flag)
161 | {
162 | $this->patchFiles = $flag;
163 |
164 | return $this;
165 | }
166 |
167 | /**
168 | * @return void
169 | */
170 | public function toggleSegmentGroup(bool $flag)
171 | {
172 | $this->segmentGroup = $flag;
173 |
174 | return $this;
175 | }
176 |
177 | /**
178 | * @return void
179 | */
180 | public function forceArrayWhenRepeatable(bool $flag)
181 | {
182 | $this->forceArrayWhenRepeatable = $flag;
183 |
184 | return $this;
185 | }
186 |
187 | /**
188 | * Patch the error messages array
189 | *
190 | * @param array $messageTextConf An array with same keys as the internal $messageTextConf
191 | * @return void
192 | */
193 | public function setMessageTextConf(array $messageTextConf)
194 | {
195 | $this->messageTextConf = \array_replace($this->messageTextConf, $messageTextConf);
196 |
197 | return $this;
198 | }
199 |
200 | /**
201 | * Add fake segments used to patch the message if a required segment is missing
202 | *
203 | * @param array $segmentTemplates An array with segments (having the segment name as key)
204 | * @return void
205 | */
206 | public function setSegmentTemplates(array $segmentTemplates)
207 | {
208 | $this->segmentTemplates = $segmentTemplates;
209 |
210 | return $this;
211 | }
212 |
213 | /**
214 | * Add fake groups used to patch the message if a required group is missing
215 | *
216 | * @param array $groupTemplates An array with segments (having the group name as key)
217 | * @return void
218 | */
219 | public function setGroupTemplates(array $groupTemplates)
220 | {
221 | $this->groupTemplates = $groupTemplates;
222 |
223 | return $this;
224 | }
225 |
226 | /**
227 | * Set the data parsed from the xml list of codes
228 | *
229 | * @param array $codes An array with codes from the chosen directory
230 | * @return void
231 | */
232 | public function setCodes(array $codes)
233 | {
234 | $this->codes = $codes;
235 |
236 | return $this;
237 | }
238 |
239 | /**
240 | * Function that implements how to compare a message segment and its definition
241 | *
242 | * @param callable $func A function accepting two arguments, first is the segment array, then the element definition
243 | * @return void
244 | */
245 | public function setComparisonFunction(callable $func)
246 | {
247 | $this->comparisonFunction = $func;
248 |
249 | return $this;
250 | }
251 |
252 | /**
253 | * Function that replaces a segment
254 | *
255 | * @param callable $func A function accepting two arguments, first is the segment array, then the element definition
256 | * @return void
257 | */
258 | public function setReplacementFunction(callable $func)
259 | {
260 | $this->replacementFunction = $func;
261 |
262 | return $this;
263 | }
264 |
265 | /**
266 | * Set to true if UNCEFACT XML ID should be used instead of names
267 | *
268 | *
269 | * @return void
270 | */
271 | public function toggleUseIdInsteadOfNameForOutput(bool $toggle)
272 | {
273 | if ($toggle) {
274 | $this->outputKey = 'id';
275 | } else {
276 | $this->outputKey = 'name';
277 | }
278 |
279 | return $this;
280 | }
281 |
282 | /**
283 | * Split multiple messages and process
284 | *
285 | * @param array $parsed An array coming from EDI\Parser
286 | */
287 | public function prepare(array $parsed): array
288 | {
289 | $this->msgs = $this->splitMessages($parsed, $this->errors);
290 | $groups = [];
291 | $service = $this->msgs['service'];
292 | $this->serviceSeg = $this->processService($service);
293 |
294 | foreach ($this->msgs as $k => $msg) {
295 | if ($k === 'service') {
296 | continue;
297 | }
298 | $grouped = $this->loopMessage($msg, $this->xmlMsg, $this->errors);
299 | $groups[] = $grouped['message'];
300 | }
301 | $this->ediGroups = $groups;
302 |
303 | return $groups;
304 | }
305 |
306 | /**
307 | * Get result as json.
308 | *
309 | *
310 | * @return false|string
311 | */
312 | public function getJson(bool $pretty = false)
313 | {
314 | if ($pretty) {
315 | return \json_encode($this->ediGroups, \JSON_PRETTY_PRINT);
316 | }
317 |
318 | return \json_encode($this->ediGroups);
319 | }
320 |
321 | /**
322 | * Get EDI groups.
323 | *
324 | * @return array
325 | */
326 | public function getEdiGroups()
327 | {
328 | return $this->ediGroups;
329 | }
330 |
331 | /**
332 | * Set EDI groups.
333 | *
334 | * @return array
335 | */
336 | public function setEdiGroups($groups)
337 | {
338 | $this->ediGroups = $groups;
339 | }
340 |
341 | /**
342 | * Get errors
343 | */
344 | public function getErrors(): array
345 | {
346 | return $this->errors;
347 | }
348 |
349 | /**
350 | * Get splitted messages.
351 | */
352 | public function getMessages(): array
353 | {
354 | return $this->msgs;
355 | }
356 |
357 | /**
358 | * Get service segments.
359 | *
360 | * @return array
361 | */
362 | public function getServiceSegments()
363 | {
364 | return $this->serviceSeg;
365 | }
366 |
367 | /**
368 | * Get json service segments.
369 | *
370 | *
371 | * @return false|string
372 | */
373 | public function getJsonServiceSegments(bool $pretty = false)
374 | {
375 | if ($pretty) {
376 | return \json_encode($this->serviceSeg, \JSON_PRETTY_PRINT);
377 | }
378 |
379 | return \json_encode($this->serviceSeg);
380 | }
381 |
382 | /**
383 | * Split multiple messages
384 | *
385 | * @param array $parsed An array coming from EDI\Parser
386 | * @param array $errors
387 | */
388 | private function splitMessages(&$parsed, &$errors): array
389 | {
390 | // init
391 | $messages = [];
392 | $tmpmsg = [];
393 | $service = [];
394 | $hasInterchangeDelimiters = 0;
395 | $hasMessageDelimiters = 0;
396 |
397 | foreach ($parsed as $c => $segment) {
398 | switch ($segment[0]) {
399 | case 'UNB':
400 | $hasInterchangeDelimiters = 0;
401 | $hasInterchangeDelimiters++;
402 | $service['UNB'] = $segment;
403 |
404 | break;
405 | case 'UNZ':
406 | $hasInterchangeDelimiters--;
407 | if ($hasInterchangeDelimiters != 0) {
408 | $sid = ($hasInterchangeDelimiters < 0) ? 'UNB' : 'UNZ';
409 | $errors[] = [
410 | 'text' => $this->messageTextConf['MISSINGINTERCHANGEDELIMITER'],
411 | 'position' => $c,
412 | 'segmentId' => $sid,
413 | ];
414 | }
415 | $service['UNZ'] = $segment;
416 | if ($this->patchFiles && $hasMessageDelimiters > 0) {
417 | $segment = ['UNT', '0', '1'];
418 | $hasMessageDelimiters--;
419 | $tmpmsg[] = $segment;
420 | $messages[] = $tmpmsg;
421 | }
422 | break;
423 | case 'UNH':
424 | $hasMessageDelimiters = 0;
425 | $hasMessageDelimiters++;
426 | $tmpmsg = [$segment];
427 |
428 | break;
429 | case 'UNT':
430 | $hasMessageDelimiters--;
431 | $tmpmsg[] = $segment;
432 | $messages[] = $tmpmsg;
433 | if ($hasMessageDelimiters != 0) {
434 | $sid = ($hasMessageDelimiters < 0) ? 'UNH' : 'UNT';
435 | $errors[] = [
436 | 'text' => $this->messageTextConf['MISSINGMESSAGEDELIMITER'],
437 | 'position' => $c,
438 | 'segmentId' => $sid,
439 | ];
440 | }
441 |
442 | break;
443 | default:
444 | $tmpmsg[] = $segment;
445 |
446 | break;
447 | }
448 | }
449 |
450 | if ($hasInterchangeDelimiters != 0) {
451 | $sid = ($hasInterchangeDelimiters < 0) ? 'UNB' : 'UNZ';
452 | $errors[] = [
453 | 'text' => $this->messageTextConf['MISSINGINTERCHANGEDELIMITER'],
454 | 'position' => $c ?? '',
455 | 'segmentId' => $sid,
456 | ];
457 | }
458 |
459 | if ($hasMessageDelimiters != 0) {
460 | $sid = ($hasMessageDelimiters < 0) ? 'UNH' : 'UNT';
461 | $errors[] = [
462 | 'text' => $this->messageTextConf['MISSINGMESSAGEDELIMITER'],
463 | 'position' => $c ?? '',
464 | 'segmentId' => $sid,
465 | ];
466 | }
467 |
468 | $messages['service'] = $service;
469 |
470 | return $messages;
471 | }
472 |
473 | /**
474 | * Transform a parsed edi in its groupings
475 | *
476 | * @param array $message Single message (Without UNB and UNZ)
477 | * @param \SimpleXMLElement $xml The xml representation of the message
478 | */
479 | private function loopMessage(array &$message, \SimpleXMLElement $xml, array &$errors): array
480 | {
481 | // init
482 | $groupedEdi = [];
483 | $segmentIdx = 0;
484 |
485 | foreach ($xml->children() as $elm) {
486 | if ($elm->getName() == 'group') {
487 | $this->processXmlGroup($elm, $message, $segmentIdx, $groupedEdi, $errors);
488 | } elseif ($elm->getName() == 'segment') {
489 | $this->processXmlSegment($elm, $message, $segmentIdx, $groupedEdi, $errors);
490 | }
491 | }
492 |
493 | if ($segmentIdx != \count($message)) {
494 | $errors[] = [
495 | 'text' => $this->messageTextConf['NOTCONFORMANT'],
496 | 'position' => $segmentIdx,
497 | 'segmentId' => $message[$segmentIdx][0],
498 | ];
499 | }
500 |
501 | return [
502 | 'message' => $groupedEdi,
503 | 'errors' => $errors,
504 | ];
505 | }
506 |
507 | /**
508 | * Process an XML Group
509 | *
510 | *
511 | * @return void
512 | */
513 | private function processXmlGroup(\SimpleXMLElement $elm, array &$message, int &$segmentIdx, array &$array, array &$errors)
514 | {
515 | // init
516 | $groupVisited = false;
517 | $newGroup = [];
518 |
519 | $this->currentGroupHeader = $elm->children()[0]['id']->__toString();
520 | $this->currentGroup = $elm['id']->__toString();
521 |
522 | for ($g = 0; $g < $elm['maxrepeat']; $g++) {
523 | $grouptemp = [];
524 | if ($message[$segmentIdx][0] != $elm->children()[0]['id']) {
525 | if (
526 | ! $groupVisited
527 | &&
528 | isset($elm['required'])
529 | ) {
530 | $elmType = $elm['id']->__toString();
531 | $fixed = false;
532 | if ($this->patchFiles && isset($this->groupTemplates[$elmType])) {
533 | \array_splice($message, $segmentIdx, 0, $this->groupTemplates[$elmType]);
534 | $fixed = true;
535 | }
536 |
537 | $errors[] = [
538 | 'text' => $this->messageTextConf['MISSINGREQUIREDGROUP'].' '.($fixed ? ' (patched)' : ''),
539 | 'position' => $segmentIdx,
540 | 'segmentId' => $elmType,
541 | ];
542 | } else {
543 | break;
544 | }
545 | }
546 |
547 | foreach ($elm->children() as $elm2) {
548 | if ($elm2->getName() == 'group') {
549 | $this->processXmlGroup($elm2, $message, $segmentIdx, $grouptemp, $errors);
550 | $this->currentGroupHeader = $elm->children()[0]['id']->__toString();
551 | $this->currentGroup = $elm['id']->__toString();
552 | } else {
553 | $this->processXmlSegment($elm2, $message, $segmentIdx, $grouptemp, $errors);
554 | }
555 | $groupVisited = true;
556 | }
557 |
558 | $newGroup[] = $grouptemp;
559 | }
560 |
561 | // if additional groups are detected we are violating the maxrepeat attribute
562 | while (isset($message[$segmentIdx]) && \call_user_func($this->comparisonFunction, $message[$segmentIdx], $elm->children()[0])) {
563 | $errors[] = [
564 | 'text' => $this->messageTextConf['TOOMANYGROUPS'],
565 | 'position' => $segmentIdx,
566 | 'segmentId' => (string) $elm['id'],
567 | ];
568 | $this->currentGroup = $elm['id']->__toString();
569 |
570 | foreach ($elm->children() as $elm2) {
571 | if ($elm2->getName() == 'group') {
572 | $this->processXmlGroup($elm2, $message, $segmentIdx, $grouptemp, $errors);
573 | $this->currentGroupHeader = $elm->children()[0]['id']->__toString();
574 | $this->currentGroup = $elm['id']->__toString();
575 | } else {
576 | $this->processXmlSegment($elm2, $message, $segmentIdx, $grouptemp, $errors);
577 | }
578 | $groupVisited = true;
579 | }
580 |
581 | $newGroup[] = $grouptemp;
582 | }
583 |
584 | $this->currentGroupHeader = '';
585 | $this->currentGroup = '';
586 |
587 | if (\count($newGroup) === 0) {
588 | return;
589 | }
590 |
591 | $array[$elm['id']->__toString()] = $newGroup;
592 | }
593 |
594 | /**
595 | * Process an XML Segment.
596 | *
597 | *
598 | * @return void
599 | */
600 | private function processXmlSegment(\SimpleXMLElement $elm, array &$message, int &$segmentIdx, array &$array, array &$errors)
601 | {
602 | // init
603 | $segmentVisited = false;
604 |
605 | for ($i = 0; $i < $elm['maxrepeat']; $i++) {
606 | if (\call_user_func($this->comparisonFunction, $message[$segmentIdx], $elm)) {
607 | $jsonMessage = $this->processSegment($message[$segmentIdx], $this->xmlSeg, $segmentIdx, $errors);
608 | $segmentVisited = true;
609 | $this->doAddArray($array, $jsonMessage, (int) $elm['maxrepeat']);
610 | $segmentIdx++;
611 | } elseif ($this->replacementFunction !== null && $replacementSegment = \call_user_func($this->replacementFunction, $message[$segmentIdx], $elm)) {
612 | //the function shall return false, true or a new segment
613 | $fixed = false;
614 |
615 | if ($elm['replacewith'] !== null) {
616 | $elmType = (string) $elm['replacewith'];
617 | } else {
618 | $elmType = (string) $elm['id'];
619 | }
620 |
621 | if ($this->patchFiles && $replacementSegment === true && isset($this->segmentTemplates[$elmType])) {
622 | $jsonMessage = $this->processSegment($this->segmentTemplates[$elmType], $this->xmlSeg, $segmentIdx, $errors);
623 | $fixed = true;
624 | $this->doAddArray($array, $jsonMessage, (int) $elm['maxrepeat']);
625 | }
626 |
627 | if ($this->patchFiles && $replacementSegment !== true) {
628 | $jsonMessage = $this->processSegment($replacementSegment, $this->xmlSeg, $segmentIdx, $errors);
629 | $fixed = true;
630 | $this->doAddArray($array, $jsonMessage, (int) $elm['maxrepeat']);
631 | }
632 |
633 | $errors[] = [
634 | 'text' => $this->messageTextConf['MISSINGREQUIREDSEGMENT'].' '.($fixed ? ' (replaced)' : ''),
635 | 'position' => $segmentIdx,
636 | 'segmentId' => (string) $elm['id'],
637 | ];
638 | $segmentIdx++;
639 |
640 | continue;
641 | } else {
642 | if (! $segmentVisited && isset($elm['required'])) {
643 | $segmentVisited = true;
644 | if (isset($message[$segmentIdx + 1]) && \call_user_func($this->comparisonFunction, $message[$segmentIdx + 1], $elm)) {
645 | $errors[] = [
646 | 'text' => $this->messageTextConf['SPURIOUSSEGMENT'].($this->patchFiles ? ' (skipped)' : ''),
647 | 'position' => $segmentIdx,
648 | 'segmentId' => (string) $message[$segmentIdx][0],
649 | ];
650 | $segmentIdx++; //just move the index
651 | $i--; //but don't count as repetition
652 |
653 | continue;
654 | }
655 |
656 | if ($elm['replacewith'] !== null) {
657 | $elmType = (string) $elm['replacewith'];
658 | } else {
659 | $elmType = (string) $elm['id'];
660 | }
661 |
662 | $fixed = false;
663 |
664 | if ($this->patchFiles && isset($this->segmentTemplates[$elmType])) {
665 | $jsonMessage = $this->processSegment($this->segmentTemplates[$elmType], $this->xmlSeg, $segmentIdx, $errors);
666 | $fixed = true;
667 | $this->doAddArray($array, $jsonMessage, (int) $elm['maxrepeat']);
668 | }
669 |
670 | $errors[] = [
671 | 'text' => $this->messageTextConf['MISSINGREQUIREDSEGMENT'].' '.($fixed ? ' (patched)' : ''),
672 | 'position' => $segmentIdx,
673 | 'segmentId' => (string) $elm['id'],
674 | ];
675 |
676 | }
677 |
678 | return;
679 | }
680 | }
681 |
682 | // if additional segments are detected we are violating the maxrepeat attribute
683 | $loopMove = 0;
684 | while (
685 | isset($message[$segmentIdx]) &&
686 | \call_user_func($this->comparisonFunction, $message[$segmentIdx + $loopMove], $elm) &&
687 | (string) $elm['id'] !== $this->currentGroupHeader
688 | ) {
689 | $errors[] = [
690 | 'text' => $this->messageTextConf['TOOMANYSEGMENTS'].($this->patchFiles ? ' (skipped)' : ''),
691 | 'position' => $segmentIdx,
692 | 'segmentId' => (string) $elm['id'],
693 | ];
694 |
695 | if ($this->patchFiles) {
696 | $segmentIdx++;
697 | } else {
698 | $loopMove++; //we move to the next segment only for this loop if the patching isn't active
699 | }
700 | }
701 | }
702 |
703 | /**
704 | * Adds a processed segment to the current group.
705 | *
706 | * @param array $array a reference to the group
707 | * @param array $jsonMessage a segment processed by processSegment()
708 | * @return void
709 | */
710 | private function doAddArray(array &$array, array &$jsonMessage, $maxRepeat = 1)
711 | {
712 | if (isset($array[$jsonMessage['key']])) {
713 | if (
714 | isset($array[$jsonMessage['key']]['segmentCode'])
715 | ||
716 | $jsonMessage['key'] === 'UnrecognisedType'
717 | ) {
718 | $temp = $array[$jsonMessage['key']];
719 | $array[$jsonMessage['key']] = [];
720 | $array[$jsonMessage['key']][] = $temp;
721 | }
722 | $array[$jsonMessage['key']][] = $jsonMessage['value'];
723 | } else {
724 | $array[$jsonMessage['key']] = $jsonMessage['value'];
725 | // if segment can be repeated then the flag forces to be an array also
726 | // if there's only one segment
727 | if ($maxRepeat > 1 && $this->forceArrayWhenRepeatable) {
728 | $array[$jsonMessage['key']] = [$jsonMessage['value']];
729 | }
730 | }
731 | }
732 |
733 | /**
734 | * Add human readable keys as in Analyser.
735 | *
736 | * @param int|null $segmentIdx
737 | */
738 | private function processSegment(array &$segment, array &$xmlMap, $segmentIdx, ?array &$errors = null): array
739 | {
740 | $id = $segment[0];
741 |
742 | $jsonsegment = [];
743 | if (isset($xmlMap[$id])) {
744 | $attributes = $xmlMap[$id]['attributes'];
745 | $details_desc = $xmlMap[$id]['details'];
746 |
747 | $jsonelements = [
748 | 'segmentIdx' => $segmentIdx,
749 | 'segmentCode' => $id,
750 | ];
751 |
752 | if ($this->segmentGroup) {
753 | $jsonelements['segmentGroup'] = $this->currentGroup;
754 | }
755 |
756 | foreach ($segment as $idx => $detail) {
757 | $n = $idx - 1;
758 | if ($idx == 0) {
759 | continue;
760 | }
761 |
762 | if (! isset($details_desc[$n])) {
763 | $errors[] = [
764 | 'text' => $this->messageTextConf['TOOMANYELEMENTS'],
765 | 'position' => $segmentIdx,
766 | 'segmentId' => $id,
767 | ];
768 | $jsonelements['Extension'.$n] = $detail;
769 |
770 | continue;
771 | }
772 |
773 | $d_desc_attr = $details_desc[$n]['attributes'];
774 |
775 | $jsoncomposite = [];
776 | if (
777 | $detail !== ''
778 | &&
779 | isset($details_desc[$n]['details'])
780 | ) {
781 | $sub_details_desc = $details_desc[$n]['details'];
782 |
783 | if (\is_array($detail)) {
784 | foreach ($detail as $d_n => $d_detail) {
785 | if (! isset($sub_details_desc[$d_n])) {
786 | $errors[] = [
787 | 'text' => $this->messageTextConf['TOOMANYELEMENTS_COMPOSITE'],
788 | 'position' => $segmentIdx.'/'.$idx,
789 | 'segmentId' => $id,
790 | ];
791 | $jsoncomposite['CompositeExtension'.$d_n] = $d_detail;
792 |
793 | continue;
794 | }
795 |
796 | $d_sub_desc_attr = $sub_details_desc[$d_n]['attributes'];
797 | //print_r($d_sub_desc_attr);
798 | //print_r($d_detail);
799 | //die();
800 | if (isset($this->codes) && isset($this->codes[$d_sub_desc_attr['id']]) && is_array($this->codes[$d_sub_desc_attr['id']])) { //if codes is set enable translation of the value
801 | if (isset($this->codes[$d_sub_desc_attr['id']][$d_detail])) {
802 | $d_detail = $this->codes[$d_sub_desc_attr['id']][$d_detail];
803 | }
804 | }
805 |
806 | if (! isset($jsoncomposite[$d_sub_desc_attr[$this->outputKey]])) { //New
807 | $jsoncomposite[$d_sub_desc_attr[$this->outputKey]] = $d_detail;
808 | } elseif (\is_string($jsoncomposite[$d_sub_desc_attr[$this->outputKey]])) { // More data than one string
809 | $jsoncomposite[$d_sub_desc_attr[$this->outputKey]] = [
810 | $jsoncomposite[$d_sub_desc_attr[$this->outputKey]],
811 | $d_detail,
812 | ];
813 | } else { // More and more
814 | $jsoncomposite[$d_sub_desc_attr[$this->outputKey]][] = $d_detail;
815 | }
816 | }
817 | } else {
818 | $d_sub_desc_attr = $sub_details_desc[0]['attributes'];
819 |
820 | if (isset($this->codes) && isset($this->codes[$d_sub_desc_attr['id']]) && is_array($this->codes[$d_sub_desc_attr['id']])) { //if codes is set enable translation of the value
821 | if (isset($this->codes[$d_sub_desc_attr['id']][$detail]) && $this->codes[$d_sub_desc_attr['id']][$detail]) {
822 | $detail = $this->codes[$d_sub_desc_attr['id']][$detail];
823 | }
824 | }
825 | $jsoncomposite[$d_sub_desc_attr[$this->outputKey]] = $detail;
826 | }
827 | } else {
828 | if (isset($this->codes) && isset($this->codes[$d_desc_attr['id']]) && is_array($this->codes[$d_desc_attr['id']])) { //if codes is set enable translation of the value
829 | if (isset($this->codes[$d_desc_attr['id']][$detail]) && $this->codes[$d_desc_attr['id']][$detail]) {
830 | $detail = $this->codes[$d_desc_attr['id']][$detail];
831 | }
832 | }
833 | $jsoncomposite = $detail;
834 | }
835 |
836 | if (\array_key_exists($d_desc_attr[$this->outputKey], $jsonelements)) {
837 | $jsonelements[$d_desc_attr[$this->outputKey].$n] = $jsoncomposite;
838 | } else {
839 | $jsonelements[$d_desc_attr[$this->outputKey]] = $jsoncomposite;
840 | }
841 | }
842 | $jsonsegment['key'] = $attributes[$this->outputKey];
843 | $jsonsegment['value'] = $jsonelements;
844 | } elseif ($xmlMap !== $this->xmlSvc) {
845 | $jsonsegment = $this->processSegment($segment, $this->xmlSvc, $segmentIdx, $errors);
846 | } else {
847 | $jsonsegment['key'] = 'UnrecognisedType';
848 | $jsonsegment['value'] = $segment;
849 | }
850 |
851 | return $jsonsegment;
852 | }
853 |
854 | /**
855 | * Process UNB / UNZ segments
856 | */
857 | private function processService(array &$segments): array
858 | {
859 | // init
860 | $processed = [];
861 |
862 | foreach ($segments as &$seg) {
863 | $jsonsegment = $this->processSegment($seg, $this->xmlSvc, null);
864 | $processed[$jsonsegment['key']] = $jsonsegment['value'];
865 | }
866 |
867 | return $processed;
868 | }
869 |
870 | public function rebuildArray()
871 | {
872 | if (isset($this->codes)) {
873 | throw new \LogicException('Run the Interpreter without calling setCodes()');
874 | }
875 | $unh = $this->serviceSeg['interchangeHeader'];
876 | unset($unh['segmentIdx']);
877 | unset($unh['segmentGroup']);
878 | $unz = $this->serviceSeg['interchangeTrailer'];
879 | unset($unz['segmentIdx']);
880 | unset($unz['segmentGroup']);
881 |
882 | $rebuilt = $this->recursionReconstruct($this->ediGroups);
883 |
884 | array_unshift($rebuilt, $unh);
885 | $rebuilt[] = $unz;
886 |
887 | return $rebuilt;
888 | }
889 |
890 | private function recursionReconstruct($tempArr)
891 | {
892 | $reconstructArr = [];
893 | if (is_array($tempArr)) {
894 | foreach ($tempArr as $idx => $arr) {
895 | if (! isset($arr['segmentIdx'])) {
896 | $recurseArr = $this->recursionReconstruct($arr);
897 | foreach ($recurseArr as $k => $i) {
898 | $reconstructArr[$k] = $i;
899 | }
900 | } else {
901 | $idx = $arr['segmentIdx'];
902 | unset($arr['segmentIdx']);
903 | if ($this->segmentGroup) {
904 | unset($arr['segmentGroup']);
905 | }
906 | $reconstructArr[$idx] = $arr;
907 | }
908 | }
909 | }
910 |
911 | return $reconstructArr;
912 | }
913 | }
914 |
--------------------------------------------------------------------------------
/src/EDI/Parser.php:
--------------------------------------------------------------------------------
1 |
107 | */
108 | private static $encodingToStripChars = [
109 | 'UNOA' => "/[\x01-\x1F\x80-\xFF]/", // not as restrictive as it should be
110 | 'UNOB' => "/[\x01-\x1F\x80-\xFF]/",
111 | 'UNOC' => "/[\x01-\x1F\x7F-\x9F]/",
112 | 'UNOE' => "/[\x20-\x7E]\xA0-\xFF/",
113 | ];
114 |
115 | private static $charsets = [
116 | // ISO 646, except lower case letters, alternative graphic chars, national or application-oriented graphic chars
117 | 'UNOA' => 'ASCII',
118 | // ISO 646, except alternative graphic chars and national or application-oriented graphic chars
119 | 'UNOB' => 'ASCII',
120 | 'UNOC' => 'ISO-8859-1',
121 | 'UNOD' => 'ISO-8859-2',
122 | 'UNOE' => 'ISO-8859-5',
123 | 'UNOF' => 'ISO-8859-7',
124 | ];
125 |
126 | /**
127 | * @var bool TRUE when UNA characters are known.
128 | */
129 | private $unaChecked = false;
130 |
131 | /**
132 | * @var bool TRUE when UNB encoding is known.
133 | */
134 | private $unbChecked = false;
135 |
136 | /**
137 | * Optionally disable workarounds.
138 | */
139 | private bool $strict = false;
140 |
141 | /**
142 | * Parse EDI array.
143 | */
144 | public function parse(): self
145 | {
146 | $rawSegments = $this->getRawSegments();
147 | if ($this->sourceEncoding && isset(self::$charsets[$this->syntaxID]) && self::$charsets[$this->syntaxID] !== $this->sourceEncoding) {
148 | $rawSegments = $this->convertEncoding($this->rawSegments, $this->sourceEncoding, self::$charsets[$this->syntaxID]);
149 | }
150 |
151 | $i = 0;
152 | foreach ($rawSegments as $line) {
153 | $i++;
154 |
155 | // Null byte and carriage return removal. (CR+LF)
156 | $line = \str_replace(["\x00", "\r", "\n"], '', $line);
157 |
158 | // Basic sanitization, remove non-printable chars.
159 | $lineTrim = \trim($line);
160 | $line = (string) \preg_replace($this->stripChars, '', $lineTrim);
161 | $line_bytes = \strlen($line);
162 |
163 | if ($line_bytes !== \strlen($lineTrim)) {
164 | $this->errors[] = 'Non-printable character on line '.$i.': '.$lineTrim;
165 | }
166 |
167 | if ($line_bytes < 2) {
168 | continue;
169 | }
170 |
171 | switch (\substr($line, 0, 3)) {
172 | case 'UNA':
173 | if (! $this->unaChecked) {
174 | $this->analyseUNA(\substr($line, 4, 6));
175 | }
176 |
177 | break;
178 | case 'UNB':
179 | $line = $this->splitSegment($line);
180 | if (! $this->unbChecked) {
181 | $this->analyseUNB($line[1]);
182 | }
183 | $this->parsedfile[] = $line;
184 |
185 | break;
186 | case 'UNH':
187 | $line = $this->splitSegment($line);
188 | $this->analyseUNH($line);
189 | $this->parsedfile[] = $line;
190 |
191 | break;
192 | default:
193 | $line = $this->splitSegment($line);
194 | $this->parsedfile[] = $line;
195 |
196 | break;
197 | }
198 | }
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * Read UNA's characters definition.
205 | *
206 | * @param string $line UNA definition line (without UNA tag). Example : :+.? '
207 | * @return void
208 | */
209 | public function analyseUNA(string $line)
210 | {
211 | $line = \substr($line, 0, 6);
212 | if (isset($line[0])) {
213 | $this->sepComp = \preg_quote($line[0], self::$DELIMITER);
214 | $this->sepUnescapedComp = $line[0];
215 | if (isset($line[1])) {
216 | $this->sepData = \preg_quote($line[1], self::$DELIMITER);
217 | if (isset($line[2])) {
218 | $this->sepDec = $line[2]; // See later if a preg_quote is needed
219 | if (isset($line[3])) {
220 | $this->symbRel = \preg_quote($line[3], self::$DELIMITER);
221 | $this->symbUnescapedRel = $line[3];
222 | if (isset($line[4])) {
223 | $this->symbRep = $line[4]; // See later if a preg_quote is needed
224 | if (isset($line[5])) {
225 | $this->symbEnd = \preg_quote($line[5], self::$DELIMITER);
226 | }
227 | }
228 | }
229 | }
230 | }
231 | $this->unaChecked = true;
232 | }
233 | }
234 |
235 | /**
236 | * UNB line analysis.
237 | *
238 | * @param string|string[] $line UNB definition line (without UNB tag). Example UNOA:2
239 | */
240 | public function analyseUNB($line): void
241 | {
242 | if (\is_array($line)) {
243 | $line = $line[0];
244 | }
245 |
246 | $this->syntaxID = $line;
247 |
248 | // If there's a regex defined for this character set, use it.
249 | /** @noinspection OffsetOperationsInspection */
250 | if (isset(self::$encodingToStripChars[$line])) {
251 | /** @noinspection OffsetOperationsInspection */
252 | $this->setStripRegex(self::$encodingToStripChars[$line]);
253 | }
254 |
255 | $this->unbChecked = true;
256 | }
257 |
258 | /**
259 | * Identify message type.
260 | *
261 | * @param array $line UNH segment
262 | */
263 | public function analyseUNH(array $line): void
264 | {
265 | if (\count($line) < 3) {
266 | return;
267 | }
268 |
269 | $this->messageNumber = $line[1];
270 |
271 | $lineElement = $line[2];
272 | if (! \is_array($lineElement)) {
273 | $this->messageFormat = $lineElement;
274 |
275 | return;
276 | }
277 |
278 | $this->messageFormat = $lineElement[0];
279 | $this->messageDirectory = $lineElement[2];
280 | }
281 |
282 | /**
283 | * Check if the file's character encoding actually matches the one declared in the UNB header.
284 | *
285 | * @throws \LogicException
286 | * @throws \RuntimeException
287 | */
288 | public function checkEncoding(): bool
289 | {
290 | if (empty($this->parsedfile)) {
291 | throw new \LogicException('No text has been parsed yet');
292 | }
293 | if (! isset(self::$charsets[$this->syntaxID])) {
294 | throw new \RuntimeException('Unsupported syntax identifier: '.$this->syntaxID);
295 | }
296 |
297 | $check = mb_check_encoding($this->parsedfile, self::$charsets[$this->syntaxID]);
298 | if (! $check) {
299 | $this->errors[] = 'Character encoding does not match declaration in UNB interchange header';
300 | }
301 |
302 | return $check;
303 | }
304 |
305 | /**
306 | * Get errors.
307 | */
308 | public function errors(): array
309 | {
310 | return $this->errors;
311 | }
312 |
313 | /**
314 | * (Un)Set strict parsing.
315 | */
316 | public function setStrict(bool $strict): void
317 | {
318 | $this->strict = $strict;
319 | }
320 |
321 | public function setSourceEncoding(string $sourceEncoding): void
322 | {
323 | $this->sourceEncoding = $sourceEncoding;
324 | }
325 |
326 | /**
327 | * Get parsed lines/segments.
328 | */
329 | public function get(?string $encoding = null): array
330 | {
331 | if (empty($this->parsedfile)) {
332 | $this->parse();
333 | }
334 |
335 | if (null === $encoding) {
336 | return $this->parsedfile;
337 | }
338 |
339 | return $this->convertEncoding($this->parsedfile, self::$charsets[$this->syntaxID], $encoding);
340 | }
341 |
342 | private function convertEncoding($data, string $from, string $to)
343 | {
344 | if (is_array($data)) {
345 | foreach ($data as $k => $v) {
346 | $data[$k] = $this->convertEncoding($v, $from, $to);
347 | }
348 | } elseif (is_string($data)) {
349 | $data = function_exists('iconv') ? iconv($from, $to.'//TRANSLIT', $data) : mb_convert_encoding($data, $to, $from);
350 | }
351 |
352 | return $data;
353 | }
354 |
355 | /**
356 | * Get raw segments array.
357 | *
358 | * @return array|string[]
359 | */
360 | public function getRawSegments(): array
361 | {
362 | return $this->rawSegments;
363 | }
364 |
365 | /**
366 | * Get syntax identifier from the UNB header.
367 | * Does not necessarily mean that the text is actually encoded as such.
368 | *
369 | * @throws \RuntimeException
370 | */
371 | public function getSyntaxIdentifier(): string
372 | {
373 | return $this->syntaxID;
374 | }
375 |
376 | /**
377 | * Load the message from file.
378 | *
379 | * @param string $location Either a local file path or a URL
380 | */
381 | public function load(string $location): self
382 | {
383 | $contents = file_get_contents($location);
384 | if ($contents === false) {
385 | throw new \RuntimeException('File could not be retrieved');
386 | }
387 |
388 | $this->loadString($contents);
389 |
390 | return $this;
391 | }
392 |
393 | /**
394 | * Load the message from a string.
395 | */
396 | public function loadString(string $txt): self
397 | {
398 | $this->resetUNA();
399 | $this->resetUNB();
400 | $this->rawSegments = $this->unwrap($txt);
401 |
402 | return $this;
403 | }
404 |
405 | /**
406 | * Load a raw or parsed message from an array of strings.
407 | *
408 | * @param bool $raw If the data hasn't been parsed yet
409 | */
410 | public function loadArray(array $lines, bool $raw = true): self
411 | {
412 | if ($raw) {
413 | $this->resetUNA();
414 | $this->resetUNB();
415 | $this->rawSegments = $lines;
416 | if (\count($lines) === 1) {
417 | $this->loadString($lines[0]);
418 | }
419 | } else {
420 | $this->rawSegments = [];
421 | $this->parsedfile = $lines;
422 | }
423 |
424 | return $this;
425 | }
426 |
427 | /**
428 | * Change the default regex used for stripping invalid characters.
429 | *
430 | * @return void
431 | */
432 | public function setStripRegex(string $regex)
433 | {
434 | $this->stripChars = $regex;
435 | }
436 |
437 | /**
438 | * Get the message type ID.
439 | *
440 | * @return string|null
441 | */
442 | public function getMessageFormat()
443 | {
444 | return $this->messageFormat;
445 | }
446 |
447 | /**
448 | * Get the message type release number.
449 | *
450 | * @return string|null
451 | */
452 | public function getMessageDirectory()
453 | {
454 | return $this->messageDirectory;
455 | }
456 |
457 | /**
458 | * @return string|null
459 | */
460 | public function getMessageNumber()
461 | {
462 | return $this->messageNumber;
463 | }
464 |
465 | /**
466 | * Reset UNA character definition to defaults.
467 | */
468 | private function resetUNA(): void
469 | {
470 | $this->sepComp = "\:";
471 | $this->sepUnescapedComp = ':';
472 | $this->sepData = "\+";
473 | $this->sepDec = '.'; // See later if a preg_quote is needed
474 | $this->symbRel = "\?";
475 | $this->symbUnescapedRel = '?';
476 | $this->symbRep = '*'; // See later if a preg_quote is needed
477 | $this->symbEnd = "'";
478 | $this->stringSafe = '§SS§';
479 | $this->unaChecked = false;
480 | }
481 |
482 | /**
483 | * Reset UNB encoding definition to defaults.
484 | */
485 | private function resetUNB(): void
486 | {
487 | $this->syntaxID = '';
488 | $this->unbChecked = false;
489 | }
490 |
491 | /**
492 | * Unwrap string splitting rows on terminator (if not escaped).
493 | *
494 | * @return string[]
495 | */
496 | private function unwrap(string &$string): array
497 | {
498 | if (
499 | ! $this->unaChecked
500 | &&
501 | \strpos($string, 'UNA') === 0
502 | ) {
503 | $this->analyseUNA(
504 | \substr(\substr($string, 3), 0, 9)
505 | );
506 | }
507 |
508 | $unbRegex = sprintf(
509 | '/^(UNA[^%1$s]+%1$s\W?\W?)?UNB%2$s(?\w{4})%3$s/m',
510 | $this->symbEnd,
511 | $this->sepData,
512 | $this->sepComp,
513 | );
514 | if (
515 | ! $this->unbChecked
516 | && false !== preg_match($unbRegex, $string, $unbMatches)
517 | && isset($unbMatches['syntax_identifier'])
518 | ) {
519 | $this->analyseUNB($unbMatches['syntax_identifier']);
520 | }
521 | if (preg_match_all("/[A-Z0-9]+(?:\?'|$)[\r\n]+/i", $string, $matches, PREG_OFFSET_CAPTURE) > 0) {
522 | $this->errors[] = 'This file contains some segments without terminators';
523 | }
524 |
525 | $terminatorRegex = '/((?symbRel.')(?:'.$this->symbRel.$this->symbRel.')*)'.$this->symbEnd.'|[\r\n]+/';
526 |
527 | if ($this->strict) {
528 | $terminatorRegex = '/((?symbRel.')(?:'.$this->symbRel.$this->symbRel.')*)'.$this->symbEnd.'/';
529 | }
530 |
531 | $string = (string) \preg_replace(
532 | $terminatorRegex,
533 | '$1'.$this->stringSafe,
534 | $string
535 | );
536 |
537 | $file = \preg_split(
538 | self::$DELIMITER.$this->stringSafe.self::$DELIMITER.'i',
539 | $string
540 | );
541 | // fallback
542 | if ($file === false) {
543 | $file = [];
544 | }
545 |
546 | $end = \stripslashes($this->symbEnd.'');
547 | foreach ($file as $fc => &$line) {
548 | if (\trim($line) == '') {
549 | /* @noinspection OffsetOperationsInspection */
550 | unset($file[$fc]);
551 | }
552 | $line .= $end;
553 | }
554 |
555 | return $file;
556 | }
557 |
558 | /**
559 | * Split segment.
560 | *
561 | * @return array|string[]
562 | */
563 | private function splitSegment(string &$str): array
564 | {
565 | // remove ending "symbEnd"
566 | $str = \trim(
567 | (string) \preg_replace(
568 | self::$DELIMITER.$this->symbEnd.'$'.self::$DELIMITER,
569 | '',
570 | $str
571 | )
572 | );
573 |
574 | // replace duplicate "symbRel"
575 | $str = \str_replace(
576 | $this->symbUnescapedRel.$this->symbUnescapedRel.'',
577 | $this->stringSafe ?? '',
578 | $str
579 | );
580 |
581 | // split on "sepData" if not escaped (negative lookbehind)
582 | $matches = \preg_split(
583 | self::$DELIMITER.'(?symbRel.')'.$this->sepData.self::$DELIMITER,
584 | $str
585 | );
586 | // fallback
587 | if ($matches === false) {
588 | $matches = [];
589 | }
590 |
591 | foreach ($matches as &$value) {
592 | if ($value === '') {
593 | continue;
594 | }
595 |
596 | // INFO:
597 | //
598 | // ? immediately preceding one of the characters '+:? restores their normal meaning
599 | //
600 | // e.g. 10?+10=20 means 10+10=20
601 | //
602 | // Question mark is represented by ??
603 |
604 | if (
605 | $this->symbEnd
606 | &&
607 | \strpos($value, $this->symbEnd) !== false
608 | ) {
609 | if (\preg_match(self::$DELIMITER.'(?symbRel.')'.$this->symbEnd.self::$DELIMITER, $value)) {
610 | $this->errors[] = "There's a ".\stripslashes($this->symbEnd).' not escaped in the data; string '.$str;
611 | }
612 | }
613 |
614 | if (
615 | $this->symbUnescapedRel
616 | &&
617 | \strpos($value, $this->symbUnescapedRel) !== false
618 | ) {
619 | if (\preg_match(self::$DELIMITER.'(?symbRel.')'.$this->symbRel.'(?!'.$this->symbRel.')(?!'.$this->sepData.')(?!'.$this->sepComp.')(?!'.$this->symbEnd.')'.self::$DELIMITER, $value)) {
620 | $this->errors[] = "There's a character not escaped with ".\stripslashes($this->symbRel ?? '').' in the data; string '.$value;
621 | }
622 | }
623 |
624 | // split on "sepComp"
625 | $value = $this->splitData($value);
626 | }
627 |
628 | return $matches;
629 | }
630 |
631 | /**
632 | * Composite data element.
633 | *
634 | * @return mixed
635 | */
636 | private function splitData(string &$str)
637 | {
638 | if ($str === '') {
639 | return $str;
640 | }
641 |
642 | $replace = function (&$string) {
643 | if ($this->symbUnescapedRel && \strpos($string, $this->symbUnescapedRel) !== false) {
644 | $string = \preg_replace(
645 | self::$DELIMITER.$this->symbRel.'(?='.$this->symbRel.')|'.$this->symbRel.'(?='.$this->sepData.')|'.$this->symbRel.'(?='.$this->sepComp.')|'.$this->symbRel.'(?='.$this->symbEnd.')'.self::$DELIMITER,
646 | '',
647 | $string
648 | );
649 | }
650 |
651 | return \str_replace(
652 | $this->stringSafe ?? '',
653 | $this->symbUnescapedRel ?? '',
654 | $string
655 | );
656 | };
657 |
658 | // check for "sepUnescapedComp" in the string
659 | if ($this->sepUnescapedComp && \strpos($str, $this->sepUnescapedComp) === false) {
660 | return $replace($str);
661 | }
662 |
663 | // split on "sepComp" if not escaped (negative lookbehind)
664 | $array = \preg_split(
665 | self::$DELIMITER.'(?symbRel.')'.$this->sepComp.self::$DELIMITER,
666 | $str
667 | );
668 | // fallback
669 | if ($array === false) {
670 | $array = [[]];
671 | }
672 |
673 | if (\count($array) === 1) {
674 | return $replace($str);
675 | }
676 |
677 | foreach ($array as &$value) {
678 | $value = $replace($value);
679 | }
680 |
681 | return $array;
682 | }
683 | }
684 |
--------------------------------------------------------------------------------
/src/EDI/Reader.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | private $errors = [];
22 |
23 | /**
24 | * Reader constructor.
25 | *
26 | * @param Parser $parser With a message already loaded.
27 | */
28 | public function __construct(Parser $parser)
29 | {
30 | $this->parser = $parser;
31 | $this->preValidate();
32 | }
33 |
34 | /**
35 | * Get errors
36 | *
37 | * @return array
38 | */
39 | public function errors(): array
40 | {
41 | return $this->errors;
42 | }
43 |
44 | /**
45 | * reset errors
46 | *
47 | * @return void
48 | */
49 | public function resetErrors()
50 | {
51 | $this->errors = [];
52 | }
53 |
54 | /**
55 | * Returns the parsed message.
56 | *
57 | * @returns array
58 | */
59 | public function getParsedFile(): array
60 | {
61 | return $this->parser->get();
62 | }
63 |
64 | /**
65 | * Do initial validation
66 | */
67 | public function preValidate(): bool
68 | {
69 | $this->errors = [];
70 |
71 | if (! \is_array($this->getParsedFile())) {
72 | $this->errors[] = 'Incorrect format parsed file';
73 |
74 | return false;
75 | }
76 |
77 | $r = $this->readUNHmessageNumber();
78 | if (
79 | ! $r
80 | &&
81 | (
82 | $this->errors !== []
83 | &&
84 | $this->errors[0] == 'Segment "UNH" is ambiguous'
85 | )
86 | ) {
87 | $this->errors = [];
88 | $this->errors[] = 'File has multiple messages';
89 |
90 | return false;
91 | }
92 |
93 | return true;
94 | }
95 |
96 | /**
97 | * Split multi messages to separate messages
98 | *
99 | *
100 | * @return array
101 | */
102 | public static function splitMultiMessage(string $ediMessage): array
103 | {
104 | // init
105 | $splicedMessages = [];
106 | $message = [];
107 | $unb = false;
108 | $segment = '';
109 |
110 | foreach (self::unwrap($ediMessage) as $segment) {
111 | if (\strpos($segment, 'UNB') === 0) {
112 | $unb = $segment;
113 |
114 | continue;
115 | }
116 |
117 | if (\strpos($segment, 'UNH') === 0) {
118 | if ($unb) {
119 | $message[] = $unb;
120 | }
121 | $message[] = $segment;
122 |
123 | continue;
124 | }
125 |
126 | if (\strpos($segment, 'UNT') === 0) {
127 | $message[] = $segment;
128 | $splicedMessages[] = $message;
129 | $message = [];
130 |
131 | continue;
132 | }
133 |
134 | if ($message) {
135 | $message[] = $segment;
136 | }
137 | }
138 |
139 | if (\strpos($segment, 'UNZ') === 0) {
140 | $segment = \preg_replace('#UNZ\+\d+\+#', 'UNZ+1+', $segment);
141 | foreach ($splicedMessages as $k => $message) {
142 | $splicedMessages[$k][] = $segment;
143 | }
144 | }
145 |
146 | foreach ($splicedMessages as $k => &$message) {
147 | $message = \implode(\PHP_EOL, $splicedMessages[$k]);
148 | }
149 |
150 | return $splicedMessages;
151 | }
152 |
153 | /**
154 | * read required value. if no found, registered error
155 | *
156 | * @param array|string $filter segment filter by segment name and values
157 | * @param false|int $l2
158 | * @return string|null
159 | */
160 | public function readEdiDataValueReq($filter, int $l1, $l2 = false)
161 | {
162 | return $this->readEdiDataValue($filter, $l1, $l2, true);
163 | }
164 |
165 | /**
166 | * read data value from parsed EDI data
167 | *
168 | * @param array|string $filter 'AGR' - segment code
169 | * or ['AGR',['1'=>'BB']], where AGR segment code and first element equal 'BB'
170 | * or ['AGR',['1.0'=>'BB']], where AGR segment code and first element zero subelement
171 | * equal 'BB'
172 | * @param int $l1 first level item number (start by 1)
173 | * @param false|int $l2 second level item number (start by 0)
174 | * @param bool $required if required, but no exist, register error
175 | * @param int|null $offset if multiple segments found, get segment by offset
176 | * @return string|null
177 | */
178 | public function readEdiDataValue($filter, int $l1, $l2 = false, bool $required = false, ?int $offset = null)
179 | {
180 | $found_segments = [];
181 | $segment_name = $filter;
182 | $filter_elements = false;
183 | if (\is_array($filter)) {
184 | $segment_name = $filter[0];
185 | $filter_elements = $filter[1];
186 | }
187 |
188 | // search segments which conform to filter
189 | foreach ($this->getParsedFile() as $edi_row) {
190 | if ($edi_row[0] == $segment_name) {
191 | if ($filter_elements) {
192 | foreach ($filter_elements as $el_id => $el_value) {
193 | $filter_ok = false;
194 | $f_el_list = \explode('.', (string) $el_id);
195 | if (\count($f_el_list) === 1) {
196 | if (isset($edi_row[$el_id]) && $edi_row[$el_id] == $el_value) {
197 | $filter_ok = true;
198 | }
199 | } elseif (
200 | isset($edi_row[$f_el_list[0]])
201 | && (
202 | (
203 | isset($edi_row[$f_el_list[0]][$f_el_list[1]])
204 | && \is_array($edi_row[$f_el_list[0]])
205 | && $edi_row[$f_el_list[0]][$f_el_list[1]] == $el_value
206 | ) || (
207 | isset($edi_row[$f_el_list[0]])
208 | && \is_string($edi_row[$f_el_list[0]])
209 | && $edi_row[$f_el_list[0]] == $el_value
210 | )
211 | )
212 | ) {
213 | $filter_ok = true;
214 | }
215 | if ($filter_ok === false) {
216 | break;
217 | }
218 | }
219 |
220 | if ($filter_ok === false) {
221 | continue;
222 | }
223 | }
224 | $found_segments[] = $edi_row;
225 | }
226 | }
227 |
228 | try {
229 | if ($offset !== null) {
230 | $segment = $this->getOffsetSegmentFromResult($found_segments, $offset, $required, $segment_name);
231 | } else {
232 | $segment = $this->getSegmentFromResult($found_segments, $required, $segment_name);
233 | }
234 | } catch (ReaderException $e) {
235 | $this->errors[] = $e->getMessage();
236 |
237 | return null;
238 | }
239 |
240 | if ($segment === false) {
241 | return null;
242 | }
243 |
244 | // validate elements
245 | if (! isset($segment[$l1])) {
246 | if ($required) {
247 | $this->errors[] = 'Segment value "'.$segment_name.'['.$l1.']" no exist';
248 | }
249 |
250 | return null;
251 | }
252 |
253 | // requested first level element
254 | if ($l2 === false) {
255 | return $segment[$l1];
256 | }
257 |
258 | // requested second level element, but not exist
259 | if (! \is_array($segment[$l1]) || ! isset($segment[$l1][$l2])) {
260 | if ($required) {
261 | $this->errors[] = 'Segment value "'.$segment_name.'['.$l1.']['.$l2.']" no exist';
262 | }
263 |
264 | return null;
265 | }
266 |
267 | // second level element
268 | return $segment[$l1][$l2];
269 | }
270 |
271 | /**
272 | * read date from DTM segment period qualifier - codelist 2005
273 | *
274 | * @param int $PeriodQualifier period qualifier (codelist/2005)
275 | * @return string|null YYYY-MM-DD HH:MM:SS
276 | */
277 | public function readEdiSegmentDTM($PeriodQualifier)
278 | {
279 | $date = $this->readEdiDataValue([
280 | 'DTM', [
281 | '1.0' => $PeriodQualifier,
282 | ]], 1, 1);
283 | $format = $this->readEdiDataValue([
284 | 'DTM', [
285 | '1.0' => $PeriodQualifier,
286 | ]], 1, 2);
287 | if (empty($date)) {
288 | return $date;
289 | }
290 | switch ($format) {
291 | case 203: //CCYYMMDDHHMM
292 | return \preg_replace('#(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)#', '$1-$2-$3 $4:$5:00', $date);
293 |
294 | case 102: //CCYYMMDD
295 | return \preg_replace('/(\d{4})(\d{2})(\d{2})/', '$1-$2-$3', $date);
296 |
297 | default:
298 | return $date;
299 | }
300 | }
301 |
302 | /**
303 | * @deprecated
304 | *
305 | * @return string|null
306 | */
307 | public function readUNBDateTimeOfPpreperation()
308 | {
309 | return $this->readUNBDateTimeOfPreparation();
310 | }
311 |
312 | /**
313 | * @deprecated
314 | *
315 | * @return string|null
316 | */
317 | public function readUNBDateTimeOfPreperation()
318 | {
319 | return $this->readUNBDateTimeOfPreparation();
320 | }
321 |
322 | /**
323 | * get message preparation time
324 | *
325 | * @return string|null
326 | */
327 | public function readUNBDateTimeOfPreparation()
328 | {
329 | // separate date (YYMMDD) and time (HHMM)
330 | $date = $this->readEdiDataValue('UNB', 4, 0);
331 | if (! empty($date)) {
332 | $time = $this->readEdiDataValue('UNB', 4, 1);
333 | if ($time !== null) {
334 | $time = (string) \preg_replace('#(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)#', '20$1-$2-$3 $4:$5:00', $date.$time);
335 | }
336 |
337 | return $time;
338 | }
339 |
340 | // common YYYYMMDDHHMM
341 | $datetime = $this->readEdiDataValue('UNB', 4);
342 | if ($datetime !== null) {
343 | $datetime = (string) \preg_replace('#(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)#', '$1-$2-$3 $4:$5:00', $datetime);
344 | }
345 |
346 | return $datetime;
347 | }
348 |
349 | public function readUNBInterchangeSender()
350 | {
351 | return $this->readEdiDataValue('UNB', 2);
352 | }
353 |
354 | public function readUNBInterchangeRecipient()
355 | {
356 | return $this->readEdiDataValue('UNB', 3);
357 | }
358 |
359 | /**
360 | * read transport identification number
361 | *
362 | * @param mixed $transportStageQualifier
363 | * @return string|null
364 | */
365 | public function readTDTtransportIdentification($transportStageQualifier)
366 | {
367 | $transportIdentification = $this->readEdiDataValue([
368 | 'TDT', [
369 | '1' => $transportStageQualifier,
370 | ]], 8, 0);
371 | if (! empty($transportIdentification)) {
372 | return $transportIdentification;
373 | }
374 |
375 | return $this->readEdiDataValue([
376 | 'TDT', [
377 | '1' => $transportStageQualifier,
378 | ]], 8);
379 | }
380 |
381 | /**
382 | * read message type
383 | *
384 | * @return string|null
385 | */
386 | public function readUNHmessageType()
387 | {
388 | return $this->readEdiDataValue('UNH', 2, 0);
389 | }
390 |
391 | /**
392 | * read message number
393 | *
394 | * @return string|null
395 | */
396 | public function readUNHmessageNumber()
397 | {
398 | return $this->readEdiDataValue('UNH', 1);
399 | }
400 |
401 | /**
402 | * read message number
403 | *
404 | * @return string|null
405 | */
406 | public function readUNHmessageRealise()
407 | {
408 | return $this->readEdiDataValue('UNH', 3);
409 | }
410 |
411 | /**
412 | * Get groups from message.
413 | *
414 | * @param string $before segment before groups
415 | * @param string $start first segment of group
416 | * @param string $end last segment of group
417 | * @param string $after segment after groups
418 | * @return array|false
419 | */
420 | public function readGroups(string $before, string $start, string $end, string $after)
421 | {
422 | // init
423 | $groups = [];
424 | $group = [];
425 | $position = 'before_search';
426 |
427 | foreach ($this->getParsedFile() as $edi_row) {
428 | // search before group segment
429 | if ($position == 'before_search' && $edi_row[0] == $before) {
430 | $position = 'before_is';
431 |
432 | continue;
433 | }
434 |
435 | if ($position == 'before_search') {
436 | continue;
437 | }
438 |
439 | if ($position == 'before_is' && $edi_row[0] == $before) {
440 | continue;
441 | }
442 |
443 | // after before search start
444 | if ($position == 'before_is' && $edi_row[0] == $start) {
445 | $position = 'group_is';
446 | $group[] = $edi_row;
447 |
448 | continue;
449 | }
450 |
451 | // if after before segment no start segment, search again before segment
452 | if ($position == 'before_is') {
453 | $position = 'before_search';
454 |
455 | continue;
456 | }
457 |
458 | // get group element
459 | if ($position == 'group_is' && $edi_row[0] != $end) {
460 | $group[] = $edi_row;
461 |
462 | continue;
463 | }
464 |
465 | // found end of group
466 | if ($position == 'group_is' && $edi_row[0] == $end) {
467 | $position = 'group_finish';
468 | $group[] = $edi_row;
469 | $groups[] = $group;
470 | $group = [];
471 |
472 | continue;
473 | }
474 |
475 | // next group start
476 | if ($position == 'group_finish' && $edi_row[0] == $start) {
477 | $group[] = $edi_row;
478 | $position = 'group_is';
479 |
480 | continue;
481 | }
482 |
483 | // finish
484 | if ($position == 'group_finish' && $edi_row[0] == $after) {
485 | break;
486 | }
487 |
488 | $this->errors[] = 'Reading group '.$before.'/'.$start.'/'.$end.'/'.$after
489 | .'. Error on position: '.$position;
490 |
491 | return false;
492 | }
493 |
494 | return $groups;
495 | }
496 |
497 | /**
498 | * Get groups from message when last segment is unknown but you know the barrier.
499 | * Useful for invoices by default.
500 | *
501 | * @param string $start first segment start a new group
502 | * @param array $barrier barrier segment (NOT in group)
503 | * @return array Containing parsed lines
504 | */
505 | public function groupsExtract(string $start = 'LIN', array $barrier = ['UNS']): array
506 | {
507 | // init
508 | $groups = [];
509 | $group = [];
510 | $position = 'before_search';
511 |
512 | foreach ($this->getParsedFile() as $edi_row) {
513 | $segment = $edi_row[0];
514 | if (
515 | $position == 'group_is'
516 | && (
517 | $segment == $start
518 | ||
519 | \in_array($segment, $barrier, true)
520 | )
521 | ) {
522 | // end of group
523 | $groups[] = $group;
524 | // start new group
525 | $group = [];
526 | $position = 'group_finish';
527 | }
528 |
529 | if ($segment == $start) {
530 | $position = 'group_is';
531 | }
532 |
533 | // add element to group
534 | if ($position == 'group_is') {
535 | $group[] = $edi_row;
536 | }
537 | }
538 |
539 | return $groups;
540 | }
541 |
542 | /**
543 | * unwrap string splitting rows on terminator (if not escaped)
544 | *
545 | * @param string $string
546 | * @return \Generator
547 | */
548 | private static function unwrap($string)
549 | {
550 | $array = \preg_split("/(? 1) {
587 | throw new ReaderException('Segment "'.$segment_name.'" is ambiguous');
588 | }
589 |
590 | if ($required && ! isset($matchingSegments[0])) {
591 | throw new ReaderException('Segment "'.$segment_name.'" no exist');
592 | }
593 |
594 | return $matchingSegments[0] ?? false;
595 | }
596 | }
597 |
--------------------------------------------------------------------------------
/src/EDI/ReaderException.php:
--------------------------------------------------------------------------------
1 |