Net_DNS2 v2: Breaking a Decade of Backwards Compatibility

I have maintained Net_DNS2 since 2010. It started as a cleanup of the old PEAR Net_DNS library, and for most of its life the guiding rule was simple: do not break anyone. That rule held through PHP 5.3, 5.4, 7.x, and into 8.x. With v2.0 I broke it deliberately, and I think it was overdue.

What PEAR-Era Naming Costs You

Net_DNS2 predates PSR-4, namespaces, and any modern autoloading convention. The class naming reflected that:

$r = new Net_DNS2_Resolver(['nameservers' => ['8.8.8.8']]);
$result = $r->query('example.com', 'MX');

Underscores as a pseudo-namespace worked, and it left the library carrying an autoloader that mapped underscores to directory separators, class names that grew to Net_DNS2_RR_OPENPGPKEY, and no way to use any of the type machinery PHP had spent a decade adding. Every resource record type was a loosely typed bag of public properties. Every enumerated value was an integer or a bare string, validated by convention.

The practical cost was in the bug reports. A meaningful share were people passing the wrong type into something, getting no error, and finding out later when the wire format came out malformed. The library could not tell them, because it had no way to say what it expected.

v2.0 Requires PHP 8.1

The v2.0 rewrite moved to PSR-4 and real namespaces, with the parallel rename you would expect:

$r = new \NetDNS2\Resolver(['nameservers' => ['8.8.8.8']]);
$result = $r->query('example.com', 'MX');

The floor is PHP 8.1, which is what enums require. That version choice is the entire reason the break was worth making. DNS is a protocol built almost entirely out of small enumerated sets: record types, classes, opcodes, response codes, DNSSEC algorithms, digest types. Modelling those as integer constants means every function that accepts one accepts any integer. Modelling them as backed enums means the type system rejects nonsense before a packet is ever assembled.

Most class, method, and property names survived the move. Someone upgrading is mostly changing Net_DNS2_Thing to \NetDNS2\Thing and raising their PHP requirement, which is a mechanical change a search and replace handles. That was the design constraint I set for myself: break the naming, keep the shape.

Deciding to Break Compatibility

The argument against was straightforward. Net_DNS2 has a long tail of users on old PHP, often embedded in something they inherited and do not want to touch. A hard PHP 8.1 floor strands all of them.

What changed my mind was noticing that I was writing new code to work around the absence of types, then writing tests to catch the bugs that the absent types would have caught, then answering issues from people who hit those bugs anyway. The library was subsidising PHP 5 compatibility with permanent complexity, and the people paying that tax were mostly not the people benefiting from it.

The compromise is that v1.x still exists and still gets fixes. That is the part I would recommend to anyone in the same position. A hard break is much easier to justify when the old branch does not disappear the same day, and tagging a final v1 release that people can pin to costs almost nothing.

What I Would Do Differently

I would have done it sooner. The signal was there for years: every feature request that involved better validation ran into the same wall, and I kept routing around it. Deprecation cycles have a cost too, and carrying a compatibility promise you have quietly stopped believing in is worse than announcing the break.

I would also have been more aggressive about the enum conversion in one pass. Doing it incrementally meant a stretch where some values were enums and some were still integers, and the mixed state was more confusing than either endpoint. If you are going to break, break cleanly and finish.

If you use the library and are still on v1, there is no urgency. If you are starting something new on PHP 8.1 or later, start on v2. The DNS lookup tool on mrdns.com runs on this code, so it gets exercised against real-world responses continuously, which has caught more parsing edge cases than my test suite ever did.