• 7 min read

PHP 8.6 pack() and unpack() Get Endianness Modifiers

PHP 8.6 adds < and > endianness modifiers to pack() and unpack() for signed integers and floats. Here is the syntax, the rules, and one upgrade gotcha.

Featured image for "PHP 8.6 pack() and unpack() Get Endianness Modifiers"

If you have ever parsed a binary file format or a network protocol in PHP, you have probably met pack() and unpack() and their pile of single-letter format codes. Most of the time they work fine. Then you need a signed 32-bit integer in little-endian byte order and discover PHP has no format code for it.

PHP 8.6 fixes that gap. Two accepted RFCs add Perl-style < and > endianness modifiers to the integer and floating-point format codes. PHP 8.6 is scheduled for general availability on November 19, 2026, according to Benjamin Crozat’s release overview, which tracks the official schedule. Release candidates are underway now, so this is a good time to read up.

A quick refresher on endianness

Endianness is the order in which a multi-byte value is stored. Little-endian puts the least significant byte first. Big-endian puts it last. The 32-bit integer 1 is 01 00 00 00 in little-endian and 00 00 00 01 in big-endian. File formats and protocols pick one, and your code has to match.

The gap before PHP 8.6

According to the integer endianness RFC, PHP’s pack() and unpack() already offered:

  • Machine-endian signed integers: s, l, q (2, 4, and 8 bytes)
  • Machine-endian unsigned integers: S, L, Q
  • Endian-specific unsigned integers: v/n, V/N, and P/J

There was no way to ask for a signed integer with a specific byte order. The RFC shows the workaround people used, which unpacks the pieces separately and shifts them back together by hand. That is exactly the sort of code that works until a negative number arrives.

Floats had a similar story. Per the float RFC, f and d use machine byte order, while g/G and e/E give you little and big endian for single and double precision.

The new syntax

Both RFCs borrow from Perl’s pack. You add < for little-endian or > for big-endian directly after the format code.

For integers, from the first RFC:

  • s< and s> for signed 2-byte values
  • l< and l> for signed 4-byte values
  • q< and q> for signed 8-byte values
  • S<, L<, Q< and their > counterparts for the unsigned versions

For floats, from the second RFC:

  • f< and f> for single precision
  • d< and d> for double precision

Here is a small example. Say PHP Tek Live sends your app a sensor packet from a badge scanner. It has a signed 16-bit temperature offset, a signed 32-bit counter, and a double-precision reading, all big-endian:

<?php

// Build a packet
$packet = pack('s>l>d>', -12, -70000, 21.5);

// Read it back
[$offset, $counter, $reading] = array_values(
    unpack('s>offset/l>counter/d>reading', $packet)
);

var_dump($offset, $counter, $reading);
// int(-12)
// int(-70000)
// float(21.5)

The result is the same on a little-endian x86 server and a big-endian machine, because you asked for a byte order instead of accepting the platform default.

I have not run this against a PHP 8.6 build myself. The syntax comes straight from the RFC examples, which use the same pattern, so check it on a beta or RC before you rely on it.

Mixing byte orders

You can mix modifiers inside a single format string. The RFC includes a mixed example that packs little-endian 16-bit integers next to big-endian 32-bit integers, with repeat counts:

<?php

$data = pack('s<2l>2', 258, -2, 16909060, -16909060);

This is useful for file formats where a header is one byte order and the payload is another, or for legacy protocols that never settled on one.

Replacing the manual workarounds

Reading a signed little-endian 32-bit integer used to look like the RFC’s example of reassembling bytes by hand. Now it is one call:

<?php

$value = unpack('l<', $binaryData)[1];

Less code means fewer places for a sign-extension bug to hide. If you maintain a parser for a binary format, this is the kind of change that lets you delete a helper class.

The rules and the errors

The modifiers are strict, which I like. Per the RFCs, these rules apply:

  1. Modifiers work on s, l, q, S, L, Q, f, and d.
  2. Modifiers are rejected on format codes that already carry an endianness: v/n, V/N, P/J, g/G, and e/E. This mirrors Perl and avoids any question about which one wins.
  3. Using a modifier on an unsupported code, such as a or Z, throws a ValueError.
  4. On 32-bit builds of PHP, the 8-byte codes (q, Q, and their modified forms) throw a ValueError, matching existing behavior for 64-bit format codes.

For example, the RFC lists this error for pack('v<', 42):

ValueError: Endianness modifier '<' cannot be applied to format code 'v' which already has inherent endianness

Throwing instead of guessing means a typo in a format string fails loudly during development, not silently in production data.

Old and new side by side

The modifiers do not replace the old letters. For unsigned integers and floats, both spellings produce the same bytes. The RFCs spell out the equivalences:

Modifier formExisting formMeaning
S<vunsigned 16-bit, little-endian
S>nunsigned 16-bit, big-endian
L<Vunsigned 32-bit, little-endian
L>Nunsigned 32-bit, big-endian
f<gsingle float, little-endian
f>Gsingle float, big-endian
d<edouble float, little-endian
d>Edouble float, big-endian

The float RFC also mentions, as a possible future step, deprecating e, E, g, and G, since Perl does not support them and the modifiers make them redundant. That was listed as future scope only. Nothing in the RFC deprecates them today, so I would not rewrite working code yet. For new code, the modifier form is easier to read because the byte order sits next to the type.

One upgrade gotcha: unpack() element names

This one is worth a test run. The integer RFC says the modifier syntax is entirely opt-in and that < and > were not previously used in format strings. However, unpack() format strings can contain element names, and my reading of the PHP 8.6 notes is that a < or > right after a format code is now parsed as a modifier instead of the first character of the name. A format such as Cname would be unaffected, but a name that began with < or > would not be.

I found this described in a search summary of the 8.6 upgrade notes and could not open the page itself, so treat it as a lead and not a verified fact. The authoritative list is the UPGRADING file in php-src. Element names starting with those characters are rare, but a quick search of your codebase is cheap:

grep -rnE "unpack\(['\"][^'\"]*[a-zA-Z][<>]" src/

That pattern is rough and will give some false positives, but it will surface anything suspicious.

Should you use it?

If you read or write binary data in PHP, yes, once you are on 8.6. Typical places include image headers, audio metadata, custom TCP protocols, and sensor feeds. The modifiers make byte order explicit, which is what you want in code that outlives its author.

If you cannot upgrade soon, nothing changes for you. The old format letters keep working, and libraries that need to support older PHP versions will need to keep their current approach for a while. A common pattern is to feature-check PHP_VERSION_ID >= 80600 and pick the format string accordingly, but write a test for each path.

Add PHP 8.6 as a non-blocking job in your CI now. If your test suite touches pack() or unpack(), you will find out early whether anything breaks, and you can start planning the cleanup.

Sources