PHP 生成代码指南

描述了 protocol buffer 编译器为任何给定的协议定义所生成的 PHP 代码。

在阅读本文档之前,您应该先阅读 proto3 语言指南Editions 语言指南。请注意,protocol buffer 编译器目前仅支持为 PHP 生成 proto3 和 editions 的代码。

编译器调用

当使用 --php_out= 命令行标志调用时,protocol buffer 编译器会生成 PHP 输出。--php_out= 选项的参数是您希望编译器写入 PHP 输出的目录。为了符合 PSR-4 规范,编译器会创建一个与 proto 文件中定义的 package 对应的子目录。此外,对于 proto 文件输入的每条消息,编译器都会在 package 的子目录中创建一个单独的文件。消息输出文件的名称由三部分组成:

  • 基础目录:proto 路径(通过 --proto_path=-I 命令行标志指定)被替换为输出路径(通过 --php_out= 标志指定)。
  • 子目录:package 名称中的 . 会被替换为操作系统的目录分隔符。每个 package 名称的组件都会被首字母大写。
  • 文件:消息名称后附加 .php

因此,举例来说,假设您像下面这样调用编译器:

protoc --proto_path=src --php_out=build/gen src/example.proto

并且 src/example.proto 定义如下:

edition = "2023";
package foo.bar;
message MyMessage {}

编译器将读取文件 src/foo.proto 并生成输出文件:build/gen/Foo/Bar/MyMessage.php。编译器会自动创建目录 build/gen/Foo/Bar(如果需要),但它*不会*创建 buildbuild/gen;它们必须已经存在。

包(Packages)

.proto 文件中定义的 package 名称默认用于为生成的 PHP 类生成模块结构。给定一个文件如下:

package foo.bar;

message MyMessage {}

protocol 编译器会生成一个名为 Foo\Bar\MyMessage 的输出类。

命名空间选项

编译器支持额外的选项来定义 PHP 和元数据命名空间。当定义了这些选项时,它们将用于生成模块结构和命名空间。给定选项如下:

package foo.bar;
option php_namespace = "baz\\qux";
option php_metadata_namespace = "Foo";
message MyMessage {}

protocol 编译器会生成一个名为 baz\qux\MyMessage 的输出类。该类将具有命名空间 namespace baz\qux

protocol 编译器会生成一个名为 Foo\Metadata 的元数据类。该类将具有命名空间 namespace Foo

生成的选项是区分大小写的。默认情况下,package 会被转换为 Pascal case(驼峰命名法)。

消息

给定一个简单的消息声明:

message Foo {
  int32 int32_value = 1;
  string string_value = 2;
  repeated int32 repeated_int32_value = 3;
  map<int32, int32> map_int32_int32_value = 4;
}

protocol buffer 编译器会生成一个名为 Foo 的 PHP 类。该类继承自一个公共基类 Google\Protobuf\Internal\Message,该基类提供了编码和解码消息类型的方法,如以下示例所示:

$from = new Foo();
$from->setInt32Value(1);
$from->setStringValue('a');
$from->getRepeatedInt32Value()[] = 1;
$from->getMapInt32Int32Value()[1] = 1;
$data = $from->serializeToString();
$to = new Foo();
try {
  $to->mergeFromString($data);
} catch (Exception $e) {
  // Handle parsing error from invalid data.
  ...
}

您*不应该*创建自己的 Foo 子类。生成的类不是为子类化设计的,可能会导致“脆弱基类”问题。

嵌套消息会生成一个 PHP 类,其名称与原消息相同,但以其包含消息的名称为前缀,并用下划线分隔,因为 PHP 不支持嵌套类。例如,如果您在 .proto 文件中有以下内容:

message TestMessage {
  message NestedMessage {
    int32 a = 1;
  }
}

编译器将生成以下类:

// PHP doesn’t support nested classes.
class TestMessage_NestedMessage {
  public function __construct($data = NULL) {...}
  public function getA() {...}
  public function setA($var) {...}
}

如果消息类名是保留字(例如,Empty),则会在类名前加上前缀 PB

class PBEmpty {...}

我们还提供了文件级别的选项 php_class_prefix。如果指定了该选项,它将被添加到所有生成的消息类的前面。

字段

对于消息类型中的每个字段,protocol buffer 编译器会生成一组用于设置和获取该字段的访问器方法。访问器方法的命名使用 snake_case(下划线命名法)的字段名转换为 PascalCase(驼峰命名法)。因此,对于一个名为 field_name 的字段,访问器方法将是 getFieldNamesetFieldName

// optional MyEnum optional_enum
$m->getOptionalEnum();
$m->setOptionalEnum(MyEnum->FOO);
$m->hasOptionalEnum();
$m->clearOptionalEnum();

// MyEnum implicit_enum
$m->getImplicitEnum();
$m->setImplicitEnum(MyEnum->FOO);

每当您设置一个字段时,都会根据该字段声明的类型对值进行类型检查。如果值的类型不正确(或超出范围),将会抛出异常。默认情况下,允许在整型、浮点型和数字字符串之间进行类型转换(例如,在为字段赋值或向重复字段添加元素时)。不允许的转换包括所有与数组或对象的相互转换。浮点型到整型的溢出转换是未定义的。

您可以在标量值类型表中查看每种标量 protocol buffers 类型对应的 PHP 类型。

has...clear...

对于具有显式存在性(presence)的字段,编译器会生成一个 has...() 方法。如果该字段已设置,此方法返回 true

编译器还会生成一个 clear...() 方法。此方法会取消设置该字段。调用此方法后,has...() 将返回 false

对于具有隐式存在性(presence)的字段,编译器不会生成 has...()clear...() 方法。对于这些字段,您可以通过将字段值与默认值进行比较来检查其存在性。

奇异消息字段

对于消息类型的字段,编译器会生成与标量类型相同的访问器方法。

消息类型的字段默认为 null,并且在访问该字段时不会自动创建。因此,您需要显式创建子消息,如下所示:

$m = new MyMessage();
$m->setZ(new SubMessage());
$m->getZ()->setFoo(42);

$m2 = new MyMessage();
$m2->getZ()->setFoo(42);  // FAILS with an exception

您可以将任何实例分配给消息字段,即使该实例也在其他地方持有(例如,作为另一条消息的字段值)。

重复字段

protocol buffer 编译器会为每个重复字段生成一个特殊的 RepeatedField。因此,例如,给定以下字段:

repeated int32 foo = 1;

生成的代码允许您这样做:

$m->getFoo()[] =1;
$m->setFoo($array);

映射字段

protocol buffer 编译器会为每个 map 字段生成一个 MapField。因此,给定此字段:

map<int32, int32> weight = 1;

您可以使用生成的代码执行以下操作:

$m->getWeight()[1] = 1;

枚举

PHP 没有原生的枚举(enum),因此 protocol buffer 编译器会为 .proto 文件中的每种枚举类型生成一个 PHP 类,就像处理消息一样,并为每个值定义常量。因此,给定此枚举:

enum TestEnum {
  Default = 0;
  A = 1;
}

编译器会生成以下类:

class TestEnum {
  const DEFAULT = 0;
  const A = 1;
}

与消息一样,嵌套的枚举将生成一个 PHP 类,其名称与原枚举相同,但以其包含消息的名称为前缀,并用下划线分隔,因为 PHP 不支持嵌套类。

class TestMessage_NestedEnum {...}

如果枚举类或值的名称是保留字(例如,Empty),则会在类名或值名前面加上前缀 PB

class PBEmpty {
  const PBECHO = 0;
}

我们还提供了文件级别的选项 php_class_prefix。如果指定了该选项,它将被添加到所有生成的枚举类的前面。

Oneof

对于 oneof,protocol buffer 编译器会为 oneof 中的每个字段生成一个 hasclear 方法,以及一个特殊的访问器方法,让您找出 oneof 中的哪个字段(如果有的话)已被设置。因此,给定此消息:

message TestMessage {
  oneof test_oneof {
    int32 oneof_int32 = 1;
    int64 oneof_int64 = 2;
  }
}

编译器会生成以下字段和特殊方法:

class TestMessage {
  private oneof_int32;
  private oneof_int64;
  public function getOneofInt32();
  public function setOneofInt32($var);
  public function getOneofInt64();
  public function setOneofInt64($var);
  public function getTestOneof();  // Return field name
}

该访问器方法的名称基于 oneof 的名称,并返回一个表示 oneof 中当前已设置字段的字符串。如果 oneof 未设置,该方法返回一个空字符串。

当您设置 oneof 中的一个字段时,它会自动清除 oneof 中的所有其他字段。如果您想在 oneof 中设置多个字段,则必须在单独的语句中进行。

$m = new TestMessage();
$m->setOneofInt32(42); // $m->hasOneofInt32() is true
$m->setOneofInt64(123); // $m->hasOneofInt32() is now false