blob: c40d83a7ae0cddd101972f54bf0f70e80a0f336b [file]
<?php
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache\Avro\DataFile;
use Apache\Avro\AvroException;
use Apache\Avro\AvroIO;
use Apache\Avro\Datum\AvroIOBinaryEncoder;
use Apache\Avro\Datum\AvroIODatumReader;
use Apache\Avro\Datum\AvroIODatumWriter;
use Apache\Avro\IO\AvroStringIO;
use Apache\Avro\Schema\AvroSchema;
/**
* Writes Avro data to an AvroIO source using an AvroSchema
*/
class AvroDataIOWriter
{
/**
* @var AvroIO object container where data is written
*/
private AvroIO $io;
/**
* @var AvroIOBinaryEncoder encoder for object container
*/
private AvroIOBinaryEncoder $encoder;
/**
* @var AvroStringIO buffer for writing
*/
private AvroStringIO $buffer;
private AvroIODatumWriter $datumWriter;
/**
* @var AvroIOBinaryEncoder encoder for buffer
*/
private AvroIOBinaryEncoder $bufferEncoder;
/**
* @var int count of items written to block
*/
private int $blockCount;
/**
* @var array<string, mixed> map of object container metadata
*/
private array $metadata;
/**
* @var string compression codec
*/
private string $codec;
/**
* @var string sync marker
*/
private string $syncMarker;
public function __construct(
AvroIO $io,
AvroIODatumWriter $datumWriter,
string|AvroSchema|null $writersSchema = null,
string $codec = AvroDataIO::NULL_CODEC
) {
$this->io = $io;
$this->datumWriter = $datumWriter;
$this->encoder = new AvroIOBinaryEncoder($this->io);
$this->buffer = new AvroStringIO();
$this->bufferEncoder = new AvroIOBinaryEncoder($this->buffer);
$this->blockCount = 0;
$this->metadata = [];
if ($writersSchema) {
if (!AvroDataIO::isValidCodec($codec)) {
throw new AvroDataIOException(
sprintf('codec %s is not supported', $codec)
);
}
$this->syncMarker = self::generateSyncMarker();
$this->metadata[AvroDataIO::METADATA_CODEC_ATTR] = $this->codec = $codec;
$this->metadata[AvroDataIO::METADATA_SCHEMA_ATTR] = (string) $writersSchema;
$this->writeHeader();
} else {
$dfr = new AvroDataIOReader($this->io, new AvroIODatumReader());
$this->syncMarker = $dfr->sync_marker;
$this->metadata[AvroDataIO::METADATA_CODEC_ATTR] = $this->codec
= $dfr->metadata[AvroDataIO::METADATA_CODEC_ATTR];
$schemaFromFile = $dfr->metadata[AvroDataIO::METADATA_SCHEMA_ATTR];
$this->metadata[AvroDataIO::METADATA_SCHEMA_ATTR] = $schemaFromFile;
$this->datumWriter->writersSchema = AvroSchema::parse($schemaFromFile);
$this->seek(0, SEEK_END);
}
}
public function append(mixed $datum): void
{
$this->datumWriter->write($datum, $this->bufferEncoder);
$this->blockCount++;
if ($this->buffer->length() >= AvroDataIO::SYNC_INTERVAL) {
$this->writeBlock();
}
}
/**
* Flushes buffer to AvroIO object container and closes it.
* @see AvroIO::close()
*/
public function close(): bool
{
$this->flush();
return $this->io->close();
}
/**
* @return string a new, unique sync marker.
*/
private static function generateSyncMarker(): string
{
// From https://php.net/manual/en/function.mt-rand.php comments
return pack(
'S8',
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
random_int(0, 0xFFFF) | 0x4000,
random_int(0, 0xFFFF) | 0x8000,
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
random_int(0, 0xFFFF)
);
}
/**
* Writes the header of the AvroIO object container
*/
private function writeHeader(): void
{
$this->write(AvroDataIO::magic());
$this->datumWriter->writeData(
AvroDataIO::metadataSchema(),
$this->metadata,
$this->encoder
);
$this->write($this->syncMarker);
}
/**
* @uses AvroIO::write()
*/
private function write(string $bytes): int
{
return $this->io->write($bytes);
}
/**
* @uses AvroIO::seek()
*/
private function seek(int $offset, int $whence): bool
{
return $this->io->seek($offset, $whence);
}
/**
* Writes a block of data to the AvroIO object container.
*/
private function writeBlock(): void
{
if ($this->blockCount > 0) {
$this->encoder->writeLong($this->blockCount);
$toWrite = (string) $this->buffer;
$toWrite = match ($this->codec) {
AvroDataIO::DEFLATE_CODEC => $this->gzCompress($toWrite),
AvroDataIO::ZSTANDARD_CODEC => $this->zstdCompress($toWrite),
AvroDataIO::SNAPPY_CODEC => $this->snappyCompress($toWrite),
AvroDataIO::BZIP2_CODEC => $this->bzCompress($toWrite),
default => $toWrite,
};
$this->encoder->writeLong(strlen($toWrite));
$this->write($toWrite);
$this->write($this->syncMarker);
$this->buffer->truncate();
$this->blockCount = 0;
}
}
/**
* Flushes biffer to AvroIO object container.
* @see AvroIO::flush()
*/
private function flush(): void
{
$this->writeBlock();
$this->io->flush();
}
/**
* @throws AvroException
*/
private function gzCompress(string $data): string
{
$data = gzdeflate($data);
if (false === $data) {
throw new AvroException('gzip compression failed.');
}
return $data;
}
/**
* @throws AvroException
*/
private function zstdCompress(string $data): string
{
if (!extension_loaded('zstd')) {
throw new AvroException('Please install ext-zstd to use zstandard compression.');
}
$data = zstd_compress($data);
if (false === $data) {
throw new AvroException('zstd compression failed.');
}
return $data;
}
/**
* @throws AvroException
*/
private function snappyCompress(string $data): string
{
if (!extension_loaded('snappy')) {
throw new AvroException('Please install ext-snappy to use snappy compression.');
}
$crc32 = crc32($data);
$compressed = snappy_compress($data);
if (false === $compressed) {
throw new AvroException('snappy compression failed.');
}
return pack('a*N', $compressed, $crc32);
}
/**
* @throws AvroException
*/
private function bzCompress(string $toWrite): string
{
if (!extension_loaded('bz2')) {
throw new AvroException('Please install ext-bz2 to use bzip2 compression.');
}
$toWrite = bzcompress($toWrite);
if (is_int($toWrite)) {
throw new AvroException("bz2 compression failed (error: {$toWrite}).");
}
return $toWrite;
}
}