spatie / laravel-event-sourcing

The easiest way to get started with event sourcing in Laravel
https://docs.spatie.be/laravel-event-sourcing
MIT License
764 stars 163 forks source link

Event properties matching ShouldBeStored methods are not serialized correctly #475

Open ganyicz opened 1 month ago

ganyicz commented 1 month ago

This is due to how Symfony serializer works. When the serialized object (event) contains a method with the name of the attribute, return value of that method is preferred and the value of the attribute is ignored.

The simplest example of this is declaring a Carbon property named createdAt. When an event with this property is stored with a value different from now, the provided value is ignored and instead the event is saved in the database with the current time being the value of this property (which is the return value of the createdAt method on ShouldBeStored)

ganyicz commented 1 month ago

As a quick fix, we wrote a custom ObjectNormalizer and replaced it in event_normalizers array in config, this fixes the issue

namespace App\Support;

use Spatie\EventSourcing\Support\ObjectNormalizer as BaseObjectNormalizer;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfo;

class ObjectNormalizer extends BaseObjectNormalizer
{
    public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyAccessorInterface $propertyAccessor = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [])
    {
        // We need to customize how properties are accessed during normalization
        // to prevent methods from being preferred over properties. This causes
        // issues with events that have property named eg. `createdAt` because
        // method with the same name already exists on ShouldBeStored class
        $readInfoExtractor = new class implements PropertyReadInfoExtractorInterface {
            protected ReflectionExtractor $reflectionExtractor;

            public function __construct()
            {
                $this->reflectionExtractor = new ReflectionExtractor([], null, null, false);
            }

            public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo
            {
                $context['enable_getter_setter_extraction'] = false;

                return $this->reflectionExtractor->getReadInfo($class, $property, $context);
            }
        };

        $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
            ->setReadInfoExtractor($readInfoExtractor)
            ->getPropertyAccessor();

        parent::__construct(
            $classMetadataFactory,
            $nameConverter,
            $propertyAccessor,
            $propertyTypeExtractor,
            $classDiscriminatorResolver,
            $objectClassResolver,
            $defaultContext,
        );
    }
}

Then, in config/event-sourcing.php:

    'event_normalizers' => [
        Spatie\EventSourcing\Support\CarbonNormalizer::class,
        Spatie\EventSourcing\Support\ModelIdentifierNormalizer::class,
        Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::class,
        Symfony\Component\Serializer\Normalizer\ArrayDenormalizer::class,
        Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer::class,
        App\Support\ObjectNormalizer::class,
    ],