sandwich/vies-bundle
Validate EU VAT numbers through the official VIES service with sandwich/vies-bundle. It wraps VIES lookups in a configurable PHP API with error handling and framework integration, making it easy to check VAT registration status reliably in your application.
The package is a thin Symfony DI wrapper around the sandwich/vies client, which talks to the EU VIES SOAP service to confirm that a VAT number is registered and active. There is no HTTP layer, no queueing, and no UI — it gives you a configured service you inject and call.
Minimal steps
composer require sandwich/vies-bundle:^2.2 sandwich/vies
2.2 is the first release with Symfony 6 support — if you are on Symfony 6, this is the release you need; 2.1 and earlier will not resolve. It is a constraint-widening release, so there are no API changes to learn.config/bundles.php:
Sandwich\ViesBundle\ViesBundle::class => ['all' => true],
php -m | grep soap
composer show sandwich/vies-bundle sandwich/vies
php -v # Symfony 6 implies PHP 8.1+
bin/console debug:container --show-private | grep -i vies
bin/console config:dump-reference vies
use Sandwich\Vies\Vies;
$response = $this->get(Vies::class)->check('BE', '0417492025');
dump($response->isValid(), $response->getName(), $response->getAddress());
Where to look first
vendor/sandwich/vies-bundle/DependencyInjection/ — the extension class tells you the exact service id and config keys. With a handful of stars and a release history that skips years, the README may lag the 2.2 code; the DI extension is authoritative.vendor/sandwich/vies/src/ViesResponse.php — this is the object you will actually live with. Check whether your version exposes isValid(), getName(), getAddress(), and the raw requestDate.config:dump-reference output — check for timeout and WSDL override keys before you need them in production.First real use case
The canonical integration is a B2B registration form: a company enters a VAT number, you validate it before creating the account, and you store the returned trader name so finance can reconcile invoices later. Start there — it exercises the whole surface (service injection, response handling, error handling) in one screen.
Inject the client, never instantiate it. The bundle exists specifically so the WSDL location and SOAP options are configured in one place. Type-hint Sandwich\Vies\Vies in a constructor and let autowiring resolve it; on Symfony 6, constructor injection plus #[Autowire]-free autowiring should just work once the bundle is registered. If aliasing fails, alias the bundle's service id to the class in services.yaml.
Not on Symfony? Skip the bundle entirely. The 2.2 release only widens the Symfony constraint; the sandwich/vies client underneath is framework-agnostic and can be newed inside a Laravel service provider as a singleton, with the SOAP options from the bundle's DI extension copied over as the reference config. Everything below about the client applies unchanged; only the wiring differs.
Wrap it in a domain service. Do not let controllers or form types see the SOAP client. A thin VatValidator service lets you add normalization, caching, and logging in one spot and keeps the VIES-specific response object out of your domain:
final class VatValidator
{
public function __construct(
private Vies $vies,
private CacheInterface $cache,
private LoggerInterface $logger,
) {}
public function validate(string $countryCode, string $number): VatResult
{
$countryCode = strtoupper($countryCode);
$number = preg_replace('/[^0-9A-Z]/', '', strtoupper($number));
$key = sprintf('vies.%s.%s', $countryCode, $number);
return $this->cache->get($key, function (ItemInterface $item) use ($countryCode, $number) {
$item->expiresAfter(86400);
try {
$r = $this->vies->check($countryCode, $number);
} catch (\SoapFault $e) {
$this->logger->warning('VIES unavailable', ['fault' => $e->getMessage()]);
$item->expiresAfter(300); // retry sooner on failure, don't poison the cache
return VatResult::unavailable();
}
$this->logger->info('VIES checked', [
'vat' => $countryCode.$number,
'requestDate' => $r->getRequestDate(),
]);
return $r->isValid() ? VatResult::valid($r->getName(), $r->getAddress()) : VatResult::invalid();
});
}
}
Normalize before you send. VIES expects a two-letter uppercase country code and a number with the country prefix stripped and no spaces, dots, or dashes. Belgian numbers in particular are entered as BE 0417.492.025 and will fail lookup if passed raw. Normalize at the boundary, store the normalized form, and keep the raw input for the user's records.
Expose it as a Symfony constraint if you use forms. A VatNumber constraint whose validator calls your VatValidator composes cleanly with Assert\NotBlank and gives you translated error messages for free. Keep "VIES is down" as a separate violation message from "number is not valid" — collapsing them causes support tickets and, in some jurisdictions, is misleading to the customer.
Treat the result as evidence, not just a boolean. VIES responses carry a requestDate. Store it alongside the name and address. Auditors and tax authorities accept a timestamped VIES confirmation as proof of due diligence; a bare boolean in your database proves nothing.
Never validate inline on a hot path. The EU service is a legacy SOAP endpoint with unpredictable latency and explicit concurrency limits. Validate on signup, on a "re-check" admin action, and in an offline job for bulk data — never inside a listing page or an API request that a crawler can hit.
For bulk re-validation, run a console command that walks VAT numbers with a small sleep (roughly one request per second) and records changes. Newer VIES deployments return MS_MAX_CONCURRENT_REQ when you exceed the per-country limit; a single-threaded command with a delay is the simplest way to stay under it.
VIES is frequently down, and it is not your bug. Member states take their validation services offline for maintenance, often at short notice. Your code must distinguish three states — valid, invalid, and could not determine — and the last one must never block a user permanently. Queue the account for re-check rather than rejecting it.
The 2.2 bump changes your dependency graph, not your application code. The release note is a single line: Symfony 6 support. Expect no service-id, config-key, or response-object changes. Still run your suite after upgrading — Symfony 6 tightened container and deprecation behaviour around bundle extensions, and a package this small will not have integration tests covering your usage.
isValid() === false does not mean fraud. A young company may not appear in VIES yet, and some legitimate traders are registered in a different member state than the one they quote. Surface the raw response and let a human review edge cases.
SOAP faults are thrown, not returned. Unlike an HTTP client, a malformed country code or a service outage raises \SoapFault. Catch it. A SoapFault on a form submission without a handler is a 500 to the customer.
Configure a timeout. The default SOAP connection timeout is generous; a stuck request will occupy a PHP-FPM worker. If the bundle's config exposes connection/response timeouts, set them to a few seconds — the service is either fast or broken. If it does not, override the WSDL/SOAP options in the bundle config or decorate the service with your own client that does.
Cache aggressively but not blindly. VAT registrations change infrequently; a 24-hour cache is reasonable for display, a fresh check is prudent at the moment of invoice issuance. Do not cache negative results for as long, and never cache the "unavailable" result at all — cache it for minutes at most, or you will serve failures long after VIES recovers.
Mock the client in tests. VIES in CI will make your suite flaky and slow, and hammering it from CI is impolite. Alias your VatValidator or Vies service in config/services_test.yaml to a stub returning canned ViesResponse objects. Test the three outcomes, not just valid and invalid.
Extension point is limited by design. The bundle's job is DI; the underlying client is where behaviour lives. If you need retries, circuit breaking, or an HTTP fallback, decorate the service rather than fork the bundle. The 2.2 release is a good sign — it means someone is still tracking upstream Symfony — but this remains a low-traffic, effectively single-maintainer package with no Symfony 7 story yet, so keep your integrations behind an interface you control. If you ever need to swap to a maintained alternative, only your VatValidator should need to change.
Pin the version and audit the lockfile. Lock sandwich/vies-bundle at ^2.2 and sandwich/vies to an exact version, and check periodically that the WSDL URL the bundle ships still resolves — the EU has moved endpoints in the past, and a silent 404 on the WSDL is a runtime failure, not a build-time one. Also watch for Symfony 7: when your framework upgrade lands, this package is the kind that gets left behind, so have the "call the client directly from a service provider" fallback in mind.
Log the requestDate from the response, not your own clock. They can differ by hours when a member state serves stale data, and the discrepancy matters if you ever need to defend a decision to a tax authority.
How can I help you explore Laravel packages today?