runcom/ean-validation-bundle
A Symfony bundle for validating EAN and GTIN barcodes. It provides validation constraints and services to check product codes such as EAN-8, EAN-13, UPC, and GTIN, helping ensure barcode data is correctly formatted and checksum-valid in your applications.
Before you install anything, internalize what this is: runcom/ean-validation-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config file, no facade, no Laravel auto-discovery. What it actually ships is a Symfony Validator Constraint + ConstraintValidator pair that checks an EAN string's digit checksum. That single isValid() method is the entire value proposition — treat the bundle as reference source code, not a dependency you'll wire into your app.
Where to look first: open the constraint class (likely Runcom\EanValidationBundle\Validator\Constraints\Ean or similar) and its paired validator. Read the checksum loop. That's ~20 lines of arithmetic you can lift wholesale. Everything else in the bundle exists to register it with Symfony's validator service container — irrelevant to you.
Minimal install (for reference reading only):
composer require --dev runcom/ean-validation-bundle
Or just git clone the repo and read it — you don't need it in composer.json if you're only copying logic.
First use case: a product/catalog form where staff type barcodes by hand. You want to reject typos like a transposed digit before the record hits the DB. That's exactly the job this handles. Known-good smoke test value: 4006381333931 (valid EAN-13). Known-bad: 4006381333930.
Pattern 1 — lift the logic into a Laravel Rule object. This is what you'll actually do 90% of the time. Take the checksum method, drop it into app/Rules/Ean.php:
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Ean implements ValidationRule
{
public function __construct(private int $length = 13) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) || !preg_match('/^\d{13}$/', $value) || !$this->checksum($value)) {
$fail("The {$attribute} is not a valid EAN-13.");
}
}
private function checksum(string $ean): bool
{
$sum = 0;
for ($i = 0; $i < 12; $i++) {
$sum += (int) $ean[$i] * ($i % 2 === 0 ? 1 : 3);
}
return ((10 - $sum % 10) % 10) === (int) $ean[12];
}
}
Pattern 2 — FormRequest integration. Once it's a Rule, it composes like any other Laravel rule. Use it in ProductRequest::rules():
return [
'name' => ['required', 'string', 'max:255'],
'ean' => ['required', 'string', new \App\Rules\Ean()],
];
Pattern 3 — bulk import validation. The highest-value day-to-day scenario. Supplier CSV with thousands of barcodes. Run each row through the rule inside a Rule::forEach() on the array, or in a chunked Bus::batch validator. Because the rule is pure (no I/O, no container lookups), you can instantiate it inside a queue worker for a million rows without breaking a sweat.
Pattern 4 — model-level guard. If you want the domain invariant enforced below the HTTP layer, add an assertEan() helper on the model or a saving observer. Keep the checksum in a single App\Support\Ean static class so both the rule and the model call the same code — don't duplicate the loop.
Pattern 5 — if you insist on keeping the dependency wired in. You'd have to bootstrap Symfony's ValidatorBuilder manually, load the bundle's constraint mapping, and register the validator factory as a Laravel singleton. This pulls symfony/validator, symfony/translation-contracts, and a few others into your composer.json. It works, but you're carrying a framework you don't use to validate 13 digits. Don't.
composer.json in production entirely.php artisan vendor:publish and config/bundles.php conventions don't apply. If you add it as a runtime dependency expecting it to "just work," you'll get a class-not-found or an unregistered validator.13 will silently accept valid checksums on the wrong length. Validate length first, then checksum.0075678164125 becomes 75678164125 if cast to int and the checksum shifts. Guard against JSON/CSV importers that coerce numeric strings — this is the #1 real-world failure. Enforce string in your rule and cast on the way in.0 rather than rejecting it, or your import reports explode with false negatives." 4006381333931"), leading-apostrophe Excel artifacts ('4006381333931), or a stray check digit from a mis-scanned label. trim() before validating and in the rule itself.isValid() may not guard against non-string input (arrays, nulls) depending on version. In Laravel you always get a mixed value — bail early with is_string() and a regex before touching indices, or PHP 8 will throw on offset access.new Ean(8), new Ean(13)) so the same rule serves EAN-8 for small products and EAN-13 for retail. One rule class, two use sites, no drift.tests/Fixtures/ean.php and assert both directions in a Pest/PHPUnit data provider. This is cheap insurance that a future refactor of the loop doesn't quietly flip a weight multiplier.How can I help you explore Laravel packages today?