diff --git a/README.md b/README.md index d26b4a9..4257dc0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Library for work with binaries Library for basic work with binary data in PHP. -See the sample below for more information, or check out [`CoderInterface`](./src/CoderInterface.php). +See the examples below for more information, or check out [`CoderInterface`](./src/CoderInterface.php) and [`SerializerInterface`](./src/SerializerInterface.php). ```php use PetrKnap\Binary\Binary; @@ -13,6 +13,19 @@ $decoded = Binary::decode($encoded)->base64()->zlib()->checksum()->getData(); printf('Data was coded into `%s` %s.', $encoded, $decoded === $data ? 'successfully' : 'unsuccessfully'); ``` +```php +use PetrKnap\Binary\Binary; + +$data = [ + 'type' => 'image/png', + 'data' => base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+L+U4T8ABu8CpCYJ1DQAAAAASUVORK5CYII='), +]; +$serialized = Binary::serialize($data); +$unserialized = Binary::unserialize($serialized); + +printf('Data was serialized into `%s` %s.', base64_encode($serialized), $unserialized === $data ? 'successfully' : 'unsuccessfully'); +``` + --- Run `composer require petrknap/binary` to install it. diff --git a/src/Binary.php b/src/Binary.php index 7b52b6e..6b16bfa 100644 --- a/src/Binary.php +++ b/src/Binary.php @@ -15,4 +15,23 @@ public static function decode(string $data): Decoder { return new Decoder($data); } + + public static function serialize(mixed $data): string + { + return self::getSerializer()->serialize(serializable: $data); + } + + public static function unserialize(string $data): mixed + { + return self::getSerializer()->unserialize(serialized: $data); + } + + private static function getSerializer(): Serializer + { + static $serializer; + return $serializer ??= new Serializer( + new Encoder(), + new Decoder(), + ); + } } diff --git a/src/Coder.php b/src/Coder.php index a304a44..362e3c0 100644 --- a/src/Coder.php +++ b/src/Coder.php @@ -17,10 +17,15 @@ abstract class Coder implements CoderInterface ]; public function __construct( - protected readonly string $data, + protected readonly string $data = '', ) { } + public function withData(string $data): static + { + return static::create($this, $data); + } + public function getData(): string { return $this->data; diff --git a/src/CoderInterface.php b/src/CoderInterface.php index fd324cc..80301e5 100644 --- a/src/CoderInterface.php +++ b/src/CoderInterface.php @@ -11,6 +11,8 @@ interface CoderInterface { public const CHECKSUM_ALGORITHM = 'crc32'; + public function withData(string $data): static; + public function getData(): string; /** diff --git a/src/Exception/CouldNotSerializeData.php b/src/Exception/CouldNotSerializeData.php new file mode 100644 index 0000000..5793f2c --- /dev/null +++ b/src/Exception/CouldNotSerializeData.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace PetrKnap\Binary\Exception; + +use PetrKnap\Binary\SerializerInterface; +use RuntimeException; +use Throwable; + +final class CouldNotSerializeData extends RuntimeException implements SerializerException +{ + public function __construct( + private readonly SerializerInterface $serializer, + private readonly mixed $data, + ?Throwable $reason = null, + ) { + parent::__construct( + sprintf( + '%s could not serialize %s', + $serializer::class, + is_object($data) ? $data::class : gettype($data), + ), + previous: $reason, + ); + } + + public function getSerializer(): SerializerInterface + { + return $this->serializer; + } + + public function getData(): mixed + { + return $this->data; + } +} diff --git a/src/Exception/CouldNotUnserializeData.php b/src/Exception/CouldNotUnserializeData.php new file mode 100644 index 0000000..0449b02 --- /dev/null +++ b/src/Exception/CouldNotUnserializeData.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace PetrKnap\Binary\Exception; + +use PetrKnap\Binary\SerializerInterface; +use RuntimeException; +use Throwable; + +final class CouldNotUnserializeData extends RuntimeException implements SerializerException +{ + public function __construct( + private readonly SerializerInterface $serializer, + private readonly string $data, + ?Throwable $reason = null, + ) { + parent::__construct( + sprintf( + '%s could not unserialize string(%d)', + $serializer::class, + strlen($data) + ), + previous: $reason, + ); + } + + public function getSerializer(): SerializerInterface + { + return $this->serializer; + } + + public function getData(): string + { + return $this->data; + } +} diff --git a/src/Exception/SerializerException.php b/src/Exception/SerializerException.php new file mode 100644 index 0000000..40acedf --- /dev/null +++ b/src/Exception/SerializerException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace PetrKnap\Binary\Exception; + +use PetrKnap\Binary\SerializerInterface; + +interface SerializerException extends BinaryException +{ + public function getSerializer(): SerializerInterface; +} diff --git a/src/Serializer.php b/src/Serializer.php new file mode 100644 index 0000000..f435a69 --- /dev/null +++ b/src/Serializer.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace PetrKnap\Binary; + +use Throwable; + +class Serializer implements SerializerInterface +{ + public function __construct( + protected readonly EncoderInterface $encoder, + protected readonly DecoderInterface $decoder, + ) { + } + + public function serialize(mixed $serializable): string + { + try { + $serialized = $this->doSerialize($serializable); + return $this->encoder->withData($serialized)->zlib()->getData(); + } catch (Throwable $reason) { + throw new Exception\CouldNotSerializeData($this, $serializable, $reason); + } + } + + public function unserialize(string $serialized): mixed + { + try { + $serialized = $this->decoder->withData($serialized)->zlib()->getData(); + return $this->doUnserialize($serialized); + } catch (Throwable $reason) { + throw new Exception\CouldNotUnserializeData($this, $serialized, $reason); + } + } + + /** + * Alternative to {@see serialize()} + * + * @throws Throwable + */ + protected function doSerialize(mixed $serializable): string + { + return serialize($serializable); + } + + /** + * Alternative to {@see unserialize()} + * + * @throws Throwable + */ + protected function doUnserialize(string $serialized): mixed + { + return unserialize($serialized); + } +} diff --git a/src/SerializerInterface.php b/src/SerializerInterface.php new file mode 100644 index 0000000..7f9a6b7 --- /dev/null +++ b/src/SerializerInterface.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace PetrKnap\Binary; + +interface SerializerInterface +{ + /** + * {@see serialize()} the serializable + * + * @throws Exception\CouldNotSerializeData + */ + public function serialize(mixed $serializable): string; + + /** + * {@see unserialize()} the serialized + * + * @throws Exception\CouldNotUnserializeData + */ + public function unserialize(string $serialized): mixed; +} diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php index b3b9c86..cbeabac 100644 --- a/tests/ReadmeTest.php +++ b/tests/ReadmeTest.php @@ -18,7 +18,8 @@ public static function getPathToMarkdownFile(): string public static function getExpectedOutputsOfPhpExamples(): iterable { return [ - 'coders' => 'Data was coded into `a8vMFCssyD2Rs5BB0Evt6tJv10J_b2Aoui0tcXT69aaPP9oIyB-fLeAHAA` successfully.', + 'coder' => 'Data was coded into `a8vMFCssyD2Rs5BB0Evt6tJv10J_b2Aoui0tcXT69aaPP9oIyB-fLeAHAA` successfully.', + 'serializer' => 'Data was serialized into `S7QysqoutjKxUiqpLEhVsi62srRSysxNTE/VL8hLB/GBUimJJYkgpoWxlVJngJ87L5cUFwMDA6+nh0sQkGYEYQ42ICkveqQTxCkOcndiWHdO5iVYlYtjiER48o/9Ux7aM7C9Z1qixnnFBCjB4Onq57LOKaFJyboWAA==` successfully.', ]; } } diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php new file mode 100644 index 0000000..2e9420a --- /dev/null +++ b/tests/SerializerTest.php @@ -0,0 +1,124 @@ +<?php declare(strict_types=1); + +namespace PetrKnap\Binary; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use stdClass; + +final class SerializerTest extends TestCase +{ + private EncoderInterface&MockObject $internalEncoder; + private DecoderInterface&MockObject $internalDecoder; + private SerializerInterface&MockObject $internalSerializer; + private Serializer $serializer; + + public function setUp(): void + { + parent::setUp(); + + $this->internalEncoder = self::createMock(EncoderInterface::class); + $this->internalDecoder = self::createMock(DecoderInterface::class); + $this->internalSerializer = self::createMock(SerializerInterface::class); + + $this->serializer = new class ( + $this->internalEncoder, + $this->internalDecoder, + $this->internalSerializer, + ) extends Serializer { + public function __construct( + EncoderInterface $encoder, + DecoderInterface $decoder, + private readonly SerializerInterface $serializer, + ) { + parent::__construct($encoder, $decoder); + } + protected function doSerialize(mixed $serializable): string + { + return $this->serializer->serialize($serializable); + } + protected function doUnserialize(string $serialized): mixed + { + return $this->serializer->unserialize($serialized); + } + }; + } + + public function testSerializesData(): void + { + $data = new stdClass(); + $data->array = []; + $data->binary = 0b0; + $data->float = .0; + $data->int = 0; + $data->null = null; + $data->string = ''; + $serializer = new Serializer( + new Encoder(), + new Decoder(), + ); + + self::assertEquals( + $data, + $serializer->unserialize( + $serializer->serialize( + $data, + ), + ), + ); + } + + public function testCallsDoSerializeAndUsesEncoder(): void + { + $serializable = (string) 0b01; + $serialized = (string) 0b10; + $encoded = (string) 0b11; + + $this->internalSerializer->expects(self::once()) + ->method('serialize') + ->with($serializable) + ->willReturn($serialized); + $this->internalEncoder->expects(self::once()) + ->method('withData') + ->with($serialized) + ->willReturn($this->internalEncoder); + $this->internalEncoder->expects(self::once()) + ->method('zlib') + ->willReturn($this->internalEncoder); + $this->internalEncoder->expects(self::once()) + ->method('getData') + ->willReturn($encoded); + + self::assertSame( + $encoded, + $this->serializer->serialize($serializable), + ); + } + + public function testUsesDecoderAndCallsDoUnserialize(): void + { + $serialized = (string) 0b01; + $decoded = (string) 0b10; + $serializable = (string) 0b11; + + $this->internalDecoder->expects(self::once()) + ->method('withData') + ->with($serialized) + ->willReturn($this->internalDecoder); + $this->internalDecoder->expects(self::once()) + ->method('zlib') + ->willReturn($this->internalDecoder); + $this->internalDecoder->expects(self::once()) + ->method('getData') + ->willReturn($decoded); + $this->internalSerializer->expects(self::once()) + ->method('unserialize') + ->with($decoded) + ->willReturn($serializable); + + self::assertSame( + $serializable, + $this->serializer->unserialize($serialized), + ); + } +}