Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
/metaPublic

Map from PHP arrays, JSON, XML and Protocol Buffers (protobuf) to objects and back

License

NotificationsYou must be signed in to change notification settings

skrz/meta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build StatusDownloads this MonthLatest stable

Different wire formats, different data sources, single object model

Requirements

Skrz\Meta requires PHP>= 5.4.0 and Symfony>= 2.7.0.

Installation

Add asComposer dependency:

$ composer require skrz/meta

Why?

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).

Usage

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

  • 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:

Annotations

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

@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

@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"}

@XmlElement &@XmlElementWrapper &@XmlAttribute &@XmlValue

// 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 &@JsonDiscriminatorMap

@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 &@JsonDiscriminatorProperty

@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"

Known limitations

  • 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 aprivateproperty. 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.

TODO

  • YAML - just like JSON
  • @XmlElementRef

License

The MIT license. SeeLICENSE file.

About

Map from PHP arrays, JSON, XML and Protocol Buffers (protobuf) to objects and back

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors7

Languages


[8]ページ先頭

©2009-2025 Movatter.jp