Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c7f9b19
Implement all 10 standard AVRO logical types with factories and docum…
thiagorb Sep 26, 2025
418db4f
Simplify logical types input, remove string parsing, focus on DateTim…
thiagorb Sep 26, 2025
117c138
Remove integer support for date, time, and timestamp logical types
thiagorb Sep 26, 2025
f1bfb0f
Refactor logical type normalization with assert and simplified error …
thiagorb Sep 26, 2025
2f3a3c9
Rename logical types
thiagorb Sep 26, 2025
fa481d6
Refactor time logical types to use value object
thiagorb Sep 26, 2025
b7ef878
Implement logical types with value objects
thiagorb Sep 26, 2025
c37b046
Fix phpstan errors
thiagorb Sep 24, 2025
74a2b45
Use DateTime for TimestampMillisType and TimestampMicrosType
thiagorb Sep 24, 2025
9e9789c
Fix logic for local timestamps
thiagorb Sep 24, 2025
c464150
Implement UUID value object
thiagorb Sep 24, 2025
b31cec1
Validate logical type attributes
thiagorb Sep 24, 2025
c0c03de
Fix serialization of Decimal with fixed schema
thiagorb Sep 24, 2025
c823ee9
Refactor exceptions
thiagorb Sep 25, 2025
c49bfb3
Remove useless tests
thiagorb Sep 25, 2025
d54fa35
Implement fromCents method
thiagorb Sep 25, 2025
713d302
Remove method fromNumeric
thiagorb Sep 25, 2025
0cdc87a
Initialize with all logical types by default
thiagorb Sep 25, 2025
8d20830
Update README.md
thiagorb Sep 25, 2025
d240707
Fix example
thiagorb Jan 12, 2026
7656138
Set node version explicitly
thiagorb Jan 12, 2026
0f31b57
Serialize zero decimal as a 1-byte string
thiagorb Jan 12, 2026
7f28aa5
Enable using int or ArbitraryPrecisionInteger
thiagorb Jan 14, 2026
f48af6f
Fix coding style
thiagorb Jan 14, 2026
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
3 changes: 1 addition & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ RUN groupadd -g $GROUP_ID application && \
useradd -m -s /bin/bash -u $USER_ID -g $GROUP_ID application

USER application
WORKDIR /avro-php
CMD ["sleep", "infinity"]
WORKDIR /avro-php
5 changes: 4 additions & 1 deletion .devcontainer/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ services:
environment:
PHP_IDE_CONFIG: serverName=avro-php
working_dir: /avro-php
command: sleep infinity

test-generator:
image: node:latest
image: node:24
volumes:
- ..:/avro-php
environment:
npm_config_cache: /tmp/.npm
working_dir: /avro-php
command: bash -c "cd tests/Integration/TestCaseGenerator && npm ci && node ."
profiles:
- build-tests
98 changes: 63 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

A PHP library that provides schema-based Avro data serialization and deserialization.

It started as a fork of the original Apache Avro implementation available at https://github.com/apache/avro, but now it has been completely rewritten. Some of the original functionality has been removed, and new features have been added.

The features added by this library are:
- Basic support for logical types
- Default values for record fields
Key features:
- Support for logical types
- Developer-friendly error messages for schema validation
- Serialization of objects through getters or public properties
- Schema resolution including promotion of primitive types
- Serialization of objects through getters or public properties
- Default values for record fields
- Configurable block count and block size for array and map encoding

## Installation
Expand All @@ -20,6 +18,65 @@ To install auxmoney/avro-php, you can use Composer:
composer require auxmoney/avro-php
```

## Usage

### Encoding/Decoding Data

Try out the example scripts:
```bash
php examples/encoding.php
php examples/decoding.php
php examples/logical-type.php
```

### Logical Types

It is possible to configure logical types in a few different ways:

#### Using Default Logical Types
There are built-in implementations for all logical types described in the AVRO specification, except for `timestamp-nanos` and `local-timestamp-nanos`, because PHP's DateTime doesn't have nanosecond precision.

To use the default logical types, simply create an AvroFactory without any options:
```php
$avroFactory = AvroFactory::create();
```

#### Overriding Logical Types
You can override default logical types or add custom ones by providing factory implementations:
```php
$defaultLogicalTypeFactories = AvroFactory::getDefaultLogicalTypeFactories();
$defaultLogicalTypeFactories['custom'] = new MyCustomLogicalTypeFactory();
$options = new Options(logicalTypeFactories: $defaultLogicalTypeFactories);
$avroFactory = AvroFactory::create($options);
```

#### Disabling Logical Types
To disable all logical type processing and treat them as their underlying primitive types:
```php
$options = new Options(logicalTypeFactories: []);
$avroFactory = AvroFactory::create($options);
```

### Value Objects for Logical Types

Some logical types work with their respective value objects to provide type safety and better representation of the data:

- `decimal`: `Auxmoney\Avro\ValueObject\Decimal`
- `duration`: `Auxmoney\Avro\ValueObject\Duration`
- `time-millis`: `Auxmoney\Avro\ValueObject\TimeOfDay`
- `time-micros`: `Auxmoney\Avro\ValueObject\TimeOfDay`
- `uuid`: `Auxmoney\Avro\ValueObject\Uuid`

The `local-timestamp-*` and `timestamp-*` types are serialized from/deserialized to `DateTimeInterface`.

## Documentation

For more detailed documentation on usage, schema design, and advanced features like schema evolution, please refer to the official Avro documentation.

## Contribution

Contributions are welcome! If you find a bug or want to suggest a new feature, feel free to open an issue or submit a pull request.

## Development Setup

This project uses VS Code Dev Containers for development. This ensures a consistent development environment across all contributors.
Expand Down Expand Up @@ -55,35 +112,6 @@ docker compose -f .devcontainer/docker-compose.yaml run --rm test-generator

For more details, see [.devcontainer/README.md](.devcontainer/README.md).

## Usage

### Encoding/Decoding Data

Try out the example scripts `examples/encoding.php` and `examples/decoding.php`:
```bash
php examples/encoding.php
php examples/decoding.php
```

### Logical Types

Although this library does not provide an implementation for any logical type, it is possible to use them by providing the factory implementation to `Auxmoney\Avro\AvroFactory::create`.

The logical type factory must implement the interface `Auxmoney\Avro\Contracts\LogicalTypeFactoryInterface`.

Try out the example script `examples/logical-type.php`:
```bash
php examples/logical-type.php
```

## Documentation

For more detailed documentation on usage, schema design, and advanced features like schema evolution, please refer to the official Avro documentation.

## Contribution

Contributions are welcome! If you find a bug or want to suggest a new feature, feel free to open an issue or submit a pull request.

## License

This library is licensed under the Apache License 2.0.
Expand Down
69 changes: 18 additions & 51 deletions examples/logical-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,24 @@
require_once __DIR__ . '/autoload.php';

use Auxmoney\Avro\AvroFactory;
use Auxmoney\Avro\Contracts\LogicalTypeFactoryInterface;
use Auxmoney\Avro\Contracts\LogicalTypeInterface;
use Auxmoney\Avro\Contracts\Options;
use Auxmoney\Avro\Contracts\ValidationContextInterface;

class Base64ExampleType implements LogicalTypeInterface
{
public function validate(mixed $datum, ?ValidationContextInterface $context): bool
{
if (!is_string($datum)) {
$context?->addError('expected string, got ' . gettype($datum));
return false;
}

return true;
}

public function denormalize(mixed $datum): mixed
{
return base64_decode($datum);
}

public function normalize(mixed $datum): mixed
{
return base64_encode($datum);
}
}

class Base64ExampleTypeFactory implements LogicalTypeFactoryInterface
{
public function getName(): string
{
return 'base64-example';
}

public function create(array $attributes): Auxmoney\Avro\Contracts\LogicalTypeInterface
{
return new Base64ExampleType();
}
}

$options = new Options(logicalTypeFactories: [new Base64ExampleTypeFactory()]);
$avroFactory = AvroFactory::create($options);

$schema = '{"type": "string", "logicalType": "base64-example"}';
use Auxmoney\Avro\ValueObject\Decimal;

$writer = $avroFactory->createWriter($schema);
$writeBuffer = $avroFactory->createStringBuffer();
$writer->write('Hello, World!', $writeBuffer);
var_dump($writeBuffer->__toString());
$avroFactory = AvroFactory::create();

$schema = '{"type": "long", "logicalType": "timestamp-millis"}';
$encodedData = "\x00";

$readBuffer = $avroFactory->createReadableStreamFromString("(SGVsbG8sIFdvcmxkIQ==");
$reader = $avroFactory->createReader($schema);
var_dump($reader->read($readBuffer));
$buffer = $avroFactory->createReadableStreamFromString($encodedData);
$decodedData = $reader->read($buffer);

var_dump($decodedData);


$schema = '{"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 4}';
$decodedData = Decimal::fromString('3.14159');

$writer = $avroFactory->createWriter($schema);
$buffer = $avroFactory->createStringBuffer();
$writer->write($decodedData, $buffer);
var_dump(bin2hex($buffer->__toString()));
3 changes: 2 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ parameters:
level: max
paths:
- src
- tests
- tests
treatPhpDocTypesAsCertain: false
33 changes: 32 additions & 1 deletion src/AvroFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Auxmoney\Avro;

use Auxmoney\Avro\Contracts\AvroFactoryInterface;
use Auxmoney\Avro\Contracts\LogicalTypeFactoryInterface;
use Auxmoney\Avro\Contracts\Options;
use Auxmoney\Avro\Contracts\ReadableStreamInterface;
use Auxmoney\Avro\Contracts\ReaderInterface;
Expand All @@ -13,6 +14,16 @@
use Auxmoney\Avro\Deserialization\BinaryDecoder;
use Auxmoney\Avro\IO\ReadableStringBuffer;
use Auxmoney\Avro\IO\WritableStringBuffer;
use Auxmoney\Avro\LogicalType\Factory\DateFactory;
use Auxmoney\Avro\LogicalType\Factory\DecimalFactory;
use Auxmoney\Avro\LogicalType\Factory\DurationFactory;
use Auxmoney\Avro\LogicalType\Factory\LocalTimestampMicrosFactory;
use Auxmoney\Avro\LogicalType\Factory\LocalTimestampMillisFactory;
use Auxmoney\Avro\LogicalType\Factory\TimeMicrosFactory;
use Auxmoney\Avro\LogicalType\Factory\TimeMillisFactory;
use Auxmoney\Avro\LogicalType\Factory\TimestampMicrosFactory;
use Auxmoney\Avro\LogicalType\Factory\TimestampMillisFactory;
use Auxmoney\Avro\LogicalType\Factory\UuidFactory;
use Auxmoney\Avro\Serialization\BinaryEncoder;
use Auxmoney\Avro\Support\DefaultValueConverter;
use Auxmoney\Avro\Support\LogicalTypeResolver;
Expand Down Expand Up @@ -50,12 +61,32 @@ public function createReadableStreamFromString(string $string): ReadableStreamIn

public static function create(Options $options = new Options()): AvroFactoryInterface
{
$logicalTypeResolver = new LogicalTypeResolver($options->logicalTypeFactories);
$logicalTypeFactories = $options->logicalTypeFactories ?? self::getDefaultLogicalTypeFactories();
$logicalTypeResolver = new LogicalTypeResolver($logicalTypeFactories);
$schemaHelper = new SchemaHelper($logicalTypeResolver);
$defaultValueConverter = new DefaultValueConverter($schemaHelper);
$writerFactory = new WriterFactory(new BinaryEncoder(), $schemaHelper, $options);
$readerFactory = new ReaderFactory(new BinaryDecoder(), $schemaHelper, $defaultValueConverter);

return new self($writerFactory, $readerFactory);
}

/**
* @return array<string, LogicalTypeFactoryInterface>
*/
public static function getDefaultLogicalTypeFactories(): array
{
return [
'date' => new DateFactory(),
'decimal' => new DecimalFactory(),
'duration' => new DurationFactory(),
'local-timestamp-micros' => new LocalTimestampMicrosFactory(),
'local-timestamp-millis' => new LocalTimestampMillisFactory(),
'time-micros' => new TimeMicrosFactory(),
'time-millis' => new TimeMillisFactory(),
'timestamp-micros' => new TimestampMicrosFactory(),
'timestamp-millis' => new TimestampMillisFactory(),
'uuid' => new UuidFactory(),
];
}
}
3 changes: 3 additions & 0 deletions src/Contracts/LogicalTypeFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace Auxmoney\Avro\Contracts;

use Auxmoney\Avro\Exceptions\InvalidSchemaException;

interface LogicalTypeFactoryInterface
{
public function getName(): string;

/**
* @param array<mixed> $attributes
* @throws InvalidSchemaException
*/
public function create(array $attributes): LogicalTypeInterface;
}
4 changes: 2 additions & 2 deletions src/Contracts/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
readonly class Options
{
/**
* @param iterable<LogicalTypeFactoryInterface> $logicalTypeFactories
* @param null|iterable<LogicalTypeFactoryInterface> $logicalTypeFactories
*/
public function __construct(
public iterable $logicalTypeFactories = [],
public ?iterable $logicalTypeFactories = null,
public bool $arrayWriteBlockSize = false,
public int $arrayBlockCount = 0,
public bool $mapWriteBlockSize = false,
Expand Down
4 changes: 2 additions & 2 deletions src/Deserialization/EnumReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Auxmoney\Avro\Contracts\ReadableStreamInterface;
use Auxmoney\Avro\Contracts\ReaderInterface;
use RuntimeException;
use Auxmoney\Avro\Exceptions\SchemaMismatchException;

class EnumReader implements ReaderInterface
{
Expand All @@ -23,7 +23,7 @@ public function read(ReadableStreamInterface $stream): string
{
$index = $this->decoder->readLong($stream);
if (!isset($this->values[$index])) {
throw new RuntimeException('Invalid enum index: ' . $index);
throw new SchemaMismatchException('Invalid enum index: ' . $index);
}

return $this->values[$index];
Expand Down
6 changes: 3 additions & 3 deletions src/Deserialization/UnionReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Auxmoney\Avro\Contracts\ReadableStreamInterface;
use Auxmoney\Avro\Contracts\ReaderInterface;
use RuntimeException;
use Auxmoney\Avro\Exceptions\SchemaMismatchException;

class UnionReader implements ReaderInterface
{
Expand All @@ -23,7 +23,7 @@ public function read(ReadableStreamInterface $stream): mixed
{
$branchIndex = $this->decoder->readLong($stream);
if (!isset($this->branchReaders[$branchIndex])) {
throw new RuntimeException('Invalid branch index: ' . $branchIndex);
throw new SchemaMismatchException('Invalid branch index: ' . $branchIndex);
}

return $this->branchReaders[$branchIndex]->read($stream);
Expand All @@ -33,7 +33,7 @@ public function skip(ReadableStreamInterface $stream): void
{
$branchIndex = $this->decoder->readLong($stream);
if (!isset($this->branchReaders[$branchIndex])) {
throw new RuntimeException('Invalid branch index: ' . $branchIndex);
throw new SchemaMismatchException('Invalid branch index: ' . $branchIndex);
}

$this->branchReaders[$branchIndex]->skip($stream);
Expand Down
17 changes: 17 additions & 0 deletions src/Exceptions/AuxmoneyAvroException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Auxmoney\Avro\Exceptions;

use Exception;

/**
* Base exception for all Auxmoney Avro library exceptions.
*
* This allows library users to catch all exceptions thrown by this library
* using a single catch block: catch (AuxmoneyAvroException $e)
*/
abstract class AuxmoneyAvroException extends Exception
{
}
Loading