killov/meta

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

1.0.5 2023-11-09 08:36 UTC

This package is auto-updated.

Last update: 2024-08-31 00:49:15 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 代码库中,我们希望有一个单一的对象模型,我们还可以在项目之间共享它。这种需求主要来自 微服务协议,它变得相当混乱 - 没有人真正知道哪些服务发送给了对方。

序列化/反序列化需要 快速,因此我们创建了所谓的 元类 概念。元类是一个处理对象从/到许多不同格式序列化/反序列化的对象伴随类。每个类都有 exactly one 个元类,其中结合了来自不同 模块 的方法 - 模块 可以使用彼此的方法(例如,JsonModule 使用由 PhpModule 生成的 methods)。

用法

简单的值对象

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 task 等)的一部分。

默认情况下,规范在 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"

已知限制

  • private 属性不能被填充。填充 private 属性需要使用反射,或者使用 unserialize() 欺骗手法,这与快速的要求相矛盾。因此,如果存在 private 属性,元类编译将失败。如果您需要 private 属性,请使用 @Transient 注解标记它,并将其忽略。

  • 一个元类中最多可以有 31/63 个组。组名使用整型中的位进行编码。PHP 整数依赖于平台,并且总是有符号的,因此最多可以有 31/63 个组,这取决于 PHP 运行的平台。

待办事项

  • YAML - 与 JSON 相同
  • @XmlElementRef

许可证

MIT 许可证。请参阅 LICENSE 文件。