¿Cómo codificar entidades Doctrine a JSON en la aplicación Symfony 2.0 AJAX?

89

Estoy desarrollando una aplicación de juego y usando Symfony 2.0. Tengo muchas solicitudes AJAX al backend. Y más respuestas está convirtiendo la entidad a JSON. Por ejemplo:

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

Y todos mis controladores hacen lo mismo: obtienen una entidad y codifican algunos de sus campos en JSON. Sé que puedo usar normalizadores y codificar todas las entidades. Pero, ¿qué pasa si una entidad tiene enlaces cíclicos a otra entidad? ¿O el gráfico de entidades es muy grande? ¿Tienes alguna sugerencia?

Pienso en algún esquema de codificación para entidades ... o en usar NormalizableInterfacepara evitar el ciclo ...,

Dmytro Krasun
fuente

Respuestas:

82

Otra opción es utilizar JMSSerializerBundle . En su controlador entonces lo hace

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

Puede configurar cómo se realiza la serialización mediante el uso de anotaciones en la clase de entidad. Consulte la documentación en el enlace de arriba. Por ejemplo, así es como excluiría entidades vinculadas:

 /**
* 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;
Sofía
fuente
7
debe agregar use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; use JMS \ SerializerBundle \ Annotation \ Exclude; en su entidad e instale JMSSerializerBundle para que esto funcione
ioleo
3
Funciona muy bien si lo cambia a: return new Response ($ informes);
Greywire
7
Dado que las anotaciones se han movido fuera del paquete, las instrucciones de uso correctas ahora son: use JMS \ Serializer \ Annotation \ ExclusionPolicy; use JMS \ Serializer \ Annotation \ Exclude;
Pier-Luc Gendreau
3
La documentación de Doctrine dice que no se deben serializar objetos o serializar con mucho cuidado.
Bluebaron
Ni siquiera necesitaba instalar JMSSerializerBundle. Su código funcionó sin requerir JMSSerializerBundle.
Derk Jan Speelman
147

Con php5.4 ahora puedes hacer:

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

Y luego llama

json_encode(MyUserEntity);
SparSio
fuente
1
¡Me gusta mucho esta solución!
Michael
3
Esta es una gran solución si desea mantener sus dependencias en otros paquetes al mínimo ...
Drmjo
5
¿Qué pasa con las entidades vinculadas?
John the Ripper
7
Esto no parece funcionar con colecciones de entidades (es decir: OneToManyrelaciones)
Pierre de LESPINAY
1
Esto viola el principio de responsabilidad única y no es bueno si sus entidades son generadas automáticamente por doctrina
jim smith
39

Puede codificar automáticamente en Json, su entidad compleja 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');
webda2l
fuente
3
Gracias, pero tengo una entidad de jugador que tiene un enlace a la colección de entidades de juego y cada entidad de juego tiene un enlace a los jugadores que jugaron en ella. Algo como esto. ¿Y cree que GetSetMethodNormalizer funcionará correctamente (utiliza un algoritmo recursivo)?
Dmytro Krasun
2
Sí, es recursivo y ese fue mi problema en mi caso. Entonces, para entidades específicas, puede usar CustomNormalizer y su NormalizableInterface como parece saber.
webda2l
2
Cuando probé esto, obtuve "Error fatal: tamaño de memoria permitido de 134217728 bytes agotado (intenté asignar 64 bytes) en /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php en línea 44 ". ¿Me pregunto porque?
Jason Swett
1
cuando lo intenté, obtuve una excepción por debajo. Error fatal: ¡Se alcanzó el nivel máximo de anidación de funciones de '100', abortando! en C: \ wamp \ www \ myapp \ application \ libraries \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php en la línea 223
user2350626
1
@ user2350626, consulte stackoverflow.com/questions/4293775/…
webda2l
11

Para completar la respuesta: Symfony2 viene con una envoltura alrededor de json_encode: Symfony / Component / HttpFoundation / JsonResponse

Uso típico en sus controladores:

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

Espero que esto ayude

J

Jerome
fuente
10

Encontré que la solución al problema de serializar entidades fue la siguiente:

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

en mi controlador:

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

otro ejemplo:

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

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

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

return new Response($json);

incluso puede configurarlo para deserializar matrices en http://api.symfony.com/2.0

rkmax
fuente
3
Hay una entrada de libro de cocina sobre el uso del componente Serializer en Symfony 2.3+, ya que ahora puede activar el integrado: symfony.com/doc/current/cookbook/serializer.html
althaus
6

Solo tenía que resolver el mismo problema: codificación json de una entidad ("Usuario") que tiene una Asociación bidireccional de uno a muchos con otra entidad ("Ubicación").

Probé varias cosas y creo que ahora encontré la mejor solución aceptable. La idea era usar el mismo código que escribió David, pero de alguna manera interceptar la recursividad infinita diciéndole al Normalizador que se detenga en algún momento.

No quería implementar un normalizador personalizado, ya que este GetSetMethodNormalizer es un buen enfoque en mi opinión (basado en la reflexión, etc.). Así que he decidido subclasificarlo, lo cual no es trivial a primera vista, porque el método para decir si incluir una propiedad (isGetMethod) es privado.

Pero, uno podría anular el método de normalización, por lo que intercepté en este punto, simplemente desarmando la propiedad que hace referencia a "Ubicación", por lo que el ciclo infinito se interrumpe.

En el código se ve así:

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

} 
oxi
fuente
1
Me pregunto qué tan fácil sería generalizar esto, de modo que 1. nunca haya necesidad de tocar las clases de Entidad, 2. No solo deje en blanco las "Ubicaciones", sino todos los campos de tipo Colecciones que potencialmente se asignen a otras Entidades. Es decir, no se requieren conocimientos internos / avanzados de Ent para serializarlo, sin recursividad.
Marcos
6

Tuve el mismo problema y decidí crear mi propio codificador, que se encargará por sí mismo de la recursividad.

Creé clases que implementan Symfony\Component\Serializer\Normalizer\NormalizerInterfacey un servicio que contiene cada 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 ejemplo de normalizador:

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

}

En un controlador:

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

El código completo está aquí: https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer

Julien Fastré
fuente
6

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

y ejemplo para su controlador:

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

pero los problemas con el tipo de campo \ DateTime permanecerán.

Lebnik
fuente
6

Esto es más una actualización (para Symfony v: 2.7+ y JmsSerializer v: 0.13. * @ Dev) , para evitar que Jms intente cargar y serializar todo el gráfico de objetos (o en caso de relación cíclica ..)

Modelo:

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

Dentro de una acción:

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
fuente
5

Si está utilizando Symfony 2.7 o superior , y no desea incluir ningún paquete adicional para serializar, tal vez pueda seguir esta forma para seializar entidades de doctrine a json:

  1. En mi controlador (común, principal), tengo una función que prepara el serializador

    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. Luego úselo para serializar Entidades a JSON

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

¡Hecho!

Pero es posible que necesite algunos ajustes. Por ejemplo -

Anis
fuente
4

Cuando necesite crear muchos puntos finales de API REST en Symfony, la mejor manera es utilizar la siguiente pila de paquetes:

  1. JMSSerializerBundle para la serialización de entidades Doctrine
  2. Paquete FOSRestBundle para escucha de vista de respuesta. También puede generar la definición de rutas basadas en el nombre del controlador / acción.
  3. NelmioApiDocBundle para generar automáticamente documentación en línea y Sandbox (que permite probar endpoints sin ninguna herramienta externa).

Cuando configure todo correctamente, el código de su entidad se verá así:

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

Luego, codifique en el controlador:

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

Los beneficios de dicha configuración son:

  • Las anotaciones @JMS \ Expose () en la entidad se pueden agregar a campos simples y a cualquier tipo de relaciones. También existe la posibilidad de exponer el resultado de la ejecución de algún método (use la anotación @JMS \ VirtualProperty () para eso)
  • Con los grupos de serialización podemos controlar los campos expuestos en diferentes situaciones.
  • Los controladores son muy simples. El método de acción puede devolver directamente una entidad o una matriz de entidades, y se serializarán automáticamente.
  • Y @ApiDoc () permite probar el punto final directamente desde el navegador, sin ningún cliente REST o código JavaScript
Maksym Moskvychev
fuente