gdbots/pbj-php

View on GitHub
src/Serializer/PhpArraySerializer.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php
declare(strict_types=1);

namespace Gdbots\Pbj\Serializer;

use Gdbots\Pbj\Assertion;
use Gdbots\Pbj\Codec;
use Gdbots\Pbj\Enum\FieldRule;
use Gdbots\Pbj\Exception\InvalidResolvedSchema;
use Gdbots\Pbj\Field;
use Gdbots\Pbj\Message;
use Gdbots\Pbj\MessageResolver;
use Gdbots\Pbj\Schema;
use Gdbots\Pbj\SchemaId;
use Gdbots\Pbj\WellKnown\DynamicField;
use Gdbots\Pbj\WellKnown\GeoPoint;
use Gdbots\Pbj\WellKnown\MessageRef;

class PhpArraySerializer implements Serializer, Codec
{
    /** Options for the serializer to use, e.g. json encoding options */
    protected array $options;

    private bool $skipValidation = false;

    public function skipValidation(?bool $skipValidation = null): bool
    {
        if (null !== $skipValidation) {
            $this->skipValidation = $skipValidation;
        }

        return $this->skipValidation;
    }

    public function serialize(Message $message, array $options = []): mixed
    {
        $this->options = $options;
        return $this->doSerialize($message);
    }

    public function deserialize(mixed $data, array $options = []): Message
    {
        $this->options = $options;

        Assertion::keyIsset(
            $data,
            Schema::PBJ_FIELD_NAME,
            'Array provided must contain the [_schema] key.',
        );

        return $this->doDeserialize($data);
    }

    public function encodeDynamicField(DynamicField|array $dynamicField, Field $field): array
    {
        if ($dynamicField instanceof DynamicField) {
            return $dynamicField->toArray();
        }

        return $dynamicField;
    }

    public function decodeDynamicField(mixed $value, Field $field): DynamicField|array
    {
        if ($value instanceof DynamicField) {
            return $value;
        }

        if ($this->skipValidation()) {
            return $value;
        }

        return DynamicField::fromArray($value);
    }

    public function encodeGeoPoint(GeoPoint|array $geoPoint, Field $field): array
    {
        if ($geoPoint instanceof GeoPoint) {
            return $geoPoint->toArray();
        }

        return $geoPoint;
    }

    public function decodeGeoPoint(mixed $value, Field $field): GeoPoint|array
    {
        if ($value instanceof GeoPoint) {
            return $value;
        }

        if ($this->skipValidation()) {
            return $value;
        }

        return GeoPoint::fromArray($value);
    }

    public function encodeMessage(Message $message, Field $field): array
    {
        return $this->doSerialize($message);
    }

    public function decodeMessage(mixed $value, Field $field): Message
    {
        if ($value instanceof Message) {
            return $value;
        }

        return $this->doDeserialize($value);
    }

    public function encodeMessageRef(MessageRef|array $messageRef, Field $field): array
    {
        if ($messageRef instanceof MessageRef) {
            return $messageRef->toArray();
        }

        return $messageRef;
    }

    public function decodeMessageRef(mixed $value, Field $field): MessageRef|array
    {
        if ($value instanceof MessageRef) {
            return $value;
        }

        if ($this->skipValidation()) {
            return $value;
        }

        return MessageRef::fromArray($value);
    }

    private function doSerialize(Message $message): array
    {
        $schema = $message::schema();
        $message->validate();
        $payload = [];

        foreach ($schema->getFields() as $field) {
            $fieldName = $field->getName();
            if (!$message->has($fieldName)) {
                continue;
            }

            $value = $message->fget($fieldName);
            $type = $field->getType();

            if ($field->isASingleValue()) {
                $payload[$fieldName] = $type->encode($value, $field, $this);
            } else {
                $payload[$fieldName] = array_map(function ($v) use ($type, $field) {
                    return $type->encode($v, $field, $this);
                }, $value);
            }
        }

        return $payload;
    }

    private function doDeserialize(array $data): Message
    {
        $schemaId = SchemaId::fromString((string)$data[Schema::PBJ_FIELD_NAME]);
        $className = MessageResolver::resolveId($schemaId);
        $message = new $className();
        Assertion::isInstanceOf($message, Message::class);
        $schema = $message::schema();

        if ($schema->getCurieMajor() !== $schemaId->getCurieMajor()) {
            throw new InvalidResolvedSchema($schema, $schemaId, $className);
        }

        foreach ($data as $fieldName => $value) {
            if (!$schema->hasField($fieldName)) {
                continue;
            }

            if (null === $value) {
                $message->clear($fieldName);
                continue;
            }

            $field = $schema->getField($fieldName);
            $type = $field->getType();

            if ($this->skipValidation()) {
                if ($field->isASingleValue()) {
                    $message->setWithoutValidation($fieldName, $type->decode($value, $field, $this));
                } else {
                    $message->setWithoutValidation($fieldName, array_map(function ($v) use ($type, $field) {
                        return $type->decode($v, $field, $this);
                    }, $value));
                }
                continue;
            }

            switch ($field->getRule()) {
                case FieldRule::A_SINGLE_VALUE:
                    $message->set($fieldName, $type->decode($value, $field, $this));
                    break;

                case FieldRule::A_SET:
                case FieldRule::A_LIST:
                    Assertion::isArray($value, sprintf('Field [%s] must be an array.', $fieldName), $fieldName);
                    $values = [];
                    foreach ($value as $v) {
                        $values[] = $type->decode($v, $field, $this);
                    }

                    if ($field->isASet()) {
                        $message->addToSet($fieldName, $values);
                    } else {
                        $message->addToList($fieldName, $values);
                    }
                    break;

                case FieldRule::A_MAP:
                    Assertion::isArray($value, 'Field must be an associative array.', $fieldName);
                    foreach ($value as $k => $v) {
                        $message->addToMap($fieldName, $k, $type->decode($v, $field, $this));
                    }
                    break;

                default:
                    break;
            }
        }

        return $message->setWithoutValidation(Schema::PBJ_FIELD_NAME, $schema->getId()->toString())->populateDefaults();
    }
}