Search by

janisvepris / gs1-decoder

janisvepris

A library for parsing GS1 codes in PHP

Package info

github.com/JanisVepris/gs1-decoder

pkg:composer/janisvepris/gs1-decoder

Statistics

Installs: 12 203

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

2.0.0 2026-08-24 10:09 UTC

This package is auto-updated.

Last update: 2026-08-24 10:33:53 UTC


README

GitHub Actions Workflow Status GitHub Tag codecov

GS1 Barcode decoder

A zero-dependency PHP library that decodes GS1 barcode strings into typed application identifier objects.

Every application identifier in the GS1 General Specifications is supported — see supported identifiers for the full list, which is generated from the code.

Installation

This package requires PHP ^8.2 and has no runtime dependencies.

composer require janisvepris/gs1-decoder

Usage

<?php

use Janisvepris\Gs1Decoder\Decoder\Decoder;
use Janisvepris\Gs1Decoder\Enum\Gs1ApplicationIdentifier;

$decoder = new Decoder();

$decoded = $decoder->decode('0109521234543213' . '3103000189' . '17250630');

$decoded->hasIdentifier(Gs1ApplicationIdentifier::Gtin);                    // true
$decoded->getIdentifier(Gs1ApplicationIdentifier::Gtin)->getValue();        // '09521234543213'
$decoded->getIdentifier(Gs1ApplicationIdentifier::NetWeightKg)->getValue(); // 0.189
$decoded->getIdentifier(Gs1ApplicationIdentifier::ExpirationDate)->getValue(); // DateTime

Application identifier codes are cases of the Gs1ApplicationIdentifier enum rather than strings, so a typo is a compile-time problem rather than a silent null. If you are handed a code as a string, tryFrom() turns it into a case, or null when the package does not know it:

$code = Gs1ApplicationIdentifier::tryFrom('01');   // Gs1ApplicationIdentifier::Gtin
$code = Gs1ApplicationIdentifier::tryFrom('9999'); // null

Set the delimiter to match your input

Variable length identifiers are terminated by an FNC1 separator. The default delimiter is the literal string [FNC1], which is not what a barcode scanner produces — scanners emit FNC1 as the ASCII group separator, 0x1D. If you are decoding scanner or raw symbol data, set the delimiter first:

$decoder = (new Decoder())->setDelimiter("\x1d");

Getting this wrong does not raise an error, it silently mis-decodes: the variable length field runs on past its real end and swallows the identifiers that follow it.

$scanned = "0109521234543213" . "10ABC123" . "\x1d" . "17250630";

(new Decoder())->decode($scanned)->getIdentifierCount();                    // 2 - AI 10 ate the rest
(new Decoder())->setDelimiter("\x1d")->decode($scanned)->getIdentifierCount(); // 3 - correct

Reading the result

decode() returns a Barcode:

$decoded->getRawValue();        // the string that was decoded
$decoded->getIdentifierCount(); // how many identifiers were found
$decoded->getAllIdentifiers();  // ApplicationIdentifierInterface[]
$decoded->toArray();            // plain nested array, safe to json_encode

Every identifier exposes:

$identifier->getCode();         // Gs1ApplicationIdentifier case
$identifier->getValue();        // typed value - see below
$identifier->getRawValue();     // the characters as they appeared in the barcode
$identifier->getEnglishTitle(); // e.g. 'Net weight, KG'

A repeated identifier code overwrites the earlier one, so a Barcode holds at most one identifier per code.

Typed values

getValue() is typed per identifier, and some shapes expose extra accessors for the parts of a compound field:

Identifiers getValue() Also available
Most identifiers string
Measures and amounts (31nn36nn, 390, 392, 394, 395) float, already shifted by the decimal point position getDecimalPosition()
Dates (1117, 4326, 7006) DateTime at the end of the day
Date and time (4324, 4325, 7003, 7250, 7251, 8008) DateTime as given
Harvest date (7007) DateTime opening the range at midnight getEndDate(): ?DateTime
Test by date (7011) DateTime hasTime(): bool
Amount with currency (391, 393) float amount getCurrencyCode()
Postal code / processor with country (421, 70307039) string without the country code getCountryCode()
Certification reference (72307239) string without the scheme getSchemeId()
GRAI (8003) string serial, empty when absent getAssetType()
ITIP (8006, 8026) string, the whole 18 digit field getGtin(), getPieceNumber(), getTotalPieces()
Temperature (43304333) float, negative when the field is signed isNegative()
$amount = $decoded->getIdentifier(Gs1ApplicationIdentifier::AmountPayableWithIsoCurrency);
$amount->getCurrencyCode(); // '978'
$amount->getValue();        // 1234.56

$itip = $decoded->getIdentifier(Gs1ApplicationIdentifier::Itip);
$itip->getGtin();           // '98410843114508'
$itip->getPieceNumber();    // 2
$itip->getTotalPieces();    // 3

What the decoder does not do

It does not validate. There is no check digit verification, no character set enforcement, and no check that an identifier is allowed to appear where it does. The decoder reads what it can and hands it back.

It does not throw for bad input. Decoding always returns a Barcode, possibly a partial one:

  • an identifier whose value cannot be parsed — an impossible date, say — is skipped, and decoding continues
  • a truncated identifier at the end of the barcode ends the scan, keeping whatever was found before it
  • characters that do not resolve to a known identifier code end the scan. The code being accumulated is never reset, so the decoder consumes the rest of the barcode looking for a match and finds nothing: 0109521234543213ZZ17250630 yields only the GTIN, and the 17 after the junk is never seen

If you need to know that a barcode was fully understood, compare getIdentifierCount() against what you expect, or check for the identifiers you require.

Undelimited variable length fields will over-consume. This is inherent to the format rather than a limitation of the decoder: a variable length identifier reads until the delimiter or its maximum length, so if it is neither last in the barcode nor followed by an FNC1, it will take the following identifier's characters with it. Setting the right delimiter is what avoids this.

Customising

Restrict or replace the identifier map

The decoder is built with a map of every identifier that ships with the package. Passing your own replaces it entirely, which is a cheap way to decode only the identifiers you care about:

<?php

use Janisvepris\Gs1Decoder\ApplicationIdentifier\Gtin;
use Janisvepris\Gs1Decoder\ApplicationIdentifier\ExpirationDate;
use Janisvepris\Gs1Decoder\Decoder\Decoder;
use Janisvepris\Gs1Decoder\Enum\Gs1ApplicationIdentifier;
use Janisvepris\Gs1Decoder\IdentifierMap\IdentifierMap;

$decoder = new Decoder(new IdentifierMap([
    Gs1ApplicationIdentifier::Gtin->value => Gtin::class,
    Gs1ApplicationIdentifier::ExpirationDate->value => ExpirationDate::class,
]));

The map is keyed by ->value rather than by the case itself, because PHP arrays cannot be keyed by an enum instance. Its methods take the case:

$map = $decoder->getIdentifierMap();

$map->hasIdentifierClass(Gs1ApplicationIdentifier::Gtin);
$map->getIdentifierClass(Gs1ApplicationIdentifier::Gtin);
$map->removeIdentifierClass(Gs1ApplicationIdentifier::Gtin);
$map->addIdentifierClass(Gs1ApplicationIdentifier::Gtin, Gtin::class);

Define your own identifier class

Extend the abstract that matches the field's shape, assign the enum case, and declare the lengths:

<?php

use Janisvepris\Gs1Decoder\ApplicationIdentifier\Abstract\SimpleIdentifier;
use Janisvepris\Gs1Decoder\Decoder\Decoder;
use Janisvepris\Gs1Decoder\Enum\Gs1ApplicationIdentifier;

class MyGtin extends SimpleIdentifier
{
    protected Gs1ApplicationIdentifier $code = Gs1ApplicationIdentifier::Gtin;
    protected int $length = 14;
    protected string $englishTitle = 'GTIN, our wording';
}

// addIdentifierClass() throws DuplicateIdentifierCodeException if the code is already
// mapped, so remove the shipped class before swapping yours in
$decoder = new Decoder();
$decoder->getIdentifierMap()
    ->removeIdentifierClass(Gs1ApplicationIdentifier::Gtin)
    ->addIdentifierClass(Gs1ApplicationIdentifier::Gtin, MyGtin::class);

The abstracts in ApplicationIdentifier\Abstract\ cover the shapes the specification uses:

Abstract Field Value
SimpleIdentifier fixed length string
VariableLengthIdentifier min to max length string
DecimalIdentifier fixed length, decimal point position float
VariableLengthDecimalIdentifier variable length, decimal point position float
CurrencyAmountIdentifier ISO 4217 currency code plus amount float
DateIdentifier YYMMDD DateTime
DateTimeIdentifier YYMMDDHHMM DateTime
LongDateIdentifier YYYYMMDD or YYYYMMDDHHMM DateTime
OptionalTimeDateIdentifier YYMMDD with optional HHMM DateTime
DateRangeIdentifier start date with optional end date DateTime
ProductionDateTimeIdentifier YYMMDDHH with optional minutes and seconds DateTime
PrefixedIdentifier leading characters that identify rather than describe string
TemperatureIdentifier six digits with an optional sign float
ItipIdentifier GTIN plus piece number plus total pieces string

PrefixedIdentifier is the base for CountryPrefixedIdentifier, CertificationReferenceIdentifier and AssetTypePrefixedIdentifier; extend it directly if you need a different prefix length.

Anything implementing ApplicationIdentifierInterface works, and implementing DecimalIdentifierInterface or VariableLengthIdentifierInterface is what makes the decoder consume a decimal point position or read up to a delimiter — the decoder branches on the contracts in ApplicationIdentifier\Contract\, not on the abstracts.

Because a code is an enum case rather than an arbitrary string, an identifier class can only carry a code the enum already lists. Overriding a shipped identifier works, but decoding a code this package does not know needs a case added to Gs1ApplicationIdentifier first — a map entry on its own is never reached.

Development

composer test        # phpunit
composer cs          # php-cs-fixer, dry run - CI fails on any diff
composer cs-fix      # php-cs-fixer, apply
composer stan        # phpstan, level 9
composer check-all   # cs + stan + test

docs/SupportedIdentifiers.md is generated; run composer generate-ai-list after adding an identifier rather than editing it by hand.

Changelog

Notable changes are recorded in CHANGELOG.md.

License

MIT. See LICENSE.