v2.0.0
This release corrects the SOAP request encoding, the response conversion and the
exception contract. Each of those changes alters observable behaviour for existing
code, which is why it is a major version. Read "Breaking changes" before upgrading.
Breaking changes
- VAT rate values can be
null.VatRate::getValue(),getDecimalValue(),
getRawValue()andgetValueAsFloat()are nullable, and__toString()returns
''for such a rate. The service schema (VatRetrievalServiceType.xsd) declares
valueas optional for every rate type, not only exempt ones — a rate a member
state does not levy (for examplePARKING_RATE) arrives without a value. Previously
any value-less rate aborted the entire response with aConversionException.
Guard withif ($rate->getValue() !== null)before arithmetic. Note that
(string) $rateyields'', which is not safe to pass toBigDecimal::of(). - Different exception types leave
retrieveVatRates(). SOAP faults the service
attributes to the caller now surface asInvalidRequestException, and faults it
attributes to itself asServiceUnavailableException, where both previously
surfaced asSoapFaultException. Every SDK exception raised while converting a
response — includingConversionException,ParseException,ValidationException
andUnexpectedResponseException— now propagates unchanged rather than being
wrapped inUnexpectedResponseException, because the client catches the
VatServiceExceptionbase class. Existingcatchblocks may no longer match. - Failures that never reached the service are reported as such. ext-soap raises
local errors (a proxy or error page returning non-XML, a truncated response, a
serialization error) with a bareClientfault code, which is indistinguishable
from a service rejection by code alone. These now raiseServiceUnavailableException,
which callers may retry, instead ofInvalidRequestExceptiontelling them their
input was wrong. A fault only counts as a service rejection when it carries a
namespace-prefixed code or a TEDB identifier. FaultEventListener::extractErrorDetails()was removed. It had no caller inside
the SDK; fault details are collected while the exception is built.- The event and middleware layers are removed, about a quarter of the source. No
default code path ever reached them: the only places the SDK constructed a listener
or a middleware were the@exampleblocks in their own docblocks. Gone are
Netresearch\EuVatSdk\Engine\*(EventAwareEngine,MiddlewareEngineand the three
event classes),Netresearch\EuVatSdk\Middleware\*(MiddlewareInterface,
LoggingMiddleware),RequestEventListener,ResponseEventListenerand
CorrelationIdProvider— the last generated an identifier that was never attached to
a request nor exposed on a response, so it could not correlate anything.
FaultEventListenerstays and is now called directly by the client.
With them goClientConfiguration::withMiddleware(),withEventSubscriber()and
VatRetrievalClientFactory::createWithEventSubscribers(), and the
ClientConfigurationconstructor no longer takes$eventSubscribersor$middleware.
To observe requests, implementTelemetryInterface; to intercept them, decorate
VatRetrievalClientInterface. - Telemetry is recorded for real.
ClientConfiguration::withTelemetry()and
VatRetrievalClientFactory::createWithTelemetry()accepted an implementation and then
never called it. The client now callsrecordRequest()with the measured duration on
success andrecordError()on failure, so an implementation that was silently idle
will start receiving calls. An exception thrown by a telemetry implementation is
logged and swallowed: metrics cannot fail a VAT lookup. - Rate categories are populated, and they live on the result.
VatRatesResponse::getResultsByCategory()could only ever return an empty array: the
converter hardcoded the category tonull. It is now read from the response.
VatRetrievalServiceType.xsdplacescategoryonvatRateResultsas a sibling of
raterather than inside it, so it is exposed asVatRateResult::getCategory()(the
identifier, for exampleFOODSTUFFS) andVatRateResult::getCategoryDescription()
(the text the service sends with it). Both arenullwhen the service reported no
category, which the schema permits and which standard rates never carry.
VatRate::getCategory()and the thirdVatRateconstructor argument are removed:
they modelled the data on the wrong object and could only returnnull. Replace
$result->getRate()->getCategory()with$result->getCategory(). The identifiers are
defined in the TEDB External Interface Specification, not in the schema, so an
unrecognised identifier passed togetResultsByCategory()returns[]rather than
raising an error. symfony/event-dispatcherandramsey/uuidare no longer required. Nothing in
the SDK used them once the event layer andCorrelationIdProviderwere gone. Projects
relying on them transitively through this package must require them directly.getResults()returns one entry per rate type and member state. The service
answers a single-country query with every rate it publishes for that country, so a
request for one member state commonly yields dozens of results. Code assuming one
result per country —$results[0], or an array keyed by member state — silently
used an arbitrary rate. Select onVatRate::isStandard()orgetType(), or use
VatRatesResponse::getResultsForCountry(). This behaviour is unchanged; it was
always the case and is documented here because the SDK's own examples got it wrong.- The documented TEDB fault codes were fictional.
TEDB-100,TEDB-101,
TEDB-102andTEDB-400appear in no service response, WSDL or schema; they
existed only in this SDK's own documentation and were never produced. The service
sends a responsibility marker infaultcodeplus a code such asTEDB-ERR-2in
faultstring. Code branching on the old constants never matched and must be
rewritten againstgetErrorCode(), which now returns the real code, ornull
when the fault string carries none. - Rate value accessors preserve the scale sent by the service.
getRawValue()
and(string) $rate->getValue()return"17.0"where they previously returned
"17".BigDecimalTypeConverterregistered forxsd:decimal, a type no schema in
resources/uses, so its typemap entry never matched: ext-soap decoded the
xs:doublerate value into a PHP float and the trailing zero was lost. It now
registers forxsd:doubleand parses the element text verbatim. String comparisons
and array keys built from these accessors change; numeric comparisons do not. BigDecimalTypeConverterno longer range-checks values. Values outside the
former -10..100 window are returned instead of raisingParseException. The check
ran on atoFloat()round-trip and would have aborted an entire multi-member-state
response over one implausible rate; plausibility is the caller's decision.BigDecimalTypeConverter::convertPhpToXml()returns a complete XML element
(<double>19.75</double>) instead of a bare numeric string, and
convertXmlToPhp()accepts the complete element ext-soap passes and returnsnull
for an empty one. This class is public; code reusing it in a custom typemap or
asserting on its return value is affected.DateTypeConverter::convertPhpToXml()returns a complete XML element
(<date>2024-01-15</date>) instead of a bare date string, as the ext-soap typemap
contract requires. This class is public; code reusing it in a custom typemap or
asserting on its return value is affected.
Added
- PHP 8.4 to the CI test and static-analysis matrices
- The
networktest group runs in CI, replaying committed cassettes offline; it was
previously excluded everywhere, which is how the broken fault mapping stayed hidden examples/is covered by static analysis, which catches an example calling a method
that does not exist. It cannot catch everything: a value the example itself types as
mixedhides an argument mismatch from the analyser
Changed
- Least-privilege permissions declared in the security workflow
- Migrated the quality-assurance workflow to the shared netresearch/.github php-ci
reusable workflow - Replaced generic contact emails with GitHub references
- Integration tests are enforced in CI instead of being advisory
- CI installs dependencies fresh from composer.json (composer.lock is no longer committed)
php-vcr/php-vcrconstrained to>=1.6.4 <1.8.2: from 1.8.2 itsSoapClient
declares a parameter the ext-soap-engine releases installable on PHP 8.2 and 8.3
do not, which is a fatal error rather than a test failure
Fixed
situationOnwas transmitted as an empty element, so the service answered
historical queries with current rates. All cassettes were re-recorded against the
corrected request body- Rates without a value no longer abort conversion of the whole response
- SOAP faults are classified against the shapes the service actually returns
- Documentation and examples called
getVatRate(), which does not exist, and
dereferenced nullable rate values unguarded - Examples keyed rates by member state, silently keeping one arbitrary rate per
country although the service returns several - Removed deprecated
libxml_disable_entity_loader()calls and restored libxml error
handling in afinallyblock - Host-identifying data (developer IP addresses, local certificate paths) removed from
the recorded cassettes - Cassettes are tracked by an explicit
.gitignorerule instead of manual force-adds,
so a fresh clone replays offline rather than recording live traffic - php-vcr is started from the PHPUnit bootstrap. It patches the SOAP transport while
that class is being included, so a suite that touched the transport first left it
unpatched and every later replay in that process reached the live service instead - The performance benchmarks are removed along with their CI job. The job executed
nothing at all — every test in it was excluded by group, so it reported success
without running one — and the suite behind it called the live service. What was
worth keeping is now two guards in the integration suite: a request for every
member state must cost a single service call, and a request no cassette answers
must fail locally instead of reaching the service failOnEmptyTestSuiteis set, so no CI gate can report success while executing no
tests
Removed
- Internal planning documents (
.github/internal/) from the repository composer.lock(consumers of a library resolve their own dependency set)