Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,82 @@ $request = $decoder->decode(/** protobuf buffer here */, CreateUserRequest::clas

echo $request->name;
```

### Unknown fields

When a protobuf message is decoded, it may contain fields that are not defined in the target class.
This happens when the sender uses a newer version of the schema, but the receiver has not been updated yet.

By default, unknown fields are silently skipped during decoding. However, you can configure the `Decoder` to capture them,
which is useful for logging, debugging, or forwarding messages without data loss.

#### Storing unknown fields in memory

The `UnknownFields` handler stores unknown fields in a `WeakMap` attached to the decoded object.
Once the object is garbage collected, the unknown fields are automatically cleaned up.

```php
use Thesis\Protobuf\Decoder;
use Thesis\Protobuf\UnknownFields;

$decoder = new Decoder\Builder()
->withUnknownHandler(UnknownFields::handler())
->build();

$request = $decoder->decode($buffer, CreateUserRequest::class);

// Get unknown fields for a specific object.
$unknowns = UnknownFields::of($request);

foreach ($unknowns as $field) {
echo "Field #{$field->tag->num}, wire type: {$field->tag->type->name}\n";
}
```

Each decoded object tracks its own unknown fields independently.
If `CreateUserRequest` has a nested message with unknown fields, you can inspect them separately:

```php
$unknowns = UnknownFields::of($request->nested);
```

#### Using a callback

The `UnknownFieldsCallback` handler calls a user-defined function each time unknown fields are detected.
This is convenient for logging without keeping the data in memory:

```php
use Thesis\Protobuf\Decoder;
use Thesis\Protobuf\UnknownFields;

$decoder = new Decoder\Builder()
->withUnknownHandler(new UnknownFields\UnknownFieldsCallback(
static function (object $message, array $unknowns): void {
$logger->warning('Unknown fields detected', [
'class' => $message::class,
'fields' => array_map(
static fn(UnknownFields\UnknownField $f) => $f->tag->num,
$unknowns,
),
]);
},
))
->build();
```

#### Custom handler

You can implement the `UnknownFields\Handler` interface to define your own strategy:

```php
use Thesis\Protobuf\UnknownFields;

final readonly class MyHandler implements UnknownFields\Handler
{
#[\Override]
public function handle(object $message, array $unknowns): void
{
// your logic here
}
}
```
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"files": [
"src/constructors.php",
"src/Internal/Buffer/buffer.php",
"src/Internal/Wire/Tag.php",
"src/Tag.php",
"src/Internal/Wire/wire.php",
"src/Reflection/Internal/numeric.php"
]
Expand Down
13 changes: 12 additions & 1 deletion src/Decoder/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Thesis\Protobuf\Decoder\Internal\ReflectionDecoder;
use Thesis\Protobuf\Reflection\Reflector;
use Thesis\Protobuf\Serializer;
use Thesis\Protobuf\UnknownFields;

/**
* @api
Expand All @@ -17,6 +18,8 @@ final class Builder
{
private ?CacheInterface $cache = null;

private ?UnknownFields\Handler $unknowns = null;

public function withCache(CacheInterface $cache): self
{
$builder = clone $this;
Expand All @@ -25,6 +28,14 @@ public function withCache(CacheInterface $cache): self
return $builder;
}

public function withUnknownHandler(UnknownFields\Handler $handler): self
{
$builder = clone $this;
$builder->unknowns = $handler;

return $builder;
}

public static function buildDefault(): Decoder
{
return new self()->build();
Expand All @@ -34,7 +45,7 @@ public function build(): Decoder
{
return new ReflectionDecoder(
new Serializer(),
Reflector::build($this->cache),
Reflector::build($this->cache, $this->unknowns),
);
}
}
2 changes: 1 addition & 1 deletion src/Internal/Serde/DeserializeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Thesis\Protobuf\Internal\Buffer;
use Thesis\Protobuf\Internal\Buffer\ReadBuffer;
use Thesis\Protobuf\Internal\Wire;
use Thesis\Protobuf\Internal\Wire\Tag;
use Thesis\Protobuf\Tag;

/**
* @internal
Expand Down
6 changes: 4 additions & 2 deletions src/Internal/Serde/DeserializeMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ public function deserialize(ReadBuffer $buffer): Message
/** @var list<FieldDescriptor<*>> $descriptors */
$descriptors = [];

$unknowns = [];

while (\count($buffer) > 0) {
$tag = Wire\readTag($buffer);

$field = $this->messageT->fields[$tag->num] ?? null;
if ($field === null) {
Wire\discardUnknown($buffer, $tag);
$unknowns[] = Wire\discardUnknown($buffer, $tag);

continue;
}
Expand All @@ -54,6 +56,6 @@ public function deserialize(ReadBuffer $buffer): Message
);
}

return new Message(...$descriptors);
return new Message($descriptors, $unknowns);
}
}
2 changes: 1 addition & 1 deletion src/Internal/Serde/SerdeBool.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ public function deserialize(ReadBuffer $buffer): bool
{
$num = SerdeVarint::T->deserialize($buffer);

return (int) $num->value === 1;
return (int) $num->value !== 0;
}
}
6 changes: 5 additions & 1 deletion src/Internal/Serde/SerdeString.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ public function serialize(WriteBuffer $buffer, mixed $value): void
public function deserialize(ReadBuffer $buffer): string
{
$length = (int) SerdeVarint::T->deserialize($buffer)->value;
if ($length <= 0) {
if ($length < 0) {
throw new \UnexpectedValueException("String length must be positive or equal to 0, '{$length}' given.");
}

if ($length === 0) {
return '';
}

Expand Down
4 changes: 2 additions & 2 deletions src/Internal/Serde/SerializeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use Thesis\Protobuf\Internal\Buffer\ByteBuffer;
use Thesis\Protobuf\Internal\Buffer\WriteBuffer;
use Thesis\Protobuf\Internal\Wire;
use Thesis\Protobuf\Internal\Wire\Tag;
use Thesis\Protobuf\Internal\Wire\WireType;
use Thesis\Protobuf\Tag;
use Thesis\Protobuf\WireType;

/**
* @internal
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/Serde/SerializeMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use Thesis\Protobuf\Internal\Buffer;
use Thesis\Protobuf\Internal\Buffer\ByteBuffer;
use Thesis\Protobuf\Internal\Buffer\WriteBuffer;
use Thesis\Protobuf\Internal\Wire\Tag;
use Thesis\Protobuf\Message;
use Thesis\Protobuf\Tag;
use Thesis\Protobuf\Type\Visitor\DetermineWireType;
use Thesis\Protobuf\Type\Visitor\TypeSerializerVisitor;

Expand Down
2 changes: 1 addition & 1 deletion src/Internal/Serde/SerializeTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Thesis\Protobuf\Internal\Buffer\WriteBuffer;
use Thesis\Protobuf\Internal\Wire;
use Thesis\Protobuf\Internal\Wire\Tag;
use Thesis\Protobuf\Tag;

/**
* @internal
Expand Down
61 changes: 45 additions & 16 deletions src/Internal/Wire/wire.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
use BcMath\Number;
use Thesis\Protobuf\Exception\BufferUnderflow;
use Thesis\Protobuf\Internal\Buffer\ReadBuffer;
use Thesis\Protobuf\Internal\Buffer\WriteBuffer;
use Thesis\Protobuf\Internal\Serde\SerdeVarint;
use Thesis\Protobuf\Tag;
use Thesis\Protobuf\UnknownFields\UnknownField;
use Thesis\Protobuf\WireType;
use Thesis\Varint;

/**
* Removes bytes from the buffer corresponding to the size of each type:
* Return bytes from the buffer corresponding to the size of each type:
* fixed32 - 4,
* fixed64 - 8,
* varint - 1 ≤ size ≤ 10,
Expand All @@ -19,25 +24,21 @@
* @internal
* @throws BufferUnderflow
*/
function discardUnknown(ReadBuffer $buffer, Tag $tag): void
function discardUnknown(ReadBuffer $buffer, Tag $tag): UnknownField
{
switch ($tag->type) {
case WireType::FIXED32:
$buffer->read(4);
break;
case WireType::FIXED64:
$buffer->read(8);
break;
case WireType::VARINT:
readVarint($buffer);
break;
case WireType::BYTES:
return new UnknownField($tag, match ($tag->type) {
WireType::FIXED32 => $buffer->read(4),
WireType::FIXED64 => $buffer->read(8),
WireType::VARINT => readVarint($buffer),
WireType::BYTES => (static function () use ($buffer): string {
$length = (int) readVarint($buffer)->value;
if ($length > 0) {
$buffer->read($length);
return $buffer->read($length);
}
break;
}

return '';
})(),
});
}

/**
Expand All @@ -51,3 +52,31 @@ function readVarint(ReadBuffer $buffer): Number

return $number->value;
}

/**
* @internal
*/
function writeTag(WriteBuffer $buffer, Tag $tag): void
{
SerdeVarint::T->serialize($buffer, $tag->number);
}

/**
* @internal
* @throws BufferUnderflow
*/
function peekTag(ReadBuffer $buffer): Tag
{
$number = Varint\BcMath::Codec->decodeVarintSized($buffer->peek(10));

return Tag::from($number->value);
}

/**
* @internal
* @throws BufferUnderflow
*/
function readTag(ReadBuffer $buffer): Tag
{
return Tag::from(readVarint($buffer));
}
6 changes: 4 additions & 2 deletions src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@

/**
* @no-named-arguments
* @param FieldDescriptor<*> ...$fields
* @param list<FieldDescriptor<*>> $fields
* @param list<UnknownFields\UnknownField> $unknowns
*/
public function __construct(
FieldDescriptor ...$fields,
array $fields = [],
public array $unknowns = [],
) {
$map = [];

Expand Down
8 changes: 8 additions & 0 deletions src/Reflection/Reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Thesis\Protobuf\Reflection\Internal\Visitor\ToProtobufValueTypeVisitor;
use Thesis\Protobuf\Reflection\Internal\Visitor\ToValueTypeVisitor;
use Thesis\Protobuf\Type;
use Thesis\Protobuf\UnknownFields;

/**
* @api
Expand All @@ -38,16 +39,19 @@ final class Reflector

public static function build(
?CacheInterface $cache = null,
?UnknownFields\Handler $unknowns = null,
): self {
return new self(
cache: new Cache(
$cache ?? new InMemoryPsr16Cache(),
),
unknowns: $unknowns,
);
}

private function __construct(
private readonly Cache $cache,
private readonly ?UnknownFields\Handler $unknowns = null,
) {
$this->typeVisitor = new ToProtobufTypeTypeVisitor($this);
$this->valueVisitor = new ToProtobufValueTypeVisitor($this);
Expand Down Expand Up @@ -164,6 +168,10 @@ public function map(Message $message, string $class): object
}
}

if ($message->unknowns !== []) {
$this->unknowns?->handle($object, $message->unknowns);
}

return $object;
}

Expand Down
7 changes: 4 additions & 3 deletions src/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Thesis\Protobuf\Exception\BufferUnderflow;
use Thesis\Protobuf\Internal\Buffer\ByteBuffer;
use Thesis\Protobuf\Internal\Wire;
use Thesis\Protobuf\Internal\Wire\Tag;
use Thesis\Protobuf\Type\MessageT;
use Thesis\Protobuf\Type\Visitor\DetermineWireType;
use Thesis\Protobuf\Type\Visitor\TypeDeserializerVisitor;
Expand Down Expand Up @@ -47,12 +46,14 @@ public function deserialize(MessageT $type, string $bytes): Message
/** @var list<FieldDescriptor<*>> $descriptors */
$descriptors = [];

$unknowns = [];

while (\count($buffer) > 0) {
$tag = Wire\readTag($buffer);

$field = $type->fields[$tag->num] ?? null;
if ($field === null) {
Wire\discardUnknown($buffer, $tag);
$unknowns[] = Wire\discardUnknown($buffer, $tag);

continue;
}
Expand All @@ -70,6 +71,6 @@ public function deserialize(MessageT $type, string $bytes): Message
);
}

return new Message(...$descriptors);
return new Message($descriptors, $unknowns);
}
}
Loading