How to pass options to json_encode function in Symfony Serializer component - php

here is the situation: I work on a rest api, based on symfony3, it uses FOSRestBundle and symfony serializer component, so methods return array and FOSRest handles encoding and response. The problem is serializer use json_encode with default settings and api return data like '\u00c9S' for some symbols. So I need to pass 'JSON_UNESCAPED_UNICODE' to json_encode() somehow. Is there any proper way to reach this goal?
Example of a method:
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use FOS\RestBundle\Controller\Annotations as Rest;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ExampleController
* #package AppBundle\Controller
*/
class ExampleController extends Controller
{
/**
* #Rest\Get("/get-some-data")
* #param Request $request
* #return array
*/
public function getSomeDataAction(Request $request)
{
$someData = [
'prop1' => 'Value',
'prop2' => 'Value',
'prop3' => 'Value',
'prop4' => 'Value',
];
return $someData;
}
So when I do request to '/get-some-data', it returns me:
{"prop1":"Value with \/","prop2":"Value with \u00c9"}
, but I need it to return:
{"prop1":"Value with /","prop2":"Value with É"}

I use Symfony 3 and the "Doctrine JSON ODM Bundle" to store my data as JSON document. I had the same problem. All the data that contained unicode characters where automatically escaped which was not what I wanted.
After some experiments I finally managed to pass JSON_UNESCAPED_UNICODE option to json_encode().
Below is my solution:
# config/services.yml
serializer.encode.json.unescaped:
class: Symfony\Component\Serializer\Encoder\JsonEncode
arguments:
- !php/const JSON_UNESCAPED_UNICODE
serializer.encoder.json:
class: Symfony\Component\Serializer\Encoder\JsonEncoder
arguments:
- '#serializer.encode.json.unescaped'

Solution for Symfony 5.
Passing an integer as first parameter of the "Symfony\Component\Serializer\Encoder\JsonEncode::__construct()" method is deprecated since Symfony 4.2, use the "json_encode_options" key of the context instead.
Add to config/services.yaml:
serializer.encode.json.unescaped:
class: Symfony\Component\Serializer\Encoder\JsonEncode
arguments:
- { "json_encode_options": !php/const JSON_UNESCAPED_UNICODE }
serializer.encoder.json:
class: Symfony\Component\Serializer\Encoder\JsonEncoder
arguments:
- '#serializer.encode.json.unescaped'

When set json type in doctrine for column under the hood is using PHP's json_encode() and json_decode() functions for storing and retrieving data from databases.
json_encode() function that convert PHP's array to JSON string has second parameter for assign set of options for set up some convertation rules. One of them is JSON_UNESCAPED_UNICODE that disable encoding multibyte symbols as \uXXXX
We can enable this option for json_encode() by default for doctrine. To levarage this you must do 2 steps.
1. Create class App\Doctrine\Types\JsonType
// App\Doctrine\Types\JsonType.php
<?php
namespace App\Doctrine\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class JsonType extends \Doctrine\DBAL\Types\JsonType
{
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE);
if (json_last_error() !== JSON_ERROR_NONE) {
throw ConversionException::conversionFailedSerialization($value, 'json', json_last_error_msg());
}
return $encoded;
}
}
2. Override configuration for DBAL JSON type corresponding to our class
# config/packages/doctrine.yaml
doctrine:
dbal:
types:
json: App\Doctrine\Types\JsonType

You can use an encoder, as found here and in the documentation
<?php
$encoder = new JsonEncoder(new JsonEncode(JSON_UNESCAPED_UNICODE), new JsonDecode(false));
$normalizer = new ObjectNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
EDIT:
In this example, I use a Response object. Note that an Action Controller must return a Response object.
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use FOS\RestBundle\Controller\Annotations as Rest;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ExampleController
* #package AppBundle\Controller
*/
class ExampleController extends Controller
{
/**
* #Rest\Get("/get-some-data")
* #param Request $request
* #return array
*/
public function getSomeDataAction(Request $request)
{
$someData = [
'prop1' => 'Value',
'prop2' => 'Value',
'prop3' => 'Value',
'prop4' => 'Value',
];
$response = new Response($someData);
$response->headers->set('Content-Type', 'application/json');
return $response;
}

$this->serializer->serialize(
$data,
JsonEncoder::FORMAT,
['json_encode_options' => JSON_UNESCAPED_UNICODE]
);

In newer versions of symfony you have to use this format:
$encoders = [new JsonEncoder(new JsonEncode(["json_encode_options" =>
JSON_UNESCAPED_SLASHES]), new JsonDecode([]))];

Related

How to configure Normalizer with injected SerializerInterface

I know that I can use and configure e.g. the DateTimeNormalizer of Symfony's Serializer like that:
$serializer = new Serializer(array(new DateTimeNormalizer()));
$dateAsString = $serializer->normalize(
new \DateTime('2016/01/01'),
null,
array(DateTimeNormalizer::FORMAT_KEY => 'Y/m')
);
// $dateAsString = '2016/01';
But if I want to use the Serializer without instantiation but with the "full Symfony app" Dependency injection instead like:
//class MyService...
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
//function MyFunction()
{
$json = $this->serializer->serialize($object, 'json');
}
it already comes with the most common Normalizers in a pre-ordered fashion.
Usually this is perfectly fine for my purpose.
But how could I e.g. configure the DateTimeNormalizer::FORMAT_KEY in the injection scenario without creating CustomNormalizers or lots of instantiating?
I just finished a sprint of my project with a similar problem.
If you only want to format by default you DateTime object, you can add those lines in your config file:
services:
serializer.normalizer.datetime:
class: ‘Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments
-
!php/const Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY: 'Y-m-d\TH:i:s.uP’
tags:
- { name: serializer.normalizer, priority: -910 }
But in case you want more flexibility and control over the serializer, i advise you to not modify config file but try this following method :
So for my needs, i finally create a MainController that extends AbstractController and each of my controllers extends MainController.
In the MainController I instanciate the serializer as property, so that you can access serializer already configured in the MainControlle by $this->serializer.
MainController :
class MainController extends AbstractController {
protected $serializer;
public function __construct() {
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizers = [
new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => "your date format"]),
new ObjectNormalizer()
];
$this->serializer = new Serializer($normalizers, $encoders);
}
}
PostController :
class PostController extends MainController {
/**
* #Route("/posts")
**/
public function listPosts() {
$e = new Entity();
return $this->serializer->serialize($e, "json");
}
}
EDIT : !! I forgot to say, be careful to the order you arrange normalizer in instanciation. As DateTime is an object, the serializer will try to normalize the DateTime object with ObjectNormalizer first if ObjectNormalizer is added first (which is used to normalize entities) and will return an empty array.
Since symfony 5.3, you could add the context as a inline metadata.
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class SomeClass
{
/**
* #Serializer\Context({ DateTimeNormalizer::FORMAT_KEY = 'Y-m-d' })
*/
public \DateTime $date;
// In PHP 8 applications you can use PHP attributes instead:
#[Serializer\Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public \DateTime $date;
}
Read more about it in the blog post:
https://symfony.com/blog/new-in-symfony-5-3-inlined-serialization-context

Symfony 4 : slashes unscaped on JSON response

Good morning guys,
I have a probleme on my symfony 4 Api.
I should return a json response, but the serializer return a string with slashes. I Don't know how escape it.
Bellow my controller :
use App\Entity\Category;
use App\Form\CategoryType;
use App\Repository\CategoryRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface as SerializerInterfaceAlias;
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* Category Controller
* #Route("/api", name="api_")
*/
class CategoryController extends AbstractController
{
/**
* #Rest\Get("/categories")
* #param CategoryRepository $categoryRepository
* #param SerializerInterfaceAlias $serializer
*/
public function index(CategoryRepository $categoryRepository, SerializerInterfaceAlias $serializer)
{
$jsonContent = $serializer->serialize($categoryRepository->findall(), 'json', ['json_encode_options' => JSON_UNESCAPED_SLASHES]);
return $jsonContent;
}
[....]
}
And my return is look like :
"[{\"id\":3,\"name\":\"toto\",\"logo\":\"tata\",\"description\":\"lolo\",\"dateCreated\":\"2019-05-09T10:30:39+00:00\"},{\"id\":4,\"name\":\"tata\",\"logo\":\"titi\",\"description\":\"tutu\",\"dateCreated\":\"2019-05-09T10:30:49+00:00\"}]"
For information I using PHP 7.1 & Symfony 4.2.
So I want a proper json format... without this slashes :(
Do you have any suggestion ? :)
Thanks in advance !
I finaly resolve my problem #RubberDuckDebugging
I don't need to use the serializer here.
I need just to return :
return $this->json($categoryRepository->findall());
That's so simple. Sorry :)

Symfony 2.8 Services issue

Since the last 4 hours I'm trying to understand the logic of Symfony 2 services and how they integrate in the application...
Basically I'm trying to set my EntityManager via a service and use it in a controller
I have the following structure
Bundle1/Controller/Bundle1Controller.php
Bundle1/Services/EntityService.php
Bundle2/Controller/Bundle2Controller.php
Bundle3/Controller/Bundle3Controller.php
....
I'm trying to make a REST API with different entry points, that's why I use multiple bundles bundle2,bundle3....
The logic is the following:
A POST is fired to Bundle2/Controller/Bundle2Controller.php
Bundle2Controller.php instances a new() Bundle1Controller.php
Inside Bundle1Controller I want to access a service entity_service in order to get my EntityManager
I have 2 cases in which I manage to land...
In Bundle1/Controller/Bundle1Controller if I try $this->container or $this->get('entity_service') I get a null everytime
If I set the container in Bundle2/Controller/Bundle2Controller and try $this->get('entity_service') I get You have requested a non-existent service "entity_service"
I will place all the code below
Bundle1/Controller/Bundle1Controller
<?php
namespace Bundle1\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use EntityBundle\Entity\TestEntity;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
class Bundle1Controller extends Controller
{
/**
* #param $response
* #return array
*/
public function verifyWebHookRespone($response){
$em = $this->get('entity_service')->getEm();
$array = json_decode($response);
$mapping = $em->getRepository('EntityBundle:TestEntity')
->findBy(["phone" => $array['usernumber']]);
return $mapping;
}
}
Bundle2/Controller/Bundle2Controller.php
<?php
namespace Bundle2\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Bundle1\Controller\Bundle1Controller;
class Bundle2Controller extends Controller
{
public function webhookAction(Request $request)
{
$data = $request->request->get('messages');
$model = new Bundle1Controller();
$responseMessage = $model->verifyWebHookRespone($data);
return new Response($responseMessage, Response::HTTP_CREATED, ['X-My-Header' => 'My Value']);
}
}
Bundle1/Services/EntityService.php
<?php
namespace EntityBundle\Services;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\Container;
class EntityService
{
protected $em;
private $container;
public function __construct(EntityManager $entityManager, Container $container)
{
$this->em = $entityManager;
$this->container = $container;
}
/**
* #return EntityManager
*/
public function getEm()
{
return $this->em;
}
}
services.yml
services:
entity_service:
class: Bundle1\Services\EntityService
arguments: [ "#doctrine.orm.entity_manager" , "#service_container" ]
Can anyone please help me with something regarding this issue?
How can I register a service and call it from anywhere no matter the bundle or another service?
You should check where your services.yml is located and whether it is imported in the config.yml
You can't just instantiate a controller and expect it to work, you need to set the container.
But you can call EntityManager without needing any other service by using;
$this->get('doctrine.orm.entity_manager');
I can't understand your structure or what you are trying to achieve, but those are the options to go about if you want to keep this structure.

Symfony and JMSSerialier, can't add listener to add extra fields

I tried to follow this answer:
Add extra fields using JMS Serializer bundle
but no change..
I want to add extra fields to a serialized entity (in json) before sending it. Is there something that I missed ?
Here is my Listener:
<?php
namespace My\MyBundle\Listener;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use JMS\DiExtraBundle\Annotation\Inject;
use JMS\DiExtraBundle\Annotation\InjectParams;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use My\MyBundle\Entity\Dossier;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\JsonSerializationVisitor;
/**
* Add data after serialization
*
* #Service("my.listener.serializationlistener")
* #Tag("jms_serializer.event_subscriber")
*/
class SerializationListener implements EventSubscriberInterface
{
/**
* #inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'class' => 'My\MyBundle\Entity\Dossier', 'method' => 'onPostSerialize'),
);
}
public function onPostSerialize(ObjectEvent $event)
{
$event->getVisitor()->addData('someKey','someValue');
}
}
and the call in my controller:
$serializer = $this->container->get('jms_serializer');
$res = $serializer->serialize($dossier, 'json');
I also add the following service declaration:
services:
my.mybundle.listener:
class: My\MyBundle\Listener\SerializationListener
I have another service declared and when I change its declaration name symfony give and error, not when I do it with the listener service.
Thanks in advance
$visitor = $event->getVisitor();
$visitor->visitProperty(new StaticPropertyMetadata('', 'some_key', null),'some_key');
The addData method is for old version of JMS Serializer. Don't forget to import StaticPropertyMetadata.
Perhaps, you forgot to add a tag. Your listener declaration should looks something like this
services:
my.bundle.listener:
class: My\MyBundle\Listener\SerializationListener
tags:
- { name: jms_serializer.event_subscriber }

Custom annotation conflict with #secure from jms/SecurityExtraBundle

I have writted an annotation who throw an AccesDeniedException when the action is not called by an AJAX request (XMLHttpRequest).
It work but when I want to use the #Secure(roles="A") annotation from JMS/SecurityExtraBundle it don't work like I omitted my custom exception.
Controller
namespace Mendrock\Bundle\SagaBundle\Controller;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Mendrock\Bundle\SagaBundle\Entity\Saison;
use Mendrock\Bundle\SagaBundle\Form\SaisonType;
use Mendrock\Bundle\ExtraBundle\Annotation\XmlHttpRequest;
/**
* #Route("/episodesAjax")
*/
class EpisodeController extends Controller {
/**
* #XmlHttpRequest()
* #Secure(roles="ROLE_SUPER_ADMIN")
*
* #Route("/saisonAdd", options={"expose"=true})
* #Template()
*/
public function saisonAddAction() {
$entity = new Saison();
$form = $this->createForm(new SaisonType(), $entity);
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
Annotation
namespace Mendrock\Bundle\ExtraBundle\Annotation;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* #Annotation
*/
class XmlHttpRequest
{
public $message = 'The action could be an XMLHttpRequest call.';
public function checkRequest($event){
if (!$event->getRequest()->isXmlHttpRequest()) {
throw new AccessDeniedHttpException($this->message);
}
}
public function execute($event){
$this->checkRequest($event);
}
}
Listener
namespace Mendrock\Bundle\ExtraBundle\Listener;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Mendrock\Bundle\ExtraBundle\Annotation\XmlHttpRequest;
class EventListener {
private $reader;
public function __construct(Reader $reader) {
$this->reader = $reader;
}
/**
* This event will fire during any controller call
*/
public function onKernelController(FilterControllerEvent $event) {
if (!is_array($controller = $event->getController())) {
return;
}
$method = new \ReflectionMethod($controller[0], $controller[1]);
foreach ($this->reader->getMethodAnnotations($method) as $configuration) {
if ($configuration instanceof XmlHttpRequest) {
$configuration->execute($event);
}
}
}
}
Any idea why I can't use at the same time #Secure(...) and #XMLHttpRequest?
Edit:
services.yml
services:
annotations.xmlhttprequest:
class: Mendrock\Bundle\ExtraBundle\Listener\EventListener
tags: [{name: kernel.event_listener, event: kernel.controller, method: onKernelController}]
arguments: [#annotation_reader]
I ran into the same problem when I wanted to add my own annotations. The solution using ClassUtils::getUserClass would not work (using Symfony 2.3, if that makes a difference).
Since we only use the #Secure annotation from JMS\SecurityExtraBundle, I made our codebase use LswSecureControllerBundle instead.
This bundle only provides #Secure, and does not do voodoo tricks with your controllers.
I am running into the same issue after upgrading to Symfony 2.1.
The issue, from my investigation, is that the JMS SecurityExtraBundle generates proxy classes whenever you use one of their annotations. The problem with the generated proxy classes is that custom annotations do not get proxied over, which is why the annotations appear to be missing.
The solution according to the author is to rewrite using AOP (facilities provided by JMSAopBundle) or to use ClassUtils::getUserClass.
Thanks to suihock for pointing this out:
$class = \CG\Core\ClassUtils::getUserClass($controller[0]);
$method = new \ReflectionMethod($class, $controller[1]);

Categories