VOOZH about

URL: https://deepwiki.com/stefanak-michal/php-bolt-driver/7.1-packstream-format-and-type-mapping

⇱ PackStream Format and Type Mapping | stefanak-michal/php-bolt-driver | DeepWiki


Loading...
Last indexed: 14 February 2026 (a283bd)
Menu

PackStream Format and Type Mapping

PackStream is a binary serialization format used by the Bolt protocol to encode data for transmission between the client and graph database. This page documents how PHP data types are mapped to Neo4j types, the marker-based encoding system, and memory-efficient serialization strategies.

For information about the actual graph structures (Node, Relationship, Path) that PackStream serializes, see Graph Structures. For temporal and spatial types, see Temporal and Spatial Types. For vector types, see Vector Type and Memory-Efficient Serialization.


PHP to Neo4j Type Mapping

PackStream defines a bidirectional mapping between PHP data types and Neo4j/Bolt types. The Bolt\packstream\v1\Packer and Bolt\packstream\v1\Unpacker classes handle this conversion:

Neo4j TypePHP TypePacker MethodUnpacker MethodMarker RangeNotes
Nullnulldirectdirect0xC0Single marker byte
Booleanbooleandirectdirect0xC2, 0xC3False/True
IntegerintegerpackInteger()unpackInteger()0x00-0x7F, 0xF0-0xFF, 0xC8-0xCBSigned 64-bit, variable encoding
FloatfloatpackFloat()unpackFloat()0xC1IEEE 754 double precision
BytesBolt\packstream\BytespackByteArray()unpackByteArray()0xCC-0xCERaw byte array
StringstringpackString()unpackString()0x80-0x8F, 0xD0-0xD2UTF-8 encoded
Listarray (sequential)packList()unpackList()0x90-0x9F, 0xD4-0xD6Numeric keys starting at 0
Dictionaryarray or objectpackDictionary()unpackDictionary()0xA0-0xAF, 0xD8-0xDANon-sequential arrays
StructureBolt\protocol\IStructurepackStructure()unpackStructure()0xB0-0xBF, 0xDC-0xDDGraph entities, temporal types

Array Classification Logic:

Arrays in PHP are classified using array_is_list() in the Packer::p() method src/packstream/v1/Packer.php87:

  • List: Consecutive integer keys starting from 0 → encodes as PackStream List
  • Dictionary: Any other key pattern → encodes as PackStream Map

Objects are processed in order of type checks src/packstream/v1/Packer.php94-104:

  1. IStructurepackStructure()
  2. BytespackByteArray()
  3. IPackListGeneratorpackList() with lazy evaluation
  4. IPackDictionaryGeneratorpackDictionary() with lazy evaluation
  5. Default (stdClass, etc.) → cast to array and encode as Dictionary

Sources: src/packstream/v1/Packer.php68-110 src/packstream/v1/Packer.php87-91 src/packstream/v1/Unpacker.php68-116


Marker Byte System

PackStream uses a single-byte marker to identify the type and, in some cases, the size of the encoded value. The Packer class defines size thresholds as constants src/packstream/v1/Packer.php18-21:


Marker Byte Encoding Reference


Size-Based Encoding Selection

Variable-Length Types (strings, lists, maps, structures):

The Packer selects the most compact representation based on element count:

  • TINY (4-bit inline): $size < SMALL (0-15) → marker contains size
  • 8-bit size: $size < MEDIUM (16-255) → 1 size byte follows marker
  • 16-bit size: $size < LARGE (256-65535) → 2 size bytes follow marker
  • 32-bit size: $size < HUGE (65536-4294967295) → 4 size bytes follow marker

Example from packString() src/packstream/v1/Packer.php115-130:


Integer Encoding src/packstream/v1/Packer.php140-158:

The packInteger() method selects encoding based on value range:

  • TINY_INT: -16 ≤ value ≤ 127pack('c', $value) (no marker byte needed)
  • INT_8: -128 ≤ value ≤ -17chr(0xC8) . pack('c', $value)
  • INT_16: -32768 ≤ value < -128 or 128 ≤ value ≤ 32767chr(0xC9) . pack('s', $value)
  • INT_32: -2147483648 ≤ value < -32768 or 32768 ≤ value ≤ 2147483647chr(0xCA) . pack('l', $value)
  • INT_64: Beyond INT_32 range → chr(0xCB) . pack('q', $value)

Sources: src/packstream/v1/Packer.php18-21 src/packstream/v1/Packer.php115-130 src/packstream/v1/Packer.php140-158 src/packstream/v1/Unpacker.php210-230


Packing Flow

The Bolt\packstream\v1\Packer class converts PHP values into binary PackStream format through a two-phase process: structure framing (via pack()) and recursive value encoding (via p()).

Packer::pack() - Structure Framing


Packer::p() - Type Dispatch


Key Implementation Methods

Entry Point src/packstream/v1/Packer.php33-63:

  • pack(int $signature, mixed ...$params): iterable - Packs a complete protocol message
    • Yields structure marker + signature byte
    • Recursively packs parameters via p()
    • Chunks output into 65535-byte segments
    • Yields end-of-message marker 0x00 0x00

Type Dispatcher src/packstream/v1/Packer.php68-110:

  • p(mixed $param): iterable - Private recursive packer
    • Uses gettype() for primitive type detection
    • Uses instanceof for object type checking
    • Uses array_is_list() for array classification
    • Yields from type-specific packing methods

Type-Specific Packers:

Sources: src/packstream/v1/Packer.php33-63 src/packstream/v1/Packer.php68-110 src/packstream/v1/Packer.php115-248


Unpacking Flow

The Bolt\packstream\v1\Unpacker class converts binary PackStream format back to PHP values through marker-based type detection and recursive parsing.

Unpacker::unpack() - Main Entry Point


Unpacker::u() - Marker Dispatch


Type-Specific Unpacking

Each type-specific unpacker method:

  1. Checks if the marker byte matches its pattern
  2. Returns null if no match (causing next unpacker to be tried)
  3. Reads size bytes if needed (using unpack() with appropriate format)
  4. Extracts payload bytes via next()
  5. Recursively calls u() for container elements

Key Implementation Methods

Entry Points src/packstream/v1/Unpacker.php32-50:

  • unpack(string $msg): mixed - Initializes offset and message, calls u()
  • getSignature(): int - Returns signature byte for unrecognized structures

Core Dispatcher src/packstream/v1/Unpacker.php68-116:

  • u(): mixed - Reads marker byte and tries unpackers in sequence
    • Direct checks for 0xC3 (true), 0xC2 (false), 0xC0 (null)
    • Calls type-specific unpackers, continuing if they return null

Utility src/packstream/v1/Unpacker.php55-63:

  • next(int $length): string - Extracts next N bytes from message, advances offset

Type-Specific Unpackers:

Sources: src/packstream/v1/Unpacker.php32-50 src/packstream/v1/Unpacker.php55-63 src/packstream/v1/Unpacker.php68-116 src/packstream/v1/Unpacker.php123-291


Endianness Handling

PackStream uses big-endian (network byte order) for multi-byte integers. Both Packer and Unpacker detect the system's byte order during construction and apply byte reversal when necessary.

Endianness Detection

Both classes detect little-endian systems using the same technique src/packstream/v1/Packer.php30 src/packstream/v1/Unpacker.php29:


If unpack('S', "\x01\x00") returns [1], the system is little-endian. If it returns [256], the system is big-endian.

Packing with Endianness Correction

For multi-byte integers, Packer uses PHP's native pack() formats, then conditionally reverses bytes src/packstream/v1/Packer.php146-154:


Unpacking with Endianness Correction

Similarly, Unpacker reverses bytes before unpacking on little-endian systems src/packstream/v1/Unpacker.php217-227:


Affected Integer Types

TypeBytesPack FormatEndianness Sensitive
TINY_INT1cNo
INT_81cNo
INT_162sYes - requires strrev() on little-endian
INT_324lYes - requires strrev() on little-endian
INT_648qYes - requires strrev() on little-endian
FLOAT_648ENo - format E is explicitly little-endian

Note on FLOAT_64: The E format code in PHP's pack()/unpack() explicitly specifies little-endian double precision src/packstream/v1/Packer.php134 so no byte reversal is needed.

Vector Structure Endianness

The V6 Vector structure also handles endianness for its numeric arrays src/protocol/v6/structures/Vector.php30 src/protocol/v6/structures/Vector.php83:


Formats requiring endianness correction in Vector: s (INT_16), l (INT_32), q (INT_64).

Sources: src/packstream/v1/Packer.php30 src/packstream/v1/Packer.php146-154 src/packstream/v1/Unpacker.php29 src/packstream/v1/Unpacker.php217-227 src/protocol/v6/structures/Vector.php30 src/protocol/v6/structures/Vector.php79-86


Message Chunking Protocol

PackStream messages are transmitted using a chunking protocol where each chunk is prefixed with a 2-byte big-endian length, followed by that many bytes of data. A zero-length chunk (0x00 0x00) marks the end of a message.


Implementation in Packer:

The chunking logic wraps each packed parameter src/packstream/v1/Packer.php49-60:


After all parameters, an end-of-message marker is sent src/packstream/v1/Packer.php62:


This chunking mechanism allows streaming large values without buffering the entire message in memory.

Sources: src/packstream/v1/Packer.php49-63


Memory-Efficient Serialization with Generators

For large datasets, the library supports generator-based serialization through two interfaces:

  • Bolt\packstream\IPackListGenerator - For streaming lists
  • Bolt\packstream\IPackDictionaryGenerator - For streaming dictionaries/maps

Generator Interfaces:

Both interfaces require a count() method to determine the size marker, then iterate with foreach:


Example Usage from Tests:

The RandomDataGenerator class tests/PerformanceTest.php9 demonstrates this pattern for generating 50,000 records without loading them all into memory simultaneously.

Packer Logic:

The packer treats generators identically to arrays src/packstream/v1/Packer.php98-101:


The packList() and packDictionary() methods accept both arrays and generators src/packstream/v1/Packer.php188-206:


This allows processing arbitrarily large datasets with constant memory usage.

Sources: src/packstream/v1/Packer.php98-101 src/packstream/v1/Packer.php163-183 src/packstream/v1/Packer.php188-207 tests/PerformanceTest.php19-63 tests/packstream/v1/PackerTest.php188-227


Bytes Type

The Bolt\packstream\Bytes class represents raw byte arrays, distinct from UTF-8 strings. This type is used for binary data that should not be interpreted as text.

Marker Byte Variants:

  • 0xCC + 1-byte size (BYTES_8): 0-255 bytes
  • 0xCD + 2-byte size (BYTES_16): 256-65535 bytes
  • 0xCE + 4-byte size (BYTES_32): 65536-2147483647 bytes

Packing Implementation src/packstream/v1/Packer.php236-248:


Unpacking Implementation src/packstream/v1/Unpacker.php278-291:


Test Example tests/structures/v1/StructuresTest.php414-434:


Sources: src/packstream/v1/Packer.php236-248 src/packstream/v1/Unpacker.php278-291 tests/packstream/v1/BytesTest.php36-46 tests/structures/v1/StructuresTest.php414-434


Structure Serialization

Structures are complex types that represent graph entities (Node, Relationship, Path) and value types (Date, DateTime, Duration, Point2D, Point3D, Vector, etc.). Each structure has a unique signature byte that identifies its type.

Structure Wire Format

[Marker Byte: 0xB0-0xBF | 0xDC | 0xDD]
[Signature Byte: 0x44-0x74, etc.]
[Field 1: PackStream Value]
[Field 2: PackStream Value]
...
[Field N: PackStream Value]

The marker byte encodes the field count using TINY_STRUCT (0-15 fields), STRUCT_8 (16-255 fields), or STRUCT_16 (256-65535 fields) encoding.

Structure Packing and Unpacking Flow


Packing Implementation

The Packer::packStructure() method src/packstream/v1/Packer.php212-231 uses reflection to introspect the structure:

  1. Lookup Signature: array_search(get_class($structure), $this->structuresLt)

    • Returns signature byte (e.g., 0x44 for Date, 0x58 for Point2D)
    • Throws PackException if structure not in lookup table
  2. Reflect on Constructor: new \ReflectionClass($structure)

    • Counts constructor parameters (= field count)
    • Iterates over parameter names
  3. Pack Marker + Signature:

    
    
    • For ≤15 fields: TINY_STRUCT marker with embedded count
    • For 16-255 fields: chr(0xDC) . pack('C', $size)
    • For 256-65535 fields: chr(0xDD) . pack('n', $size)
  4. Pack Fields: Recursively calls p() for each constructor parameter value

Unpacking Implementation

The Unpacker::unpackStructure() method src/packstream/v1/Unpacker.php123-156:

  1. Extract Field Count:

    
    
  2. Read Signature: $signature = ord($this->next(1))

  3. Lookup Class: $this->structuresLt[$signature]

    • If found: instantiate the class
    • If not found: store signature in $this->signature and return raw data
  4. Unpack Fields: Loop $size times, calling u() for each field

  5. Instantiate: new $class(...$values) using unpacked field values

Structure Lookup Tables

Each protocol version defines two lookup tables as traits. Example from V6 src/protocol/v6/AvailableStructures.php34-62:

Pack Lookup (PHP class → signature byte):


Unpack Lookup (signature byte → PHP class):


Notice that graph structures are only in the unpack table - they cannot be used as query parameters.

Bidirectional vs Unidirectional Structures

StructurePack SupportUnpack SupportSignature
Node0x4E
Relationship0x52
UnboundRelationship0x72
Path0x50
Date0x44
Time0x54
LocalTime0x74
DateTime0x49
DateTimeZoneId0x69
LocalDateTime0x64
Duration0x45
Point2D0x58
Point3D0x59
Vector (V6+)0x56

Graph structures are server-generated and cannot be serialized as query parameters. Attempting to pack them results in a PackException.

Sources: src/packstream/v1/Packer.php212-231 src/packstream/v1/Unpacker.php123-156 src/packstream/v1/Packer.php28 src/packstream/v1/Unpacker.php27 src/protocol/v6/AvailableStructures.php34-62


Type Detection and PHP Considerations

The packer uses PHP's gettype() function to determine the appropriate encoding strategy src/packstream/v1/Packer.php70:


Array Classification:

PHP arrays serve dual purposes as both lists and dictionaries. The packer uses array_is_list() to distinguish src/packstream/v1/Packer.php87-91:


An array is considered a list if and only if its keys are consecutive integers starting from 0. Otherwise, it's encoded as a dictionary.

Object Handling:

Objects are checked in order for specific interfaces src/packstream/v1/Packer.php93-104:

  1. IStructure → Structure serialization
  2. Bytes → Byte array serialization
  3. IPackListGenerator → List with lazy evaluation
  4. IPackDictionaryGenerator → Dictionary with lazy evaluation
  5. Default → Cast to array and serialize as dictionary

String Length Calculation:

All string operations use mb_strlen() and mb_strcut() with '8bit' encoding to ensure byte-accurate length calculations rather than character counts src/packstream/v1/Packer.php117 src/packstream/v1/Unpacker.php58-60 This is critical for proper PackStream encoding.

Sources: src/packstream/v1/Packer.php68-110 src/packstream/v1/Packer.php87-91 src/packstream/v1/Unpacker.php55-63

Refresh this wiki

On this page