diff --git a/config/verbs.php b/config/verbs.php index b6251283..54dbc992 100644 --- a/config/verbs.php +++ b/config/verbs.php @@ -1,4 +1,23 @@ [ + SelfSerializingNormalizer::class, + CollectionNormalizer::class, + StateNormalizer::class, + BitsNormalizer::class, + CarbonNormalizer::class, + DateTimeNormalizer::class, + BackedEnumNormalizer::class, + PropertyNormalizer::class, + ], ]; diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index e05137c3..4589c14f 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -17,8 +17,7 @@ use Thunk\Verbs\Models\VerbEvent; use Thunk\Verbs\Models\VerbStateEvent; use Thunk\Verbs\State; -use Thunk\Verbs\Support\EventSerializer; -use Thunk\Verbs\Support\MetadataSerializer; +use Thunk\Verbs\Support\Serializer; class EventStore { @@ -129,8 +128,8 @@ protected function formatForWrite(array $event_objects): array return array_map(fn (Event $event) => [ 'id' => Verbs::toId($event->id), 'type' => $event::class, - 'data' => app(EventSerializer::class)->serialize($event), - 'metadata' => app(MetadataSerializer::class)->serialize($this->metadata->get($event)), + 'data' => app(Serializer::class)->serialize($event), + 'metadata' => app(Serializer::class)->serialize($this->metadata->get($event)), 'created_at' => now(), 'updated_at' => now(), ], $event_objects); diff --git a/src/Lifecycle/SnapshotStore.php b/src/Lifecycle/SnapshotStore.php index ad73689c..df15a432 100644 --- a/src/Lifecycle/SnapshotStore.php +++ b/src/Lifecycle/SnapshotStore.php @@ -9,7 +9,7 @@ use Thunk\Verbs\Facades\Verbs; use Thunk\Verbs\Models\VerbSnapshot; use Thunk\Verbs\State; -use Thunk\Verbs\Support\StateSerializer; +use Thunk\Verbs\Support\Serializer; class SnapshotStore { @@ -55,7 +55,7 @@ protected static function formatForWrite(array $states): array return array_map(fn (State $state) => [ 'id' => Verbs::toId($state->id), 'type' => $state::class, - 'data' => app(StateSerializer::class)->serialize($state), + 'data' => app(Serializer::class)->serialize($state), 'last_event_id' => Verbs::toId($state->last_event_id), 'created_at' => now(), 'updated_at' => now(), diff --git a/src/Models/VerbEvent.php b/src/Models/VerbEvent.php index 95853afc..b2fddd28 100644 --- a/src/Models/VerbEvent.php +++ b/src/Models/VerbEvent.php @@ -7,8 +7,7 @@ use Thunk\Verbs\Event; use Thunk\Verbs\Metadata; use Thunk\Verbs\State; -use Thunk\Verbs\Support\EventSerializer; -use Thunk\Verbs\Support\MetadataSerializer; +use Thunk\Verbs\Support\Serializer; class VerbEvent extends Model { @@ -32,14 +31,14 @@ class VerbEvent extends Model public function event(): Event { - $this->event ??= app(EventSerializer::class)->deserialize($this->type, $this->data); + $this->event ??= app(Serializer::class)->deserialize($this->type, $this->data); return $this->event; } public function metadata(): Metadata { - $this->meta ??= app(MetadataSerializer::class)->deserialize(Metadata::class, $this->metadata); + $this->meta ??= app(Serializer::class)->deserialize(Metadata::class, $this->metadata); return $this->meta; } diff --git a/src/Models/VerbSnapshot.php b/src/Models/VerbSnapshot.php index 0eadfecf..fb6fde3c 100644 --- a/src/Models/VerbSnapshot.php +++ b/src/Models/VerbSnapshot.php @@ -5,7 +5,7 @@ use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Model; use Thunk\Verbs\State; -use Thunk\Verbs\Support\StateSerializer; +use Thunk\Verbs\Support\Serializer; /** * @property int $id @@ -24,7 +24,7 @@ class VerbSnapshot extends Model public function state(): State { - $this->state ??= app(StateSerializer::class)->deserialize($this->type, $this->data); + $this->state ??= app(Serializer::class)->deserialize($this->type, $this->data); $this->state->id = $this->id; $this->state->last_event_id = $this->last_event_id; diff --git a/src/State.php b/src/State.php index 2d282df5..9b8a69b4 100644 --- a/src/State.php +++ b/src/State.php @@ -7,7 +7,7 @@ use Symfony\Component\Uid\AbstractUid; use Thunk\Verbs\Lifecycle\EventStore; use Thunk\Verbs\Lifecycle\StateManager; -use Thunk\Verbs\Support\StateSerializer; +use Thunk\Verbs\Support\Serializer; abstract class State { @@ -21,7 +21,7 @@ public static function make(...$args): static $args = $args[0]; } - $state = app(StateSerializer::class)->deserialize(static::class, $args); + $state = app(Serializer::class)->deserialize(static::class, $args); app(StateManager::class)->register($state); diff --git a/src/Support/EventSerializer.php b/src/Support/EventSerializer.php deleted file mode 100644 index 68628b9f..00000000 --- a/src/Support/EventSerializer.php +++ /dev/null @@ -1,21 +0,0 @@ - $target */ - public function deserialize( - Event|string $target, - string|array $data, - ): Event { - if (! is_a($target, Event::class, true)) { - throw new InvalidArgumentException(class_basename($this).'::deserialize must be passed an Event class.'); - } - - return parent::unserialize($target, $data); - } -} diff --git a/src/Support/MetadataSerializer.php b/src/Support/MetadataSerializer.php deleted file mode 100644 index 8a78f8f4..00000000 --- a/src/Support/MetadataSerializer.php +++ /dev/null @@ -1,21 +0,0 @@ - $target */ - public function deserialize( - Metadata|string $target, - string|array $data, - ): Metadata { - if (! is_a($target, Metadata::class, true)) { - throw new InvalidArgumentException(class_basename($this).'::deserialize must be passed a Metadata class.'); - } - - return $this->unserialize($target, $data); - } -} diff --git a/src/Support/PendingEvent.php b/src/Support/PendingEvent.php index 57aecdd2..02f75829 100644 --- a/src/Support/PendingEvent.php +++ b/src/Support/PendingEvent.php @@ -87,7 +87,7 @@ public function shouldFire(): static public function hydrate(array $data): static { - $this->event = app(EventSerializer::class)->deserialize($this->event, $data); + $this->event = app(Serializer::class)->deserialize($this->event, $data); app(MetadataManager::class)->initialize($this->event); diff --git a/src/Support/Serializer.php b/src/Support/Serializer.php index 4311cfd4..27c046ba 100644 --- a/src/Support/Serializer.php +++ b/src/Support/Serializer.php @@ -3,57 +3,17 @@ namespace Thunk\Verbs\Support; use BackedEnum; -use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; -use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; -use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; -use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; -use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer as SymfonySerializer; -use Thunk\Verbs\Event; -use Thunk\Verbs\Metadata; -use Thunk\Verbs\Support\Normalization\BitsNormalizer; -use Thunk\Verbs\Support\Normalization\CarbonNormalizer; -use Thunk\Verbs\Support\Normalization\CollectionNormalizer; -use Thunk\Verbs\Support\Normalization\SelfSerializingNormalizer; -use Thunk\Verbs\Support\Normalization\StateNormalizer; -abstract class Serializer +class Serializer { - // FIXME: We need an API for normalizers - public static array $custom_normalizers = []; - public function __construct( public SymfonySerializer $serializer, ) { } - public static function defaultSymfonySerializer(): SymfonySerializer - { - return new SymfonySerializer( - normalizers: array_merge(self::$custom_normalizers, [ - new SelfSerializingNormalizer(), - new CollectionNormalizer(), - new StateNormalizer(), - new BitsNormalizer(), - new CarbonNormalizer(), - new DateTimeNormalizer(), - new BackedEnumNormalizer(), - new ObjectNormalizer( - propertyTypeExtractor: new ReflectionExtractor(), - classDiscriminatorResolver: new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader())), - ), - ]), - encoders: [ - new JsonEncoder(), - ], - ); - } - - public function serialize(Event|Metadata $class): string + public function serialize(object $class): string { if (method_exists($class, '__sleep')) { $class = $class->__sleep(); @@ -62,8 +22,10 @@ public function serialize(Event|Metadata $class): string return $this->serializer->serialize($class, 'json'); } - protected function unserialize($target, $data) - { + public function deserialize( + object|string $target, + string|array $data + ) { $type = $target; $context = []; @@ -74,10 +36,14 @@ protected function unserialize($target, $data) // FIXME: Symfony's serializer is a little wonky. May need to re-think things. if (is_array($data)) { - $data = array_map(fn ($value) => $value instanceof BackedEnum ? $value->value : $value, $data); + $data = array_map(fn ($value) => $value instanceof BackedEnum + ? $value->value + : $value, $data); } - $callback = is_array($data) ? $this->serializer->denormalize(...) : $this->serializer->deserialize(...); + $callback = is_array($data) + ? $this->serializer->denormalize(...) + : $this->serializer->deserialize(...); return $callback( data: $data, diff --git a/src/Support/StateSerializer.php b/src/Support/StateSerializer.php deleted file mode 100644 index e67673eb..00000000 --- a/src/Support/StateSerializer.php +++ /dev/null @@ -1,80 +0,0 @@ -__sleep(); - } - - return $this->serializer->serialize($state, 'json'); - } - - /** @param State|class-string $target */ - public function deserialize( - State|string $target, - string|array $data, - ): State { - if (! is_a($target, State::class, true)) { - throw new InvalidArgumentException(class_basename($this).'::deserialize must be passed a State class.'); - } - - $type = $target; - $context = []; - - if ($target instanceof State) { - $type = $target::class; - $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $target; - } - - $callback = is_array($data) ? $this->serializer->denormalize(...) : $this->serializer->deserialize(...); - - return $callback( - data: $data, - type: $type, - format: 'json', - context: $context, - ); - } -} diff --git a/src/VerbsServiceProvider.php b/src/VerbsServiceProvider.php index 6b131399..1d87473e 100644 --- a/src/VerbsServiceProvider.php +++ b/src/VerbsServiceProvider.php @@ -3,9 +3,18 @@ namespace Thunk\Verbs; use Glhd\Bits\Snowflake; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Container\Container; use Illuminate\Events\Dispatcher as LaravelDispatcher; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Serializer as SymfonySerializer; use Thunk\Verbs\Commands\MakeVerbEventCommand; use Thunk\Verbs\Commands\MakeVerbStateCommand; use Thunk\Verbs\Lifecycle\Broker; @@ -16,11 +25,8 @@ use Thunk\Verbs\Lifecycle\SnapshotStore; use Thunk\Verbs\Lifecycle\StateManager; use Thunk\Verbs\Livewire\SupportVerbs; -use Thunk\Verbs\Support\EventSerializer; use Thunk\Verbs\Support\EventStateRegistry; -use Thunk\Verbs\Support\MetadataSerializer; use Thunk\Verbs\Support\Serializer; -use Thunk\Verbs\Support\StateSerializer; class VerbsServiceProvider extends PackageServiceProvider { @@ -55,17 +61,25 @@ public function packageRegistered() $this->app->singleton(StateManager::class); $this->app->singleton(EventStateRegistry::class); $this->app->singleton(MetadataManager::class); + $this->app->singleton(Serializer::class); - $this->app->singleton(EventSerializer::class, function () { - return new EventSerializer(Serializer::defaultSymfonySerializer()); + $this->app->singleton(PropertyNormalizer::class, function () { + return new PropertyNormalizer( + propertyTypeExtractor: new ReflectionExtractor(), + classDiscriminatorResolver: new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AttributeLoader())), + ); }); - $this->app->singleton(MetadataSerializer::class, function () { - return new MetadataSerializer(Serializer::defaultSymfonySerializer()); - }); + $this->app->singleton(SymfonySerializer::class, function (Container $app) { + $config = $app->make(Repository::class); - $this->app->singleton(StateSerializer::class, function () { - return new StateSerializer(StateSerializer::defaultSymfonySerializer()); + return new SymfonySerializer( + normalizers: collect($config->get('verbs.normalizers')) + ->map(fn ($class_name) => app($class_name)) + ->values() + ->all(), + encoders: [new JsonEncoder()], + ); }); } diff --git a/tests/Unit/CollectionNormalizerTest.php b/tests/Unit/CollectionNormalizerTest.php index 2afae8cd..6b9ef279 100644 --- a/tests/Unit/CollectionNormalizerTest.php +++ b/tests/Unit/CollectionNormalizerTest.php @@ -5,7 +5,7 @@ use Illuminate\Support\Collection; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer as SymfonySerializer; use Thunk\Verbs\Lifecycle\StateManager; use Thunk\Verbs\SerializedByVerbs; @@ -105,7 +105,7 @@ normalizers: [ $normalizer = new CollectionNormalizer(), new StateNormalizer(), - new ObjectNormalizer(propertyTypeExtractor: new ReflectionExtractor()), + new PropertyNormalizer(propertyTypeExtractor: new ReflectionExtractor()), ], encoders: [ new JsonEncoder(), @@ -138,7 +138,7 @@ $normalizer = new CollectionNormalizer(), new CarbonNormalizer(), new SelfSerializingNormalizer(), - new ObjectNormalizer(propertyTypeExtractor: new ReflectionExtractor()), + new PropertyNormalizer(propertyTypeExtractor: new ReflectionExtractor()), ], encoders: [ new JsonEncoder(),