Como codificar entidades Doctrine para JSON no aplicativo Symfony 2.0 AJAX?

89

Estou desenvolvendo um aplicativo de jogo e usando Symfony 2.0. Tenho muitas solicitações AJAX para o back-end. E mais respostas está convertendo entidade em JSON. Por exemplo:

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 todos os meus controladores fazem a mesma coisa: obtêm uma entidade e codificam alguns de seus campos para JSON. Eu sei que posso usar normalizadores e codificar todas as entidades. Mas e se uma entidade tiver um ciclo de links para outra entidade? Ou o gráfico de entidades é muito grande? Você tem alguma sugestão?

Eu penso em algum esquema de codificação para entidades ... ou usando NormalizableInterfacepara evitar ciclos ...,

Dmytro Krasun
fonte

Respostas:

82

Outra opção é usar o JMSSerializerBundle . Em seu controlador, você faz

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

Você pode configurar como a serialização é feita usando anotações na classe de entidade. Veja a documentação no link acima. Por exemplo, veja como você excluiria 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;
Sofia
fonte
7
você precisa adicionar use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; use JMS \ SerializerBundle \ Annotation \ Exclude; em sua entidade e instale JMSSerializerBundle para que isso funcione
ioleo
3
Funciona muito bem se você alterar para: return new Response ($ reports);
Greywire
7
Como as anotações foram removidas do pacote, as instruções de uso corretas agora são: use JMS \ Serializer \ Annotation \ ExclusionPolicy; use JMS \ Serializer \ Annotation \ Exclude;
Pier-Luc Gendreau
3
A documentação do Doctrine diz para não serializar objetos ou serializar com muito cuidado.
Bluebaron
Nem precisei instalar o JMSSerializerBundle. Seu código funcionou sem exigir JMSSerializerBundle.
Derk Jan Speelman
147

Com php5.4 agora você pode fazer:

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 então ligue

json_encode(MyUserEntity);
SparSio
fonte
1
eu gosto muito dessa solução!
Michael
3
Esta é uma ótima solução se você está tentando manter suas dependências de outros pacotes no mínimo ...
Drmjo
5
E quanto às entidades vinculadas?
John the Ripper,
7
Isso não parece funcionar com coleções de entidades (por exemplo: OneToManyrelações)
Pierre de LESPINAY
1
Isso viola o princípio de responsabilidade única e não é bom se suas entidades são geradas automaticamente por doutrina
Jim Smith
39

Você pode codificar automaticamente em Json, sua entidade complexa com:

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
fonte
3
Obrigado, mas eu tenho a entidade Jogador que tem um link para a coleção de entidades do Jogo e cada entidade do Jogo tem um link para os jogadores que jogaram nela. Algo assim. E você acha que GetSetMethodNormalizer funcionará corretamente (ele usa algoritmo recursivo)?
Dmytro Krasun
2
Sim, é recursivo e esse foi o meu problema no meu caso. Portanto, para entidades específicas, você pode usar o CustomNormalizer e sua NormalizableInterface como você parece saber.
webda2l
2
Quando tentei fazer isso, recebi "Erro fatal: tamanho de memória permitido de 134217728 bytes esgotados (tentei alocar 64 bytes) em /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php em linha 44 ". Eu quero saber porque?
Jason Swett
1
Quando tentei, obtive abaixo da exceção. Erro fatal: Nível máximo de aninhamento de função de '100' atingido, interrompendo! em C: \ wamp \ www \ myapp \ application \ libraries \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php na linha 223
user2350626
1
@ user2350626, consulte stackoverflow.com/questions/4293775/…
webda2l
11

Para completar a resposta: Symfony2 vem com um wrapper em torno de json_encode: Symfony / Component / HttpFoundation / JsonResponse

Uso típico em seus controladores:

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

Espero que isto ajude

J

jerome
fonte
10

Descobri que a solução para o problema de serialização de entidades foi a seguinte:

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

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

outro exemplo:

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

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

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

return new Response($json);

você pode até configurá-lo para desserializar matrizes em http://api.symfony.com/2.0

rkmax
fonte
3
Há uma entrada no livro de receitas sobre o uso do componente Serializer no Symfony 2.3+, já que agora você pode ativar o componente integrado: symfony.com/doc/current/cookbook/serializer.html
althaus
6

Eu apenas tive que resolver o mesmo problema: codificação json de uma entidade ("Usuário") tendo uma Associação Bidirecional Um-para-Muitos para outra Entidade ("Local").

Tentei várias coisas e acho que agora encontrei a solução mais aceitável. A ideia era usar o mesmo código escrito por David, mas de alguma forma interceptar a recursão infinita dizendo ao Normalizador para parar em algum ponto.

Eu não queria implementar um normalizador personalizado, já que GetSetMethodNormalizer é uma boa abordagem na minha opinião (com base em reflexão, etc.). Portanto, decidi criar uma subclasse, o que não é trivial à primeira vista, porque o método para dizer se incluir uma propriedade (isGetMethod) é privado.

Mas, alguém poderia sobrescrever o método normalize, então eu interceptei neste ponto, simplesmente desmarcando a propriedade que faz referência a "Location" - então o loop infinito é interrompido.

No código, é assim:

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
fonte
1
Eu me pergunto como seria fácil generalizar isso, de modo que 1. nunca precise tocar nas classes de Entidade, 2. Não apenas em branco os "Locais", mas todos os campos do tipo Coleções que potencialmente mapeiam para outras Entidades. Ou seja, nenhum conhecimento interno / avançado de Ent é necessário para serializá-lo, sem recursão.
Marcos
6

Eu tive o mesmo problema e optei por criar meu próprio codificador, que irá lidar sozinho com a recursão.

Eu criei classes que implementam Symfony\Component\Serializer\Normalizer\NormalizerInterfacee um serviço que contém todosNormalizerInterface .

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

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

}

Em um controlador:

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

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

Julien Fastré
fonte
6

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

e exemplo para o seu 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 );
}

mas os problemas com o tipo de campo \ DateTime permanecerão.

Lebnik
fonte
6

Esta é mais uma atualização (para Symfony v: 2.7+ e JmsSerializer v: 0.13. * @ Dev) , para evitar que Jms tente carregar e serializar todo o gráfico do objeto (ou no caso de relação 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 uma ação:

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

Se você estiver usando Symfony 2.7 ou superior e não quiser incluir nenhum pacote adicional para serialização, talvez você possa seguir este caminho para serializar entidades de doutrina para json -

  1. No meu controlador (pai comum), tenho uma função que prepara o 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. Em seguida, use-o para serializar entidades para JSON

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

Feito!

Mas você pode precisar de alguns ajustes. Por exemplo -

Anis
fonte
4

Quando você precisa criar muitos endpoints da API REST no Symfony, a melhor maneira é usar a seguinte pilha de pacotes:

  1. JMSSerializerBundle para serialização de entidades Doctrine
  2. Pacote FOSRestBundle para listener de visualização de resposta. Também pode gerar definição de rotas com base no nome do controlador / ação.
  3. NelmioApiDocBundle para geração automática de documentação online e Sandbox (que permite testar endpoint sem qualquer ferramenta externa).

Quando você configurar tudo corretamente, seu código de entidade terá a seguinte aparência:

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

Então, codifique no 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();
    }
}

Os benefícios dessa configuração são:

  • As anotações @JMS \ Expose () na entidade podem ser adicionadas a campos simples e a quaisquer tipos de relações. Também existe a possibilidade de expor o resultado da execução de algum método (use a anotação @JMS \ VirtualProperty () para isso)
  • Com os grupos de serialização, podemos controlar os campos expostos em diferentes situações.
  • Os controladores são muito simples. O método de ação pode retornar diretamente uma entidade ou matriz de entidades, e elas serão serializadas automaticamente.
  • E @ApiDoc () permite testar o endpoint diretamente do navegador, sem qualquer cliente REST ou código JavaScript
Maksym Moskvychev
fonte
2

Agora você também pode usar Doctrine ORM Transformations para converter entidades em matrizes aninhadas de escalares e vice-versa

ScorpioT1000
fonte