Unserialize PHP Array in Javascript
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 unserialize
with JavaScript rather than PHP ?
Otherwise I will have to load all the rows, loop them and unserialize them and assign them to a temporary PHP array and then json_encode it back to JavaScript which seems highly inefficient if I can send the data still serialized so that JavaScript can unserialize the data when it needs to.
Is there a built in Javascript function that does it or will I have to loop the rows in PHP before I encode it?
Note I am not using jQuery.
EDIT: Example of my serialized data in PHP from my table:
a:8:{i:0;a:2:{i:0;i:10;i:1;i:11;}i:1;a:2:{i:0;i:9;i:1;i:11;}i:2;a:2:
{i:0;i:8;i:1;i:11;}i:3;a:2:{i:0;i:8;i:1;i:10;}i:4;a:2:{i:0;i:8;i:1;i:9;}i:5;a:2:
{i:0;i:8;i:1;i:8;}i:6;a:2:{i:0;i:8;i:1;i:7;}i:7;a:2:{i:0;i:8;i:1;i:6;}}
Solution 1:
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.
Solution 2:
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:
- The format produced by PHP's
serialize
function is PHP specific and so the best option is to use PHP'sunserialize
to have 100% guarantee that it is doing the job right. - PHP can store class information in those strings and even the output of some custom serialization methods. So to unserialize such strings you would need to know about those classes and methods.
- PHP data structures do not correspond 1-to-1 to JavaScript data structures: PHP associative arrays can have strings as keys, and so they look more like JavaScript objects than JS arrays, but in PHP the keys keep insertion order, and keys can have a truly numeric data type which is not possible with JS objects. One could say that then we should look at
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... - PHP serializes protected and private properties of objects, which not only is strange (how private is that?), but are concepts which do not (yet) exist in JS, or at least not in the same way. If one would somehow implement (hard) private properties in JS, then how would some unserialization be able to set such a private property?
- JSON is an alternative that is not specific to PHP, and does not care about custom classes either. If you can access the PHP source of were the serialization happens, then change this to produce JSON instead. PHP offers
json_encode
for that, and JavaScript hasJSON.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 } ));
Solution 3:
wrap json_encode
around unserialize
echo json_encode( unserialize( $array));