phpsymfonyserializationjmsserializerbundlejms-serializer

How to serialize object as its own property (array) using JMS Serializer EventSubscriberInterface (php, symfony)


I need to serialize an object as its own property (it's type is array), I mean that the object has an array property books, and after transforming it I want to skip the books key, so the structure will be more flat [book1, book2] (not [books => [book1, book2]]. I have the following classes:

<?php

class Store
{
    private ?BooksCollection $booksCollection = null;

    public function __construct(?BooksCollection $booksCollection = null)
    {
        $this->booksCollection = $booksCollection;
    }

    public function getBooksCollection(): ?BooksCollection
    {
        return $this->booksCollection;
    }
}

class BooksCollection
{
    /** @var Book[] */
    private array $books;

    public function __construct(Book ...$books)
    {
        $this->books = $books;
    }

    public function getBooks(): array
    {
        return $this->books;
    }
}

class Book
{
    private string $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}

and serialization config:

Store:
  exclusion_policy: ALL
  properties:
    booksCollection:
      type: BooksCollection
BooksCollection:
  exclusion_policy: ALL
  properties:
    books:
      type: array<int, Book>
Book:
  exclusion_policy: ALL
  properties:
    title:
      type: string

The test I want to pass:

<?php

use JMS\Serializer\ArrayTransformerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class StoreSerializeTest extends KernelTestCase
{
    /** @var ArrayTransformerInterface */
    private $serializer;

    protected function setUp(): void
    {
        self::bootKernel();
        $this->serializer = self::$kernel->getContainer()->get('jms_serializer');
    }

    public function testSerialization(): void
    {
        $store = new Store(new BooksCollection(new Book('Birdy'), new Book('Lotr')));

        $serializedStore = $this->serializer->toArray($store);
        $storeUnserialized = $this->serializer->fromArray($serializedStore, Store::class);

        self::assertSame(
            [
                'books_collection' => [
                    ['title' => 'Birdy'],
                    ['title' => 'Lotr']
                ]
            ],
            $serializedStore
        );
        self::assertEquals($store, $storeUnserialized);
    }
}

As you can see below the test is failing. How can I get rid of one nesting 'books'? jms serializer

The main idea I had, was to use EventSubscriberInterface and onPreSerialize event, but I really can't figure out how can I replace an object BooksCollection with an array made of its own property books. Is there anyone who already know how to do it?


Solution

  • Finally, I figured it out. I implemented SubscribingHandlerInterface

    <?php
    
    use JMS\Serializer\Context;
    use JMS\Serializer\GraphNavigatorInterface;
    use JMS\Serializer\Handler\SubscribingHandlerInterface;
    use JMS\Serializer\JsonDeserializationVisitor;
    use JMS\Serializer\JsonSerializationVisitor;
    use Book;
    use BooksCollection;
    
    class BooksCollectionHandler implements SubscribingHandlerInterface
    {
        public static function getSubscribingMethods(): array
        {
            return [
                [
                    'type' => BooksCollection::class,
                    'format' => 'json',
                    'method' => 'serialize',
                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                ],
                [
                    'type' => BooksCollection::class,
                    'format' => 'json',
                    'method' => 'deserialize',
                    'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                ]
            ];
        }
    
        public function serialize(
            JsonSerializationVisitor $visitor,
            BooksCollection $booksCollection,
            array $type,
            Context $context
        ) {
            return $visitor->visitArray($booksCollection->getBooks(), ['name' => 'array'], $context);
        }
    
        public function deserialize(
            JsonDeserializationVisitor $visitor,
            array $data,
            array $type,
            Context $context
        ): BooksCollection {
            $collection = [];
    
            foreach ($data as $book) {
                $collection[] =
                    $visitor->getNavigator()->accept($book, ['name' => Book::class], $context);
            }
    
            return new BooksCollection(...$collection);
        }
    }
    

    service config:

        books_handler:
            class: BooksCollectionHandler
            tags:
                - { name: jms_serializer.subscribing_handler }