self::ISBN10, 'ISBN13' => self::ISBN13, 'EAN' => self::EAN, 'EAN8' => self::EAN8, 'EAN13' => self::EAN13, 'UPC' => self::UPC, 'UPC-A' => self::UPC_A, 'UPCA' => self::UPC_A, 'UPC-E' => self::UPC_E, 'UPCE' => self::UPC_E, 'GTIN' => self::GTIN, 'SKU' => self::SKU, 'MPN' => self::MPN, 'ASIN' => self::ASIN, 'JAN' => self::JAN, 'ISSN' => self::ISSN, 'ISMN' => self::ISMN, 'BARCODE' => self::BARCODE, 'QRCODE' => self::QR_CODE, 'QR CODE' => self::QR_CODE, 'QR PAYLOAD' => self::QR_PAYLOAD, 'MANUFACTURER CODE' => self::MANUFACTURER_CODE, 'INTERNAL CODE' => self::INTERNAL_CODE, 'AIC' => self::AIC, 'AIC6' => self::AIC6, 'AIC9' => self::AIC9, 'ATC' => self::ATC, ]; private IdentifierNormalizer $normalizer; private IdentifierValidator $validator; private IdentifierChecksum $checksum; private IdentifierMetadata $metadata; public function __construct( ?IdentifierNormalizer $normalizer = null, ?IdentifierValidator $validator = null, ?IdentifierChecksum $checksum = null, ?IdentifierMetadata $metadata = null ) { $this->normalizer = $normalizer ?? new IdentifierNormalizer(); $this->checksum = $checksum ?? new IdentifierChecksum(); $this->validator = $validator ?? new IdentifierValidator($this->checksum); $this->metadata = $metadata ?? new IdentifierMetadata(); } public function detect(string $value): string { $input = trim($value); if ($input === '') { return self::UNKNOWN; } $prefixedType = $this->detectPrefixedType($input); if ($prefixedType !== null) { return $prefixedType; } $compact = strtoupper(str_replace([' ', '-'], '', $input)); if (preg_match('/^[0-9]{9}[0-9X]$/', $compact) === 1) { return self::ISBN10; } if (preg_match('/^97[89][0-9]{10}$/', $compact) === 1) { return self::ISBN13; } if (preg_match('/^[0-9]{7}[0-9X]$/', $compact) === 1 && strpos($input, '-') !== false) { return self::ISSN; } if (preg_match('/^[A-Z0-9]{10}$/', $compact) === 1 && preg_match('/[A-Z]/', $compact) === 1) { return self::ASIN; } if (preg_match('/^[0-9]{14}$/', $compact) === 1) { return self::GTIN; } if (preg_match('/^[A-V][0-9]{2}(?:[A-Z]{1,2}(?:[0-9]{2})?)?$/', $compact) === 1) { return self::ATC; } if (preg_match('/^[0-9]{12,13}$/', $compact) === 1) { return self::EAN; } if (preg_match('/^[0-9]{8,11}$/', $compact) === 1) { return self::BARCODE; } if (preg_match('/[A-Za-z]/', $input) === 1 && strlen($input) >= 3) { return self::TITLE; } return self::UNKNOWN; } public function classify(string $value): string { return $this->detect($value); } public function resolve(string $value): IdentifierResult { $identifierType = $this->detectUniversalType($value); $normalizedValue = $this->normalizer->normalize($value, $identifierType); $validation = $this->validator->validate($identifierType, $normalizedValue); $metadata = $this->metadata->resolve($identifierType, $normalizedValue); $notes = array_merge($validation['notes'], $metadata['notes']); if ($identifierType === self::ISBN13 && $validation['checksum_valid'] === false) { $notes[] = 'Possible ISBN.'; } if ($identifierType === self::EAN13) { $notes[] = 'Looks like EAN.'; } $convertedValue = $identifierType === self::ISBN10 ? $this->checksum->convertIsbn10ToIsbn13($normalizedValue) : null; return new IdentifierResult( $identifierType, $normalizedValue, $validation['checksum_valid'], $metadata['country_prefix'], $metadata['gs1_prefix'], $metadata['expected_domains'], $this->resolveConfidence($identifierType, $validation['checksum_valid']), $notes, $convertedValue ); } public static function getSupportedTypes(): array { return [ self::ISBN10, self::ISBN13, self::EAN8, self::EAN13, self::UPC_A, self::UPC_E, self::EAN, self::UPC, self::GTIN, self::SKU, self::MPN, self::ASIN, self::JAN, self::ISSN, self::ISMN, self::BARCODE, self::QR_CODE, self::QR_PAYLOAD, self::TITLE, self::MANUFACTURER_CODE, self::INTERNAL_CODE, self::AIC6, self::AIC9, self::AIC, self::ATC, self::UNKNOWN, ]; } private function detectPrefixedType(string $input): ?string { if (preg_match('/^([A-Za-z0-9 -]+):\s*\S.+$/', $input, $matches) !== 1) { return null; } $prefix = strtoupper(trim($matches[1])); return self::PREFIX_TYPES[$prefix] ?? null; } private function detectUniversalType(string $value): string { $input = trim($value); if ($input === '') { return self::UNKNOWN; } $prefixedType = $this->detectPrefixedType($input); if ($prefixedType !== null) { $prefixedValue = preg_replace('/^[^:]+:\s*/', '', $input) ?? $input; $numericValue = preg_replace('/\D/', '', $prefixedValue) ?? ''; if ($prefixedType === self::EAN) { return strlen($numericValue) === 8 ? self::EAN8 : self::EAN13; } if ($prefixedType === self::UPC) { return strlen($numericValue) === 8 ? self::UPC_E : self::UPC_A; } if ($prefixedType === self::AIC6 || $prefixedType === self::AIC9) { return $prefixedType; } if ($prefixedType === self::ATC) { return self::ATC; } if ($prefixedType === self::AIC) { return strlen($numericValue) === 9 ? self::AIC9 : self::AIC6; } $universalAliases = [self::QR_CODE => self::QR_PAYLOAD]; return $universalAliases[$prefixedType] ?? $prefixedType; } if (filter_var($input, FILTER_VALIDATE_URL) !== false) { return self::QR_PAYLOAD; } $compact = strtoupper(preg_replace('/[\s-]+/', '', $input) ?? $input); if (preg_match('/^[A-V][0-9]{2}(?:[A-Z]{1,2}(?:[0-9]{2})?)?$/', $compact) === 1) { return self::ATC; } if (preg_match('/^\d{9}[\dX]$/', $compact) === 1) { return self::ISBN10; } if (preg_match('/^97[89]\d{10}$/', $compact) === 1) { return self::ISBN13; } if (preg_match('/^B0[A-Z0-9]{8}$/', $compact) === 1) { return self::ASIN; } if (preg_match('/^\d{7}[\dX]$/', $compact) === 1 && strpos($input, '-') !== false) { return self::ISSN; } if (preg_match('/^\d{8}$/', $compact) === 1) { return $compact[0] === '0' ? self::UPC_E : self::EAN8; } if (preg_match('/^\d{12}$/', $compact) === 1) { return self::UPC_A; } if (preg_match('/^\d{13}$/', $compact) === 1) { $prefix = (int) substr($compact, 0, 3); return ($prefix >= 450 && $prefix <= 459) || ($prefix >= 490 && $prefix <= 499) ? self::JAN : self::EAN13; } if (preg_match('/^\d{14}$/', $compact) === 1) { return self::GTIN; } if (preg_match('/^\d+$/', $compact) === 1) { return self::BARCODE; } if (preg_match('/[A-Za-z]/', $input) === 1 && strlen($input) >= 3) { return self::TITLE; } return self::UNKNOWN; } private function resolveConfidence(string $identifierType, ?bool $checksumValid): float { if ($identifierType === self::UNKNOWN) { return 0.1; } if ($checksumValid === true) { return 0.98; } if ($checksumValid === false) { return 0.65; } return 0.9; } }