Come codificare entità Doctrine in JSON nell'applicazione AJAX di Symfony 2.0?


89

Sto sviluppando un'app di gioco e utilizzo Symfony 2.0. Ho molte richieste AJAX al backend. E più risposte stanno convertendo l'entità in JSON. Per esempio:

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');
    }
}

E tutti i miei controller fanno la stessa cosa: ottenere un'entità e codificare alcuni dei suoi campi in JSON. So di poter utilizzare i normalizzatori e codificare tutte le entità. Ma cosa succede se un'entità ha ciclato i collegamenti ad un'altra entità? O il grafico delle entità è molto grande? Hai qualche suggerimento?

Penso a qualche schema di codifica per entità ... o utilizzo NormalizableInterfaceper evitare il ciclismo ..,

Risposte:


82

Un'altra opzione è utilizzare JMSSerializerBundle . Nel tuo controller allora fai

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

È possibile configurare la modalità di esecuzione della serializzazione utilizzando le annotazioni nella classe entità. Vedere la documentazione nel collegamento sopra. Ad esempio, ecco come escluderesti le entità collegate:

 /**
* 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;

7
è necessario aggiungere use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; usa JMS \ SerializerBundle \ Annotation \ Exclude; nella tua entità e installa JMSSerializerBundle affinché funzioni
ioleo

3
Funziona alla grande se lo modifichi in: return new Response ($ reports);
Greywire

7
Poiché le annotazioni sono state spostate fuori dal bundle, le istruzioni di utilizzo corrette ora sono: use JMS \ Serializer \ Annotation \ ExclusionPolicy; usa JMS \ Serializer \ Annotation \ Exclude;
Pier-Luc Gendreau

3
La documentazione di Doctrine dice di non serializzare oggetti o serializzare con grande cura.
Bluebaron

Non avevo nemmeno bisogno di installare JMSSerializerBundle. Il tuo codice ha funzionato senza richiedere JMSSerializerBundle.
Derk Jan Speelman il

147

Con php5.4 ora puoi fare:

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,
        );
    }
}

E poi chiama

json_encode(MyUserEntity);

1
mi piace molto questa soluzione!
Michael

3
Questa è un'ottima soluzione se stai legando per mantenere le tue dipendenze da altri bundle al minimo ...
Drmjo

5
E le entità collegate?
John the Ripper

7
Questo non sembra funzionare con le raccolte di entità (ad esempio: OneToManyrelazioni)
Pierre de LESPINAY

1
Ciò viola il principio di responsabilità unica e non va bene se le tue entità sono generate automaticamente dalla dottrina
jim smith

39

Puoi codificare automaticamente in Json, la tua entità complessa con:

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');

3
Grazie, ma ho un'entità Giocatore che ha un collegamento alla raccolta di entità del gioco e ogni entità del gioco ha un collegamento ai giocatori che ci hanno giocato. Qualcosa come questo. E pensi che GetSetMethodNormalizer funzionerà correttamente (utilizza un algoritmo ricorsivo)?
Dmytro Krasun

2
Sì, è ricorsivo e questo era il mio problema nel mio caso. Quindi, per entità specifiche, puoi usare CustomNormalizer e la sua NormalizableInterface come sembri sapere.
webda2l

2
Quando l'ho provato ho ricevuto "Errore irreversibile: dimensione della memoria consentita di 134217728 byte esaurita (ho provato ad allocare 64 byte) in /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php su linea 44 ". Mi chiedo perché?
Jason Swett

1
quando ho provato ho ottenuto sotto l'eccezione .. Errore fatale: raggiunto il livello massimo di annidamento della funzione di '100', interruzione! in C: \ wamp \ www \ miaapp \ application \ libraries \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php alla riga 223
user2350626


11

Per completare la risposta: Symfony2 viene fornito con un wrapper attorno a json_encode: Symfony / Component / HttpFoundation / JsonResponse

Utilizzo tipico nei controller:

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

Spero che questo ti aiuti

J


10

Ho scoperto che la soluzione al problema della serializzazione delle entità era la seguente:

#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 }

nel mio controller:

$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));

altro esempio:

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

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

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

return new Response($json);

puoi persino configurarlo per deserializzare gli array in http://api.symfony.com/2.0


3
C'è una voce del libro di cucina sull'utilizzo del componente Serializer in Symfony 2.3+, poiché ora puoi attivare quello integrato: symfony.com/doc/current/cookbook/serializer.html
althaus

6

Dovevo solo risolvere lo stesso problema: codificare in json un'entità ("Utente") con un'associazione bidirezionale uno-a-molti a un'altra entità ("Posizione").

Ho provato diverse cose e penso di aver trovato la migliore soluzione accettabile. L'idea era di utilizzare lo stesso codice scritto da David, ma in qualche modo intercettare la ricorsione infinita dicendo al Normalizer di fermarsi a un certo punto.

Non volevo implementare un normalizzatore personalizzato, poiché questo GetSetMethodNormalizer è un buon approccio secondo me (basato sulla riflessione, ecc.). Quindi ho deciso di sottoclassarlo, il che non è banale a prima vista, perché il metodo per dire se includere una proprietà (isGetMethod) è privato.

Ma si potrebbe sovrascrivere il metodo normalize, quindi ho intercettato a questo punto, semplicemente deselezionando la proprietà che fa riferimento a "Location" - così il ciclo infinito viene interrotto.

Nel codice assomiglia a questo:

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);
    }

} 

1
Mi chiedo quanto sarebbe facile generalizzare questo, in modo che 1. non sia mai necessario toccare le classi di entità, 2. non solo vuoto le "posizioni", ma ogni campo del tipo di collezioni che potenzialmente mappa ad altre entità. Cioè nessuna conoscenza interna / avanzata di Ent necessaria per serializzarlo, senza ricorsione.
Marcos

6

Ho avuto lo stesso problema e ho scelto di creare il mio codificatore, che si occuperà da solo della ricorsione.

Ho creato classi che implementano Symfony\Component\Serializer\Normalizer\NormalizerInterfacee un servizio che contiene ogni 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 esempio di normalizzatore:

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.)
    );
}

}

In un controller:

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

Il codice completo è qui: https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer


6

in 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 }

ed esempio per il tuo controller:

/**
 * Поиск сущности по ИД объекта и ИД языка
 * @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 );
}

ma i problemi con il tipo di campo \ DateTime rimarranno.


6

Questo è più un aggiornamento (per Symfony v: 2.7+ e JmsSerializer v: 0.13. * @ Dev) , così da evitare che Jms tenti di caricare e serializzare l'intero oggetto grafico (o in caso di relazione ciclica ..)

Modello:

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
      ......................
      ......................
   */

All'interno di un'azione:

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);

5

Se stai usando Symfony 2.7 o superiore e non vuoi includere alcun bundle aggiuntivo per la serializzazione, forse puoi seguire questo modo per seializzare le entità doctrine in json -

  1. Nel mio controller (comune, genitore), ho una funzione che prepara il serializzatore

    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. Quindi usalo per serializzare le entità in JSON

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

Fatto!

Ma potresti aver bisogno di qualche regolazione fine. Per esempio -


4

Quando è necessario creare molti endpoint API REST su Symfony, il modo migliore è utilizzare il seguente stack di bundle:

  1. JMSSerializerBundle per la serializzazione di entità Doctrine
  2. Bundle FOSRestBundle per listener della visualizzazione delle risposte. Inoltre può generare la definizione delle rotte in base al nome del controller / azione.
  3. NelmioApiDocBundle per la generazione automatica di documentazione in linea e Sandbox (che permette di testare endpoint senza alcun tool esterno).

Quando configuri tutto correttamente, il tuo codice entità sarà simile a:

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;
}

Quindi, codifica nel controller:

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();
    }
}

I vantaggi di tale configurazione sono:

  • Le annotazioni @JMS \ Expose () nell'entità possono essere aggiunte a campi semplici ea qualsiasi tipo di relazione. Inoltre c'è la possibilità di esporre il risultato dell'esecuzione di alcuni metodi (usa l'annotazione @JMS \ VirtualProperty () per quello)
  • Con i gruppi di serializzazione possiamo controllare i campi esposti in diverse situazioni.
  • I controller sono molto semplici. Il metodo di azione può restituire direttamente un'entità o un array di entità e verranno serializzate automaticamente.
  • E @ApiDoc () permette di testare l'endpoint direttamente dal browser, senza alcun client REST o codice JavaScript

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.