Comment encoder des entités Doctrine en JSON dans l'application Symfony 2.0 AJAX?

89

Je développe une application de jeu et j'utilise Symfony 2.0. J'ai de nombreuses requêtes AJAX au backend. Et plus de réponses convertissent l'entité en JSON. Par exemple:

class DefaultController extends Controller
{           
    public function launchAction()
    {   
        $user = $this->getDoctrine()
                     ->getRepository('UserBundle:User')                
                     ->find($id);

        // encode user to json format
        $userDataAsJson = $this->encodeUserDataToJson($user);
        return array(
            'userDataAsJson' => $userDataAsJson
        );            
    }

    private function encodeUserDataToJson(User $user)
    {
        $userData = array(
            'id' => $user->getId(),
            'profile' => array(
                'nickname' => $user->getProfile()->getNickname()
            )
        );

        $jsonEncoder = new JsonEncoder();        
        return $jsonEncoder->encode($userData, $format = 'json');
    }
}

Et tous mes contrôleurs font la même chose: obtenir une entité et encoder certains de ses champs en JSON. Je sais que je peux utiliser des normalisateurs et encoder tous les droits. Mais que se passe-t-il si une entité a cyclé des liens vers une autre entité? Ou le graphe d'entités est très grand? Avez-vous des suggestions?

Je pense à un schéma d'encodage pour les entités ... ou à l'utilisation NormalizableInterfacepour éviter le cyclisme ..,

Dmytro Krasun
la source

Réponses:

82

Une autre option consiste à utiliser JMSSerializerBundle . Dans votre contrôleur, vous faites alors

$serializer = $this->container->get('serializer');
$reports = $serializer->serialize($doctrineobject, 'json');
return new Response($reports); // should be $reports as $doctrineobject is not serialized

Vous pouvez configurer la façon dont la sérialisation est effectuée à l'aide d'annotations dans la classe d'entité. Voir la documentation dans le lien ci-dessus. Par exemple, voici comment exclure les entités liées:

 /**
* Iddp\RorBundle\Entity\Report
*
* @ORM\Table()
* @ORM\Entity(repositoryClass="Iddp\RorBundle\Entity\ReportRepository")
* @ExclusionPolicy("None")
*/
....
/**
* @ORM\ManyToOne(targetEntity="Client", inversedBy="reports")
* @ORM\JoinColumn(name="client_id", referencedColumnName="id")
* @Exclude
*/
protected $client;
Sofia
la source
7
vous devez ajouter use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; utilisez JMS \ SerializerBundle \ Annotation \ Exclude; dans votre entité et installez JMSSerializerBundle pour que cela fonctionne
ioleo
3
Fonctionne très bien si vous le changez en: return new Response ($ reports);
Greywire
7
Puisque les annotations ont été déplacées hors du bundle, les instructions d'utilisation correctes sont désormais: use JMS \ Serializer \ Annotation \ ExclusionPolicy; utilisez JMS \ Serializer \ Annotation \ Exclude;
Pier-Luc Gendreau
3
La documentation de Doctrine dit de ne pas sérialiser les objets ni de sérialiser avec le plus grand soin.
Bluebaron
Je n'ai même pas eu besoin d'installer JMSSerializerBundle. Votre code a fonctionné sans nécessiter JMSSerializerBundle.
Derk Jan Speelman
147

Avec php5.4, vous pouvez maintenant faire:

use JsonSerializable;

/**
* @Entity(repositoryClass="App\Entity\User")
* @Table(name="user")
*/
class MyUserEntity implements JsonSerializable
{
    /** @Column(length=50) */
    private $name;

    /** @Column(length=50) */
    private $login;

    public function jsonSerialize()
    {
        return array(
            'name' => $this->name,
            'login'=> $this->login,
        );
    }
}

Et puis appelle

json_encode(MyUserEntity);
SparSio
la source
1
j'aime beaucoup cette solution!
Michael
3
C'est une excellente solution si vous essayez de réduire au minimum vos dépendances sur d'autres bundles ...
Drmjo
5
Qu'en est-il des entités liées?
John the Ripper
7
Cela ne semble pas fonctionner avec les collections d'entités (ex: OneToManyrelations)
Pierre de LESPINAY
1
Cela viole le principe de responsabilité unique et n'est pas bon si vos entités sont générées automatiquement par la doctrine
jim smith
39

Vous pouvez encoder automatiquement dans Json, votre entité complexe avec:

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;

$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new 
JsonEncoder()));
$json = $serializer->serialize($entity, 'json');
webda2l
la source
3
Merci, mais j'ai une entité Player qui a un lien vers la collection d'entités de jeu et chaque entité de jeu a un lien vers les joueurs qui y ont joué. Quelque chose comme ça. Et pensez-vous que GetSetMethodNormalizer fonctionnera correctement (il utilise un algorithme récursif)?
Dmytro Krasun
2
Oui, c'est récursif et c'était mon problème dans mon cas. Ainsi, pour des entités spécifiques, vous pouvez utiliser le CustomNormalizer et son NormalizableInterface comme vous semblez le savoir.
webda2l
2
Quand j'ai essayé cela, j'ai eu "Erreur fatale: taille de mémoire autorisée de 134217728 octets épuisée (j'ai essayé d'allouer 64 octets) dans /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php sur ligne 44 ". Je me demande pourquoi?
Jason Swett
1
quand j'ai essayé, je suis tombé en dessous de l'exception .. Erreur fatale: Niveau maximum d'imbrication de fonction de «100» atteint, abandon! dans C: \ wamp \ www \ myapp \ application \ bibliothèques \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php sur la ligne 223
user2350626
1
@ user2350626, voir stackoverflow.com/questions/4293775/…
webda2l
11

Pour compléter la réponse: Symfony2 est livré avec un wrapper autour de json_encode: Symfony / Component / HttpFoundation / JsonResponse

Utilisation typique dans vos contrôleurs:

...
use Symfony\Component\HttpFoundation\JsonResponse;
...
public function acmeAction() {
...
return new JsonResponse($array);
}

J'espère que cela t'aides

J

Jérôme
la source
10

J'ai trouvé que la solution au problème de la sérialisation des entités était la suivante:

#config/config.yml

services:
    serializer.method:
        class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
    serializer.encoder.json:
        class: Symfony\Component\Serializer\Encoder\JsonEncoder
    serializer:
        class: Symfony\Component\Serializer\Serializer
        arguments:
            - [@serializer.method]
            - {json: @serializer.encoder.json }

dans mon contrôleur:

$serializer = $this->get('serializer');

$entity = $this->get('doctrine')
               ->getRepository('myBundle:Entity')
               ->findOneBy($params);


$collection = $this->get('doctrine')
               ->getRepository('myBundle:Entity')
               ->findBy($params);

$toEncode = array(
    'response' => array(
        'entity' => $serializer->normalize($entity),
        'entities' => $serializer->normalize($collection)
    ),
);

return new Response(json_encode($toEncode));

autre exemple:

$serializer = $this->get('serializer');

$collection = $this->get('doctrine')
               ->getRepository('myBundle:Entity')
               ->findBy($params);

$json = $serializer->serialize($collection, 'json');

return new Response($json);

vous pouvez même le configurer pour désérialiser les tableaux dans http://api.symfony.com/2.0

rkmax
la source
3
Il y a une entrée de livre de recettes sur l'utilisation du composant Serializer dans Symfony 2.3+, car vous pouvez maintenant activer celui intégré: symfony.com/doc/current/cookbook/serializer.html
althaus
6

Je devais juste résoudre le même problème: encoder json une entité ("User") ayant une association bidirectionnelle un-à-plusieurs avec une autre entité ("Location").

J'ai essayé plusieurs choses et je pense que maintenant j'ai trouvé la meilleure solution acceptable. L'idée était d'utiliser le même code que celui écrit par David, mais d'intercepter d'une manière ou d'une autre la récursion infinie en disant au Normalizer de s'arrêter à un moment donné.

Je ne voulais pas implémenter de normalisateur personnalisé, car ce GetSetMethodNormalizer est une belle approche à mon avis (basée sur la réflexion, etc.). J'ai donc décidé de le sous-classer, ce qui n'est pas trivial à première vue, car la méthode pour dire s'il faut inclure une propriété (isGetMethod) est privée.

Mais, on pourrait remplacer la méthode de normalisation, donc j'ai intercepté à ce stade, en désactivant simplement la propriété qui fait référence à "Location" - donc la boucle inifinite est interrompue.

Dans le code, cela ressemble à ceci:

class GetSetMethodNormalizer extends \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer {

    public function normalize($object, $format = null)
    {
        // if the object is a User, unset location for normalization, without touching the original object
        if($object instanceof \Leonex\MoveBundle\Entity\User) {
            $object = clone $object;
            $object->setLocations(new \Doctrine\Common\Collections\ArrayCollection());
        }

        return parent::normalize($object, $format);
    }

} 
oxy
la source
1
Je me demande à quel point il serait facile de généraliser ceci, de sorte que 1. ne jamais avoir besoin de toucher les classes Entity, 2. Pas simplement vide les "Locations", mais chaque champ de type Collections qui correspond potentiellement à d'autres Entités. C'est-à-dire qu'aucune connaissance interne / avancée d'Ent n'est requise pour le sérialiser, sans récursivité.
Marcos
6

J'ai eu le même problème et j'ai choisi de créer mon propre encodeur, qui se chargera tout seul de la récursivité.

J'ai créé des classes qui implémentent Symfony\Component\Serializer\Normalizer\NormalizerInterface, et un service qui contient tout NormalizerInterface.

#This is the NormalizerService

class NormalizerService 
{

   //normalizer are stored in private properties
   private $entityOneNormalizer;
   private $entityTwoNormalizer;

   public function getEntityOneNormalizer()
   {
    //Normalizer are created only if needed
    if ($this->entityOneNormalizer == null)
        $this->entityOneNormalizer = new EntityOneNormalizer($this); //every normalizer keep a reference to this service

    return $this->entityOneNormalizer;
   }

   //create a function for each normalizer



  //the serializer service will also serialize the entities 
  //(i found it easier, but you don't really need it)
   public function serialize($objects, $format)
   {
     $serializer = new Serializer(
            array(
                $this->getEntityOneNormalizer(),
                $this->getEntityTwoNormalizer()
            ),
            array($format => $encoder) );

     return $serializer->serialize($response, $format);
}

Un exemple de normalisateur:

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class PlaceNormalizer implements NormalizerInterface {

private $normalizerService;

public function __construct($normalizerService)
{
    $this->service = normalizerService;

}

public function normalize($object, $format = null) {
    $entityTwo = $object->getEntityTwo();
    $entityTwoNormalizer = $this->service->getEntityTwoNormalizer();

    return array(
        'param' => object->getParam(),
        //repeat for every parameter
        //!!!! this is where the entityOneNormalizer dealt with recursivity
        'entityTwo' => $entityTwoNormalizer->normalize($entityTwo, $format.'_without_any_entity_one') //the 'format' parameter is adapted for ignoring entity one - this may be done with different ways (a specific method, etc.)
    );
}

}

Dans un contrôleur:

$normalizerService = $this->get('normalizer.service'); //you will have to configure services.yml
$json = $normalizerService->serialize($myobject, 'json');
return new Response($json);

Le code complet est ici: https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer

Julien Fastré
la source
6

dans Symfony 2.3

/app/config/config.yml

framework:
    # сервис конвертирования объектов в массивы, json, xml и обратно
    serializer:
        enabled: true

services:
    object_normalizer:
        class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
        tags:
        # помечаем к чему относится этот сервис, это оч. важно, т.к. иначе работать не будет
          - { name: serializer.normalizer }

et exemple pour votre contrôleur:

/**
 * Поиск сущности по ИД объекта и ИД языка
 * @Route("/search/", name="orgunitSearch")
 */
public function orgunitSearchAction()
{
    $array = $this->get('request')->query->all();

    $entity = $this->getDoctrine()
        ->getRepository('IntranetOrgunitBundle:Orgunit')
        ->findOneBy($array);

    $serializer = $this->get('serializer');
    //$json = $serializer->serialize($entity, 'json');
    $array = $serializer->normalize($entity);

    return new JsonResponse( $array );
}

mais les problèmes avec le type de champ \ DateTime resteront.

Lebnik
la source
6

Ceci est plus une mise à jour (pour Symfony v: 2.7+ et JmsSerializer v: 0.13. * @ Dev) , donc pour éviter que Jms tente de charger et sérialiser l'ensemble du graphe d'objets (ou en cas de relation cyclique ..)

Modèle:

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;  
use JMS\Serializer\Annotation\Exclude;  
use JMS\Serializer\Annotation\MaxDepth; /* <=== Required */
/**
 * User
 *
 * @ORM\Table(name="user_table")
///////////////// OTHER Doctrine proprieties //////////////
 */
 public class User
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected   $id;

    /**
     * @ORM\ManyToOne(targetEntity="FooBundle\Entity\Game")
     * @ORM\JoinColumn(nullable=false)
     * @MaxDepth(1)
     */
    protected $game;
   /*
      Other proprieties ....and Getters ans setters
      ......................
      ......................
   */

À l'intérieur d'une action:

use JMS\Serializer\SerializationContext;
  /* Necessary include to enbale max depth */

  $users = $this
              ->getDoctrine()
              ->getManager()
              ->getRepository("FooBundle:User")
              ->findAll();

  $serializer = $this->container->get('jms_serializer');
  $jsonContent = $serializer
                   ->serialize(
                        $users, 
                        'json', 
                        SerializationContext::create()
                                 ->enableMaxDepthChecks()
                  );

  return new Response($jsonContent);
Timmz
la source
5

Si vous utilisez Symfony 2.7 ou supérieur et que vous ne souhaitez pas inclure de bundle supplémentaire pour la sérialisation, vous pouvez peut-être suivre cette méthode pour saisir les entités de doctrine en json -

  1. Dans mon contrôleur (commun, parent), j'ai une fonction qui prépare le sérialiseur

    use Symfony\Component\Serializer\Encoder\JsonEncoder;
    use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
    use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
    use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
    use Symfony\Component\Serializer\Serializer;
    
    // -----------------------------
    
    /**
     * @return Serializer
     */
    protected function _getSerializer()
    {  
        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $normalizer           = new ObjectNormalizer($classMetadataFactory);
    
        return new Serializer([$normalizer], [new JsonEncoder()]);
    }
  2. Ensuite, utilisez-le pour sérialiser les entités en JSON

    $this->_getSerializer()->normalize($anEntity, 'json');
    $this->_getSerializer()->normalize($arrayOfEntities, 'json');

Terminé!

Mais vous aurez peut-être besoin de quelques ajustements. Par exemple -

Anis
la source
4

Lorsque vous devez créer de nombreux points de terminaison d'API REST sur Symfony, le meilleur moyen est d'utiliser la pile de bundles suivante:

  1. JMSSerializerBundle pour la sérialisation des entités Doctrine
  2. FOSRestBundle pour l'écouteur de vue de réponse. Il peut également générer la définition des routes en fonction du nom du contrôleur / de l'action.
  3. NelmioApiDocBundle pour générer automatiquement la documentation en ligne et Sandbox (qui permet de tester les endpoint sans aucun outil externe).

Lorsque vous configurez tout correctement, votre code d'entité ressemblera à:

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;

/**
 * @ORM\Table(name="company")
 */
class Company
{

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     *
     * @JMS\Expose()
     * @JMS\SerializedName("name")
     * @JMS\Groups({"company_overview"})
     */
    private $name;

    /**
     * @var Campaign[]
     *
     * @ORM\OneToMany(targetEntity="Campaign", mappedBy="company")
     * 
     * @JMS\Expose()
     * @JMS\SerializedName("campaigns")
     * @JMS\Groups({"campaign_overview"})
     */
    private $campaigns;
}

Ensuite, codez dans le contrôleur:

use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\View;

class CompanyController extends Controller
{

    /**
     * Retrieve all companies
     *
     * @View(serializerGroups={"company_overview"})
     * @ApiDoc()
     *
     * @return Company[]
     */
    public function cgetAction()
    {
        return $this->getDoctrine()->getRepository(Company::class)->findAll();
    }
}

Les avantages d'une telle configuration sont:

  • Les annotations @JMS \ Expose () dans entity peuvent être ajoutées à des champs simples et à tout type de relations. Il est également possible d'exposer le résultat de l'exécution de certaines méthodes (utilisez l'annotation @JMS \ VirtualProperty () pour cela)
  • Avec les groupes de sérialisation, nous pouvons contrôler les champs exposés dans différentes situations.
  • Les contrôleurs sont très simples. La méthode d'action peut renvoyer directement une entité ou un tableau d'entités, et elles seront automatiquement sérialisées.
  • Et @ApiDoc () permet de tester le point de terminaison directement depuis le navigateur, sans client REST ni code JavaScript
Maksym Moskvychev
la source
2

Désormais, vous pouvez également utiliser Doctrine ORM Transformations pour convertir des entités en tableaux imbriqués de scalaires et inversement.

ScorpionT1000
la source