This document is for working on the port itself. For using the library, start with the README.
composer install
composer test # full suite: unit + conformance
composer lint # PHPCS (WordPress Coding Standards, naming sniffs excluded)
composer lint:fix # PHPCBF auto-fixesRun a single test class or filter by name:
vendor/bin/phpunit tests/Unit/YTextTest.php
vendor/bin/phpunit --filter testBasicInsertAndDeleteThe wire format is the contract. PHP output must be byte-identical to JS Yjs v13.6.31 for the same logical operations. When in doubt, mirror the JS source (in node_modules/yjs/src/ after npm install, or the yjs repository at tag v13.6.31) line for line rather than improving it. Refactors that change encoded bytes, iteration order, or the struct class hierarchy are bugs, even if all unit tests still pass.
The full rules live in plan/CONTEXT.md, and cross-cutting choices made during the port are logged in plan/DECISIONS.md. Read both before changing library code; append to DECISIONS.md when you make a new cross-cutting decision.
The suite has two complementary halves:
Each upstream yjs/tests/*.tests.js file is translated to a PHPUnit class (y-array.tests.js becomes YArrayTest.php, and so on). These include the fuzz tests (applyRandomTests), which are driven by an exact port of lib0's Xoroshiro128plus PRNG, so a given seed produces the identical operation sequence in PHP and in JS. The supporting machinery lives in tests/Support/:
TestConnector/TestYInstance: the simulated network from upstreamtestHelper.js.compare(): asserts that all simulated users converge to identical JSON, state vectors, delete sets, and struct stores.
Because translations can drift from the JS source, unit tests alone are not trusted for interop. That is the job of:
These replay fixtures captured from the real JavaScript Yjs and assert byte-for-byte equality in both directions (decode JS bytes, re-encode to identical bytes, and produce identical bytes from the same logical operations). The fixtures live in tests/fixtures/ and cover encoding primitives, update codecs (V1 and V2), delete sets, update utilities, multi-user convergence logs, and updates captured from real browser clients.
Fixtures are generated by a Node script that drives the actual JS implementation:
npm install
npm run gen-fixturesThe JS source this port mirrors is pinned in package.json (yjs 13.6.31 and lib0 0.2.99) and resolved from node_modules. The one upstream file the published yjs package does not ship, tests/compatibility.tests.js, is vendored verbatim at tools/compatibility.tests.js. Regenerate fixtures only when intentionally changing what the fixtures cover; the point of the suite is that the PHP side moves to match JS, never the reverse.
src/
├── Lib0/ # ported lib0 primitives (Encoding, Decoding, Buffer, Prng, ...)
├── Structs/ # Item, GC, Skip, Content* (the CRDT structs)
├── Types/ # AbstractType, YArray, YMap, YText, YXml*
├── Utils/ # Doc, Transaction, StructStore, DeleteSet, codecs, ...
├── Functions/ # namespace functions (applyUpdate, snapshot, ...)
├── Protocols/ # Sync (y-protocols/sync subset)
├── functions.php # function loader (Composer "files" autoload)
├── aliases.php # short Yjs\* class aliases
└── Yjs.php # static facade over the namespace functions
The class layout intentionally matches yjs/src/ one-to-one so that diffs against upstream stay mechanical.
WordPress Coding Standards via PHPCS (see phpcs.xml.dist), with the naming and file-name sniffs disabled so the camelCase Yjs API and PSR-4 file names are preserved. PHP floor is 7.4 (WordPress compatibility): typed properties and arrow functions are fine; enums, readonly, and union type declarations are not. Use PHPDoc for types the language level cannot express.