Skip to content

Commit

Permalink
Introduce serializer and introduce json serialization instead of php …
Browse files Browse the repository at this point in the history
…serialization
  • Loading branch information
loevgaard committed May 21, 2024
1 parent 5a30419 commit 1526ad9
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 64 deletions.
51 changes: 51 additions & 0 deletions src/Serializer/CompositeSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Setono\TagBag\Serializer;

use Setono\TagBag\Exception\SerializationException;

final class CompositeSerializer implements SerializerInterface
{
/** @var list<SerializerInterface> */
private array $serializers = [];

public function __construct(SerializerInterface ...$serializers)
{
foreach ($serializers as $serializer) {
$this->add($serializer);
}
}

public function add(SerializerInterface $serializer): void
{
$this->serializers[] = $serializer;
}

public function serialize(array $tags): string
{
foreach ($this->serializers as $serializer) {
try {
return $serializer->serialize($tags);
} catch (SerializationException) {
continue;

Check warning on line 32 in src/Serializer/CompositeSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/CompositeSerializer.php#L31-L32

Added lines #L31 - L32 were not covered by tests
}
}

throw new SerializationException('No serializer was able to serialize the tags');

Check warning on line 36 in src/Serializer/CompositeSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/CompositeSerializer.php#L36

Added line #L36 was not covered by tests
}

public function deserialize(string $data): array
{
foreach ($this->serializers as $serializer) {
try {
return $serializer->deserialize($data);
} catch (SerializationException) {
continue;
}
}

throw new SerializationException('No serializer was able to deserialize the data');

Check warning on line 49 in src/Serializer/CompositeSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/CompositeSerializer.php#L49

Added line #L49 was not covered by tests
}
}
53 changes: 53 additions & 0 deletions src/Serializer/JsonSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Setono\TagBag\Serializer;

use JsonException;
use Setono\TagBag\Exception\SerializationException;
use Setono\TagBag\Tag\RenderedTag;
use Throwable;
use Webmozart\Assert\Assert;

final class JsonSerializer implements SerializerInterface
{
public function serialize(array $tags): string
{
try {
return json_encode($tags, \JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new SerializationException($e->getMessage(), $e->getCode(), $e);

Check warning on line 20 in src/Serializer/JsonSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/JsonSerializer.php#L19-L20

Added lines #L19 - L20 were not covered by tests
}
}

public function deserialize(string $data): array
{
try {
/** @var mixed $data */
$data = json_decode($data, true, 512, \JSON_THROW_ON_ERROR);

Check warning on line 28 in src/Serializer/JsonSerializer.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ { try { /** @var mixed $data */ - $data = json_decode($data, true, 512, \JSON_THROW_ON_ERROR); + $data = json_decode($data, true, 511, \JSON_THROW_ON_ERROR); Assert::isArray($data); $tags = []; foreach ($data as $section => $sectionTags) {

Check warning on line 28 in src/Serializer/JsonSerializer.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ { try { /** @var mixed $data */ - $data = json_decode($data, true, 512, \JSON_THROW_ON_ERROR); + $data = json_decode($data, true, 513, \JSON_THROW_ON_ERROR); Assert::isArray($data); $tags = []; foreach ($data as $section => $sectionTags) {
Assert::isArray($data);

Check warning on line 29 in src/Serializer/JsonSerializer.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ try { /** @var mixed $data */ $data = json_decode($data, true, 512, \JSON_THROW_ON_ERROR); - Assert::isArray($data); + $tags = []; foreach ($data as $section => $sectionTags) { Assert::string($section);

$tags = [];

foreach ($data as $section => $sectionTags) {
Assert::string($section);

Check warning on line 34 in src/Serializer/JsonSerializer.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ Assert::isArray($data); $tags = []; foreach ($data as $section => $sectionTags) { - Assert::string($section); + Assert::isArray($sectionTags); /** @var mixed $tag */ foreach ($sectionTags as $tag) {
Assert::isArray($sectionTags);

Check warning on line 35 in src/Serializer/JsonSerializer.php

View workflow job for this annotation

GitHub Actions / Mutation tests (8.3, highest)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ $tags = []; foreach ($data as $section => $sectionTags) { Assert::string($section); - Assert::isArray($sectionTags); + /** @var mixed $tag */ foreach ($sectionTags as $tag) { Assert::isArray($tag);

/** @var mixed $tag */
foreach ($sectionTags as $tag) {
Assert::isArray($tag);

$tags[$section][] = RenderedTag::createFromArray($tag);
}
}

return $tags;
} catch (Throwable $e) {
throw new SerializationException(
message: $e->getMessage(),
previous: $e,
);
}
}
}
75 changes: 75 additions & 0 deletions src/Serializer/PhpSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Setono\TagBag\Serializer;

use Setono\TagBag\Exception\SerializationException;
use Setono\TagBag\Tag\RenderedTag;
use Webmozart\Assert\Assert;

/**
* @deprecated since 2.3 and will be removed in 3.0. Use the `JsonSerializer` instead
*/
final class PhpSerializer implements SerializerInterface
{
public function serialize(array $tags): string
{
return serialize($tags);
}

/**
* Most of this method is taken from here: https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php
*/
public function deserialize(string $data): array
{
if ('' === $data) {
throw SerializationException::emptyData();

Check warning on line 27 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L27

Added line #L27 was not covered by tests
}

$serializationException = new SerializationException(sprintf('Could not unserialize data: %s.', $data));
$prevUnserializeHandler = ini_set('unserialize_callback_func', self::class . '::handleUnserializeCallback');
/** @psalm-suppress MixedArgumentTypeCoercion,UndefinedVariable */
$prevErrorHandler = set_error_handler(static function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $serializationException) {
if (__FILE__ === $file) {
throw $serializationException;

Check warning on line 35 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L34-L35

Added lines #L34 - L35 were not covered by tests
}

/** @psalm-suppress MixedFunctionCall */
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;

Check warning on line 39 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L39

Added line #L39 was not covered by tests
});

try {
/** @var array<string, list<RenderedTag>> $result */
$result = unserialize($data, [
'allowed_classes' => [RenderedTag::class],
]);
/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::isArray($result);
foreach ($result as $section => $tags) {
/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::string($section);

/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::isArray($tags);

Assert::allIsInstanceOf($tags, RenderedTag::class);
}
} catch (\InvalidArgumentException) {
throw new SerializationException(sprintf('The unserialized data was incorrect. Here is the original data: %s', $data));

Check warning on line 59 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L58-L59

Added lines #L58 - L59 were not covered by tests
} finally {
restore_error_handler();
ini_set('unserialize_callback_func', $prevUnserializeHandler);
}

return $result;
}

/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never

Check warning on line 71 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L71

Added line #L71 was not covered by tests
{
throw new SerializationException(sprintf('Message class "%s" not found during decoding.', $class));

Check warning on line 73 in src/Serializer/PhpSerializer.php

View check run for this annotation

Codecov / codecov/patch

src/Serializer/PhpSerializer.php#L73

Added line #L73 was not covered by tests
}
}
25 changes: 25 additions & 0 deletions src/Serializer/SerializerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Setono\TagBag\Serializer;

use Setono\TagBag\Exception\SerializationException;
use Setono\TagBag\Tag\RenderedTag;

interface SerializerInterface
{
/**
* @param array<string, list<RenderedTag>> $tags
*
* @throws SerializationException if the serialization fails
*/
public function serialize(array $tags): string;

/**
* @return array<string, list<RenderedTag>>
*
* @throws SerializationException if the deserialization fails
*/
public function deserialize(string $data): array;
}
36 changes: 35 additions & 1 deletion src/Tag/RenderedTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace Setono\TagBag\Tag;

use InvalidArgumentException;
use JsonSerializable;
use Stringable;

/**
* This class is not intended to be used outside the Setono\TagBag\TagBag class.
*
Expand All @@ -12,7 +16,7 @@
*
* @internal
*/
final class RenderedTag implements \Stringable
final class RenderedTag implements Stringable, JsonSerializable
{
private function __construct(
public readonly string $value,
Expand All @@ -34,8 +38,38 @@ public static function createFromTag(TagInterface $tag, string $value, string $f
);
}

public static function createFromArray(array $data): self
{
if (!isset($data['value'], $data['section'], $data['priority'], $data['unique'], $data['fingerprint'])) {
throw new InvalidArgumentException('The data array must contain the keys "value", "section", "priority", "unique", and "fingerprint"');

Check warning on line 44 in src/Tag/RenderedTag.php

View check run for this annotation

Codecov / codecov/patch

src/Tag/RenderedTag.php#L44

Added line #L44 was not covered by tests
}

if (!is_string($data['value']) || !is_string($data['section']) || !is_int($data['priority']) || !is_bool($data['unique']) || !is_string($data['fingerprint'])) {
throw new InvalidArgumentException('The data array must have the correct types for the keys "value" (string), "section" (string), "priority" (int), "unique" (bool), and "fingerprint" (string)');

Check warning on line 48 in src/Tag/RenderedTag.php

View check run for this annotation

Codecov / codecov/patch

src/Tag/RenderedTag.php#L48

Added line #L48 was not covered by tests
}

return new self(
$data['value'],
$data['section'],
$data['priority'],
$data['unique'],
$data['fingerprint'],
);
}

public function __toString(): string
{
return $this->value;
}

public function jsonSerialize(): array
{
return [
'value' => $this->value,
'section' => $this->section,
'priority' => $this->priority,
'unique' => $this->unique,
'fingerprint' => $this->fingerprint,
];
}
}
75 changes: 12 additions & 63 deletions src/TagBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Setono\TagBag;

use InvalidArgumentException;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
Expand All @@ -20,11 +19,14 @@
use Setono\TagBag\Renderer\ContentAwareRenderer;
use Setono\TagBag\Renderer\ElementRenderer;
use Setono\TagBag\Renderer\RendererInterface;
use Setono\TagBag\Serializer\CompositeSerializer;
use Setono\TagBag\Serializer\JsonSerializer;
use Setono\TagBag\Serializer\PhpSerializer;
use Setono\TagBag\Serializer\SerializerInterface;
use Setono\TagBag\Storage\StorageInterface;
use Setono\TagBag\Tag\RenderedTag;
use Setono\TagBag\Tag\TagInterface;
use Throwable;
use Webmozart\Assert\Assert;

final class TagBag implements TagBagInterface, LoggerAwareInterface
{
Expand All @@ -41,13 +43,19 @@ final class TagBag implements TagBagInterface, LoggerAwareInterface

private readonly FingerprintGeneratorInterface $fingerprintGenerator;

private readonly SerializerInterface $serializer;

public function __construct(
RendererInterface $renderer = null,
FingerprintGeneratorInterface $fingerprintGenerator = null,
SerializerInterface $serializer = null,
) {
$this->logger = new NullLogger();
$this->renderer = $renderer ?? new CompositeRenderer(new ElementRenderer(), new ContentAwareRenderer());
$this->fingerprintGenerator = $fingerprintGenerator ?? new ValueBasedFingerprintGenerator();

/** @psalm-suppress DeprecatedClass */
$this->serializer = $serializer ?? new CompositeSerializer(new JsonSerializer(), new PhpSerializer());
}

public function add(TagInterface $tag): void
Expand Down Expand Up @@ -176,7 +184,7 @@ public function store(): void
if (count($this->tags) === 0) {
$this->storage->remove();
} else {
$this->storage->store(serialize($this->tags));
$this->storage->store($this->serializer->serialize($this->tags));
}
} catch (StorageException $e) {
$this->logger->error($e->getMessage());
Expand All @@ -202,7 +210,7 @@ public function restore(): void
$this->tags = [];
if (null !== $data) {
try {
$this->tags = $this->unserialize($data);
$this->tags = $this->serializer->deserialize($data);
} catch (SerializationException $e) {
$this->logger->error(sprintf('Exception thrown when trying to unserialize data. Error was: %s. Data was: %s', $e->getMessage(), $data));
}
Expand Down Expand Up @@ -248,63 +256,4 @@ private function findTagByFingerprint(string $fingerprint): ?array

return null;
}

/**
* @throws SerializationException if the data cannot be unserialized
*
* @return array<string, list<RenderedTag>>
*
* Most of this method is taken from here: https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php
*/
private function unserialize(string $data): array
{
if ('' === $data) {
throw SerializationException::emptyData();
}

$serializationException = new SerializationException(sprintf('Could not unserialize data: %s.', $data));
$prevUnserializeHandler = ini_set('unserialize_callback_func', self::class . '::handleUnserializeCallback');
/** @psalm-suppress MixedArgumentTypeCoercion,UndefinedVariable */
$prevErrorHandler = set_error_handler(static function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $serializationException) {
if (__FILE__ === $file) {
throw $serializationException;
}

/** @psalm-suppress MixedFunctionCall */
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
});

try {
/** @var array<string, list<RenderedTag>> $result */
$result = unserialize($data, [
'allowed_classes' => [RenderedTag::class],
]);
/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::isArray($result);
foreach ($result as $section => $tags) {
/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::string($section);

/** @psalm-suppress RedundantConditionGivenDocblockType */
Assert::isArray($tags);

Assert::allIsInstanceOf($tags, RenderedTag::class);
}
} catch (InvalidArgumentException) {
throw new SerializationException(sprintf('The unserialized data was incorrect. Here is the original data: %s', $data));
} finally {
restore_error_handler();
ini_set('unserialize_callback_func', $prevUnserializeHandler);
}

return $result;
}

/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never
{
throw new SerializationException(sprintf('Message class "%s" not found during decoding.', $class));
}
}
Loading

0 comments on commit 1526ad9

Please sign in to comment.