Xson is an extension to Json format, offering a way to express crossing references and keep control of the size of generated data during the serialization.
This format extends Json, which means that any Json is valid Xson. Moreover, it introduces Xpaths, allowing any node to reference another one inside the same Xson object.
{
"users" : [
{
"name": "Bill",
"addess": {
"city": "Paris",
"zipCode": 75010
}
}
],
"billing": [
{
"price": "250 €",
"billingAddess": _.users[0].address
}
]
}
An Xpath is an expression JS friendly, as it can be read as direct access, using properties as well as indices, from the
top object referenced as _
. In this particular case:
_.users[0].address
references the object
{
"city": "Paris",
"zipCode": 75010
}
Method XEncoder::encode()
, given the right configuration, encodes any object to Xson format; if there is no self-referencing duplicate inside the structure, the result
will simply be a Json string. Depending on the tracking policy, presence of duplicates may be more or less permissive.
There are three tracking policies available:
- Safe: Whenever encoding a value typed as
object
, we verify that it has not already been visited directly on top of the current node. If it is the case, an Xpath will replace the second occurrence of the object.
$foo = new \stdClass();
$foo->name = 'foo';
$foo->bar = $foo;
json_encode($foo);
echo json_last_error_msg().PHP_EOL;
// Recursion detected
However, the encoding of this object, using XEncoder
, will output:
{
"name": "foo",
"bar": _
}
Method XEncoder::xEncode()
is way more interesting, as it really relies on the use of XPath, not only as a way to fix
recursion problems, but also as a way to access easily to specific data objects, based on their type, and yet it may
preserve all relationships with other objects contained in the Xson.
To do so, the structure of the Xson generated encapsulates the main object, which can be found at specific key $
. All
other objects present at depth 1 are arrays, collections of objects grouped together, usually by their type. For example,
let's say we want to describe the structure of a not-so-complex family, such as this one:
$Hal = new Character('Hal');
$Lois = new Character('Lois');
$Reese = new Character('Reese');
$Malcolm = new Character('Malcolm');
$Dewey = new Character('Dewey');
$Ida = new Character('Ida');
$Hal->wife = $Lois;
$Lois->husband = $Hal;
$Hal->children =
$Lois->children = [$Reese, $Malcolm, $Dewey];
$Reese->father =
$Malcolm->father =
$Dewey->father = $Hal;
$Reese->mother =
$Malcolm->mother =
$Dewey->mother = $Lois;
$Reese->siblings = [$Malcolm, $Dewey];
$Malcolm->siblings = [$Reese, $Dewey];
$Dewey->siblings = [$Reese, $Malcolm];
$Lois->mother = $Ida;
$Ida->children = [$Lois];
At this point, json_encode
is out of the question. We could use encode
:
$encoder = new XEncoder(new XsonFormat(XsonFormat::PRETTY));
$encoder->setTrackerFactory(new XStdTrackerFactory(XStdTrackerFactory::FULL));
echo $encoder->encode($Hal).PHP_EOL;
(We will see above the detail of these configuration tricks)
{
"name": "Hal",
"wife": {
"name": "Lois",
"husband": _,
"children": [
{
"name": "Reese",
"father": _,
"mother": _.wife,
"siblings": [
{
"name": "Malcolm",
"father": _,
"mother": _.wife,
"siblings": [
_.wife.children[0],
{
"name": "Dewey",
"father": _,
"mother": _.wife,
"siblings": [
_.wife.children[0],
_.wife.children[0].siblings[0]
]
}
]
},
_.wife.children[0].siblings[0].siblings[1]
]
},
_.wife.children[0].siblings[0],
_.wife.children[0].siblings[0].siblings[1]
],
"mother": {
"name": "Ida",
"children": [
_.wife
]
}
},
"children": [
_.wife.children[0],
_.wife.children[0].siblings[0],
_.wife.children[0].siblings[0].siblings[1]
]
}
Not very easy to read, right? Let's use method xEncode()
instead:
$encoder->setMapper(new XStaticMapper([Character::class => 'characters']));
echo $encoder->xEncode($Hal).PHP_EOL;
(Once again, we will describe in greater detail how to configure a XEncoder
)
Output:
{
"$": {
"name": "Hal",
"wife": _.characters[0],
"children": [
_.characters[1],
_.characters[2],
_.characters[3]
]
},
"characters": [
{
"name": "Lois",
"husband": _.$,
"children": [
_.characters[1],
_.characters[2],
_.characters[3]
],
"mother": _.characters[4]
},
{
"name": "Reese",
"father": _.$,
"mother": _.characters[0],
"siblings": [
_.characters[2],
_.characters[3]
]
},
{
"name": "Malcolm",
"father": _.$,
"mother": _.characters[0],
"siblings": [
_.characters[1],
_.characters[3]
]
},
{
"name": "Dewey",
"father": _.$,
"mother": _.characters[0],
"siblings": [
_.characters[1],
_.characters[2]
]
},
{
"name": "Ida",
"children": [
_.characters[0]
]
}
]
}
Now, this representation is way more readable.
The XEncoder
component is divided into several parts, which can be customized as you want:
- The
XProvider
processes visited objects, and extract their subobjects and their corresponding keys. It can therefore control what should be, or should not be, considered in the serialization. It also decides which type is every node met. - The
XFormat
determines how to output the normalized morsels. For now, it actually only controls the spacing and indentation, but it is destined to take more responsibilities. This is how we might be very easily able to reuse theXEncoder
for other format (YAML, XML, etc.) - The
XMapper
- Finally, the
XTracker
keeps track of the itinerary amongst the structure to serialize. It supposedly record all objects visited, in order to avoid duplicating them (in which case an Xpath is generated instead). Note that the encoder requires aXTrackerFactory
, since a new tracker must be instantiated at each encoding. The three standard implementations are:XFullTracker
: keeps track of every object visited, therefore we are ensured to never have any duplicate in the normalized version of the structure.XSafeTracker
: only checks the parent objects, preventing infinite loops.XLazyTracker
: no checks.
Some basic implementations come with this library, so that XEncoder
is usable as is. Nevertheless, the jobs have been
separated as much as possible, so that implementing new behaviours is accessible enough.
Particularly, the current implementation of XProvider
is static, as it only considers the type of the object visited.
It is highly likely that an implementation of GraphQL would be very much eased by this code architecture.
If you use the default implementation, XStdProvider
, know that three interfaces can be used to declare explicitly the
behaviour of your classes (in addition to \JsonSerializable
):
-
XScalar
: must implement methodxScalar(): mixed
, which may only returnnull
,bool
,int
,float
orstring
. -
XArray
/XObject
: must implement methodxIterator(): iterable
.Hint: Any sort of
iterable
will be accepted, even\Generator
, which may ease coding your serialization, usingyield
expression!
More to come about deserialization.
See you later, space cowboy...