skrz / meta
不同的线格式,不同的数据源,单一对象模型
Requires
- php: >=7.4
- doctrine/annotations: ~1.10
- nette/php-generator: ~2.6
- symfony/console: ~2.7|~3.0|~4.0|~5.0|~6.0|~7.0
- symfony/finder: ~2.7|~3.0|~4.0|~5.0|~6.0|~7.0
Requires (Dev)
- phpunit/phpunit: ~4.6
README
不同的线格式,不同的数据源,单一对象模型
要求
Skrz\Meta
需要 PHP >= 5.4.0
和 Symfony >= 2.7.0
.
安装
作为 Composer 依赖项添加
$ composer require skrz/meta
为什么?
在 Skrz.cz,我们与许多不同的输入/输出格式和数据源(数据库)进行大量工作。例如,合作伙伴的数据以 XML 联播 的形式到来;在我们的内部 微服务架构 中,数据被编码为 JSON 作为线格式;数据可以从 MySQL、Redis 和 Elasticsearch 数据库中获取,并且还需要将数据放入其中。
然而,在我们的 PHP 代码库中,我们希望有一个单一的对象模型,我们也可以在项目之间共享它。这种需求主要来自 微服务协议,它们变得相当混乱 - 没有人真正知道哪些服务发送给彼此。
序列化/反序列化必须 快速,因此我们创建了所谓的 元类 概念。元类是处理对象从/到许多不同格式的序列化/反序列化的对象伴随类。每个类都有一个元类,其中结合了来自不同 模块 的方法 - 模块 可以使用彼此的方法(例如,JsonModule
使用由 PhpModule
生成的 方法)。
使用方法
拥有简单的值对象
namespace Skrz\API; class Category { /** @var string */ public $name; /** @var string */ public $slug; /** @var Category */ public $parentCategory; }
您希望将对象序列化为 JSON。您可能要做的是创建方法 toJson
public function toJson() { return json_encode(array( "name" => $this->name, "slug" => $this->slug, "parentCategory" => $this->parentCategory ? $this->parentCategory->toJson() : null )); }
为每个通过网络发送的值对象创建此类方法既乏味又容易出错。因此,您生成实现这些方法的元类。
元类是根据 元规范 生成的。元规范是扩展 Skrz\Meta|AbstractMetaSpec
的类
namespace Skrz; use Skrz\Meta\AbstractMetaSpec; use Skrz\Meta\JSON\JsonModule; use Skrz\Meta\PHP\PhpModule; class ApiMetaSpec extends AbstractMetaSpec { protected function configure() { $this->match("Skrz\\API\\*") ->addModule(new PhpModule()) ->addModule(new JsonModule()); } }
方法 configure()
使用 匹配器 和 模块 初始化规范。匹配器是一组满足某些标准(例如命名空间、类名)的类。模块是生成器,它通过匹配器匹配的类生成元类中特定模块的方法。 ApiMetaSpec
为 Skrz\API
命名空间中的每个类直接创建元类(它不包括子命名空间中的类,例如 Skrz\API\Meta
)。元类由 PHP 和 JSON 模块生成(Skrz\Meta\BaseModule
自动添加元类的基本功能)。
要实际生成类,您需要向规范提供一些要处理的文件
use Symfony\Component\Finder\Finder; $files = array_map(function (\SplFileInfo $file) { return $file->getPathname(); }, iterator_to_array( (new Finder()) ->in(__DIR__ . "/API") ->name("*.php") ->notName("*Meta*") ->files() )); $spec = new ApiMetaSpec(); $spec->processFiles($files);
类似的代码应成为您构建过程的一部分(或在 Grunt watch 任务等开发部分)。
默认情况下,规范在 Meta
子命名空间中生成元类,并带有 Meta
后缀(例如,Skrz\API\Category
-> Skrz\API\Meta\CategoryMeta
),并存储在原始类目录的 Meta
子目录中。
元类生成后,使用方法非常简单
use Skrz\API\Category; use Skrz\API\Meta\CategoryMeta; $parentCategory = new Category(); $parentCategory->name = "The parent category"; $parentCategory->slug = "parent-category"; $childCategory = new Category(); $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($someCategory instanceof Category); // TRUE var_export($someCategory->name === "Some category"); // TRUE
字段
- 字段表示一组符号字段路径。
- 它们是复合的(字段可以有子字段)。
- 字段可以作为
to*()
方法中的$filter
参数提供。
use Skrz\API\Category; use Skrz\API\Meta\CategoryMeta; use Skrz\Meta\Fields\Fields; $parentCategory = new Category(); $parentCategory->name = "The parent category"; $parentCategory->slug = "parent-category"; $childCategory = new Category(); $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", // ), // )
字段灵感来源于
注解
Skrz\Meta
使用 Doctrine 注解解析器。注解可以改变映射。此外,Skrz\Meta
还提供了所谓的 分组 - 不同来源可以提供不同的字段名,但它们映射到同一个对象。
@PhpArrayOffset
@PhpArrayOffset
注解可用于更改由 toArray
生成的数组中输出的键以及 fromArray
的输入。
namespace Skrz\API; use Skrz\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; public function getName() { return $this->name; } public function getSlug() { return $this->slug; } } // ... use Skrz\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
标记 JSON 属性的名称。内部,每个由 @JsonProperty
创建的组都会创建一个以 json:
为前缀的 PHP 组 - 首先将 PHP 对象映射到数组使用 json:
组,然后使用 json_encode()
序列化数组。
namespace Skrz\API; use Skrz\Meta\PHP\PhpArrayOffset; use Skrz\Meta\JSON\JsonProperty; class Category { /** * @var string * * @PhpArrayOffset("THE_NAME") * @JsonProperty("NAME") */ protected $name; /** * @var string * * @PhpArrayOffset("THE_SLUG") * @JsonProperty("sLuG") */ protected $slug; public function getName() { return $this->name; } public function getSlug() { return $this->slug; } } // ... use Skrz\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
- 模仿 javax.xml.bind.annotation。
- 与
XMLWriter
或DOMDocument
(用于流或基于 DOM 的 XML API)一起使用。
// 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 = new Product(); $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>
有关更多示例,请参阅 test/Skrz/Meta/Fixtures/XML
和 test/Skrz/Meta/XmlModuleTest.php
中的类。
@PhpDiscriminatorMap
& @JsonDiscriminatorMap
@PhpDiscriminatorMap
和 @JsonDiscriminatorMap
封装继承。
namespace Animals; use Skrz\Meta\PHP\PhpArrayOffset; /** * @PhpDiscriminatorMap({ * "cat" => "Animals\Cat", // specify subclass * "dog" => "Animals\Dog" * }) */ class Animal { /** * @var string */ protected $name; } class Cat extends Animal { public function meow() { echo "{$this->name}: meow"; } } class Dog extends Animal { public function bark() { echo "{$this->name}: woof"; } } // ... use Animals\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
和 @JsonDiscriminatorProperty
通过偏移/属性使子类区分开来。
namespace Animals; use Skrz\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 Cat extends Animal { public function meow() { echo "{$this->name}: meow"; } } class Dog extends Animal { public function bark() { echo "{$this->name}: woof"; } } // ... use Animals\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"
已知限制
-
私有属性无法进行填充。私有属性的填充需要使用反射,或使用
unserialize()
漏洞,这与快速要求相矛盾。因此,如果存在私有属性,元类编译将失败。如果您需要私有属性,请使用@Transient
注解标记它,它将被忽略。 -
一个元类中最多可以有 31/63 个分组。组名使用整型中的位进行编码。PHP 整型依赖于平台并且总是有符号的,因此,根据 PHP 运行的平台,最多可以有 31/63 个分组。
待办事项
- YAML - 与 JSON 相同
@XmlElementRef
许可
MIT 许可证。请参阅 LICENSE
文件。