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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ protected array $otherExamples;
// ^ items in the array will not be converted
```

#### Serializer and Deserializer
A custom Serializer and Deserializer can be specified for a property.
This can be useful if you want to serialize a specific property in a different way.

```php
#[Serialize(serializer: new Base64Serializer(), deserializer: new Base64Deserializer(TestClass::class))]
protected TestClass $example;
```

Note that the custom Deserializer is responsible for returning the correct type.
If an incompatible type is returned, an IncorrectTypeException is thrown.

### Exceptions
The following exceptions may be thrown during serialization or deserialization:
- [MissingPropertyException](#missingpropertyexception)
Expand Down
17 changes: 15 additions & 2 deletions src/ArrayDeserializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionUnionType;
use TypeError;

/**
* Deserializes arrays into objects using the Serialize attribute.
Expand All @@ -26,7 +27,7 @@
* @see Serialize
* @template T
*/
class ArrayDeserializer
class ArrayDeserializer implements DeserializerInterface
{

/**
Expand All @@ -49,10 +50,13 @@ public function __construct(
* @throws UnsupportedTypeException if the type of the property is unsupported
*/
public function deserialize(
array $data,
mixed $data,
string $path = "",
): object
{
if (!is_array($data)) {
throw new InvalidArgumentException("Data must be an array.");
}
try {
$reflectionClass = new ReflectionClass($this->class);
$result = new $this->class;
Expand Down Expand Up @@ -112,6 +116,15 @@ protected function deserializeProperty(
}

$value = $data[$name];
if ($customDeserializer = $attribute->getDeserializer()) {
$value = $customDeserializer->deserialize($value, $path);
try {
$property->setValue($result, $value);
} catch (TypeError) {
throw new IncorrectTypeException($path . "." . $name, $type, $value);
}
return;
}

$nullable = $attribute->allowsNull() ?? $type?->allowsNull() ?? true;
if ($value === null) {
Expand Down
8 changes: 5 additions & 3 deletions src/ArraySerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* @see Serialize
* @see JsonSerializer
*/
class ArraySerializer
class ArraySerializer implements SerializerInterface
{
/**
* Create a serializer from an object
Expand Down Expand Up @@ -67,7 +67,9 @@ public function serialize(object $item): array
$name = $attribute->getName() ?? $property->getName();
$value = $property->getValue($item);

if ($value instanceof JsonSerializable) {
if ($customSerializer = $attribute->getSerializer()) {
$value = $customSerializer->serialize($value);
} else if ($value instanceof JsonSerializable) {
$value = $value->jsonSerialize();
} elseif (is_object($value)) {
$value = $this->serialize($value);
Expand All @@ -78,4 +80,4 @@ public function serialize(object $item): array

return $serializedProperties;
}
}
}
30 changes: 30 additions & 0 deletions src/DeserializerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Aternos\Serializer;

use Aternos\Serializer\Exceptions\IncorrectTypeException;
use Aternos\Serializer\Exceptions\MissingPropertyException;
use Aternos\Serializer\Exceptions\UnsupportedTypeException;

/**
* @template T
*/
interface DeserializerInterface
{
/**
* Create a deserializer for a class
*
* @param class-string<T> $class the class to deserialize into
*/
public function __construct(string $class);

/**
* Deserialize the data into an object
*
* @return T
* @throws IncorrectTypeException if the type of the property is incorrect
* @throws MissingPropertyException if a required property is missing
* @throws UnsupportedTypeException if the type of the property is unsupported
*/
public function deserialize(mixed $data, string $path = ""): object;
}
12 changes: 9 additions & 3 deletions src/Json/JsonDeserializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
namespace Aternos\Serializer\Json;

use Aternos\Serializer\ArrayDeserializer;
use Aternos\Serializer\DeserializerInterface;
use Aternos\Serializer\Exceptions\IncorrectTypeException;
use Aternos\Serializer\Exceptions\MissingPropertyException;
use Aternos\Serializer\Exceptions\UnsupportedTypeException;
use InvalidArgumentException;
use JsonException;

/**
Expand All @@ -20,7 +22,7 @@
* @see Serialize
* @template T
*/
class JsonDeserializer
class JsonDeserializer implements DeserializerInterface
{
protected ArrayDeserializer $arrayDeserializer;

Expand All @@ -45,7 +47,7 @@ public function __construct(
* @throws UnsupportedTypeException if the type of the property is unsupported
* @throws JsonException if the data is invalid json
*/
public function deserialize(array|string $data, string $path = ""): object
public function deserialize(mixed $data, string $path = ""): object
{
if (is_string($data)) {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
Expand All @@ -54,6 +56,10 @@ public function deserialize(array|string $data, string $path = ""): object
}
}

if (!is_array($data)) {
throw new InvalidArgumentException("Data must be a string or an array.");
}

return $this->arrayDeserializer->deserialize($data, $path);
}
}
}
5 changes: 3 additions & 2 deletions src/Json/JsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Aternos\Serializer\ArraySerializer;
use Aternos\Serializer\Exceptions\IncorrectTypeException;
use Aternos\Serializer\Exceptions\MissingPropertyException;
use Aternos\Serializer\SerializerInterface;

/**
* A class that serializes objects using the Serialize attribute.
Expand All @@ -18,7 +19,7 @@
* @see ArraySerializer
* @see PropertyJsonSerializer
*/
class JsonSerializer
class JsonSerializer implements SerializerInterface
{
protected ArraySerializer $arraySerializer;

Expand All @@ -38,4 +39,4 @@ public function serialize(object $item): string
{
return json_encode($this->arraySerializer->serialize($item));
}
}
}
22 changes: 21 additions & 1 deletion src/Serialize.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ public static function getAttribute(ReflectionProperty $property): ?self
* @param bool|null $required Whether the field is required
* @param bool|null $allowNull Whether the field can be null
* @param class-string|null $itemType The type of the items in the array
* @param SerializerInterface|null $serializer A custom serializer for this field
* @param DeserializerInterface|null $deserializer A custom deserializer for this field
*/
public function __construct(
protected ?string $name = null,
protected ?bool $required = null,
protected ?bool $allowNull = null,
protected ?string $itemType = null,
protected ?SerializerInterface $serializer = null,
protected ?DeserializerInterface $deserializer = null,
)
{
}
Expand All @@ -58,4 +62,20 @@ public function getItemType(): ?string
{
return $this->itemType;
}
}

/**
* @return SerializerInterface|null
*/
public function getSerializer(): ?SerializerInterface
{
return $this->serializer;
}

/**
* @return DeserializerInterface|null
*/
public function getDeserializer(): ?DeserializerInterface
{
return $this->deserializer;
}
}
14 changes: 14 additions & 0 deletions src/SerializerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Aternos\Serializer;

interface SerializerInterface
{
/**
* Serialize an object into a scalar value or an array.
*
* @param object $item
* @return int|float|string|array|null
*/
public function serialize(object $item): int|float|string|array|null;
}
24 changes: 24 additions & 0 deletions tests/src/Base64Deserializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Aternos\Serializer\Test\Src;

use Aternos\Serializer\DeserializerInterface;

class Base64Deserializer implements DeserializerInterface
{

/**
* @inheritDoc
*/
public function __construct(protected string $class)
{
}

/**
* @inheritDoc
*/
public function deserialize(mixed $data, string $path = ""): object
{
return unserialize(base64_decode($data));
}
}
16 changes: 16 additions & 0 deletions tests/src/Base64Serializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Aternos\Serializer\Test\Src;

use Aternos\Serializer\SerializerInterface;

class Base64Serializer implements SerializerInterface
{
/**
* @inheritDoc
*/
public function serialize(object $item): int|float|string|array|null
{
return base64_encode(serialize($item));
}
}
28 changes: 28 additions & 0 deletions tests/src/CustomSerializerInvalidTypeTestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Aternos\Serializer\Test\Src;

use Aternos\Serializer\Json\PropertyJsonSerializer;
use Aternos\Serializer\Serialize;
use JsonSerializable;

class CustomSerializerInvalidTypeTestClass implements JsonSerializable
{
use PropertyJsonSerializer;

#[Serialize(serializer: new Base64Serializer(), deserializer: new Base64Deserializer(SecondTestClass::class))]
protected TestClass $testClass;

public function __construct()
{
$this->testClass = new TestClass();
}

/**
* @return TestClass
*/
public function getTestClass(): TestClass
{
return $this->testClass;
}
}
28 changes: 28 additions & 0 deletions tests/src/CustomSerializerTestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Aternos\Serializer\Test\Src;

use Aternos\Serializer\Json\PropertyJsonSerializer;
use Aternos\Serializer\Serialize;
use JsonSerializable;

class CustomSerializerTestClass implements JsonSerializable
{
use PropertyJsonSerializer;

#[Serialize(serializer: new Base64Serializer(), deserializer: new Base64Deserializer(TestClass::class))]
protected TestClass $testClass;

public function __construct()
{
$this->testClass = new TestClass();
}

/**
* @return TestClass
*/
public function getTestClass(): TestClass
{
return $this->testClass;
}
}
30 changes: 29 additions & 1 deletion tests/tests/DeserializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
use Aternos\Serializer\Exceptions\IncorrectTypeException;
use Aternos\Serializer\Exceptions\MissingPropertyException;
use Aternos\Serializer\Exceptions\UnsupportedTypeException;
use Aternos\Serializer\Json\JsonDeserializer;
use Aternos\Serializer\Serialize;
use Aternos\Serializer\Test\Src\ArrayDeserializerAccessor;
use Aternos\Serializer\Test\Src\ArrayTests;
use Aternos\Serializer\Test\Src\BuiltInTypeTestClass;
use Aternos\Serializer\Test\Src\CustomSerializerInvalidTypeTestClass;
use Aternos\Serializer\Test\Src\CustomSerializerTestClass;
use Aternos\Serializer\Test\Src\DefaultValueTestClass;
use Aternos\Serializer\Test\Src\IntersectionTestClass;
use Aternos\Serializer\Test\Src\SerializerTestClass;
Expand All @@ -25,6 +28,7 @@
#[UsesClass(IncorrectTypeException::class)]
#[UsesClass(MissingPropertyException::class)]
#[UsesClass(UnsupportedTypeException::class)]
#[UsesClass(JsonDeserializer::class)]
class DeserializerTest extends TestCase
{
public function testDeserialize(): void
Expand Down Expand Up @@ -480,4 +484,28 @@ public function testUnknownBuiltInType(): void
$this->expectExceptionMessage("Unsupported type 'not-a-real-type' for property '.name'");
$deserializer->isBuiltInTypeValid("not-a-real-type", "test", ".name");
}
}

public function testArrayDeserializerArgumentIsNotAnArray(): void
{
$deserializer = new ArrayDeserializerAccessor(TestClass::class);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("Data must be an array.");
$deserializer->deserialize("not-an-array");
}

public function testCustomDeserializer(): void
{
$deserializer = new JsonDeserializer(CustomSerializerTestClass::class);
$testClass = $deserializer->deserialize('{"testClass":"TzozNzoiQXRlcm5vc1xTZXJpYWxpemVyXFRlc3RcU3JjXFRlc3RDbGFzcyI6OTp7czo2OiIAKgBhZ2UiO2k6MDtzOjE1OiIAKgBvcmlnaW5hbE5hbWUiO047czoxMToiACoAbnVsbGFibGUiO047czoxMjoiACoAYm9vbE9ySW50IjtiOjA7czoxNjoiACoAbm90QUpzb25GaWVsZCI7czo0OiJ0ZXN0IjtzOjE4OiIAKgBzZWNvbmRUZXN0Q2xhc3MiO047czo4OiIAKgBtaXhlZCI7TjtzOjg6IgAqAGZsb2F0IjtOO3M6ODoiACoAYXJyYXkiO047fQ=="}');
$this->assertInstanceOf(CustomSerializerTestClass::class, $testClass);
$this->assertInstanceOf(TestClass::class, $testClass->getTestClass());
}

public function testCustomDeserializerReturnsInvalidType(): void
{
$deserializer = new JsonDeserializer(CustomSerializerInvalidTypeTestClass::class);
$this->expectException(IncorrectTypeException::class);
$this->expectExceptionMessage("Expected '.testClass' to be 'Aternos\Serializer\Test\Src\TestClass' found: \Aternos\Serializer\Test\Src\SecondTestClass::__set_state(array(\n))");
$deserializer->deserialize('{"testClass":"Tzo0MzoiQXRlcm5vc1xTZXJpYWxpemVyXFRlc3RcU3JjXFNlY29uZFRlc3RDbGFzcyI6MDp7fQ=="}');
}
}
Loading