Как кодировать объекты Doctrine в JSON в приложении Symfony 2.0 AJAX?

91

Я разрабатываю игровое приложение и использую Symfony 2.0. У меня много запросов AJAX к бэкэнду. И еще больше ответов - это преобразование объекта в JSON. Например:

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

И все мои контроллеры делают то же самое: получают объект и кодируют некоторые из его полей в JSON. Я знаю, что могу использовать нормализаторы и кодировать все права. Но что, если у объекта есть циклические ссылки на другой объект? Или граф сущностей очень большой? Есть ли у вас какие-либо предложения?

Я думаю о какой-то схеме кодирования для сущностей ... или об использовании, NormalizableInterfaceчтобы избежать зацикливания ..,

Дмитрий Красун
источник

Ответы:

83

Другой вариант - использовать JMSSerializerBundle . Затем в вашем контроллере вы делаете

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

Вы можете настроить, как выполняется сериализация, используя аннотации в классе сущности. См. Документацию по ссылке выше. Например, вот как можно исключить связанные объекты:

 /**
* 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
вам нужно добавить use JMS \ SerializerBundle \ Annotation \ ExclusionPolicy; используйте JMS \ SerializerBundle \ Annotation \ Exclude; в вашем объекте и установите JMSSerializerBundle, чтобы это работало
ioleo
3
Отлично работает, если вы измените его на: return new Response ($ reports);
Greywire 03
7
Поскольку аннотации были перемещены из пакета, теперь правильными операторами использования являются: use JMS \ Serializer \ Annotation \ ExclusionPolicy; используйте JMS \ Serializer \ Annotation \ Exclude;
Пьер-Люк Жендро
3
В документации к Doctrine сказано, что сериализовать объекты и сериализовать нельзя с большой осторожностью.
Bluebaron
Мне даже не нужно было устанавливать JMSSerializerBundle. Ваш код работал без использования JMSSerializerBundle.
Дерк Ян Спилман,
149

С php5.4 теперь вы можете:

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

А потом позвони

json_encode(MyUserEntity);
SparSio
источник
1
мне очень нравится это решение!
Майкл
3
Это отличное решение, если вы хотите свести к минимуму свои зависимости от других пакетов ...
Drmjo,
5
А как насчет связанных сущностей?
Джон Потрошитель
7
Похоже, что это не работает с коллекциями сущностей (то есть с OneToManyотношениями)
Пьер де ЛЕСПИНЕ,
1
Это нарушает принцип единой ответственности и бесполезно, если ваши сущности автоматически генерируются доктриной
Джим Смит
39

Вы можете автоматически кодировать свою сложную сущность в Json с помощью:

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
источник
3
Спасибо, но у меня есть сущность Player, которая имеет ссылку на коллекцию сущностей Game, и каждая сущность Game имеет ссылку на игроков, которые в нее играли. Что-то вроде этого. Как вы думаете, будет ли правильно работать GetSetMethodNormalizer (использует рекурсивный алгоритм)?
Дмитрий Красун
2
Да, это рекурсивно, и в моем случае это была моя проблема. Итак, для определенных сущностей вы можете использовать CustomNormalizer и его NormalizableInterface, как вам кажется.
webda2l
2
Когда я попробовал это, я получил «Неустранимая ошибка: разрешенный размер памяти 134217728 байт исчерпан (попытался выделить 64 байта) в /home/jason/pressbox/vendor/symfony/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php на строка 44 ". Интересно, почему?
Джейсон Светт,
1
при попытке я получил исключение ниже .. Неустранимая ошибка: достигнут максимальный уровень вложенности функции «100», прерывание! в C: \ wamp \ www \ myapp \ application \ libraries \ doctrine \ Symfony \ Component \ Serializer \ Normalizer \ GetSetMethodNormalizer.php в строке 223
user2350626 02 окт.13
1
@ user2350626, см. stackoverflow.com/questions/4293775/…
webda2l 03 окт.13
11

Чтобы завершить ответ: Symfony2 поставляется с оболочкой для json_encode: Symfony / Component / HttpFoundation / JsonResponse

Типичное использование в ваших контроллерах:

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

Надеюсь это поможет

J

Джером
источник
10

Я нашел решение проблемы сериализации сущностей следующим образом:

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

в моем контроллере:

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

другой пример:

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

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

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

return new Response($json);

вы даже можете настроить его для десериализации массивов в http://api.symfony.com/2.0

rkmax
источник
3
В кулинарной книге есть запись об использовании компонента Serializer в Symfony 2.3+, так как теперь вы можете активировать встроенный: symfony.com/doc/current/cookbook/serializer.html
althaus
6

Мне просто нужно было решить ту же проблему: json-кодирование объекта («Пользователь»), имеющего двунаправленную связь «один ко многим» с другим объектом («Местоположение»).

Я попробовал несколько вещей и теперь, думаю, нашел наилучшее приемлемое решение. Идея заключалась в том, чтобы использовать тот же код, что и Дэвид, но каким-то образом перехватить бесконечную рекурсию, приказав нормализатору остановиться в какой-то момент.

Я не хотел реализовывать собственный нормализатор, так как этот GetSetMethodNormalizer, на мой взгляд, является хорошим подходом (на основе отражения и т. Д.). Поэтому я решил создать его подкласс, что на первый взгляд нетривиально, потому что метод определения включения свойства (isGetMethod) является частным.

Но можно было переопределить метод нормализации, поэтому я перехватил на этом этапе, просто отключив свойство, которое ссылается на «Местоположение» - так что бесконечный цикл будет прерван.

В коде это выглядит так:

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
Интересно, насколько легко было бы это обобщить, чтобы 1. никогда не приходилось касаться классов сущностей, 2. не просто очищать поле «Местоположение», но каждое поле типа Коллекции, которое потенциально сопоставляется с другими объектами. Т.е. для его сериализации не требуется никаких внутренних / предварительных знаний Ent, без рекурсии.
Маркос,
6

У меня была такая же проблема, и я решил создать свой собственный кодировщик, который сам справится с рекурсией.

Я создал классы, которые реализуют Symfony\Component\Serializer\Normalizer\NormalizerInterface, и сервис, который содержит все 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);
}

Пример нормализатора:

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

}

В контроллере:

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

Полный код находится здесь: https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer

Жюльен Фастре
источник
6

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

и пример для вашего контроллера:

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

но проблемы с типом поля \ DateTime останутся.

Лебник
источник
6

Это скорее обновление (для Symfony v: 2.7+ и JmsSerializer v: 0.13. * @ Dev) , чтобы избежать того, что Jms пытается загрузить и сериализовать весь граф объекта (или в случае циклической связи ..)

Модель:

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

Внутри действия:

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

Если вы используете Symfony 2.7 или выше и не хотите включать какой-либо дополнительный пакет для сериализации, возможно, вы можете воспользоваться этим способом для выделения сущностей доктрины в json -

  1. В моем (общем, родительском) контроллере у меня есть функция, которая подготавливает сериализатор

    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. Затем используйте его для сериализации объектов в JSON

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

Выполнено!

Но вам может потребоваться тонкая настройка. Например -

Анис
источник
4

Когда вам нужно создать много конечных точек REST API на Symfony, лучший способ - использовать следующий стек пакетов:

  1. JMSSerializerBundle для сериализации сущностей Doctrine
  2. Пакет FOSRestBundle для прослушивателя представления ответа. Также он может генерировать определение маршрутов на основе имени контроллера / действия.
  3. NelmioApiDocBundle для автоматического создания онлайн-документации и песочницы (которая позволяет тестировать конечную точку без каких-либо внешних инструментов).

Когда вы все настроите правильно, ваш код сущности будет выглядеть так:

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

Затем введите код в контроллере:

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

Преимущества такой настройки:

  • Аннотации @JMS \ Expose () в сущности можно добавлять как в простые поля, так и в любые типы отношений. Также есть возможность показать результат выполнения какого-либо метода (для этого используйте аннотацию @JMS \ VirtualProperty ())
  • С помощью групп сериализации мы можем управлять открытыми полями в различных ситуациях.
  • Контроллеры очень простые. Метод действия может напрямую возвращать объект или массив объектов, и они будут автоматически сериализованы.
  • А @ApiDoc () позволяет тестировать конечную точку прямо из браузера, без какого-либо клиента REST или кода JavaScript.
Максим Москвичев
источник
2

Теперь вы также можете использовать Doctrine ORM Transformations для преобразования сущностей во вложенные массивы скаляров и обратно.

СкорпионT1000
источник