kaskadia/serializer

A wrapper for object serialization and deserialization.

v1.4 2023-11-10 19:50 UTC

This package is auto-updated.

Last update: 2024-04-10 23:23:19 UTC


README

Serializer is a wrapper library for Symfony Serializer that helps serialize and deserialize objects.

[[TOC]]

Installation

Use composer package manager to install.

composer require kaskadia/serializer

Usage

Let's say we have these objects set up.

namespace Kaskadia\Lib\Serializer\Tests\Resources\Objects;

use DateTime;

class User {
    /** @var string */
    private string $name;
    /** @var string */
    private string $username;
    /** @var int */
    private int $age;
    /** @var DateTime */
    private DateTime $birthDate;
    /** @var float */
    private float $randomDecimal;
    /** @var Company */
    private Company $company;

    private function __construct(string $name, string $username, int $age, DateTime $birthDate, float $randomDecimal, Company $company) {
        $this->setName($name)
            ->setUsername($username)
            ->setAge($age)
            ->setBirthDate($birthDate)
            ->setRandomDecimal($randomDecimal)
            ->setCompany($company);
    }

    public static function initialize(string $name = null, string $username = null, int $age = null, DateTime $birthDate = null, float $randomDecimal = null, Company $company = null) : self {
        return new self($name, $username, $age, $birthDate, $randomDecimal, $company);
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param string $name
     * @return User
     */
    public function setName(string $name): self
    {
        if(!isset($this->name)) {
            $this->name = $name;
        }
        return $this;
    }

    public function getUsername(): string {
        return $this->username;
    }

    public function setUsername(string $username): self {
        if(!isset($this->username)) {
            $this->username = $username;
        }
        return $this;
    }

    /**
     * @return int
     */
    public function getAge(): int
    {
        return $this->age;
    }

    /**
     * @param int $age
     * @return User
     */
    public function setAge(int $age): self
    {
        if(!isset($this->age)) {
            $this->age = $age;
        }
        return $this;
    }

    /**
     * @return DateTime
     */
    public function getBirthDate(): DateTime
    {
        return $this->birthDate;
    }

    /**
     * @param DateTime $birthDate
     * @return User
     */
    public function setBirthDate(DateTime $birthDate): self
    {
        if(!isset($this->birthDate)) {
            $this->birthDate = $birthDate;
        }
        return $this;
    }

    /**
     * @return float
     */
    public function getRandomDecimal(): float
    {
        return $this->randomDecimal;
    }

    /**
     * @param float $randomDecimal
     * @return User
     */
    public function setRandomDecimal(float $randomDecimal): self
    {
        if(!isset($this->randomDecimal)) {
            $this->randomDecimal = $randomDecimal;
        }
        return $this;
    }

    /**
     * @return Company
     */
    public function getCompany(): Company
    {
        return $this->company;
    }

    /**
     * @param Company $company
     * @return User
     */
    public function setCompany(Company $company): self
    {
        if(!isset($this->company)) {
            $this->company = $company;
        }
        return $this;
    }
}

class Company {
    /** @var string */
    private string $name;
    /** @var DateTime */
    private DateTime $createdDate;

    protected function __construct(string $name, DateTime $createdDate) {
        $this->setName($name)
            ->setCreatedDate($createdDate);
    }

    public static function initialize(string $name = null, DateTime $createdDate = null) : self {
        return new self($name, $createdDate);
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param string $name
     * @return Company
     */
    public function setName(string $name): self
    {
        if(!isset($this->name)) {
            $this->name = $name;
        }
        return $this;
    }

    /**
     * @return DateTime
     */
    public function getCreatedDate(): DateTime
    {
        return $this->createdDate;
    }

    /**
     * @param DateTime $createdDate
     * @return Company
     */
    public function setCreatedDate(DateTime $createdDate): self
    {
        if(!isset($this->createdDate)) {
            $this->createdDate = $createdDate;
        }
        return $this;
    }
}

Using the serializer is quite straight forward. If you look inside the tests, there is a simple object factory that enables us to spit out instances of these objects quickly with test data.

namespace Kaskadia\Lib\Serializer\Tests\Resources\Objects\{User,Company};
namespace Kaskadia\Lib\Serializer\Tests\Resources\ObjectFactory;
$factory = new ObjectFactory();
$user = $factory->makeOne(User::class);
$json = $this->serializer->toJson($user);
//json contains
//{"name":"Dr. Noemi Smith DVM","username":"joanie99@hotmail.com","age":18,"birthDate":"1991-04-22 00:25:05.000000 UTC","randomDecimal":8.3836838,"company":{"name":"Ortiz, Frami and Kshlerin","createdDate":"2013-04-20 18:12:31.000000 UTC"}}

$deserializedUser = $this->serializer->fromJson($json, User::class);
// We get back the same object passed in.

By using this serializer, we only need to decide which serialization method we want to use. Options are toJson($data) and toXml($data).

In order to deserialize($data, $className) the data into an existing class, we need to pass in the data, and the className we are wanting to reconstruct.

Arrays/Collections

The serializer will handle serialization of an iterable, by making an array in the serialized objects. When deserializing these strings, you need to choose one of the following options: fromJsonArray($data, $className) or fromXmlArray($data, $className). The deserialization process will give you back an array, from there if you want to use a collection, like Laravel or Doctrine's collections, you'll have to pass that array on to whichever is your preference.

Ignoring attributes & Groups

Sometimes you have an object which contains information you don't want to include in serialization. A user for instance, could have a password that you wouldn't want to expose. There are a few ways to handle this, and this library has tried to make that a little bit easier. By adding the Ignore annotation in your property definition, you can easily ensure that serialized strings will not contain that property. For example, let's say we want to add the User to the Company class in the example above. However, if we were to add that without choosing to ignore either the $company property on the User or the $user property on the Company we would end up with a circular reference error.

This allows you to have nested objects serialized without fields that you don't want included. Please look at the example below, and refer to:

use Kaskadia\Lib\Serializer\Tests\Resources\Objects\User;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\Groups;

class Company {
    /** 
 		* @var string
 		* @Groups({"list_company", "show_company"})
 		*/
    private string $name;
    /** 
 		* @var DateTime
 		* @Groups({"show_company"})
 		*/
    private DateTime $createdDate;
    /** 
    * @var ?User
    * @Ignore()
    */
    private ?User $user; 
    
    ...
    
    /** 
    * If you choose to ignore a property in your serialization, you'll have to adjust
    * your get function for that property in order to handle the null state on deserializing the object.
    * If you don't, you'll end up with an error that states Typed property {PROPERTY} must not be accessed before initialization.
    */
    public getUser(): ?User {
      if(!isset($this->user)) {
        return null;
      }
      return $this->user;
    }
}

NOTE You need to load in the Groups and Ignore Annotations into your AnnotationReader. You can add more annotations as you see fit as well, but this is just an example.

In Laravel, add a new ServiceProvider and load that in config/app.php.

For example add file app/Providers/SerializerAnnotationsServiceProvider.php which looks like this:

<?php

namespace App\Providers;

use Doctrine\Common\Annotations\AnnotationRegistry;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;

class SerializerAnnotationsServiceProvider extends ServiceProvider {
	public function register() {
		AnnotationRegistry::loadAnnotationClass(Groups::class);
		AnnotationRegistry::loadAnnotationClass(Ignore::class);
	}
}

Add the new ServiceProvider to config/app.php like this:

<?php

return [
	...
	'providers' => [
		...
		/*
		 * Application Service Providers ...
		 */
		 ...
		 App\Providers\SerializerAnnotationsServiceProvider::class
	],
	...
];

Symfony/Serializer Context

Lastly, in order to make use of the Groups annotation, and I'm certain that the simple Ignore() annotation exposed won't be able to satisfy every customization needed when interacting with this library, the $context[] attribute has been made an optional parameter for the serialization methods toJson/toXml.

namespace Kaskadia\Lib\Serializer\Tests\Resources\Objects\Company;
namespace Kaskadia\Lib\Serializer\Tests\Resources\ObjectFactory;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

$factory = new ObjectFactory();
$company = $factory->makeOne(Company::class);

// In order to use the groups, we have to pass in the context to the serializer
$listJson = $this->serializer->toJson($company, [AbstractNormalizer::GROUPS => ["list_company"]]);
$showJson = $this->serializer->toJson($company, [AbstractNormalizer::GROUPS => ["show_company"]]);
//listJson contains
//{"name":"ACME Ltd."}
//showJson contains
//{"name":"ACME Ltd.","createdDate":"2013-04-20 18:12:31.000000 UTC"}

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

MIT