Download the PHP package iliaal/phpser without Composer

On this page you can find all versions of the php package iliaal/phpser. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package phpser

phpser

Tests Version License: BSD-3-Clause Follow @iliaa

A PHP serialization extension in C, targeting read-heavy cache workloads where decode time matters more than encode time or payload size.

Why phpser?

PHP cache workloads pay decode cost on every read and encode cost once per write. igbinary has been the default for over a decade, but it leaves performance on the table for common cache shapes: database rowsets, packed numeric arrays, deep-nested structures, and same-class DTO batches (Laravel queue payloads, cached models).

phpser optimizes for decode. It uses pointer-equality dict interning with a bounded content fallback, reuses decoded zend_strings by refcount, pre-sizes hash tables, writes straight into packed zval storage, and emits tagged scalar runs. On the current ARM benchmark, phpser beats igbinary on encode and decode in all ten cases. Integer ranges collapse to constant-size affine runs and decode 91-92% faster, shuffled integer arrays decode 78% faster, deep nesting decodes 26% faster, and DTO batches decode 62-65% faster.

Rowsets keep the pointer-equality fast path for shared strings, while columnar encoding also deduplicates low-cardinality strings and equal packed-string vectors by content. In rowset_distinct_1000, where equal repeated strings have separate allocations, phpser is 56% smaller, 63% faster to encode, and 53% faster to decode than igbinary.

📖 Design writeup: phpser: a fast, secure binary serializer for PHP cache workloads covers what the decoder does differently and why decode time is the metric to optimize. The interactive benchmark page compares phpser against igbinary, native serialize(), and msgpack across every cache shape.

Install

On a minimal PHP image (e.g. php:8.x-cli from Docker Hub), PIE needs a few build tools installed first:

Install unzip on Debian. Composer shells out to /usr/bin/unzip to extract PIE's prebuilt-binary zip. Without it, composer silently falls back to PHP's ZipArchive, which puts the .so at a path PIE doesn't check, and install fails with ExtensionBinaryNotFound even though the download succeeded.

From source

Pre-built binaries

Pre-built .dlls for Windows (PHP 8.2-8.5, TS/NTS, x86 and x64) and .sos for Linux glibc (x86_64, arm64) and macOS arm64 (PHP 8.4-8.5) are attached to each GitHub release. PIE fetches the matching binary automatically and builds from source when no asset matches.

Usage

Basic round-trip. The encoded payload is opaque bytes; treat it as a binary blob in storage (no JSON-safety, no UTF-8 guarantees):

HMAC-signed mode for untrusted storage (memcached, redis, files, cookies). The signed entry points wrap the payload in a constant-time HMAC-SHA256 frame; tampered or foreign-keyed input is rejected before any decoding work runs:

allowed_classes option on both unserialize entry points. Same shape as PHP's native unserialize($payload, ['allowed_classes' => ...]):

When decoding attacker-controlled bytes, use one of the two restricted modes or the signed entry point. See SECURITY.md for the full threat model.

Signed sessions. Set a key and every session write carries an HMAC-SHA256 tag bound to the session ID; a read whose tag does not verify is rejected before decoding, and PHP destroys the session:

Quote the value: an unquoted key containing = (base64) is a php.ini syntax error that drops every line after it. The key must be at least 32 bytes. A shorter one (including On, which INI turns into "1") fails closed: every session_start() fails, new sessions included, and PHP destroys each existing session it reads, so a typo during rollout takes sessions down for everyone. Off, no, false, null, and none become an empty value, which means unsigned; yes and true become "1", like On. Use a distinct key per application and environment: the tag binds a blob to its session ID, not to the app that wrote it. phpser.session_key is PHP_INI_SYSTEM | PHP_INI_PERDIR, so ini_set() from application code cannot change it. Set it, and the handler, for every script that shares the session store: in php.ini or an FPM pool's php_admin_value, not a .user.ini that covers only part of the tree. A script that runs without the key, or switches session.serialize_handler (which any script can, unless it is locked with php_admin_value), reads the store unverified. Lock both settings with php_admin_value where you can.

No application code changes, with two consequences to know about:

APCu. When APCu is loaded, phpser registers itself as an APCu serializer, so APCu stores arrays and objects as phpser frames:

Set apc.serializer globally, in the php.ini the FPM master reads, the same for every pool. APCu binds its serializer once per process: at startup when the value is php (the default), otherwise at the first request that runs with APCu enabled. With opcache.preload, a non-root FPM master runs that request itself before forking, so every pool inherits the php.ini value; a root master with opcache.preload_user preloads in a forked child, so each worker binds at its own first request. A pool's php_admin_value[apc.serializer] can therefore be ignored. The APCu row in the phpser section of phpinfo() reports what phpser observed, not what will happen: inactive when APCu bound php at startup, used by APCu in this process or the parent it forked from once APCu has called phpser in that process or in a parent before it forked, not yet used before then (or when another serializer is bound), and not registered when APCu was not loaded at startup or phpser came from dl(). phpser registers with APCu only when it is loaded at startup through an extension= line.

Without phpser.key the frames are plain and APCu shared memory is trusted, as with APCu's own php serializer. With it, every value APCu passes through the serializer (objects always, and arrays) carries an HMAC-SHA256 tag bound to the key and to the context apcu. A value whose tag does not verify (written under another key, by another serializer, or before the key was set) is never decoded and raises an E_WARNING: apcu_fetch() returns false, as for a miss, but apcu_entry() returns null without calling its generator until the entry expires or is deleted. Under an error handler that turns warnings into exceptions (as Laravel's does), the fetch throws instead, so a hostile pool that overwrites an entry can make another pool's reads of it throw. Scalars, and arrays that a process with no resolvable serializer stored natively, are read back without a check.

This is a defense against object injection between PHP-FPM pools that share one master, and therefore one APCu shared memory segment: give each pool its own php_admin_value[phpser.key] and one pool cannot make another pool's apcu_fetch() instantiate objects. It can still make the other pool's apcu_entry() return null for an entry it overwrites. It does not authenticate APCu data in general, so do not keep authorization data (roles, permissions) in APCu on a master shared with untrusted pools. A key shorter than 32 bytes fails closed: apcu_store() of an array or object returns false with a warning, and nothing is stored unsigned.

Keyed values for other stores. phpser_serialize_keyed($value, $context) and phpser_unserialize_keyed($payload, $context, $options) sign with phpser.key, so the key stays in server configuration instead of application code:

The context names the store and must be 1 to 255 bytes, contain no NUL byte, and not be session. A payload signed for redis does not verify as memcached or apcu. Under the same key and context, the output is byte-identical to what the C API below produces, so PHP code and an extension can share a store.

C API. Other extensions (cache clients, session stores) can serialize through phpser with phpser_api_serialize() and phpser_api_unserialize() from the installed header ext/phpser/php_phpser_api.h. Both follow phpser.key: plain frames when it is unset, context-bound signed frames when it is set. Failed verification returns PHPSER_API_ERR_TAG, distinct from other failures, so a client that falls back to raw bytes on a decode error can treat it as a miss instead. The header documents the ownership and error contract. A consumer declares ZEND_MOD_REQUIRED("phpser"), and extension=phpser must come before the consumer's extension= line: the dependency orders startup among loaded extensions but does not load phpser.

✨ Features

Bench (PHP 8.4.25 aarch64, idle box, 1000 iters, median of 35)

Shape Size: ig → ps Encode: ig → ps Decode: ig → ps
rowset_100 4570 → 2287 (-50%) 18.2k → 9.9k ns (-45%) 21.9k → 9.3k ns (-57%)
rowset_1000 47K → 23K (-52%) 256.5k → 100.1k ns (-61%) 219.7k → 102.2k ns (-53%)
rowset_distinct_1000 59K → 26K (-56%) 328.5k → 120.6k ns (-63%) 310.2k → 147.1k ns (-53%)
packed_1k 5495 → 7 (-99.9%) 9.6k → 1.5k ns (-84%) 16.2k → 1.4k ns (-91%)
packed_10k 59K → 7 (-99.9%) 93.1k → 14.3k ns (-85%) 161.4k → 12.6k ns (-92%)
packed_rand_10k 78K → 30K (-62%) 104.4k → 71.0k ns (-32%) 182.6k → 39.6k ns (-78%)
deep_50 419 → 425 (+1%) 2.7k → 1.9k ns (-29%) 3.6k → 2.7k ns (-26%)
dto_100 7083 → 5186 (-27%) 28.1k → 22.6k ns (-20%) 55.8k → 21.5k ns (-62%)
dto_1000 73K → 54K (-27%) 314.7k → 256.4k ns (-19%) 620.7k → 215.1k ns (-65%)
dto_mixed 22K → 14K (-34%) 109.2k → 86.8k ns (-20%) 231.6k → 82.3k ns (-64%)

phpser encodes 19-85% faster and decodes 26-92% faster than igbinary across all ten cases. Integer ranges collapse to a 7-byte affine run and decode 91-92% faster; shuffled integers (packed_rand_10k) are 62% smaller, 32% faster to encode, and 78% faster to decode. Deep nesting is 29% faster to encode and 26% faster to decode with a six-byte size difference.

The table's rowset_100 and rowset_1000 reuse PHP literal strings, so the pointer-equality intern path remains the cheapest case, and every row shares one literal tags array, which phpser encodes once and shares by refcount on decode. Columnar TAG_TABLE also performs a bounded content-cardinality scan for separately allocated strings and equal packed-string vectors, so rowset_distinct_1000 (distinct string and tags allocations on every row) is 56% smaller, 63% faster to encode, and 53% faster to decode than igbinary.

DTO workloads (Laravel-queue-style payloads, single-class arrays) are 27-34% smaller, 62-65% faster to decode, 19-20% faster to encode than igbinary. Wire-v2 TAG_OBJECT_SLOTS drops the per-property key indices and installs declared values straight into property slots; the dict dedups prop names once, and the class-entry lookup cache amortizes zend_lookup_class_ex across same-typed batches.

bench.php --extended adds single values, wrapped envelopes, signed payloads, collector-on retained decode, and Laravel-app shapes (Eloquent models whose attributes and original share one array, a document wallet, a queue job, a catalog, JWKS). Run it with -d opcache.enable_cli=1 for the opcache configuration.

Sizes are byte-identical on x86 (the wire format is architecture-neutral); the ns/op columns are from an idle aarch64 box, median of 35. For the full four-way picture, phpser vs igbinary vs native serialize() vs msgpack, with size, encode, and decode side by side on every shape, see the interactive benchmark page. Regenerate it with php ... bench.php --html > docs/index.html.

Design highlights

Where phpser diverges from igbinary

igbinary is the closest reference point. phpser takes measurable performance in these areas, all shipped:

  1. Pre-sized HT + direct arPacked writes on decode. When the wire format declares PACKED_LEN N, allocate the HT once via zend_new_array(N) and write directly into arPacked with ZVAL_* macros. Skips N zend_hash_next_index_insert calls, including their hash computation, growth checks, and capacity tuning.
  2. Tagged scalar runs. [1, 2, 3, ...] (1000 longs) emits as a single PACKED_LONGS header + N zigzag varints, not 1000 (tag, varint) pairs. Decode is one tight loop with no per-element tag dispatch.
  3. O(1) pointer-hash intern. Open-addressed zend_string* → slot hash, grown without eviction. Hit rate near 100% on literal rowset shapes (PHP interns literals; the same "id" zend_string pointer flows through every row), and unique value strings (names, emails) hit a single-probe miss instead of a linear scan. Separately allocated low-cardinality table columns use a bounded content scan, which puts encode ahead of igbinary on every measured shape. Pointer hits skip the byte-hash entirely.
  4. Eager dict materialization with warm hashes. All dict slots are resolved up front during header parse, against the engine's interned-string table first. Property names, class names, and hot literals come back as the engine's own interned strings (no allocation, no refcount traffic, pointer-equality hash lookups), with a regular allocation as the fallback. Hashes are set on both paths; zend_hash_add_new reuses the cached hash.
  5. Invariant-gated add_new on assoc decode. Wire-controlled duplicate keys must collapse to last-write-wins rather than produce phantom buckets (count($arr) != count(array_unique(array_keys($arr)))), and canonical integer strings must coerce to integer keys. Authentication proves key possession, not that the bytes came from phpser's encoder: a key holder can sign a handcrafted frame. TAG_ASSOC therefore always uses update semantics; schema-based paths use add_new only after validating distinct, non-numeric keys once at schema read.
  6. Inline-short-string tag with upgrade-on-second-encounter. TAG_STR_INLINE (0x0c) and KEY_STR_INLINE (0x02) are emitted on a string's first occurrence; the next occurrence triggers an in-place upgrade to a dict entry, and all subsequent ones emit TAG_STR_DICT. Singletons (e.g. row_X values in a rowset) never hit the upgrade branch. They cost nothing in the dict header. The intern cache doubles as the "seen once?" signal: high bit of idx distinguishes INLINE_EMITTED from DICT_IDX. No pre-pass; single walk of the zval tree as before.

    A count-then-emit pre-pass (tag occurrences first, then emit inline for singletons and dict for repeats) cost ~200 ns per string and ate the per-singleton savings. The single-walk upgrade moved rowset_1000 encode from 8% to 25% faster than igbinary; the later columnar TAG_TABLE format and delta id columns reached -45% size and -58% encode versus igbinary.

  7. Skip refcount machinery during build. All zvals built during decode are fresh and unshared until handed back to PHP. Internal writes can skip Z_TRY_ADDREF guards.

Local dev build

The hand-rolled Makefile.dev builds against an in-tree ~/php-src-8.4-opt checkout without phpize/autoconf, for working on the extension and PHP at the same time:

It is kept separate from the Makefile generated by phpize/configure, so a standard source build cannot overwrite the developer build rules. Override PHP_SRC= to target a different in-tree PHP checkout. Load alongside igbinary for the A/B bench:

Session integration is compiled when the target PHP headers define HAVE_PHP_SESSION, with either a phpize or Makefile.dev build; a session-less PHP build omits it. Registration is attempted only when phpser is loaded at startup, not through dl(). php --ri phpser reports whether registration succeeded.

Limitations / known gaps

Wire format (V1 / V2)

Varints are LEB128 (unsigned); signed values use zigzag encoding. Tags 0x0a/0x0d/0x0e/0x0f/0x11/0x12 each claim the next id in encounter order, before their contents, so the decoder reconstructs back-refs by counting container tags as it parses. 0x18 SHARED_ARRAY claims its id once its array is complete, after every id claimed inside it; a REF from inside the array to its own id is out of range and rejects. 0x10 REF never claims an id; it is lookup-only.

The encoder emits SHARED_ARRAY only for a non-empty nested array that is pointer-shared (refcount above one, not counting the encoder's own snapshots, or immutable) and whose walk reached no PHP reference; a later visit of the same HashTable emits REF. The top-level value never claims. An array directly holding a reference stays by-value on every visit: COW separation keeps a reference in each copy, and one shared decoded array would lose the aliasing a later write through either copy depends on. The decoder enforces the same traversal rule and rejects a SHARED_ARRAY whose contents create or reuse a reference (NEW_REF, or a REF to a reference). An existing object's back-reference does not traverse its properties, which may hold references. The encoder retries arrays that were initially unshareable once their objects have registered identities, avoiding repeated expansion through those objects. Arrays are pinned exactly like tracked objects, so a freed table's reused address cannot masquerade as a back-reference. Pins activate at the first hook. Before that, the encoder releases its private copies of shared arrays and property tails without the possible-root check, so a full root buffer cannot start a cycle collection whose destructors are user code. A shared array is captured at its first visit; see SECURITY.md for the one case where that differs from native.

The version byte is emitted as 0x02 only when the body actually uses a v2-only tag (0x12–0x18); otherwise it stays 0x01. On decode it is a minimum-reader signal, not a gate: the tag dispatch is version-agnostic, so a hand-built frame carrying a v2 tag under a 0x01 header still decodes. This keeps the version byte additive. Don't rely on it alone to reject a future format; a backwards-incompatible change gets a new version constant and explicit tag rejection.

🔗 Native PHP extensions

Companion native PHP extensions:


Follow on X • Read the writeup • If this cut your cache decode CPU, ⭐ star it!


All versions of phpser with dependencies

PHP Build Version
Package Version
Requires php Version >=8.2
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package iliaal/phpser contains the following files

Loading the files please wait ...