A stack of seven package tiles inside a plugin zip outline, five of them labelled with a different licence, one tile outlined in red as GPLv3-only, with only two tiles marked as declared in composer.json.

We ship a WordPress plugin that emails each person who registers for an event a PDF ticket with a QR code on it. Two libraries do the heavy lifting: one draws the PDF, one encodes the QR. Last month the QR one had to go. Our own changelog records why:

Changed: QR codes are now generated with bacon/bacon-qr-code (BSD-2-Clause) instead of chillerlan/php-qrcode. The previous library carries ZXing-derived files under Apache-2.0, which is not compatible with GPLv2.

Apache-2.0 is a perfectly good free licence. It is also, in the Free Software Foundation’s own words, GPL-compatible. That is what makes the trap worth writing down, so after the swap I stopped trusting the two names in our composer.json and read the zip we would actually submit instead. Seven packages are in there, under five different licences. We had written down two of the seven. And one of the five is still the same problem we had just spent a day removing.

“GPL-compatible” is a direction, not a property

Apache-2.0 can be combined with the GPL version 3. It cannot be combined with GPL version 2: its patent-termination and indemnity terms count as extra restrictions that GPLv2 does not permit, which is a large part of why GPLv3 was written the way it was. So “is this library GPL-compatible” has no answer on its own. The only answer is compatible with which version, and the version that matters is the one your own plugin claims.

Ours claims this, in the plugin header and again in readme.txt:

 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html

The trap is the words “or later”

Read that header quickly and “or later” looks like the escape hatch: we allow GPLv3, Apache-2.0 is fine under GPLv3, so where is the problem? The problem is who the words are addressed to. “Or later” is not a licence we picked; it is a choice we handed to the recipient. Anybody who downloads the plugin may elect to treat it as GPLv2 and nothing else, and that person now holds a GPLv2 work with Apache-2.0 code inside it, which they cannot legally pass on.

Which means the escape hatch runs the wrong way. Offering a later version adds a permission for the user, so it cannot subtract a requirement from us: every bundled file has to clear the earliest version we offer, not the most convenient one. A GPLv3-only-compatible dependency is incompatible with a GPLv2-or-later plugin, full stop, and that holds for Apache-2.0 and for LGPLv3 alike.

The WordPress.org reviewers do not spell the versions out, which is probably why this keeps biting people. Here is the relevant line verbatim from a review thread on another of our plugins, dated 9 September 2026:

Any included code must be GPL-compatible. This means, among others, that the users have to receive the four essential freedoms: (0) to run the program, (1) to study and change the program in source code form, (2) to redistribute exact copies, and (3) to distribute modified versions. (Guidelines 1 & 4)

“Any included code” is the load-bearing phrase. Not the code you wrote. Not the code you chose.

Read the zip, not the manifest

Our composer.json names two requirements. The build script copies the whole of vendor/ into the zip, so what we would hand a reviewer is 409 files, 319 of them PHP, 12 MB — and seven packages, because dependencies have dependencies. One command over the built zip, no tooling to install:

unzip -q your-plugin.zip -d /tmp/lic && cd /tmp/lic/your-plugin
for f in vendor/*/*/composer.json; do
  printf "%-32s %s\n" "${f#vendor/}" "$(grep -m1 -o '"license":[^,}]*' "$f")"
done

Here is what ours printed. I have added the column that is the whole point:

PackageDeclared licenceClears GPLv2?
bacon/bacon-qr-codeBSD-2-ClauseYes
dasprid/enumBSD-2-ClauseYes
masterminds/html5MITYes
sabberworm/php-css-parserMITYes
dompdf/dompdfLGPL-2.1Yes — LGPL-2.1 §3 lets you convert to GPLv2 or later
dompdf/php-font-libLGPL-2.1-or-laterYes
dompdf/php-svg-lib 1.0.2LGPL-3.0-or-laterNo — LGPLv3 goes with GPLv3, not GPLv2

The two we declared are the first and the fifth. The problem is the last one.

php-svg-lib appears zero times in our composer.json and eight times in composer.lock. Nobody on this end ever considered it. It is a hard requirement of dompdf — "dompdf/php-svg-lib": "^1.0.0", not a suggestion — so it arrives with the PDF renderer and gets copied into the zip by the same rsync line that copies everything else. We had removed one GPLv3-only dependency by hand and shipped another in the same archive, for the same reason, in a package we had never typed.

The lesson, which is smaller than it sounds

A licence audit that reads composer.json is reading a wish list. The zip is the distribution, and the only list that matters is the one inside it. Four things follow, and they take about two minutes:

  • Audit the built artifact, every time the build changes, not the manifest.
  • Write the version down. “GPL-compatible” in a dependency’s README means nothing until you say compatible with what.
  • Treat Apache-2.0 and LGPL-3.0 as the same finding. Both are GPLv3-and-later company.
  • Remember that composer require --no-dev pulls a tree, not a package. The licence you have to defend is the strictest leaf on it.

The plugin this came out of

Event Tickets & Registration with QR Check-in publishes an event, takes free RSVPs, emails every attendee a QR-coded PDF ticket, and checks them in when a logged-in phone scans it at the door. No separate app and no external service: the QR is drawn on your own server, which is why whose code draws it was a question in the first place. Paid tickets, seating and the rest are in Pro, a one-time $79. Stated plainly because this article is about reading the small print: the free core is not in the WordPress.org directory yet, so today the way to get it is the Pro download, which contains it.

What I don’t know

Whether a WordPress.org reviewer would actually block on php-svg-lib. I can find no public case of one doing so, the library is inside a very widely used renderer, and a reviewer might reasonably read “or later” the generous way round. The argument above says they would be right to block it, and the argument is mine, not theirs. We have not fixed it yet either — dompdf hard-requires the package, so this is a fork, a renderer swap or a conversation, not a line edit, and I would rather publish the finding today than sit on it until there is a tidy ending. And none of the above is legal advice: I read seven LICENSE files and the FSF’s compatibility notes, which is enough to know where to look and not enough to tell you what to do.