- Notifications
You must be signed in to change notification settings - Fork5
Map from PHP arrays, JSON, XML and Protocol Buffers (protobuf) to objects and back
License
skrz/meta
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Different wire formats, different data sources, single object model
Skrz\Meta
requires PHP>= 5.4.0
and Symfony>= 2.7.0
.
Add asComposer dependency:
$ composer require skrz/meta
AtSkrz.cz, we work heavily with many different input/output formats and data sources (databases).E.g. data from partners come in asXML feeds; internally ourmicro-service architecture encodes data intoJSON as wireformat; data can come fromMySQL, Redis, and Elasticsearch databases, and also has to be put in there.
However, in our PHP code base we want single object model that we could also share between projects. This need came mainlyfrommicro services' protocols that got quite wild - nobody really knew what services sent to each other.
Serialization/deserialization had to befast, therefore we created concept of so-calledmeta classes. A meta class is anobject's companion class that handles object's serialization/deserialization from/into many different formats. Every classhas exactly one meta class, in which methods from differentmodules are combined -modules can use each others methods(e.g.JsonModule
uses methods generated byPhpModule
).
Have simple value object:
namespaceSkrz\API;class Category{/** @var string */public$name;/** @var string */public$slug;/** @var Category */public$parentCategory;}
You would like to serialize object into JSON. What you might do is to create methodtoJson
:
publicfunctiontoJson(){returnjson_encode(array("name" =>$this->name,"slug" =>$this->slug,"parentCategory" =>$this->parentCategory ?$this->parentCategory->toJson() :null ));}
Creating such method for every value object that gets sent over wire is tedious and error-prone. So you generatemeta class that implements such methods.
Meta classes are generated according tometa spec. A meta spec is a class extendingSkrz\Meta|AbstractMetaSpec
:
namespaceSkrz;useSkrz\Meta\AbstractMetaSpec;useSkrz\Meta\JSON\JsonModule;useSkrz\Meta\PHP\PhpModule;class ApiMetaSpecextends AbstractMetaSpec{protectedfunctionconfigure() {$this->match("Skrz\\API\\*") ->addModule(newPhpModule()) ->addModule(newJsonModule()); }}
Methodconfigure()
initializes spec withmatchers andmodules. A matcher is a set of classes that satisfy certaincriteria (e.g. namespace, class name). A module is generator that takes class matched by the matcher and generatesmodule-specific methods in the meta class.ApiMetaSpec
creates meta classes for every class directly inSkrz\API
namespace(it does not include classes in sub-namespaces, e.g.Skrz\API\Meta
). The meta classes are generated from PHP and JSON modules(Skrz\Meta\BaseModule
providing basic functionality of a meta class is added automatically).
To actually generate classes, you have supply some files to spec to process:
useSymfony\Component\Finder\Finder;$files =array_map(function (\SplFileInfo$file) {return$file->getPathname();},iterator_to_array( (newFinder()) ->in(__DIR__ ."/API") ->name("*.php") ->notName("*Meta*") ->files()));$spec =newApiMetaSpec();$spec->processFiles($files);
Similar code should be part of your build process (or in development part of Grunt watch task etc.).
By default, spec generates meta class inMeta
sub-namespace withMeta
suffix (e.g.Skrz\API\Category
->Skrz\API\Meta\CategoryMeta
)and stores it insideMeta
sub-directory of original class's directory.
After the meta classes has been generated, usage is quite simple:
useSkrz\API\Category;useSkrz\API\Meta\CategoryMeta;$parentCategory =newCategory();$parentCategory->name ="The parent category";$parentCategory->slug ="parent-category";$childCategory =newCategory();$childCategory->name ="The child category";$childCategory->slug ="child-category";$childCategory->parentCategory =$parentCategory;var_export(CategoryMeta::toArray($childCategory));// array(// "name" => "The child category",// "slug" => "child-category",// "parentCategory" => array(// "name" => "The parent category",// "slug" => "parent-category",// "parentCategory" => null,// ),// )echo CategoryMeta::toJson($childCategory);// {"name":"The child category","slug":"child-category","parentCategory":{"name":"The parent category","slug":"parent-category","parentCategory":null}}$someCategory = CategoryMeta::fromJson(array("name" =>"Some category","ufo" =>42,// unknown fields are ignored));var_export($someCategoryinstanceof Category);// TRUEvar_export($someCategory->name ==="Some category");// TRUE
- Fields represent set of symbolic field paths.
- They are composite (fields can have sub-fields).
- Fields can be supplied as
$filter
parameters into*()
methods.
useSkrz\API\Category;useSkrz\API\Meta\CategoryMeta;useSkrz\Meta\Fields\Fields;$parentCategory =newCategory();$parentCategory->name ="The parent category";$parentCategory->slug ="parent-category";$childCategory =newCategory();$childCategory->name ="The child category";$childCategory->slug ="child-category";$childCategory->parentCategory =$parentCategory;var_export(CategoryMeta::toArray($childCategory,null, Fields::fromString("name,parentCategory{name}")));// array(// "name" => "The child category",// "parentCategory" => array(// "name" => "The parent category",// ),// )
Fields are inspired by:
- Facebook Graph API's
?fields=...
query parameter - Google Protocol Buffers'
FieldMask
(and itsJSON serialization)
Skrz\Meta
usesDoctrine annotation parser. Annotations can change mappings.AlsoSkrz\Meta
offers so calledgroups - different sources can offer different field names, however, they map onto same object.
@PhpArrayOffset
annotation can be used to change name of outputted keys in arrays generated bytoArray
and inputs tofromArray
:
namespaceSkrz\API;useSkrz\Meta\PHP\PhpArrayOffset;class Category{/** * @var string * * @PhpArrayOffset("THE_NAME") * @PhpArrayOffset("name", group="javascript") */protected$name;/** * @var string * * @PhpArrayOffset("THE_SLUG") * @PhpArrayOffset("slug", group="javascript") */protected$slug;publicfunctiongetName() {return$this->name; }publicfunctiongetSlug() {return$this->slug; }}// ...useSkrz\API\Meta\CategoryMeta;$category = CategoryMeta::fromArray(array("THE_NAME" =>"My category name","THE_SLUG" =>"category","name" =>"Different name"// name is not an unknown field, so it is ignored));var_export($category->getName());// "My category name"var_export($category->getSlug());// "category"var_export(CategoryMeta::toArray($category,"javascript"));// array(// "name" => "My category name",// "slug" => "category",// )
@JsonProperty
marks names of JSON properties. (Internally every group created by@JsonProperty
creates PHP groupprefixed byjson:
- PHP object is first mapped to array usingjson:
group, then the array is serialized usingjson_encode()
.)
namespaceSkrz\API;useSkrz\Meta\PHP\PhpArrayOffset;useSkrz\Meta\JSON\JsonProperty;class Category{/** * @var string * * @PhpArrayOffset("THE_NAME") * @JsonProperty("NAME") */protected$name;/** * @var string * * @PhpArrayOffset("THE_SLUG") * @JsonProperty("sLuG") */protected$slug;publicfunctiongetName() {return$this->name; }publicfunctiongetSlug() {return$this->slug; }}// ...useSkrz\API\Meta\CategoryMeta;$category = CategoryMeta::fromArray(array("THE_NAME" =>"My category name","THE_SLUG" =>"category",));var_export(CategoryMeta::toJson($category));// {"NAME":"My category name","sLuG":"category"}
- Modelled afterjavax.xml.bind.annotation.
- Works with
XMLWriter
orDOMDocument
(for streaming or DOM-based XML APIs).
// example: serialize object to XMLWriter/** * @XmlElement(name="SHOPITEM") */class Product{/** * @var string * * @XmlElement(name="ITEM_ID") */public$itemId;/** * @var string[] * * @XmlElement(name="CATEGORYTEXT") */public$categoryTexts;}$product =newProduct();$product->itemId ="SKU123";$product->categoryTexts =array("Home Appliances","Dishwashers");$xml =new \XMLWriter();$xml->openMemory();$xml->setIndent(true);$xml->startDocument();$meta->toXml($product,null,$xml);$xml->endDocument();echo$xml->outputMemory();// <?xml version="1.0"?>// <SHOPITEM>// <ITEM_ID>SKU123</ITEM_ID>// <CATEGORYTEXT>Home Appliances</CATEGORYTEXT>// <CATEGORYTEXT>Dishwashers</CATEGORYTEXT>// </SHOPITEM>
For more examples see classes intest/Skrz/Meta/Fixtures/XML
andtest/Skrz/Meta/XmlModuleTest.php
.
@PhpDiscriminatorMap
and@JsonDiscriminatorMap
encapsulate inheritance.
namespaceAnimals;useSkrz\Meta\PHP\PhpArrayOffset;/** * @PhpDiscriminatorMap({ * "cat" => "Animals\Cat", // specify subclass * "dog" => "Animals\Dog" * }) */class Animal{/** * @var string */protected$name; }class Catextends Animal {publicfunctionmeow() {echo"{$this->name}: meow"; }}class Dogextends Animal{publicfunctionbark() {echo"{$this->name}: woof"; }}// ...useAnimals\Meta\AnimalMeta;$cat = AnimalMeta::fromArray(["cat" => ["name" =>"Oreo"]]);$cat->meow();// prints "Oreo: meow"$dog = AnimalMeta::fromArray(["dog" => ["name" =>"Mutt"]]);$dog->bark();// prints "Mutt: woof"
@PhpDiscriminatorOffset
and@JsonDiscriminatorProperty
make subclasses differentiated using offset/property.
namespaceAnimals;useSkrz\Meta\PHP\PhpArrayOffset;/** * @PhpDiscriminatorOffset("type") * @PhpDiscriminatorMap({ * "cat" => "Animals\Cat", // specify subclass * "dog" => "Animals\Dog" * }) */class Animal{/** * @var string */protected$type;/** * @var string */protected$name; }class Catextends Animal {publicfunctionmeow() {echo"{$this->name}: meow"; }}class Dogextends Animal{publicfunctionbark() {echo"{$this->name}: woof"; }}// ...useAnimals\Meta\AnimalMeta;$cat = AnimalMeta::fromArray(["type" =>"cat","name" =>"Oreo"]);$cat->meow();// prints "Oreo: meow"$dog = AnimalMeta::fromArray(["type" =>"dog","name" =>"Mutt"]);$dog->bark();// prints "Mutt: woof"
private
properties cannot be hydrated. Hydration of private properties would require using reflection, or usingunserialize()
hack,which is contrary to requirement of being fast. Therefore meta classes compilation will fail if there is aprivate
property. If you need aprivate
property, mark it using@Transient
annotation and it will be ignored.There can be at most 31/63 groups in one meta class. Group name is encoded using bit in integer type. PHP integeris platform dependent and always signed, therefore there can be at most 31/63 groups depending on platform the PHP's running on.
- YAML - just like JSON
@XmlElementRef
The MIT license. SeeLICENSE
file.
About
Map from PHP arrays, JSON, XML and Protocol Buffers (protobuf) to objects and back
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors7
Uh oh!
There was an error while loading.Please reload this page.