skrz/meta

不同的线格式,不同的数据源,单一对象模型

3.1.4 2024-02-12 12:16 UTC

README

Build Status Downloads this Month Latest stable

不同的线格式,不同的数据源,单一对象模型

要求

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() 使用 匹配器模块 初始化规范。匹配器是一组满足某些标准(例如命名空间、类名)的类。模块是生成器,它通过匹配器匹配的类生成元类中特定模块的方法。 ApiMetaSpecSkrz\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

// 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/XMLtest/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 文件。