I have a table with a load of rows of serialized arrays that I plan to request and pass it to JavaScript
.
The problem is - is it possible to unser
I thought I would have a go at writing a JS function that can unserialize PHP-serialized data.
But before going for this solution please be aware that:
serialize
function is PHP specific and so the best option is to use PHP's unserialize
to have 100% guarantee that it is doing the job right.Map
objects in JS, but those allow to store 13 and "13" as separate keys, which PHP does not allow. And we're only touching the tip of the iceberg here...json_encode
for that, and JavaScript has JSON.parse
to decode it. This is certainly the way to go if you can.If with those remarks you still see the need for a JS unserialise function, then read on.
Here is a JS implementation that provides a PHP
object with similar methods as the built-in JSON
object: parse
and stringify
.
When an input to the parse
method references a class, then it will first check if you passed a reference to that class in the (optional) second argument. If not, a mock will be created for that class (to avoid undesired side-effects). In either case an instance of that class will be created. If the input string specifies a custom serialization happened then the method unserialize
on that object instance will be called. You must provide the logic in that method as the string itself does not give information about how that should be done. It is only known in the PHP code that generated that string.
This implementation also supports cyclic references. When an associative array turns out to be a sequential array, then a JS array will be returned.
const PHP = {
stdClass: function() {},
stringify(val) {
const hash = new Map([[Infinity, "d:INF;"], [-Infinity, "d:-INF;"], [NaN, "d:NAN;"], [null, "N;"], [undefined, "N;"]]);
const utf8length = str => str ? encodeURI(str).match(/(%.)?./g).length : 0;
const serializeString = (s,delim='"') => `${utf8length(s)}:${delim[0]}${s}${delim[delim.length-1]}`;
let ref = 0;
function serialize(val, canReference = true) {
if (hash.has(val)) return hash.get(val);
ref += canReference;
if (typeof val === "string") return `s:${serializeString(val)};`;
if (typeof val === "number") return `${Math.round(val) === val ? "i" : "d"}:${(""+val).toUpperCase().replace(/(-?\d)E/, "$1.0E")};`;
if (typeof val === "boolean") return `b:${+val};`;
const a = Array.isArray(val) || val.constructor === Object;
hash.set(val, `${"rR"[+a]}:${ref};`);
if (typeof val.serialize === "function") {
return `C:${serializeString(val.constructor.name)}:${serializeString(val.serialize(), "{}")}`;
}
const vals = Object.entries(val).filter(([k, v]) => typeof v !== "function");
return (a ? "a" : `O:${serializeString(val.constructor.name)}`)
+ `:${vals.length}:{${vals.map(([k, v]) => serialize(a && /^\d{1,16}$/.test(k) ? +k : k, false) + serialize(v)).join("")}}`;
}
return serialize(val);
},
// Provide in second argument the classes that may be instantiated
// e.g. { MyClass1, MyClass2 }
parse(str, allowedClasses = {}) {
allowedClasses.stdClass = PHP.stdClass; // Always allowed.
let offset = 0;
const values = [null];
const specialNums = { "INF": Infinity, "-INF": -Infinity, "NAN": NaN };
const kick = (msg, i = offset) => { throw new Error(`Error at ${i}: ${msg}\n${str}\n${" ".repeat(i)}^`) }
const read = (expected, ret) => expected === str.slice(offset, offset+=expected.length) ? ret
: kick(`Expected '${expected}'`, offset-expected.length);
function readMatch(regex, msg, terminator=";") {
read(":");
const match = regex.exec(str.slice(offset));
if (!match) kick(`Exected ${msg}, but got '${str.slice(offset).match(/^[:;{}]|[^:;{}]*/)[0]}'`);
offset += match[0].length;
return read(terminator, match[0]);
}
function readUtf8chars(numUtf8Bytes, terminator="") {
const i = offset;
while (numUtf8Bytes > 0) {
const code = str.charCodeAt(offset++);
numUtf8Bytes -= code < 0x80 ? 1 : code < 0x800 || code>>11 === 0x1B ? 2 : 3;
}
return numUtf8Bytes ? kick("Invalid string length", i-2) : read(terminator, str.slice(i, offset));
}
const create = className => !className ? {}
: allowedClasses[className] ? Object.create(allowedClasses[className].prototype)
: new {[className]: function() {} }[className]; // Create a mock class for this name
const readBoolean = () => readMatch(/^[01]/, "a '0' or '1'", ";");
const readInt = () => +readMatch(/^-?\d+/, "an integer", ";");
const readUInt = terminator => +readMatch(/^\d+/, "an unsigned integer", terminator);
const readString = (terminator="") => readUtf8chars(readUInt(':"'), '"'+terminator);
function readDecimal() {
const num = readMatch(/^-?(\d+(\.\d+)?(E[+-]\d+)?|INF)|NAN/, "a decimal number", ";");
return num in specialNums ? specialNums[num] : +num;
}
function readKey() {
const typ = str[offset++];
return typ === "s" ? readString(";")
: typ === "i" ? readUInt(";")
: kick("Expected 's' or 'i' as type for a key, but got ${str[offset-1]}", offset-1);
}
function readObject(obj) {
for (let i = 0, length = readUInt(":{"); i < length; i++) obj[readKey()] = readValue();
return read("}", obj);
}
function readArray() {
const obj = readObject({});
return Object.keys(obj).some((key, i) => key != i) ? obj : Object.values(obj);
}
function readCustomObject(obj) {
if (typeof obj.unserialize !== "function") kick(`Instance of ${obj.constructor.name} does not have an "unserialize" method`);
obj.unserialize(readUtf8chars(readUInt(":{")));
return read("}", obj);
}
function readValue() {
const typ = str[offset++].toLowerCase();
const ref = values.push(null)-1;
const val = typ === "n" ? read(";", null)
: typ === "s" ? readString(";")
: typ === "b" ? readBoolean()
: typ === "i" ? readInt()
: typ === "d" ? readDecimal()
: typ === "a" ? readArray() // Associative array
: typ === "o" ? readObject(create(readString())) // Object
: typ === "c" ? readCustomObject(create(readString())) // Custom serialized object
: typ === "r" ? values[readInt()] // Backreference
: kick(`Unexpected type ${typ}`, offset-1);
if (typ !== "r") values[ref] = val;
return val;
}
const val = readValue();
if (offset !== str.length) kick("Unexpected trailing character");
return val;
}
}
/**************** EXAMPLE USES ************************/
// Unserialize a sequential array
console.log(PHP.parse('a:4:{i:0;s:4:"This";i:1;s:2:"is";i:2;s:2:"an";i:3;s:5:"array";}'));
// Unserialize an associative array into an object
console.log(PHP.parse('a:2:{s:8:"language";s:3:"PHP";s:7:"version";d:7.1;}'));
// Example with class that has custom serialize function:
var MyClass = (function () {
const priv = new WeakMap(); // This is a way to implement private properties in ES6
return class MyClass {
constructor() {
priv.set(this, "");
this.wordCount = 0;
}
unserialize(serialised) {
const words = PHP.parse(serialised);
priv.set(this, words);
this.wordCount = words.split(" ").length;
}
serialize() {
return PHP.stringify(priv.get(this));
}
}
})();
// Unserialise a PHP string that needs the above class to work, and will call its unserialize method
// The class needs to be passed as object key/value as second argument, so to allow this side effect to happen:
console.log(PHP.parse('C:7:"MyClass":23:{s:15:"My private data";}', { MyClass } ));
wrap json_encode
around unserialize
echo json_encode( unserialize( $array));
Php.js has javascript implementations of unserialize and serialize:
http://phpjs.org/functions/unserialize/
http://phpjs.org/functions/serialize/
That said, it's probably more efficient to convert to JSON on the server side. JSON.parse is going to be a lot faster than PHP.js's unserialize.
http://php.net/manual/en/book.json.php
Just noticed your comment so here we go:
in PHP
json_encode(unserialize(SerializedVal));
in JavaScript:
JSON.parse(JsonString);