stopsopa/jms-serializer-lite

Simple library to easy dump/serialize objects/arrays from db to make it ready to expose through RESTful API - one direction

v1.3.0 2016-10-09 20:37 UTC

This package is auto-updated.

Last update: 2024-05-01 00:05:52 UTC


README

Build Status Coverage Status Latest Stable Version

DEPRECATED

Created in 2016 - quite old now and not maintained.

That was actually usefull a lot back then. But please don't use it now.

Why?

Usually first choice of library for Symfony 2/3 to dump data from db to provide any RESTful feeds is jms/serializer. This tool is designed to serialize and unserialize data to xml, json, or yml and back to initial data structures. But usually there is need to just provide data into one direction - to json feeds. Additionally usually there is need to serialize the same object in different ways. In jms/serializer and similar complex tools usually you can use "groups", unfortunately this solution is not flexible enough to deal with real life situations.

So what does this library special do?

This library gives you ability to serialize any nested data structures (usually ORM objects) to any array structure, ready to json_encode in simplest possible way, without loosing flexibility and without loosing inheritance to provide new serialization format by changing (overriding) old format. This library is also framework agnostic.

Readme

Installation

composer require stopsopa/jms-serializer-lite

Documentation

When You have ORM entities like ...

Article:
    id
    title
    content
    comments # <one-to-many with Comment entity>
    
Comment
    id
    article # <many-to-one with Article entity>
    user # <many-to-one with User entity>
    content
    
User
    id
    login
    name
    surname
    comments # <one-to-many with Comment entity>

... and there is need to serialize Article to RESTful feed:

{
    "id": 1,
    "name": "First article",
    "body": "Content of first article"
}

The simplest way to start do that using this library is to create simple class (e.g.) NewDumper that extends class Stopsopa\LiteSerializer\Dumper ...

<?php

namespace MyProject;

use Stopsopa\LiteSerializer\Dumper;

class NewDumper extends Dumper
{
}

... and use it to get array ready to json_encode like ...

$article = $man->find(...);

$array = NewDumper::getInstance()->dump($article);

echo json_encode($array, JSON_PRETTY_PRINT);

After executing this you will end up with error ...

Article exception screen

... that means that You need to implement method dumpMyProject_Article to "explain" new class how to transform entity to flat array ...

namespace MyProject;

class NewDumper extends Dumper
{
    public function dumpMyProject_Article($entity) {
        return array(
            'id'    => $entity->getId(),
            'name'  => $entity->getTitle(),
            'body'  => $entity->getContent()
        );
    }
}

... now when You run this code again You will have what You need.

Nested entities

To serialize Article with all it's comments like ...

{
    "id": 1,
    "name": "First article",
    "body": "Content of first article",
    "comments": [
        {
            "id": 2,
            "body": "Content of comment 1"
        },
        {
            "id": 1,
            "body": "Content of comment 2"
        }
    ]
}

... simply change method dumpMyProject_Article to ...

namespace MyProject;

class NewDumper extends Dumper
{
    public function dumpMyProject_Article($entity) {
        $data = array(
            'id'        => $entity->getId(),
            'name'      => $entity->getTitle(),
            'body'      => $entity->getContent(),
        );

        $data['comments'] = $this->innerDump($entity->getComments());

        return $data;
    }
}

... now when You try to execute it, You will see that NewDump require method dumpMyProject_Comment ...

Comment exception screen

namespace MyProject;

class NewDumper extends Dumper
{
    ...
    public function dumpMyProject_Comment($entity) {
        return array(
            'id'        => $entity->getId(),
            'body'      => $entity->getContent()
        );
    }
}

... and that's it.

Serialize from different angles

Worth to mention is fact that class prepared above is ready to serialize classes Article and Comment for different use cases ...

$dumper = NewDumper::getInstance();

# to serialize single Article entity
$array = $dumper->dump($article); 
# result: {"id":1, ... , "comments":[...]}

# to serialize array/collection of Article entities
$array = $dumper->dump(array($article1, $article2, ...));  
# result: [ {"id":1, ... , "comments":[...] }, {"id":2, ... , "comments":[...] } ]

# to serialize single Comment entity
$array = $dumper->dump($comment); 
# result: {"id":1, ...}

# to serialize array/collection of Comment entities
$array = $dumper->dump(array($comment1, $comment2, ...));  
# result: [ {"id":1, ... }, {"id":2, ... } ]        
    

Shorter syntax and helper

All logic explained above can be much more condensed using helper 'toArray':

class NewDumper extends Dumper
{
    public function dumpMyProject_Article($entity) {
        return $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'title',
            'body'      => 'content',
            'comments'  => 'comments'
        ));
    }
    public function dumpMyProject_Comment($entity) {
        return $this->toArray($entity, array(
            'id'        => 'id',
            'body'      => 'content'
        ));
    }
}   

... let's hold for a minute at this code, and try to understand what's going on here.

    So "key side" of array (the left side with 'id', 'name', 'body', 'comments') is side where we declare keys for result array - that's obvious.

    Right side (the value side of array with 'id', 'title', 'content', 'comments') is little more sophisticated. This values are passed to AbstractEntity->get() method, these are "paths" describing how to get values for these keys. See more about AbstractEntity in another page.

 

Note:

It is good idea to read chapter about AbstractEntity before continue reading this documentation.

 

Default values

You can also specify default value to prevent throwing AbstractEntityException if path is wrong (wrong i mean if leads to nowhere). But to do that on the right hand side you need to use extended version of options ...

public function dumpMyProject_Article($entity) {
    return $this->toArray($entity, array(
        'id'        => 'id',
        'name'      => array(
            'path'      => 'title',
            'default'   => 'defaultname'            
        ),
    ));
}  

... so from now on if in object Article field 'title' will be missing you won't see Exception but method will return (like nothing happened) value 'defaultname'.

When i say "missing" it means:

  • if $article is an array OR if object implements interface ArrayAccess

    • if 'title' is valid key return value
  • if $article is an object

    • if path have postfix '()' look for public method 'title()' explicitely and try to execute it and return value

    • else throw AbstractEntityException

    • if there is public method 'getTitle' execute it and return value

    • else if there is public method 'isTitle' execute it and return value

    • else if there is public method 'hasTitle' execute it and return value

    • else if object has (public or private) property 'title' then return value of this prop.

  • throw AbstractEntityException because path is wrong, leads to nowhere...

... if all above can't reach value then "path is wrong" and value is missing because there is no value under this path.

 

Note:

Empty string or null value are still valid values it doesn't mean that path is wrong.

 

If there is need to replace false value by something else it should be done like this:

class NewDumper extends Dumper
{
    public function dumpMyProject_Article($entity) {
        $data = $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => array('title', null), 
                # if path 'title' is wrong then 
                # return null instead of throw AbstractEntityException
            'body'      => 'content',
            'comments'  => 'comments'
        ));

        if (!$data['name']) {
            $data['name'] = 'default value if here is something false';
        }

        return $data;
    }
}

Excluding/Omitting entities

Sometimes we don't want to have some particular entities in feed. To skip them just throw DumperContinueException:

use Stopsopa\LiteSerializer\Exceptions\DumperContinueException;

class NewDumper extends Dumper
{
    public function dumpMyProject_Comment($entity) {
    
        if (!$entity->isModerated()) {
            throw new DumperContinueException();
        }
        
        return $this->toArray($entity, array(
            'id'        => 'id',
            'body'      => 'content'
        ));
    }
}   

Save keys

By default dumper don't maintains key association during iteration:

namespace MyProject;

use Stopsopa\LiteSerializer\Dumper;
use Stopsopa\LiteSerializer\Exceptions\DumperContinueException;

class Group {
    protected $id;
    protected $name;
    public static function getInstance() { return new self(); }
    public function getId() { return $this->id; }
    public function setId($id) { $this->id = $id; return $this; }
    public function getName() { return $this->name; }
    public function setName($name) { $this->name = $name; return $this; }
}
# extend just to make this example shorter
# the case is that we can build one-to-many relation using this classes
class User extends Group {
    protected $groups = array();
    public function getGroups() { return $this->groups; }
    public function setGroups($groups) { $this->groups = $groups; return $this; }
}

$user = User::getInstance()->setId(50)->setName('user')->setGroups(array(
    'group-1' => Group::getInstance()->setId(10)->setName('gr 1'),
    'group-2' => Group::getInstance()->setId(11)->setName('gr 2'),
    'group-3' => Group::getInstance()->setId(12)->setName('gr 3'),
));

class NewDumper extends Dumper
{
    public function dumpMyProject_User($entity) {
        return $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'name',
            'groups'    => 'groups'
        ));
    }
    public function dumpMyProject_Group($entity) {
        if ($entity->getName() === 'gr 2') {
            throw new DumperContinueException();
        }
        return $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'name'
        ));
    }
}

echo json_encode(NewDumper::getInstance()->dump($user), JSON_PRETTY_PRINT);

... return ...

{
    "id": 50,
    "name": "user",
    "groups": [
        {
            "id": 10,
            "name": "gr 1"
        },
        {
            "id": 12,
            "name": "gr 3"
        }
    ]
}

but when you change ...

    ...
    public function dumpMyProject_User($entity) {
        return $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'name',
            'groups'    => array(
                'path' => 'groups',
                'savekeys' => true
            )
        ));
    }
    ...

... keys will be maintained ...

{
    "id": 50,
    "name": "user",
    "groups": {
        "group-1": {
            "id": 10,
            "name": "gr 1"
        },
        "group-3": {
            "id": 12,
            "name": "gr 3"
        }
    }
}

DumperInterface

There is special interface Stopsopa\LiteSerializer\DumpToArrayInterface. Implement this interface to declare how to dump entity right in entity itself.

Default types serialization (integer, string, float, etc.)

Anything else that is not array and is not object is serialized by method dumpPrimitives. You can easily change way of serializing even such values by overriding this method.

Force mode

As you probably noticed everything what is 'foreachable' will be iterated and each element of "collection" will be serialized individually. But there is one situation when this behaviour "can" (don't must) be wrong. When entity implements interface Traversable and in json feed you need fields that are not accessible through 'foreach' on this class. In such situation would be good to have mechanizm to decide manually if serialize such class in normal mode or let dumper to iterate through. You can achieve this like:

    public function dumpMyProject_User($entity) {
        return $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'name',
            'order'    => array(
                'path'     => 'order',
                'mode'     => Dumper::MODE_ENTITY # or Dumper::MODE_COLLECTION
                # default is Dumper::MODE_AUTO
            )
        ));
    }

... or You can dump these entity manually (not in 'toArray' method) like:

    public function dumpMyProject_User($entity) {
        $data = $this->toArray($entity, array(
            'id'        => 'id',
            'name'      => 'name',
        ));
        
        $tmp = array();
        foreach ($entity->getOrder() as $o) {
            $tmp[] = array(
                'id'        => 'id',
                'prise'     => 'price',
                ...
            );
        }
        $data['order'] = $tmp;
        
        return $data;
    }

... naaa, too long.

Scope and stack

As we saw higher in documentation (link), it is possible to use one dumper class to dump bunch of entities from different perspective. Doing this though sometimes You might want to change way of working individual dumping methods. For example if You want to dump article it is good in feed provide also information about user that created this article, but You don't need in this use case all informations about user. But if You will dump User and his all articles it would be good to provide all information about user and less from article. To distinguish these use cases in method itself You can use two private properties available in all methods - scope and stack.

Example of using 'stack':

...
   public function dumpMyProject_User($entity) {

       $map = array(
           'id'        => 'id',
           'name'      => 'name'
       );

       if (count($this->stack) < 1) {
           # dump groups of user only if this is feed where
           # user is on higher level of hierarchy
           $map['groups'] = 'groups';
       }
       
       # stack is array and contain more information about where we 
       # are in execution stack, inspect this later if You need.

       return $this->toArray($entity, $map);
   }
...

Example of using 'scope':

class NewDumper extends Dumper
{
   ...
   public function dumpMyProject_User($entity) {

       $map = array(
           'id'        => 'id',
           'name'      => 'name'
       );

       if ($this->scope === 'dumpalsogroups') {
           $map['groups'] = 'groups';
       }

       return $this->toArray($entity, $map);
   }
}

NewDumper::getInstance()->dumpScope($user, 'dumpalsogroups');