skip to Main Content

For example I have SimpleDto class

class SimpleDto implements GetPhoneInterface
{
    public string $name;

    public int $value;
}

And json

{"name":"Jane"}

When I serialize it, i get not valid object.

$serializer = self::getContainer()->get(SerializerInterface::class);
$dto = $serializer->deserialize($json, $dtoClass);

$dto has not initial variable $value.

How can I make sure that during deserialization an exception occurs that the class has unvalidated values?

If this cannot be solved using a serializer, maybe there is some way to check using a validator?

Upd:
I tried to implement it like this, but the code behaves incorrectly. In addition, ObjectNormalizer is a final class.
Maybe someone knows a better solution?

class InitialObjectNormalizer extends ObjectNormalizer
{
    // circle check
    private array $visitedObjects = [];
    private array $errors = [];

    public function denormalize(mixed $data, string $type, ?string $format = null, array $context = [])
    {
        $data = parent::denormalize($data, $type, $format, $context);

        $this->handleObject($data);
        if (!empty($this->errors)) {
            throw new PartialDenormalizationException(null, $this->errors);
        }

        return $data;
    }

    public function handleObject($obj): void
    {
        if (in_array($obj, $this->visitedObjects, true)) {
            return;
        }
        $this->visitedObjects[] = $obj;

        $attributes = new ReflectionObject($obj);

        foreach ($attributes->getProperties() as $attribute) {
            if (!$attribute->isInitialized($obj)) {
                $this->errors[] = new NotNormalizableValueException('Attribute ' . $attribute->getName() . ' not initial',);
            } else {
                $value = $attribute->getValue($obj);
                if (is_array($value) || is_object($value)) {
                    $this->handleObject($value);
                }
            }
        }
    }
}

2

Answers


  1. SerializerInterface is an interface and you can have your own implementation. Example:

    namespace AppMessengerSerializer;
    
    use SymfonyComponentMessengerEnvelope;
    use SymfonyComponentMessengerTransportSerializationSerializerInterface;
    
    class MessageWithTokenDecoder implements SerializerInterface
    {
        public function decode(array $encodedEnvelope): Envelope
        {
            $envelope = json_decode($encodedEnvelope, true);
    
            try {
                // parse the data you received with your custom fields
                $data = $envelope['data'];
                $data['token'] = $envelope['token'];
    
                // other operations like getting information from stamps
            } catch (Throwable $throwable) {
                // wrap any exception that may occur in the envelope to send it to the failure transport
                return new Envelope($throwable);
            }
    
            return new Envelope($data);
        }
    
        public function encode(Envelope $envelope): array
        {
            // this decoder does not encode messages, but you can implement it by returning
            // an array with serialized stamps if you need to send messages in a custom format
            throw new LogicException('This serializer is only used for decoding messages.');
        }
    }
    

    and then you can set your own serializer:

    # config/packages/messenger.yaml
    framework:
        messenger:
            transports:
                my_transport:
                    dsn: '%env(MY_TRANSPORT_DSN)%'
                    serializer: 'AppMessengerSerializerMessageWithTokenDecoder'
    

    Code taken from this source.

    Of course, you can implement it completely differently, as your requirements direct you and you can name it differently too. You can also implement a custom validator and set it up, like

    # config/validator/validation.yaml
    AppEntityUser:
        properties:
            name:
                - NotBlank: ~
                - AppValidatorContainsAlphanumeric:
                    mode: 'loose'
    

    and implement it like

    // src/Validator/ContainsAlphanumericValidator.php
    namespace AppValidator;
    
    use SymfonyComponentValidatorConstraint;
    use SymfonyComponentValidatorConstraintValidator;
    use SymfonyComponentValidatorExceptionUnexpectedTypeException;
    use SymfonyComponentValidatorExceptionUnexpectedValueException;
    
    class ContainsAlphanumericValidator extends ConstraintValidator
    {
        public function validate(mixed $value, Constraint $constraint): void
        {
            if (!$constraint instanceof ContainsAlphanumeric) {
                throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
            }
    
            // custom constraints should ignore null and empty values to allow
            // other constraints (NotBlank, NotNull, etc.) to take care of that
            if (null === $value || '' === $value) {
                return;
            }
    
            if (!is_string($value)) {
                // throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
                throw new UnexpectedValueException($value, 'string');
    
                // separate multiple types using pipes
                // throw new UnexpectedValueException($value, 'string|int');
            }
    
            // access your configuration options like this:
            if ('strict' === $constraint->mode) {
                // ...
            }
    
            if (preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
                return;
            }
    
            // the argument must be a string or an object implementing __toString()
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
    

    Code taken from this source.

    Of course, you will need to do your own coding in order to customize your serializer and/or validator.

    Login or Signup to reply.
  2. Try rewriting your DTO using a constructor:

    class SimpleDto implements GetPhoneInterface
    {
        public function __construct(public string $name, public int $value) {}
    }
    

    The standard ObjectNormalizer will now throw a MissingConstructorArgumentsException which you can handle in your code.

    If you can not rewrite the DTO, you can implement a custom denormalizer and check whether all required fields are present. This is a very basic example, adapt to your own need.

    final class SimpleDtoDenormalizer implements DenormalizerInterface
    {
        public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
        {
            if (!is_array($data)) {
                throw new InvalidArgumentException('Expected an array, got ' . get_debug_type($data));
            }
    
            $dto = new SimpleDto();
            $dto->name = $data['name'] ?? throw new InvalidArgumentException('Missing "name"');
            $dto->value = $data['value'] ?? throw new InvalidArgumentException('Missing "value"');
    
            return $dto;
        }
    
        public function supportsDenormalization(
            mixed $data,
            string $type,
            ?string $format = null,
            array $context = []
        ): bool {
            return $type === SimpleDto::class;
        }
    
        public function getSupportedTypes(?string $format): array
        {
            return [
                SimpleDto::class => true
            ];
        }
    }
    

    You can find more about custom denormalizers in the Symfony documentation.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search