ddeboer/vatin-bundle
Symfony bundle integrating ddeboer/vatin. Validates EU VAT ID (VATIN) format via Symfony Validator and can optionally verify existence via the VIES SOAP service. Also exposes services for direct VATIN validation and VIES lookups.
composer require ddeboer/vatin-bundle
config/app.php under providers:
Ddeboer\VatinBundle\DdeboerVatinBundle::class,
php artisan vendor:publish --tag=vatin-config
Add the Vatin constraint to a Laravel Form Request or model property:
use Ddeboer\VatinBundle\Validator\Constraints\Vatin;
class StoreCompanyRequest extends FormRequest
{
#[Vatin(checkExistence: true)] // Validates format + existence via VIES
public string $vat_number;
}
Test with:
php artisan request:validate StoreCompanyRequest --input "vat_number=NL123456789B01"
Leverage Laravel’s built-in validation pipeline:
public function rules()
{
return [
'vat_number' => [
'required',
new Vatin(), // Format validation
// new Vatin(checkExistence: true), // Format + existence (VIES API)
],
];
}
Tip: Use checkExistence: false by default to avoid API calls during development.
Inject the validator via Laravel’s container:
use Ddeboer\VatinBundle\Validator\VatValidatorInterface;
class CompanyService
{
public function __construct(
private VatValidatorInterface $validator
) {}
public function validateVat(string $vatNumber): bool
{
return $this->validator->isValid($vatNumber);
}
}
Use the VIES client for real-time checks:
use Ddeboer\VatinBundle\Vies\Client;
class VatApiService
{
public function __construct(
private Client $viesClient
) {}
public function checkVatExistence(string $countryCode, string $vatNumber): bool
{
try {
return $this->viesClient->checkVat($countryCode, $vatNumber)->isValid();
} catch (\Exception $e) {
// Log and handle API failures
return false;
}
}
}
Extend Laravel’s validation rules for reusable logic:
use Ddeboer\VatinBundle\Validator\VatValidatorInterface;
use Illuminate\Contracts\Validation\Rule;
class ValidVat extends Rule
{
public function __construct(
private VatValidatorInterface $validator,
private bool $checkExistence = false
) {}
public function passes($attribute, $value): bool
{
return $this->validator->isValid($value, $this->checkExistence);
}
public function message(): string
{
return 'The :attribute is not a valid VAT number.';
}
}
Usage:
'vat_number' => ['required', new ValidVat($validator, checkExistence: true)],
Service Binding:
Bind the bundle’s services to Laravel’s container in AppServiceProvider:
public function register()
{
$this->app->bind(
\Ddeboer\VatinBundle\Validator\VatValidatorInterface::class,
\Ddeboer\VatinBundle\Validator\VatValidator::class
);
$this->app->bind(
\Ddeboer\VatinBundle\Vies\ClientInterface::class,
\Ddeboer\VatinBundle\Vies\Client::class
);
}
Configuration:
Override default settings in config/vatin.php:
'vies' => [
'timeout' => 5, // Seconds for VIES API calls
'cache' => true, // Enable caching for API responses
],
Testing: Mock the VIES client in tests to avoid real API calls:
$this->mock(Ddeboer\VatinBundle\Vies\Client::class)
->shouldReceive('checkVat')
->andReturn(new \Ddeboer\VatinBundle\Vies\Response(true));
Cache VIES Responses: Use Laravel’s cache to store VIES API responses:
$cacheKey = "vies_{$countryCode}_{$vatNumber}";
$response = Cache::remember($cacheKey, now()->addHours(1), function () use ($viesClient, $countryCode, $vatNumber) {
return $viesClient->checkVat($countryCode, $vatNumber);
});
Batch Validation: For bulk operations (e.g., importing companies), validate VAT numbers offline first, then sync with VIES in batches.
VIES API Unreliability:
try {
$response = $viesClient->checkVat('NL', '123456789B01');
} catch (\Symfony\Component\Validator\Exception\ValidatorException $e) {
// Fallback to local validation or notify admin
}
Symfony Dependency Conflicts:
symfony/http-client:^5.0), conflicts may arise with Symfony 8.composer.json:
"symfony/http-client": "^6.0",
"symfony/validator": "^6.0"
Non-EU VAT Numbers:
public function passes($attribute, $value): bool
{
if (!Str::startsWith($value, ['US', 'CA', 'GB'])) {
return $this->validator->isValid($value);
}
return true;
}
Attribute Syntax in Older PHP:
/**
* @Vatin(checkExistence=true)
*/
protected string $vatNumber;
Case Sensitivity:
NL123456789B01 vs. nl123456789b01). Normalize inputs:
$vatNumber = strtoupper(trim($request->vat_number));
Enable Debugging for VIES:
Add debug logging to config/vatin.php:
'debug' => env('APP_DEBUG', false),
This will log VIES API requests/responses during development.
Validate Format Locally: Test VAT number formats offline before hitting the VIES API:
$validator = app(VatValidatorInterface::class);
if (!$validator->isValid($vatNumber, false)) {
// Format is invalid; no need to call VIES
return false;
}
Handle Partial Failures: The VIES API may return partial failures (e.g., valid format but invalid number). Check the response:
$response = $viesClient->checkVat('NL', '123456789B01');
if ($response->isValid()) {
// Valid
} elseif ($response->isInvalid()) {
// Invalid number
} else {
// Unknown (e.g., API error)
}
Custom Validation Logic:
Extend the VatValidator to add business rules:
class CustomVatValidator extends \Ddeboer\VatinBundle\Validator\VatValidator
{
public function isValid(string $vatNumber, bool $checkExistence = false): bool
{
// Add custom logic (e.g., blacklist certain VAT numbers)
if (in_array($vatNumber, $this->blacklistedVats)) {
return false;
}
return parent::isValid($vatNumber, $checkExistence);
}
}
Override VIES Client: Replace the default VIES client with a custom implementation (e.g., for testing or caching):
$this->app->bind(
\Ddeboer\VatinBundle\Vies
How can I help you explore Laravel packages today?