protobuf-php/protobuf

PHP对Google的Protocol Buffers的实现

v0.1.3 2016-09-21 23:47 UTC

This package is auto-updated.

Last update: 2024-09-07 10:14:22 UTC


README

Build Status Coverage Status Total Downloads License

PHP的Protobuf是一个Google的Protocol Buffers的PHP语言实现,支持其二进制数据序列化,并包括一个protoc插件,用于从.proto文件生成PHP类。

安装

运行以下composer命令

$ composer require "protobuf-php/protobuf"

概述

本教程提供对使用协议缓冲区的基本介绍。通过逐步创建一个简单的示例应用程序,它展示了如何

  • .proto文件中定义消息格式。
  • 使用协议缓冲区编译器。
  • 使用PHP协议缓冲区API编写和读取消息。

为什么使用Protocol Buffers?

我们将要使用的示例是一个非常简单的“地址簿”应用程序,可以读取和写入人们的联系信息。地址簿中的每个人都有一个名字、一个ID、一个电子邮件地址和一个联系电话号码。

如何序列化和检索这种结构化数据?有几种方法可以解决这个问题

  • 使用PHP序列化。这是默认方法,因为它内置在语言中,但它的空间效率不高,并且如果需要与其他语言(Nodejs、Java、Python等)共享数据,则表现不佳。

  • 可以发明一种临时方法将数据项编码为单个字符串 – 例如将4个整数编码为“12:3:-23:67”。这是一种简单灵活的方法,尽管它需要编写一次性编码和解码代码,并且解析会带来一定的运行时成本。这对于编码非常简单的数据效果最好。

  • 将数据序列化为XML。这种方法可能非常吸引人,因为XML(某种程度上)是可读的,并且有很多语言的绑定库。如果想要与其他应用程序/项目共享数据,这是一个不错的选择。然而,XML非常占用空间,并且编码/解码它会对应用程序的性能造成巨大的影响。此外,导航XML DOM树比在类中导航简单字段要复杂得多。

协议缓冲区正是解决这个问题的灵活、高效、自动化的解决方案。使用协议缓冲区,您编写要存储的数据结构的.proto描述。然后,协议缓冲区编译器创建一个类,该类使用高效的二进制格式自动编码和解码协议缓冲区数据。生成的类为构成协议缓冲区的字段提供getter和setter,并处理读取和写入协议缓冲区的细节。重要的是,协议缓冲区格式支持随着时间的推移扩展格式,这样代码仍然可以读取使用旧格式编码的数据。

定义您的协议格式

要创建您的地址簿应用程序,您需要从一个.proto文件开始。一个.proto文件中的定义很简单:为每个要序列化的数据结构添加一个消息,然后指定消息中每个字段的名称和类型。以下是定义您的消息的.proto文件,即addressbook.proto

package tutorial;
import "php.proto";
option (php.package) = "Tutorial.AddressBookProtos";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

如您所见,语法与C++或Java相似。让我们逐部分查看文件并了解其功能。`.proto`文件以包声明开始,这有助于防止不同项目之间的命名冲突。在PHP中,包名称用作PHP命名空间,除非您明确指定了(php.package),就像我们在这里做的那样。即使您提供了(php.package),您也应该定义一个正常的包,以避免在Protocol Buffers命名空间以及非PHP语言中发生名称冲突。

在包声明之后,您可以看到两个PHP特定的选项:import "php.proto";(php.package)

  • import "php.proto"为proto文件添加了对一些PHP特定选项的支持。
  • (php.package)指定生成的类应位于哪个PHP命名空间中。如果您没有明确指定此选项,它将简单地匹配包声明中给出的包名称,但这些名称通常不是合适的PHP命名空间名称。

接下来,您有您的消息定义。消息只是一个包含一组类型化字段的聚合体。许多标准简单数据类型可用作字段类型,包括boolint32floatdoublestring。您还可以通过使用其他消息类型作为字段类型来为您的消息添加进一步的结构 – 在上述示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以在其他消息内部定义嵌套的消息类型 – 正如您所看到的,PhoneNumber类型是在Person内部定义的。您还可以定义enum类型,如果您希望某个字段具有预定义值列表之一 – 这里您想要指定电话号码可以是MOBILEHOMEWORK之一。

每个元素上的" = 1"" = 2"标记标识了字段在二进制编码中使用的唯一tag。标签号1-15比高数值少一个字节用于编码,因此作为优化,您可以选择为常用或重复的元素使用这些标签,将标签16及以上留给不常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段是进行这种优化的理想候选。

每个字段必须用以下修饰符之一进行注释

  • required:字段必须提供一个值,否则消息将被视为“未初始化”。尝试构建未初始化的消息将抛出RuntimeException。解析未初始化的消息将抛出IOException。除此之外,必填字段的行为与可选字段完全相同。
  • optional:字段可能设置也可能不设置。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,使用系统默认值:数值类型为零,字符串为空字符串,布尔值为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其中没有设置任何字段。调用访问器获取未显式设置的(或必填的)字段的值始终返回该字段的默认值。
  • repeated:字段可以重复任何次数(包括零次)。协议缓冲区中保留重复值的顺序。将重复字段视为动态大小的数组。

您可以在协议缓冲语言指南中找到关于编写.proto文件的完整指南,包括所有可能的字段类型。不过,不要寻找类似类继承的功能——协议缓冲区不支持这些功能。

编译您的协议缓冲区

现在您已经有了.proto文件,下一步需要生成用于读取和写入AddressBook(以及因此PersonPhoneNumber)消息的类。为此,您需要在您的.proto文件上运行协议缓冲区插件。

如果您尚未安装编译器(protoc)或您没有PHP插件,请参阅https://github.com/protobuf-php/protobuf-plugin

现在运行编译器插件,指定proto文件源目录(如果未提供值,则使用文件目录),目标目录(您希望生成的代码所在的位置),以及您的.proto的路径。在这种情况下

php ./vendor/bin/protobuf --include-descriptors -i . -o ./src/ ./addressbook.proto

这将在指定的目标目录中生成以下PHP类

src/
└── Tutorial
    └── AddressBookProtos
        ├── AddressBook.php
        ├── Person
        │   ├── PhoneNumber.php
        │   └── PhoneType.php
        └── Person.php

协议缓冲区API

让我们看看一些生成的代码,并了解编译器为您创建了哪些类和方法。如果您查看src/Tutorial/AddressBookProtos/Person.php,您可以看到它定义了一个名为Person的类。

消息为每个字段的每个字段自动生成访问器方法。以下是Person类的一些访问器(省略了实现以节省空间)

<?php
###################### required string name = 1; ###################################
/** @return bool */
public function hasName();
/** @return string */
public function getName();
/** @param string $value */
public function setName($value);
####################################################################################


###################### required int32 id = 2; ######################################
/** @return bool */
public function hasId();
/** @return int */
public function getId();
/** @param int $value */
public function setId($value);
####################################################################################


###################### optional string email = 3; ##################################
/** @return bool */
public function hasEmail();
/** @return string */
public function getEmail();
/** @param string $value */
public function setEmail($value);
####################################################################################


###################### repeated .tutorial.Person.PhoneNumber phone = 4; ############
/** @return bool */
public function hasPhoneList();
/** @return \Protobuf\Collection<\ProtobufTest\Protos\Person\PhoneNumber> */
public function getPhoneList();
/** @param \Protobuf\Collection<\ProtobufTest\Protos\Person\PhoneNumber> $value */
public function setPhoneList(\Protobuf\Collection $value);
####################################################################################
?>

如您所见,为每个字段提供了简单的getter和setter。还有为每个单字段提供has getters,如果该字段已被设置则返回true。重复字段还有一个额外的方法,即添加方法,该方法将新元素追加到列表中。

请注意,尽管.proto文件使用小写和下划线,但这些访问器方法使用了驼峰式命名。这种转换是由协议缓冲区编译器自动完成的,以便生成的类符合标准的PHP样式约定。您应该始终在您的.proto文件中使用小写和下划线作为字段名称;这确保了所有生成的语言的命名习惯良好。有关更多关于良好的.proto样式的信息,请参阅样式指南。

协议缓冲区类型映射到以下PHP类型

枚举和嵌套类

生成的代码包括一个PhoneType 枚举

<?php
namespace Tutorial\AddressBookProtos\Person;

class PhoneType extends \Protobuf\Enum
{
    /**
     * @return \Tutorial\AddressBookProtos\Person\PhoneType
     */
    public static function MOBILE() { /** ... */ }

    /**
     * @return \Tutorial\AddressBookProtos\Person\PhoneType
     */
    public static function HOME() { /** ... */ }

    /**
     * @return \Tutorial\AddressBookProtos\Person\PhoneType
     */
    public static function WORK() { /** ... */ }
?>

所有嵌套类型都是使用父类Person作为其命名空间的一部分生成的。

<?php
use Tutorial\AddressBookProtos\Person;

$person = new Person();
$phone  = new Person\PhoneNumber();
$type   = Person\PhoneType::MOBILE();

$person->setId(1);
$person->setName('Fabio B. Silva');
$person->setEmail('fabio.bat.silva@gmail.com');

$phone->setType($type);
$phone->setNumber('1231231212');
?>

已知问题

  • Protobuf使用IEEE 754标准存储浮点值,对于double类型使用64位字,对于float类型使用32位字。PHP原生支持IEEE 754,尽管精度取决于平台,但通常支持64位双精度浮点数。这意味着如果您的PHP是用64位大小的双精度浮点数(或更大的)编译的,您不应该在编码和解码浮点数和双精度浮点数类型值时遇到任何问题。

  • 整数值在PHP中也是平台依赖的。库已经针对用64位整数编译的PHP二进制文件开发和测试。从理论上讲,无论PHP内部使用32位还是64位整数,编码和解码算法都应该正常工作,只是要注意,如果使用32位整数,则数字不能超过PHP_INT_MAX值(2147483647)。

    虽然 Protobuf 支持无符号整数,但 PHP 不支持。实际上,大于编译的 PHP 最大整数值(PHP_INT_MAX,64位为 0x7FFFFFFFFFFFFFFF)的数字将被自动转换为双精度浮点数,这通常提供 53 位的十进制精度,允许安全地处理高达 0x20000000000000(2^53)的数字,即使它们在 PHP 中表示为浮点数而不是整数。更高的数字可能会丢失精度,甚至可能返回一个 无穷大 值。请注意,库不会对这些数字进行检查,使用它们可能会引发意外的行为。

    当以 int32int64fixed64 类型编码负值时,需要在 PHP 环境中提供大整数扩展 GMPBC Math。原因是,在编码这些负数而不使用 zigzag 时,二进制表示使用最高位作为符号位,因此这些数字超过了 PHP 支持的最大值。库将检查这些条件,并自动尝试使用 GMP 或 BC 来处理值。

解析和序列化

每个协议缓冲区类都有使用协议缓冲区二进制格式编写和读取所选类型消息的方法。这些包括

<?php

/**
 * Message constructor
 *
 * @param \Protobuf\Stream|resource|string $stream
 * @param \Protobuf\Configuration          $configuration
 */
public function __construct($stream = null, Configuration $configuration = null);

/**
 * Creates message from the given stream.
 *
 * @param \Protobuf\Stream|resource|string $stream
 * @param \Protobuf\Configuration          $configuration
 *
 * @return \Protobuf\Message
 */
public static function fromStream($stream, Configuration $configuration = null);


/**
 * Serializes the message and returns a stream containing its bytes.
 *
 * @param \Protobuf\Configuration $configuration
 *
 * @return \Protobuf\Stream
 */
public function toStream(Configuration $configuration = null);

/**
 * Returns a human-readable representation of the message, particularly useful for debugging.
 *
 * @return string
 */
public function __toString();
?>

编写消息

现在让我们尝试使用您的协议缓冲区类。您希望您的通讯录应用程序首先能够做到的是将个人详细信息写入通讯录文件。为此,您需要创建并填充您的协议缓冲区类的实例,然后将它们写入输出流。

以下是一个程序,它从文件中读取 AddressBook,根据用户输入添加一个新的 Person,并将新的 AddressBook 写回文件。直接调用或引用由协议编译器生成的代码的部分被突出显示。

#!/usr/bin/env php
<?php

use Tutorial\AddressBookProtos\Person;
use Tutorial\AddressBookProtos\AddressBook;

// Read the existing address book or create a new one.
$addressBook = is_file($argv[1])
    ? new AddressBook(file_get_contents($argv[1]))
    : new AddressBook();

$person = new Person();
$id     = intval(readline("Enter person ID: "));
$name   = trim(readline("Enter person name: "));
$email  = trim(readline("Enter email address (blank for none): "));

$person->setId($id);
$person->setName($name);

if ( ! empty($email)) {
    $person->setEmail($email);
}

while (true) {
    $number = trim(readline("Enter a phone number (or leave blank to finish):"));

    if (empty($number)) {
        break;
    }

    $phone  = new Person\PhoneNumber();
    $type   = trim(readline("Is this a mobile, home, or work phone? "));

    switch (strtolower($type)) {
        case 'mobile':
            $phone->setType(Person\PhoneType::MOBILE());
            break;
        case 'work':
            $phone->setType(Person\PhoneType::WORK());
            break;
        case 'home':
            $phone->setType(Person\PhoneType::HOME());
            break;
        default:
            echo "Unknown phone type. Using default." . PHP_EOL;
    }

    $phone->setNumber($number);
    $person->addPhone($phone);
}

// Add a person.
$addressBook->addPerson($person);

// Print current address book
echo $addressBook;

// Write the new address book back to disk.
file_put_contents($argv[1], $addressBook->toStream());
?>

本教程文档基于 Protocol Buffer Basics Tutorial