Value Objects
Since version 1.3, sabre/xml comes with a new facility to map XML elements to PHP classes, in two directions: Value Objects.
Setup
To demonstrate, lets take the following (partial) atom feed:
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<link href="http://example.org/"/>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>
A PHP classes implementation might look something like this:
namespace My\Atom;
class Feed {
public $title;
public $link = [];
public $updated;
public $author;
public $id;
public $entry = [];
}
class Author {
public $name;
public $email;
}
class Entry {
public $title;
public $link = [];
public $id;
public $updated;
public $summary;
}
To let sabre/xml automatically map between these two, simply use the Service class:
$service = new Sabre\Xml\Service();
$service->namespaceMap['http//www.w3.org/2005/Atom'] = 'atom';
$service->mapValueObject('{http://www.w3.org/2005/Atom}feed', 'My\Atom\Feed');
$service->mapValueObject('{http://www.w3.org/2005/Atom}author', 'My\Atom\Author');
$service->mapValueObject('{http://www.w3.org/2005/Atom}entry', 'My\Atom\Entry');
In case you are curious about the weird notation with the {
and }
, read
clark-notation.
If you are running PHP 5.5 and up, you can also use ::class
. Example:
$service->mapValueObject('{http://www.w3.org/2005/Atom}feed', Feed::class);
$service->mapValueObject('{http://www.w3.org/2005/Atom}author', Author::class);
$service->mapValueObject('{http://www.w3.org/2005/Atom}entry', Entry::class);
The ::class
construct basically returns a full class name. Because it's no
longer specified as a string, you can import classes into the current scope.
Parsing
After that, all it takes to parse the atom feed is:
$feed = $service->parse($xml);
// Feed is an instance of My\Atom\Feed;
To automatically throw an error if the root xml element is an atom feed,
you can also use the expect
method instead of parse
.
$feed = $service->expect('{http://www.w3.org/2005/Atom}feed', $xml);
// Feed is an instance of My\Atom\Feed;
Writing
Writing is similarly easy. Given that you have a $feed
variable which refers
to a fully setup My\Atom\Feed
object, all you have to do is call the following:
$xml = $service->writeValueObject($feed);
How it works
When you pass a classname to mapValueObject
, sabre/xml automatically creates
an instance of that class when it comes across the element name you specified.
Take for instance this portion of the XML document:
<entry xmlns="http://www.w3.org/2005/Atom">
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
When we call the following function:
$service->mapValueObject('{http://www.w3.org/2005/Atom}entry', 'My\Atom\Entry');
We are basically saying:
- Map the element
entry
. - In the XML namespace
http://www.w3.org/2005/Atom
- To the PHP class
My\Atom\Entry
.
In this example all the child elements (title
, link
, id
) are also all in
the same XML namespace. If this is the case, we will see if the class
My\Atom\Entry
has a public
property with the same name, and set its value.
If entry had sub-elements in a different XML namespace, they would be discarded.
One special trick is that if you define your class with a property and give it
a default value that's an array, sabre/xml
will immediately assume that more
than one element may appear. In the above example, both $authors
and $entry
was defaulted to an empty array. This signals sabre/xml
that multiple
<author>
and <entry>
elements may appear as children and it will append
those to the array.
Under the hood
Ultimately this only works for simple mappings. As soon as your objects have multiple namespaces, or if you need to parse out attributes, ValueObjects are immediately too simplistic for you.
In those cases you need to write custom serializers/deserializers for your objects. If you paid attention to the examples so far, you will have noted that the atom feed contained this element:
<link href="http://example.org/2003/12/13/atom03"/>
The parser in fact discarded the href
attribute and its value. The only
way around that is to write a custom deserializer.
The following example demonstrates how you would parse <link>
. First, we
need a class representing atom links:
namespace My\Atom;
class Link {
public $href;
public $rel;
public $type;
public $hrefLang;
public $title;
public $length;
}
And now, a custom deserializer, defined on the Service:
$service->elementMap['{http://www.w3.org/2005/Atom}link'] = function($reader) {
$link = new My\Atom\Link();
foreach($reader->parseAttributes() as $key=>$value) {
if (isset($link->{$key})) {
$link->$key = $value;
}
}
// Tell the reader we are done with this element
$reader->next();
return $link;
};
The serializer is even simpler:
$server->classMap['My\Atom\Link'] = function($writer, $link) {
$writer->writeAttributes(
get_object_vars($link)
);
}
Under the hood, that's also how the mapValueObject
operates. It adds a
mapping to both the $elementMap
and $classMap
.
A more complete atom example
We've built a full atom parser for demonstration purposes. You can find it on GitHub and Packagist.