7

I have to log the changes of each entity. I've Listener which listens for doctrine's events on preRemove, postUpdate and postDelete. My entity AccessModule has relations:

App\Entity\AccessModule.php

/**
 * @ORM\OneToMany(targetEntity="App\Entity\AccessModule", mappedBy="parent")
 * @ORM\OrderBy({"id" = "ASC"})
 */
private $children;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\AccessModule", inversedBy="children")
 * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
 */
private $parent;

/**
 * @ORM\ManyToMany(targetEntity="App\Entity\AccessModuleRoute", inversedBy="access_modules")
 * @ORM\JoinTable(name="access_routes",
 *     joinColumns={@ORM\JoinColumn(name="access_module_id", referencedColumnName="id")},
 *     inverseJoinColumns={@ORM\JoinColumn(name="route_id", referencedColumnName="id")})
 *
 */
private $routes;

in listener: App\EventListener\EntityListener.php

use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;


    $encoders = [new XmlEncoder(), new JsonEncoder()];
    $normalizer = new ObjectNormalizer();

        $normalizer->setCircularReferenceHandler(function ($object) {
            return $object->getId();
        });

    $this->serializer = new Serializer([$normalizer], $encoders);


public function createLog(LifecycleEventArgs $args, $action){
    $em = $args->getEntityManager();
    $entity = $args->getEntity();
    if ($this->tokenStorage->getToken()->getUser()) {
        $username = $this->tokenStorage->getToken()->getUser()->getUsername();
    } else {
        $username = 'anon'; // TODO Remove anon. set null value
    }

    $log = new Log();
//      $log->setData('dddd')
        $log->setData($this->serializer->serialize($entity, ''json)
            ->setAction($action)
            ->setActionTime(new \DateTime())
            ->setUser($username)
            ->setEntityClass(get_class($entity));
        $em->persist($log);
        $em->flush();
    }

I've problem with serialization When I use $log->setData($entity) I get problem with Circular. Whan I do serialization $log->setData($this->serializer->serialize($entity, ''json) I get full of relations, with parent's children, with children children. In a result I get full tree :/ I'd like to get

Expect

[
 'id' => ID,
 'name' => NAME,
 'parent' => parent_id // ManyToOne, I'd like get its id
 'children' => [$child_id, $child_id, $child_id] // array of $id of children array collection
]

(ofcourse this is draft before encode it to json)

How can I get expected data without full relations?

Dhia Djobbi
  • 1,176
  • 2
  • 15
  • 35
nicram
  • 353
  • 3
  • 7
  • 24

3 Answers3

9

Thing you are looking for is called serialization groups: here and here.

Now let me explain how it works. It's quite simple. Say you have Post Entity:

class Post
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"default"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User\User")
     * @Groups({"default"})
     */
    private $author;
}

And you have also User Entity:

class User
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"default"})
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=40)
     * @Groups({"default"})
     */
    private $firstName;

    /**
     * @ORM\Column(type="string", length=40)
     */
    private $lastName;
}

Post can have author(User) but I don't want to return all User data every single time. I'm interested only in id and first name.

Take a closer look at @Groups annotation. You can specify so called serialization groups. It's nothing more than convinient way of telling Symfony which data you would like to have in your result set.

You have to tell Symfony serializer which relationships you would like to keep by adding relevant groups in form of annotation above property/getter. You also have to specify which properties or getters of your relationships you would like to keep.

Now how to let Symfony know about that stuff?

When you prepare/configure your serializaition service you just simply have to provide defined groups like that:

return $this->serializer->serialize($data, 'json', ['groups' => ['default']]);

It's good to build some kind of wrapper service around native symfony serializer so you can simplify the whole process and make it more reusable.

Also make sure that serializer is correctly configured - otherwise it will not take these group into account.

That is also one way(among other ways) of "handling" circular references.

Now you just need to work on how you will format your result set.

Robert
  • 1,206
  • 1
  • 17
  • 33
  • 2
    Great answer. I'd like to add just this little thing: To tell Symfony that you want to read the Groups from the annotation you must do this. $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); Defining the classMetadataFactory and passing it as a perameter to your ObjectNormalizer. $normalizer = new ObjectNormalizer($classMetadataFactory); https://symfony.com/doc/current/components/serializer.html#configure-name-conversion-using-metadata – SpicyTacos23 Nov 20 '19 at 09:10
6

ignored_attributes provides a quick, and easy way to accomplish what you're looking for.

$serializer->serialize($object, 'json', ['ignored_attributes' => ['ignored_property']]); 

Heres how its used with the serializer:

use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

$person = new Person();
$person->setName('foo');
$person->setAge(99);

$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();

$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($person, 'json', ['ignored_attributes' => ['age']]); 

Documentation: https://symfony.com/doc/current/components/serializer.html#ignoring-attributes

Brennan Walsh
  • 410
  • 4
  • 8
3

Tested in Symfony 4.1, here is the documentation that actually works https://symfony.com/blog/new-in-symfony-2-7-serialization-groups

Robert's explanation https://stackoverflow.com/a/48756847/579646 is missing the $classMetadataFactory in order to work. Here is my code:

    $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
    $encoders = [new JsonEncoder()];
    $normalizer = new ObjectNormalizer($classMetadataFactory);
    $normalizer->setCircularReferenceLimit(2);
    // Add Circular reference handler
    $normalizer->setCircularReferenceHandler(function ($object) {
        return $object->getId();
    });
    $normalizers = [$normalizer];
    $serializer = new Serializer($normalizers, $encoders);
    $jsonContent = $serializer->serialize($jobs, 'json', array('groups' => ['default']));

    return JsonResponse::fromJsonString($jsonContent);
max4ever
  • 11,909
  • 13
  • 77
  • 115
  • Well spotted. I was just trying to briefly explain what is the problem without getting into too much detail. But yeah you can hook things up in number of ways and minimise amount of code you have to write later on. – Robert Apr 02 '19 at 09:19