Jak zakodować encje Doctrine do JSON w aplikacji Symfony 2.0 AJAX?

91

Tworzę grę i używam Symfony 2.0. Mam wiele żądań AJAX do zaplecza. Więcej odpowiedzi konwertuje encję na JSON. Na przykład:

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

Wszystkie moje kontrolery robią to samo: pobierają encję i kodują niektóre jej pola do JSON. Wiem, że mogę używać normalizatorów i zakodować wszystkie uprawnienia. Ale co się stanie, jeśli podmiot ma cykliczne połączenia z innym podmiotem? Czy wykres jednostek jest bardzo duży? Masz jakieś sugestie?

Myślę o schemacie kodowania dla jednostek ... lub używaniu go, NormalizableInterfaceaby uniknąć cykli ...,

Dmytro Krasun
źródło

Odpowiedzi:

83

Inną opcją jest użycie JMSSerializerBundle . W swoim kontrolerze robisz to

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

Możesz skonfigurować sposób serializacji przy użyciu adnotacji w klasie jednostki. Zobacz dokumentację w linku powyżej. Na przykład, oto jak możesz wykluczyć połączone podmioty:

 /**
* 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
źródło
7
musisz dodać use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; użyj JMS \ SerializerBundle \ Annotation \ Exclude; w swojej jednostce i zainstalować JMSSerializerBundle aby to zadziałało
ioleo
3
Działa świetnie, jeśli zmienisz to na: zwraca nową odpowiedź (raporty $);
Greywire
7
Ponieważ adnotacje zostały usunięte z pakietu, prawidłowe instrukcje użycia są teraz następujące: use JMS \ Serializer \ Annotation \ ExclusionPolicy; użyj JMS \ Serializer \ Annotation \ Exclude;
Pier-Luc Gendreau
3
Dokumentacja Doctrine mówi, aby nie serializować obiektów ani nie serializować z wielką ostrożnością.
Bluebaron
Nie musiałem nawet instalować JMSSerializerBundle. Twój kod działał bez wymagania JMSSerializerBundle.
Derk Jan Speelman
149

Dzięki php5.4 możesz teraz:

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

A potem zadzwoń

json_encode(MyUserEntity);
SparSio
źródło
1
bardzo lubię to rozwiązanie!
Michael
3
Jest to świetne rozwiązanie jeśli wiążąc aby utrzymać współzależności z innymi wiązkami do minimum ...
Drmjo
5
A co z powiązanymi podmiotami?
John the Ripper
7
Wydaje się, że to nie działa w przypadku kolekcji podmiotów (tj .: OneToManyrelacji)
Pierre de LESPINAY
1
To narusza zasadę pojedynczej odpowiedzialności i nie jest dobre, jeśli twoje istoty są automatycznie generowane przez doktrynę
Jim Smith
39

Możesz automatycznie kodować do Json, złożonej jednostki za pomocą:

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
źródło
3
Dziękuję, ale mam jednostkę Gracza, która ma łącze do kolekcji jednostek gier, a każda jednostka gry ma łącze do graczy, którzy w niej grali. Coś takiego. Czy myślisz, że GetSetMethodNormalizer będzie działał poprawnie (używa algorytmu rekurencyjnego)?
Dmytro Krasun
2
Tak, jest rekurencyjny i to był mój problem w moim przypadku. Tak więc w przypadku określonych jednostek można użyć CustomNormalizer i jego NormalizableInterface, jak się wydaje.
webda2l
2
Kiedy próbowałem tego, otrzymałem komunikat „Błąd krytyczny: wyczerpano dozwolony rozmiar pamięci 134217728 bajtów (próbowano przydzielić 64 bajty) w /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php on linia 44 ". Zastanawiam się dlaczego?
Jason Swett
1
kiedy próbowałem, otrzymałem wyjątek poniżej. Błąd krytyczny: osiągnięto maksymalny poziom zagnieżdżenia funkcji „100”, przerywanie! w C: \ wamp \ www \ myapp \ application \ libraries \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php on line 223
user2350626
1
@ user2350626, patrz stackoverflow.com/questions/4293775/ ...
webda2l
11

Aby uzupełnić odpowiedź: Symfony2 zawiera opakowanie wokół json_encode: Symfony / Component / HttpFoundation / JsonResponse

Typowe zastosowanie w kontrolerach:

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

Mam nadzieję że to pomoże

jot

jerome
źródło
10

Znalazłem rozwiązanie problemu serializacji jednostek:

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

w moim kontrolerze:

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

inny przykład:

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

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

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

return new Response($json);

możesz nawet skonfigurować go do deserializacji tablic w http://api.symfony.com/2.0

rkmax
źródło
3
W książce kucharskiej jest wpis o używaniu komponentu Serializer w Symfony 2.3+, ponieważ możesz teraz aktywować wbudowany: symfony.com/doc/current/cookbook/serializer.html
althaus
6

Po prostu musiałem rozwiązać ten sam problem: kodowanie json jednostki („Użytkownik”) mającej jedno-do-wielu dwukierunkowe skojarzenie z inną jednostką („lokalizacja”).

Próbowałem kilku rzeczy i myślę, że teraz znalazłem najlepsze akceptowalne rozwiązanie. Pomysł polegał na użyciu tego samego kodu, który napisał David, ale w jakiś sposób przechwycił nieskończoną rekurencję, nakazując Normalizatorowi zatrzymanie się w pewnym momencie.

Nie chciałem implementować niestandardowego normalizatora, ponieważ ten GetSetMethodNormalizer jest moim zdaniem fajnym podejściem (opartym na refleksji itp.). Postanowiłem więc utworzyć podklasę, co na pierwszy rzut oka nie jest trywialne, ponieważ metoda określania, czy należy dołączyć właściwość (isGetMethod), jest prywatna.

Ale można by przesłonić metodę normalize, więc przechwyciłem w tym momencie, po prostu wyłączając właściwość, która odwołuje się do „Location” - tak więc nieskończona pętla zostaje przerwana.

W kodzie wygląda to tak:

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

} 
oksy
źródło
1
Zastanawiam się, jak łatwo byłoby to uogólnić, tak aby 1. nigdy nie trzeba było dotykać klas Encji, 2. Nie tylko wyczyścić pole „Lokalizacje”, ale wszystkie pola typu Kolekcje, które potencjalnie są mapowane na inne Jednostki. To znaczy żadna wewnętrzna / zaawansowana wiedza o Ent jest wymagana do serializacji, bez rekurencji.
Marcos
6

Miałem ten sam problem i zdecydowałem się stworzyć własny koder, który sam sobie poradzi z rekurencją.

Stworzyłem klasy, które implementują Symfony\Component\Serializer\Normalizer\NormalizerInterfacei usługę, która przechowuje wszystkie pliki 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);
}

Przykład normalizatora:

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

}

W kontrolerze:

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

Pełny kod jest tutaj: https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer

Julien Fastré
źródło
6

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

i przykład dla twojego kontrolera:

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

ale problemy z typem pola \ DateTime pozostaną.

Lebnik
źródło
6

Jest to bardziej aktualizacja (dla Symfony v: 2.7+ i JmsSerializer v: 0.13. * @ Dev) , aby uniknąć prób załadowania i serializacji całego grafu obiektu (lub w przypadku relacji cyklicznej).

Model:

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

Wewnątrz akcji:

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
źródło
5

Jeśli używasz Symfony 2.7 lub nowszego i nie chcesz dołączać żadnego dodatkowego pakietu do serializacji, być może możesz skorzystać z tego sposobu, aby przenieść encje doktryny do json -

  1. W moim (wspólnym, nadrzędnym) kontrolerze mam funkcję, która przygotowuje serializator

    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. Następnie użyj go do serializacji jednostek do formatu JSON

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

Gotowe!

Ale możesz potrzebować dopracowania. Na przykład -

Anis
źródło
4

Kiedy potrzebujesz stworzyć wiele punktów końcowych REST API w Symfony, najlepszym sposobem jest użycie następującego stosu pakietów:

  1. JMSSerializerBundle do serializacji jednostek Doctrine
  2. Pakiet FOSRestBundle dla odbiornika widoku odpowiedzi. Może również generować definicje tras na podstawie nazwy kontrolera / akcji.
  3. NelmioApiDocBundle do automatycznego generowania dokumentacji online i Sandbox (co pozwala na testowanie punktu końcowego bez żadnego zewnętrznego narzędzia).

Po prawidłowym skonfigurowaniu wszystkiego kod encji będzie wyglądał następująco:

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

Następnie kod w kontrolerze:

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

Korzyści z takiej konfiguracji to:

  • Adnotacje @JMS \ Expose () w encji można dodawać do prostych pól i do dowolnego typu relacji. Istnieje również możliwość ujawnienia wyniku wykonania jakiejś metody (użyj do tego adnotacji @JMS \ VirtualProperty ())
  • Dzięki grupom serializacji możemy kontrolować ujawnione pola w różnych sytuacjach.
  • Kontrolery są bardzo proste. Metoda Action może bezpośrednio zwrócić jednostkę lub tablicę jednostek i zostaną one automatycznie serializowane.
  • @ApiDoc () pozwala na testowanie punktu końcowego bezpośrednio z przeglądarki, bez żadnego klienta REST lub kodu JavaScript
Maksym Moskvychev
źródło
2

Teraz możesz również użyć Doctrine ORM Transformations do konwersji encji na zagnieżdżone tablice skalarów iz powrotem

ScorpioT1000
źródło